Introduction
This article will cover the build of a two-node high-availability cluster using DRBD (RAID1 over TCP/IP), the Corosync cluster engine, and the Pacemaker resource manager on CentOS 6.4. There are many applications for this type of cluster - as a free alternative to RHCS for example. However, this example does have a couple of caveats. As this is being built in a lab environment on KVM guests, there will be no STONITH (Shoot The Other Node In The Head) (a type of fencing). If this cluster goes split-brain, there may be manual recovery required to intervene, tell DRBD who is primary and who is secondary, and so on. In a Production environment, we’d use STONITH to connect to ILOMs (for example) and power off or reboot a misbehaving node. Quorum will also need to be disabled, as this stack doesn’t yet support the use of quorum disks - if you want that go with RHCS (and use cman with the two_node parameter, with or without qdiskd).
This article, as always, presumes that you know what you are doing. The nodes used in this article are as follows:
- 192.168.122.30 - rhcs-node01.local - first cluster node - running CentOS 6.4
- 192.168.122.31 - rhcs-node02.local - second cluster node - running CentOS 6.4
- 192.168.122.33 - failover IP address
DRBD will be used to replicate a volume between the two nodes (in a Master/Slave fashion), and the hosts will eventually run the nginx webserver in a failover topology, with this example having documents being served from the replicated volume.
Ideally, four network interfaces per host should be used (1 for “standard” node communications, 1 for DRBD replication, 2 for Corosync), but for a lab environment a single interface per node is fine.
Let’s start the build …
Initial Node Preparation
For the sake of simplicity, disable iptables and SELinux on each node:
|
1 2 3 4 |
# service iptables stop # chkconfig iptables off # sed -i 's/^\(SELINUX=\)enforcing/\1permissive/' /etc/selinux/config # setenforce permissive |
Ensure that NTP is correctly configured (I do this in my kickstart configuration anyway):
|
1 2 3 4 |
# vi /etc/ntp.conf # ntpdate 1.au.pool.ntp.org # service ntpd start # chkconfig ntpd on |
If you are not using DNS, add entries to each of your host’s /etc/hosts files for all nodes in the cluster - or do so anyway as a safeguard against DNS failure:
|
1 2 3 |
# vi /etc/hosts 192.168.122.30 rhcs-node01 192.168.122.31 rhcs-node02 |
At this point, I configured LVM. I presented a disk to each node (themselves running as KVM guests, as discussed earlier), at /dev/vdb - verified with fdisk -l:
|
1 2 3 4 5 6 7 8 |
# fdisk -l ... Disk /dev/vdb: 2147 MB, 2147483648 bytes 16 heads, 63 sectors/track, 4161 cylinders Units = cylinders of 1008 * 512 = 516096 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disk identifier: 0x00000000 |
Next, I partitioned the entire disk as type 8e (Linux LVM), created a new physical volume using the new partition (/dev/vdb1), a new volume group using that PV (vg_data) and then a 1GB logical volume within the new volume group - lv_data_01:
|
1 2 3 4 5 6 |
# sfdisk /dev/vdb <<_EOF_ > 0,,8e > _EOF_ # pvcreate /dev/vdb1 # vgcreate vg_data /dev/vdb1 # lvcreate -L 1G -n lv_data_01 vg_data |
This 1GB volume will be used for the DRBD replicated storage volume on each node. We can now move on to software package installation.
Package Installation
I installed pacemaker and corosync from the standard CentOS repositories:
|
1 |
# yum -y install corosync pacemaker |
However, between CentOS 6.3 and 6.4 being released, pacemaker has been updated from 1.1.7 to 1.1.8, which means that the CRM shell is no longer included. This is now maintained in its own project - crmsh - and CentOS 6 RPMs are available via the OpenSUSE package repositories.
So, I installed crmsh by way of its dependencies:
|
1 2 3 |
# yum -y install python-dateutil redhat-rpm-config # rpm -ivh http://download.opensuse.org/repositories/network:/ha-clustering/CentOS_CentOS-6/x86_64/pssh-2.3.1-15.1.x86_64.rpm # rpm -ivh http://download.opensuse.org/repositories/network:/ha-clustering/CentOS_CentOS-6/x86_64/crmsh-1.2.5-55.6.x86_64.rpm |
Next, I installed DRBD 8.4 from the ELrepo repository:
|
1 2 3 |
# rpm --import http://elrepo.org/RPM-GPG-KEY-elrepo.org # rpm -Uvh http://elrepo.org/elrepo-release-6-5.el6.elrepo.noarch.rpm # yum -y install drbd84-utils kmod-drbd84 |
I then ensured that it wasn’t set to start automatically at boot (as the cluster will manage this):
|
1 |
# chkconfig drbd off |
I left nginx package installation for later in the procedure.
Corosync Configuration
As I’m using KVM guests, and multicast is particularly troublesome over the bridged interface (and I didn’t want to hassle with smcroute, or similar, to set up multicast routing) I used unicast for cluster communications. A sample unicast configuration file is included at /etc/corosync/corosync.conf.example.udpu. I took that file and edited as follows:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# cp -p /etc/corosync/corosync.conf.example.udpu /etc/corosync/corosync.conf # vi /etc/corosync/corosync.conf compatibility: whitetank totem { version: 2 secauth: on interface { member { memberaddr: 192.168.122.30 } member { memberaddr: 192.168.122.31 } ringnumber: 0 bindnetaddr: 192.168.122.30 mcastport: 5405 ttl: 1 } transport: udpu } logging { fileline: off to_logfile: yes to_syslog: yes debug: on logfile: /var/log/cluster/corosync.log debug: off timestamp: on logger_subsys { subsys: AMF debug: off } } |
Next, I created a service definition for the Pacemaker resource manager that we’ll be using:
|
1 2 3 4 5 6 |
# vi /etc/corosync/service.d/pcmk service { # Load the Pacemaker Cluster Resource Manager name: pacemaker ver: 1 } |
As I’d changed secauth to on (it was off in the example), I then needed to create a key to encrypt cluster communications and distribute that around the cluster. On the first node:
|
1 |
# corosync-keygen |
This command will sit waiting for entropy to be generated. A quick way of doing this is to ssh in to the node via another session and run
|
1 |
# find / >/dev/null 2>&1 |
a few times. Once the key is generated, it’ll be available at /etc/corosync/authkey. Copy it to the other cluster node, and secure it:
|
1 2 |
# scp /etc/corosync/authkey root@rhcs-node02:/etc/corosync # chmod 400 /etc/corosync/authkey |
Now, we can start our bare-bones cluster:
|
1 2 |
# service corosync start # service pacemaker start |
And verify the status:
|
1 2 3 4 5 6 7 8 9 10 |
# crm status Last updated: Thu Jun 13 01:15:24 2013 Last change: Thu Jun 13 01:07:57 2013 via crmd on rhcs-node01.local Stack: classic openais (with plugin) Current DC: rhcs-node01.local - partition with quorum Version: 1.1.8-7.el6-394e906 2 Nodes configured, 2 expected votes 0 Resources configured. Online: [ rhcs-node01.local rhcs-node02.local ] |
Awesome. The cluster is running. A quick check via the crm shell shows that there isn’t much to the configuration at this point:
|
1 2 3 4 5 6 7 |
# crm configure show node rhcs-node01.local node rhcs-node02.local property $id="cib-bootstrap-options" \ dc-version="1.1.8-7.el6-394e906" \ cluster-infrastructure="classic openais (with plugin)" \ expected-quorum-votes="2" |
DRBD Configuration
The next piece of the puzzle is to configure the replicated storage volume. On both nodes, take a backup of /etc/drbd.conf (which itself includes files under /etc/drbd.d - I’ll just use /etc/drbd.conf) and edit as follows. As you can see, I have commented the configuration file directives very well, so will not explain them in this text apart from saying that the LVM logical volume at /dev/vg_data/lv_data_01 created earlier is used as our backing store device, and the new DRBD volume will be available at /dev/drbd0:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# cp -p /etc/drbd.conf /etc/drbd.conf-orig # vi /etc/drbd.conf # see http://www.drbd.org/users-guide/re-drbdconf.html global { # do not participate in online usage survey usage-count no; } resource data { # write IO is reported as completed if it has reached both local # and remote disk protocol C; net { # set up peer authentication cram-hmac-alg sha1; shared-secret "s3cr3tp@ss"; # default value 32 - increase as required max-buffers 512; # highest number of data blocks between two write barriers max-epoch-size 512; # size of the TCP socket send buffer - can tweak or set to 0 to # allow kernel to autotune sndbuf-size 0; } startup { # wait for connection timeout - boot process blocked # until DRBD resources are connected wfc-timeout 30; # WFC timeout if peer was outdated outdated-wfc-timeout 20; # WFC timeout if this node was in a degraded cluster (i.e. only had one # node left) degr-wfc-timeout 30; } disk { # the next two are for safety - detach on I/O error # and set up fencing - resource-only will attempt to # reach the other node and fence via the fence-peer # handler on-io-error detach; fencing resource-only; # no-disk-flushes; # if we had battery-backed RAID # no-md-flushes; # if we had battery-backed RAID # ramp up the resync rate resync-rate 10M; } handlers { # specify the two fencing handlers # see: http://www.drbd.org/users-guide-8.4/s-pacemaker-fencing.html fence-peer "/usr/lib/drbd/crm-fence-peer.sh"; after-resync-target "/usr/lib/drbd/crm-unfence-peer.sh"; } # first node on rhcs-node01.local { # DRBD device device /dev/drbd0; # backing store device disk /dev/vg_data/lv_data_01; # IP address of node, and port to listen on address 192.168.122.30:7789; # use internal meta data (don't create a filesystem before # you create metadata!) meta-disk internal; } # second node on rhcs-node02.local { # DRBD debice device /dev/drbd0; # backing store device disk /dev/vg_data/lv_data_01; # IP address of node, and port to listen on address 192.168.122.31:7789; # use internal meta data (don't create a filesystem before # you create metadata!) meta-disk internal; } } |
On both nodes, create the DRBD metadata:
|
1 |
# drbdadm create-md data |
You can use a separate volume for metadata, but if you’ve not yet created a filesystem you can use internal metadata as is the case above. You may also opt to use a separate volume for metadata for performance reasons. You can read the DRBD Internals page for more information on that. For most simple applications, internal metadata will suffice.
Start the DRBD service on both nodes (remembering that we will be shutting it down again as it will be cluster managed):
|
1 |
# service drbd start |
On the MASTER NODE only, promote the volume to the primary role:
|
1 |
# drbdadm primary --force data |
Note that in DRBD versions <8.4 (e.g. 8.3) you will need to use a different command:
|
1 |
# drbdadm -- --overwrite-data-of-peer primary data |
You can cat /proc/drbd to watch the synchronisation operation in progress:
|
1 2 3 4 5 |
# cat /proc/drbd version: 8.4.2 (api:1/proto:86-101) GIT-hash: 7ad5f850d711223713d6dcadc3dd48860321070c build by dag@Build64R6, 2012-09-06 08:16:10 0: cs:Connected ro:Primary/Secondary ds:UpToDate/UpToDate C r----- ns:1048508 nr:0 dw:0 dr:1049172 al:0 bm:64 lo:0 pe:0 ua:0 ap:0 ep:1 wo:f oos:0 |
Or, run service drbd status:
|
1 2 3 4 5 6 |
# service drbd status drbd driver loaded OK; device status: version: 8.4.2 (api:1/proto:86-101) GIT-hash: 7ad5f850d711223713d6dcadc3dd48860321070c build by dag@Build64R6, 2012-09-06 08:16:10 m:res cs ro ds p mounted fstype 0:data Connected Primary/Secondary UpToDate/UpToDate C |
On both nodes, create the mountpoint for the DRBD-replicated volume, which in our case will be /data. It is needless to say (but I’ll say it anyway) that this volume will be only ever mounted from a single node at a time.
|
1 |
# mkdir /data |
From the MASTER NODE, create an ext4 filesystem and mount/umount the new filesystem to test. Ensure that you use /dev/drbd0 - i.e. the DRBD device - not the backing store device:
|
1 2 3 |
# mkfs.ext4 /dev/drbd0 # mount /dev/drbd0 /data # umount /data |
Now, on both nodes, stop DRBD:
|
1 |
# service drbd stop |
We have our storage. Next, the murky world of the cluster resource manager.
Cluster Resource Configuration
Whilst there is lots of documentation available (both in articles like mine, and the excellent (and definitive) documentation at clusterlabs.org such as Clusters from Scratch), the CRM shell can still be a daunting place to be. Whilst there are GUI cluster resource configuration tools available, I much prefer to use the CRM shell due to the flexibility it offers. It is worth taking some time to learn the shell - you will be rewarded for doing so as you will have full control over your cluster.
First off, as we’re only running a demo cluster and do not have any real fencing methods available to us, let’s disable STONITH:
|
1 |
# crm configure property stonith-enabled=false |
and as this is a two-node cluster, ignore a loss of quorum:
|
1 |
# crm configure property no-quorum-policy=ignore |
Let’s create our first resource - the failover IP address. Using the ocf:heartbeat:IPaddr2 resource agent:
|
1 |
# crm configure primitive failover_ip ocf:heartbeat:IPaddr2 params ip=192.168.122.33 cidr_netmask=32 op monitor interval=30s |
Here, you can see that the IP address resource (or primitive) of agent type ocf:heartbeat:IPaddr2 has been created with a name of failover_ip. There are parameters passed (an ip of 192.168.122.33 and a cidr_netmask of 32) and operations (here, a monitor of interval 20s). If you are ever unsure of which parameters and operations a resource agent supports, you can view extensive documentation via the crm ra meta <ra-name> command:
|
1 |
# crm ra meta ocf:heartbeat:IPaddr2 |
Once the failover_ip resource has been created, you can view the status of it with crm resource status:
|
1 2 |
# crm resource status failover_ip resource failover_ip is running on: rhcs-node01.local |
Running ip addr will also show you that the new IP address is online:
|
1 2 |
# ip addr | grep '\.33' inet 192.168.122.33/32 brd 192.168.122.33 scope global eth0 |
You can now use crm move resource <resource> to move the resource to the second node:
|
1 2 3 4 5 |
# crm resource move failover_ip WARNING: Creating rsc_location constraint 'cli-standby-failover_ip' with a score of -INFINITY for resource failover_ip on rhcs-node01.local. This will prevent failover_ip from running on rhcs-node01.local until the constraint is removed using the 'crm_resource -U' command or manually with cibadmin This will be the case even if rhcs-node01.local is the last node in the cluster This message can be disabled with -Q |
Oh noes! What does this mean? Essentially - you, the sysadmin, know what you are doing, and have manually forced a move of a cluster-managed resource. The cluster manager therefore deems the current node unfit for purpose and creates a location constraint with a score of -INFINITY (negative-infinity) - the resource will never run there again. You can remove the constraint via the crm shell, or just fail it back with crm unmove resource <resource> which will also deem the node fit for purpose once again, and remove the constraint:
|
1 |
# crm unmove resource failover_ip |
You can also test failover by making the current node standby:
|
1 |
# crm node standby |
Remembering to bring it online again once done:
|
1 |
# crm node online |
You can view the current status of the cluster and any resources with crm status, or crm_mon -1 which will do the same thing - more on that later:
|
1 2 |
# crm status # crm_mon -1 |
You now have the first clustered resource working, and failing over as needed - the “floating” IP address. Next, nginx.
nginx Installation and Configuration
At this point, I’ll install nginx as it’ll be the next clustered resource to be configured:
|
1 2 |
# yum install -y http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm # yum install -y nginx |
Again, ensure that it doesn’t start automatically on boot, as the cluster will manage the nginx service:
|
1 |
# chkconfig nginx off |
Reading the output of crm ra meta nginx, we learn that the agent works by polling /nginx_status. This is not present in the configuration provided by the nginx RPMs just installed, so we will need to add it manually. I configured it as per the HttpStubStatusModule documentation, with appropriate ACLs:
|
1 2 3 4 5 6 7 8 9 |
# vi /etc/nginx/conf.d/default.conf ... location /nginx_status { stub_status on; access_log off; allow 127.0.0.1; allow 192.168.122.0/24; deny all; ... |
Once done, start nginx and test that this URL is reachable:
|
1 2 3 |
# service nginx start # curl http://localhost/nginx_status # service nginx stop |
Next, configure the nginx resource via the crm shell:
|
1 2 3 4 |
# crm configure primitive nginx_res ocf:heartbeat:nginx params configfile=/etc/nginx/nginx.conf httpd=/usr/sbin/nginx op monitor interval=60s timeout=10s op start timeout=40s op stop timeout=60s WARNING: nginx_res: specified timeout 10s for monitor is smaller than the advised 30s # crm configure colocation nginx_ip_colo INFINITY: nginx_res failover_ip # crm configure order nginx_after_ip mandatory: failover_ip nginx_res |
The WARNING I’ll ignore - a 10s timeout for a response from /nginx_status is still quite conservative. Quite a bit happened above. First, I configured another primitive (i.e. resource) called nginx_res, using the ocf:heartbeat:nginx resource agent. I specified a path to the configfile and the nginx binary (httpd=) and then specified various operations to define monitor, start and stop timeouts.
Next, I created a colocation called nginx_ip_colo. Colocations tell the cluster that resources need to be colocated on the same node. An affinity of INFINITY is specified - i.e. nginx_res and failover_ip MUST be on the same node, no exceptions. -INFINITY (i.e. negative infinity) would have the two resources repel and run on different nodes, no exceptions - not what we want.
Finally a mandatory (does the same thing as INFINITY) order directive, nginx_after_ip, was created, forcing nginx_res to start after failover_ip.
crm configure show now shows the current status of our cluster configuration:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# crm configure show node rhcs-node01.local \ attributes standby="off" node rhcs-node02.local \ attributes standby="off" primitive failover_ip ocf:heartbeat:IPaddr2 \ params ip="192.168.122.33" cidr_netmask="32" \ op monitor interval="30s" primitive nginx_res ocf:heartbeat:nginx \ params configfile="/etc/nginx/nginx.conf" httpd="/usr/sbin/nginx" \ op monitor interval="60s" timeout="10s" \ op start interval="0" timeout="40s" \ op stop interval="0" timeout="60s" colocation nginx_ip_colo inf: nginx_res failover_ip order nginx_after_ip inf: failover_ip nginx_res property $id="cib-bootstrap-options" \ dc-version="1.1.8-7.el6-394e906" \ cluster-infrastructure="classic openais (with plugin)" \ expected-quorum-votes="2" \ stonith-enabled="false" \ no-quorum-policy="ignore" |
A “one-shot” view of our cluster status with crm_mon -1 shows the cluster operating as expected, with the failover_ip and nginx_res resources being served from the same node (rhcs-node02.local):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# crm_mon -1 Last updated: Thu Jun 13 14:58:55 2013 Last change: Thu Jun 13 14:57:09 2013 via cibadmin on rhcs-node01.local Stack: classic openais (with plugin) Current DC: rhcs-node01.local - partition with quorum Version: 1.1.8-7.el6-394e906 2 Nodes configured, 2 expected votes 2 Resources configured. Online: [ rhcs-node01.local rhcs-node02.local ] failover_ip (ocf::heartbeat:IPaddr2): Started rhcs-node02.local nginx_res (ocf::heartbeat:nginx): Started rhcs-node02.local |
We can again move the resources around with crm move/unmove resource:
|
1 2 3 4 5 6 |
# crm resource move nginx_res WARNING: Creating rsc_location constraint 'cli-standby-nginx_res' with a score of -INFINITY for resource nginx_res on rhcs-node02.local. This will prevent nginx_res from running on rhcs-node02.local until the constraint is removed using the 'crm_resource -U' command or manually with cibadmin This will be the case even if rhcs-node02.local is the last node in the cluster This message can be disabled with -Q # crm resource unmove nginx_res |
Next, DRBD integration into the cluster.
DRBD Cluster Configuration
All this time, we’ve been issuing commands and making direct changes to the cluster configuration, e.g. crm configure <some-configuration> - the changes are applied immediately. However, if we try to configure the multi-part DRBD resources like this, some of the configuration will be applied before the entire resource has been configured, which will not give us the desired result. Therefore, I’ll take a copy of the current CIB configuration and make changes there on a “shadow instance”, verify, then commit back into the main cluster configuration. This is a much safer way of working, and should be used when making any significant change on a Production cluster.
Let’s start by creating the shadow configuration instance:
|
1 2 3 4 |
# crm crm(live)# cib new drbd INFO: drbd shadow CIB created crm(drbd)# |
You can see that the crm prompt has changed from crm(live) to crm(drbd) indicating that we are working with the shadow instance.
Next, I’ll create the DRBD primitive and a new type of resource - ms:
|
1 2 |
crm(drbd)# configure primitive drbd_res ocf:linbit:drbd params drbd_resource=data op monitor interval=29s role=Master op monitor interval=31s role=Slave crm(drbd)# configure ms drbd_master_slave drbd_res meta master-max=1 master-node-max=1 clone-max=2 clone-node-max=1 notify=true |
Again, much has happened. The drbd_res primitive has been created of type ocf:linbit:drbd, with the drbd_resource parameter set to data - the same resource name we defined for our DRBD replicated resource in /etc/drbd.conf. Then, the monitor intervals for both master and slave are defined. Next, the drbd_master_slave ms resource is created - ms resources are used where master/slave relationships are required. Meta data is passed during the creation of this resource ensuring that we only ever have a single master online at any one time. These parameters are taken from the DRBD on Pacemaker documentation.
Run a configure show and verify that all is well - if you’re happy, commit your changes to the live cluster configuration:
|
1 2 3 |
crm(drbd)# crm configure crm(drbd)# cib commit drbd crm(live)# quit |
Next, another shadow instance will be created to configure the filesystem resource that runs atop our DRBD resource:
|
1 2 |
# crm crm(live)# cib new fs |
Create the filesystem resource and appropriate colocation and ordering definitions:
|
1 2 3 4 5 6 7 |
crm(fs)# configure primitive fs_res ocf:heartbeat:Filesystem params device=/dev/drbd0 directory=/data fstype=ext4 crm(fs)# configure colocation fs_drbd_colo INFINITY: fs_res drbd_master_slave:Master crm(fs)# configure order fs_after_drbd mandatory: drbd_master_slave:promote fs_res:start crm(fs)# configure colocation nginx_fs_colo inf: nginx_res fs_res crm(fs)# configure order nginx_after_fs inf: fs_res nginx_res crm(fs)# configure show crm(fs)# cib commit fs |
So - another primitive, fs_res, of type ocf:heartbeat:Filesystem is created on device /dev/drbd0, to be mounted on directory /data, of fstype ext4.
Colocations are then created so that the fs_res is colocated on whichever node is running drbd_master_slave:Master, and that nginx_res runs wherever fs_res is running. Ordering is configured so that drbd_master_slave:promote occurs before fs_res:start (i.e. the DRBD resource is promoted to master before the filesystem resource is started and any mount occurs), and that nginx_res is only started after fs_res is online. The configuration is then checked, and committed. I’ve substituted “INFINITY:“, “inf:” and “mandatory:” above to show that they all do exactly the same thing, and as you’ll see below, all get converted to “inf:” in the committed configuration.
If this were a production cluster, I’d just need to configure STONITH here as an additional step. However, for our purposes, the cluster configuration is now complete - and we have a replicated storage volume that is promoted/demoted appropriately across the master/slave, and a failover nginx instance. One final step is to have nginx serve files from the shared storage.
Final Steps
Let’s take a look at the completed cluster configuration:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# crm configure show node rhcs-node01.local \ attributes standby="off" node rhcs-node02.local \ attributes standby="off" primitive drbd_res ocf:linbit:drbd \ params drbd_resource="data" \ op monitor interval="29s" role="Master" \ op monitor interval="31s" role="Slave" primitive failover_ip ocf:heartbeat:IPaddr2 \ params ip="192.168.122.33" cidr_netmask="32" \ op monitor interval="30s" primitive fs_res ocf:heartbeat:Filesystem \ params device="/dev/drbd0" directory="/data" fstype="ext4" primitive nginx_res ocf:heartbeat:nginx \ params configfile="/etc/nginx/nginx.conf" httpd="/usr/sbin/nginx" \ op monitor interval="60s" timeout="10s" \ op start interval="0" timeout="40s" \ op stop interval="0" timeout="60s" ms drbd_master_slave drbd_res \ meta master-max="1" master-node-max="1" clone-max="2" clone-node-max="1" notify="true" colocation fs_drbd_colo inf: fs_res drbd_master_slave:Master colocation nginx_fs_colo inf: nginx_res fs_res colocation nginx_ip_colo inf: nginx_res failover_ip order fs_after_drbd inf: drbd_master_slave:promote fs_res:start order nginx_after_fs inf: fs_res nginx_res order nginx_after_ip inf: failover_ip nginx_res property $id="cib-bootstrap-options" \ dc-version="1.1.8-7.el6-394e906" \ cluster-infrastructure="classic openais (with plugin)" \ expected-quorum-votes="2" \ stonith-enabled="false" \ no-quorum-policy="ignore" |
As you can see, it’s not really that daunting at all once you’ve worked through the confirguation steps.
Let’s move the nginx document root onto the replicated storage - make the following configuration change on both nodes:
|
1 2 3 4 5 6 |
# vi /etc/nginx/conf.d/default.conf location / { root /data/htdocs; index index.html index.htm; } |
and populate index.html from the master node:
|
1 |
# echo "On shared storage..." > /data/htdocs/index.html |
Fail the services over to the other node, and ensure that the new content is visible. The best way to do this at this point is by taking the current master node offline, then bringing online again (including an appropriate sleep so that the failure is detected). If you leave the node offline, the DRBD slave will not run. As no preference configuration has taken place, the services will not automatically fail back when the node is brought back online. Or, of course, just reboot the master node and ensure failover occurs as expected:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# crm_mon -1 Last updated: Thu Jun 13 17:44:19 2013 Last change: Thu Jun 13 17:43:16 2013 via cibadmin on rhcs-node01.local Stack: classic openais (with plugin) Current DC: rhcs-node02.local - partition with quorum Version: 1.1.8-7.el6-394e906 2 Nodes configured, 2 expected votes 5 Resources configured. Online: [ rhcs-node01.local rhcs-node02.local ] failover_ip (ocf::heartbeat:IPaddr2): Started rhcs-node02.local nginx_res (ocf::heartbeat:nginx): Started rhcs-node02.local Master/Slave Set: drbd_master_slave [drbd_res] Masters: [ rhcs-node02.local ] Slaves: [ rhcs-node01.local ] fs_res (ocf::heartbeat:Filesystem): Started rhcs-node02.local # curl http://192.168.122.33 On shared storage... # reboot -f # crm_mon -1 Last updated: Thu Jun 13 17:45:07 2013 Last change: Thu Jun 13 17:45:00 2013 via cibadmin on rhcs-node01.local Stack: classic openais (with plugin) Current DC: rhcs-node01.local - partition WITHOUT quorum Version: 1.1.8-7.el6-394e906 2 Nodes configured, 2 expected votes 5 Resources configured. Online: [ rhcs-node01.local ] OFFLINE: [ rhcs-node02.local ] failover_ip (ocf::heartbeat:IPaddr2): Started rhcs-node01.local nginx_res (ocf::heartbeat:nginx): Started rhcs-node01.local Master/Slave Set: drbd_master_slave [drbd_res] Masters: [ rhcs-node01.local ] Stopped: [ drbd_res:1 ] fs_res (ocf::heartbeat:Filesystem): Started rhcs-node01.local # curl http://192.168.122.33 On shared storage... |
Excellent - failover works correctly, and the content is correctly served from the replicated volume.
Conclusion
This has been a bit of a whirlwind tour of a complicated stack of software. A very viable alternative to “enterprise” cluster solutions such as RHCS or VCS, this approach can be adopted to cluster a wide range of applications.
There is a vast amount of documentation available, and a very active user base (i.e. mailing lists, etc.). If you need to implement a high-availability solution on Linux, then this is probably for you. I’ve never been much of a fan of RHCS, although in my opinion it still doesn’t come close to the ease of administration and general rock-solid stability of Solaris Cluster.
One place I’ve used this is a highly-available MySQL master server, with several standalone replicated slaves.