6. Coding Particular Pacemaker Components
The Pacemaker code can be intricate and difficult to follow. This chapter has some high-level descriptions of how individual components work.
6.1. Controller
pacemaker-controld
is the Pacemaker daemon that utilizes the other daemons
to orchestrate actions that need to be taken in the cluster. It receives CIB
change notifications from the CIB manager, passes the new CIB to the scheduler
to determine whether anything needs to be done, uses the executor and fencer to
execute any actions required, and sets failure counts (among other things) via
the attribute manager.
As might be expected, it has the most code of any of the daemons.
6.1.1. Join sequence
Most daemons track their cluster peers using Corosync’s membership and CPG only. The controller additionally requires peers to join, which ensures they are ready to be assigned tasks. Joining proceeds through a series of phases referred to as the join sequence or join process.
A node’s current join phase is tracked by the join
member of crm_node_t
(used in the peer cache). It is an enum crm_join_phase
that (ideally)
progresses from the DC’s point of view as follows:
The node initially starts at
crm_join_none
The DC sends the node a join offer (
CRM_OP_JOIN_OFFER
), and the node proceeds tocrm_join_welcomed
. This can happen in three ways:The joining node will send a join announce (
CRM_OP_JOIN_ANNOUNCE
) at its controller startup, and the DC will reply to that with a join offer.When the DC’s peer status callback notices that the node has joined the messaging layer, it registers
I_NODE_JOIN
(which leads toA_DC_JOIN_OFFER_ONE
->do_dc_join_offer_one()
->join_make_offer()
).After certain events (notably a new DC being elected), the DC will send all nodes join offers (via A_DC_JOIN_OFFER_ALL ->
do_dc_join_offer_all()
).
These can overlap. The DC can send a join offer and the node can send a join announce at nearly the same time, so the node responds to the original join offer while the DC responds to the join announce with a new join offer. The situation resolves itself after looping a bit.
The node responds to join offers with a join request (
CRM_OP_JOIN_REQUEST
, viado_cl_join_offer_respond()
andjoin_query_callback()
). When the DC receives the request, the node proceeds tocrm_join_integrated
(viado_dc_join_filter_offer()
).As each node is integrated, the current best CIB is sync’ed to each integrated node via
do_dc_join_finalize()
. As each integrated node’s CIB sync succeeds, the DC acks the node’s join request (CRM_OP_JOIN_ACKNAK
) and the node proceeds tocrm_join_finalized
(viafinalize_sync_callback()
+finalize_join_for()
).Each node confirms the finalization ack (
CRM_OP_JOIN_CONFIRM
viado_cl_join_finalize_respond()
), including its current resource operation history (viacontrold_query_executor_state()
). Once the DC receives this confirmation, the node proceeds tocrm_join_confirmed
viado_dc_join_ack()
.
Once all nodes are confirmed, the DC calls do_dc_join_final()
, which checks
for quorum and responds appropriately.
When peers are lost, their join phase is reset to none (in various places).
crm_update_peer_join()
updates a node’s join phase.
The DC increments the global current_join_id
for each joining round, and
rejects any (older) replies that don’t match.
6.2. Fencer
pacemaker-fenced
is the Pacemaker daemon that handles fencing requests. In
the broadest terms, fencing works like this:
The initiator (an external program such as
stonith_admin
, or the cluster itself via the controller) asks the local fencer, “Hey, could you please fence this node?”The local fencer asks all the fencers in the cluster (including itself), “Hey, what fencing devices do you have access to that can fence this node?”
Each fencer in the cluster replies with a list of available devices that it knows about.
Once the original fencer gets all the replies, it asks the most appropriate fencer peer to actually carry out the fencing. It may send out more than one such request if the target node must be fenced with multiple devices.
The chosen fencer(s) call the appropriate fencing resource agent(s) to do the fencing, then reply to the original fencer with the result.
The original fencer broadcasts the result to all fencers.
Each fencer sends the result to each of its local clients (including, at some point, the initiator).
A more detailed description follows.
6.2.1. Initiating a fencing request
A fencing request can be initiated by the cluster or externally, using the libstonithd API.
The cluster always initiates fencing via
daemons/controld/controld_fencing.c:te_fence_node()
(which calls thefence()
API method). This occurs when a transition graph synapse contains aCRM_OP_FENCE
XML operation.The main external clients are
stonith_admin
andcts-fence-helper
. TheDLM
project also uses Pacemaker for fencing.
Highlights of the fencing API:
stonith_api_new()
creates and returns a newstonith_t
object, whosecmds
member has methods for connect, disconnect, fence, etc.the
fence()
method creates and sends aSTONITH_OP_FENCE XML
request with the desired action and target node. Callers do not have to choose or even have any knowledge about particular fencing devices.
6.2.2. Fencing queries
The function calls for a fencing request go something like this:
The local fencer receives the client’s request via an IPC or messaging layer callback, which calls
stonith_command()
, which (for requests) callshandle_request()
, which (forSTONITH_OP_FENCE
from a client) callsinitiate_remote_stonith_op()
, which creates aSTONITH_OP_QUERY
XML request with the target, desired action, timeout, etc. then broadcasts the operation to the cluster group (i.e. all fencer instances) and starts a timer. The query is broadcast because (1) location constraints might prevent the local node from accessing the stonith device directly, and (2) even if the local node does have direct access, another node might be preferred to carry out the fencing.
Each fencer receives the original fencer’s STONITH_OP_QUERY
broadcast
request via IPC or messaging layer callback, which calls:
stonith_command()
, which (for requests) callshandle_request()
, which (forSTONITH_OP_QUERY
from a peer) calls
stonith_query()
, which callsget_capable_devices()
withstonith_query_capable_device_cb()
to add device information to an XML reply and send it. (A message is considered a reply if it containsT_STONITH_REPLY
, which is only set by fencer peers, not clients.)
The original fencer receives all peers’ STONITH_OP_QUERY
replies via IPC
or messaging layer callback, which calls:
stonith_command()
, which (for replies) callshandle_reply()
which (forSTONITH_OP_QUERY
) callsprocess_remote_stonith_query()
, which allocates a new query result structure, parses device information into it, and adds it to the operation object. It increments the number of replies received for this operation, and compares it against the expected number of replies (i.e. the number of active peers), and if this is the last expected reply, callsrequest_peer_fencing()
, which calculates the timeout and sendsSTONITH_OP_FENCE
request(s) to carry out the fencing. If the target node has a fencing “topology” (which allows specifications such as “this node can be fenced either with device A, or devices B and C in combination”), it will choose the device(s), and send out as many requests as needed. If it chooses a device, it will choose the peer; a peer is preferred if it has “verified” access to the desired device, meaning that it has the device “running” on it and thus has a monitor operation ensuring reachability.
6.2.3. Fencing operations
Each STONITH_OP_FENCE
request goes something like this:
The chosen peer fencer receives the STONITH_OP_FENCE
request via
IPC or messaging layer callback, which calls:
stonith_command()
, which (for requests) callshandle_request()
, which (forSTONITH_OP_FENCE
from a peer) callsstonith_fence()
, which callsschedule_stonith_command()
(using supplied device ifF_STONITH_DEVICE
was set, otherwise the highest-priority capable device obtained viaget_capable_devices()
withstonith_fence_get_devices_cb()
), which adds the operation to the device’s pending operations list and triggers processing.
The chosen peer fencer’s mainloop is triggered and calls
stonith_device_dispatch()
, which callsstonith_device_execute()
, which pops off the next item from the device’s pending operations list. If acting as the (internally implemented) watchdog agent, it panics the node, otherwise it callsstonith_action_create()
andstonith_action_execute_async()
to call the fencing agent.
The chosen peer fencer’s mainloop is triggered again once the fencing agent returns, and calls
stonith_action_async_done()
which adds the results to an action object then calls itsdone callback (
st_child_done()
), which callsschedule_stonith_command()
for a new device if there are further required actions to execute or if the original action failed, then builds and sends an XML reply to the original fencer (viasend_async_reply()
), then checks whether any pending actions are the same as the one just executed and merges them if so.
6.2.4. Fencing replies
The original fencer receives the STONITH_OP_FENCE
reply via IPC or
messaging layer callback, which calls:
stonith_command()
, which (for replies) callshandle_reply()
, which callsfenced_process_fencing_reply()
, which calls eitherrequest_peer_fencing()
(to retry a failed operation, or try the next device in a topology if appropriate, which issues a newSTONITH_OP_FENCE
request, proceeding as before) orfinalize_op()
(if the operation is definitively failed or successful).finalize_op()
broadcasts the result to all peers.
Finally, all peers receive the broadcast result and call
finalize_op()
, which sends the result to all local clients.
6.2.5. Fencing History
The fencer keeps a running history of all fencing operations. The bulk of the relevant code is in fenced_history.c and ensures the history is synchronized across all nodes even if a node leaves and rejoins the cluster.
In libstonithd, this information is represented by stonith_history_t and is queryable by the stonith_api_operations_t:history() method. crm_mon and stonith_admin use this API to display the history.
6.3. Scheduler
pacemaker-schedulerd
is the Pacemaker daemon that runs the Pacemaker
scheduler for the controller, but “the scheduler” in general refers to related
library code in libpe_status
and libpe_rules
(lib/pengine/*.c
), and
some of libpacemaker
(lib/pacemaker/pcmk_sched_*.c
).
The purpose of the scheduler is to take a CIB as input and generate a transition graph (list of actions that need to be taken) as output.
The controller invokes the scheduler by contacting the scheduler daemon via
local IPC. Tools such as crm_simulate
, crm_mon
, and
crm_resource
can also invoke the scheduler, but do so by calling the
library functions directly. This allows them to run using a CIB_file
without the cluster needing to be active.
The main entry point for the scheduler code is
lib/pacemaker/pcmk_scheduler.c:pcmk__schedule_actions()
. It sets
defaults and calls a series of functions for the scheduling. Some key steps:
unpack_cib()
parses most of the CIB XML into data structures, and determines the current cluster status.apply_node_criteria()
applies factors that make resources prefer certain nodes, such as shutdown locks, location constraints, and stickiness.pcmk__create_internal_constraints()
creates internal constraints, such as the implicit ordering for group members, or start actions being implicitly ordered before promote actions.pcmk__handle_rsc_config_changes()
processes resource history entries in the CIB status section. This is used to decide whether certain actions need to be done, such as deleting orphan resources, forcing a restart when a resource definition changes, etc.assign_resources()
assigns resources to nodes.schedule_resource_actions()
schedules resource-specific actions (which might or might not end up in the final graph).pcmk__apply_orderings()
processes ordering constraints in order to modify action attributes such as optional or required.pcmk__create_graph()
creates the transition graph.
6.3.1. Challenges
Working with the scheduler is difficult. Challenges include:
It is far too much code to keep more than a small portion in your head at one time.
Small changes can have large (and unexpected) effects. This is why we have a large number of regression tests (
cts/cts-scheduler
), which should be run after making code changes.It produces an insane amount of log messages at debug and trace levels. You can put resource ID(s) in the
PCMK_trace_tags
environment variable to enable trace-level messages only when related to specific resources.Different parts of the main
pcmk_scheduler_t
structure are finalized at different points in the scheduling process, so you have to keep in mind whether information you’re using at one point of the code can possibly change later. For example, data unpacked from the CIB can safely be used anytime afterunpack_cib(),
but actions may become optional or required anytime beforepcmk__create_graph()
. There’s no easy way to deal with this.Many names of struct members, functions, etc., are suboptimal, but are part of the public API and cannot be changed until an API backward compatibility break.
6.3.2. Cluster Working Set
The main data object for the scheduler is pcmk_scheduler_t
, which contains
all information needed about nodes, resources, constraints, etc., both as the
raw CIB XML and parsed into more usable data structures, plus the resulting
transition graph XML. The variable name is usually scheduler
.
6.3.3. Resources
pcmk_resource_t
is the data object representing cluster resources. A
resource has a variant: primitive, group, clone, or bundle.
The resource object has members for two sets of methods,
resource_object_functions_t
from the libpe_status
public API, and
resource_alloc_functions_t
whose implementation is internal to
libpacemaker
. The actual functions vary by variant.
The object functions have basic capabilities such as unpacking the resource XML, and determining the current or planned location of the resource.
The assignment functions have more obscure capabilities needed
for scheduling, such as processing location and ordering constraints. For
example, pcmk__create_internal_constraints()
simply calls the
internal_constraints()
method for each top-level resource in the cluster.
6.3.4. Nodes
Assignment of resources to nodes is done by choosing the node with the highest score for a given resource. The scheduler does a bunch of processing to generate the scores, then the actual assignment is straightforward.
Node lists are frequently used. For example, pcmk_scheduler_t
has a
nodes
member which is a list of all nodes in the cluster, and
pcmk_resource_t
has a running_on
member which is a list of all nodes on
which the resource is (or might be) active. These are lists of pcmk_node_t
objects.
The pcmk_node_t
object contains a struct pe_node_shared_s *details
member with all node information that is independent of resource assignment
(the node name, etc.).
The working set’s nodes
member contains the original of this information.
All other node lists contain copies of pcmk_node_t
where only the
details
member points to the originals in the working set’s nodes
list.
In this way, the other members of pcmk_node_t
(such as weight
, which is
the node score) may vary by node list, while the common details are shared.
6.3.5. Actions
pcmk_action_t
is the data object representing actions that might need to be
taken. These could be resource actions, cluster-wide actions such as fencing a
node, or “pseudo-actions” which are abstractions used as convenient points for
ordering other actions against.
It has a flags
member which is a bitmask of enum pe_action_flags
. The
most important of these are pe_action_runnable
(if not set, the action is
“blocked” and cannot be added to the transition graph) and
pe_action_optional
(actions with this set will not be added to the
transition graph; actions often start out as optional, and may become required
later).
6.3.6. Colocations
pcmk__colocation_t
is the data object representing colocations.
Colocation constraints come into play in these parts of the scheduler code:
When sorting resources for assignment, so resources with highest node score are assigned first (see
cmp_resources()
)When updating node scores for resource assigment or promotion priority
When assigning resources, so any resources to be colocated with can be assigned first, and so colocations affect where the resource is assigned
When choosing roles for promotable clone instances, so colocations involving a specific role can affect which instances are promoted
The resource assignment functions have several methods related to colocations:
apply_coloc_score():
This applies a colocation’s score to either the dependent’s allowed node scores (if called while resources are being assigned) or the dependent’s priority (if called while choosing promotable instance roles). It can behave differently depending on whether it is being called as the primary’s method or as the dependent’s method.add_colocated_node_scores():
This updates a table of nodes for a given colocation attribute and score. It goes through colocations involving a given resource, and updates the scores of the nodes in the table with the best scores of nodes that match up according to the colocation criteria.colocated_resources():
This generates a list of all resources involved in mandatory colocations (directly or indirectly via colocation chains) with a given resource.
6.3.7. Orderings
Ordering constraints are simple in concept, but they are one of the most important, powerful, and difficult to follow aspects of the scheduler code.
pe__ordering_t
is the data object representing an ordering, better thought
of as a relationship between two actions, since the relation can be more
complex than just “this one runs after that one”.
For an ordering “A then B”, the code generally refers to A as “first” or “before”, and B as “then” or “after”.
Much of the power comes from enum pe_ordering
, which are flags that
determine how an ordering behaves. There are many obscure flags with big
effects. A few examples:
pe_order_none
means the ordering is disabled and will be ignored. It’s 0, meaning no flags set, so it must be compared with equality rather thanpcmk_is_set()
.pe_order_optional
means the ordering does not make either action required, so it only applies if they both become required for other reasons.pe_order_implies_first
means that if action B becomes required for any reason, then action A will become required as well.