mirror of https://github.com/hashicorp/consul
Browse Source
* First commit * Fix navigation * Add some commands * Structure draft * Complete usage doc structure * Fix link * Apply suggestions from code review Co-authored-by: Aimee Ukasick <aimee.ukasick@hashicorp.com> * Apply suggestions from code review Co-authored-by: Jeff Boruszak <104028618+boruszak@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Jeff Boruszak <104028618+boruszak@users.noreply.github.com> * Apply suggestions from code review * Replace tutorial path * Apply suggestions from code review Co-authored-by: Jeff Boruszak <104028618+boruszak@users.noreply.github.com> --------- Co-authored-by: Aimee Ukasick <aimee.ukasick@hashicorp.com> Co-authored-by: Jeff Boruszak <104028618+boruszak@users.noreply.github.com> Co-authored-by: boruszak <jeffrey.boruszak@hashicorp.com>pull/21614/head
danielehc
3 months ago
committed by
GitHub
7 changed files with 413 additions and 8 deletions
@ -0,0 +1,396 @@
|
||||
--- |
||||
layout: docs |
||||
page_title: Application leader election |
||||
description: >- |
||||
Learn how to perform client-side leader elections using sessions and Consul key/value (KV) store. |
||||
--- |
||||
|
||||
# Application leader election |
||||
|
||||
This topic describes the process for building client-side leader elections for service instances using Consul's [session mechanism for building distributed locks](/consul/docs/dynamic-app-config/sessions) and the [Consul key/value store](/consul/docs/dynamic-app-config/kv), which is Consul's key/value datastore. |
||||
|
||||
This topic is not related to Consul's leader election. For more information about the Raft leader election used internally by Consul, refer to |
||||
[consensus protocol](/consul/docs/architecture/consensus) documentation. |
||||
|
||||
## Background |
||||
|
||||
Some distributed applications, like HDFS or ActiveMQ, require setting up one instance as a leader to ensure application data is current and stable. |
||||
|
||||
Consul's support for [sessions](/consul/docs/dynamic-app-config/sessions) and [watches](/consul/docs/dynamic-app-config/watches) allows you to build a client-side leader election process where clients use a lock on a key in the KV datastore to ensure mutual exclusion and to gracefully handle failures. |
||||
|
||||
All service instances that are participating should coordinate on a key format. We recommend the following pattern: |
||||
|
||||
```plaintext |
||||
service/<service name>/leader |
||||
``` |
||||
|
||||
## Requirements |
||||
|
||||
- A running Consul server |
||||
- A path in the Consul KV datastore to acquire locks and to store information about the leader. The instructions on this page use the following key: `service/leader`. |
||||
- If ACLs are enabled, a token with the following permissions: |
||||
- `session:write` permissions over the service session name |
||||
- `key:write` permissions over the key |
||||
- The `curl` command |
||||
|
||||
Expose the token using the `CONSUL_HTTP_TOKEN` environment variable. |
||||
|
||||
## Client-side leader election procedure |
||||
|
||||
The workflow for building a client-side leader election process has the following steps: |
||||
|
||||
- For each client trying to acquire the lock: |
||||
1. [Create a session](#create-a-new-session) associated with the client node. |
||||
1. [Acquire the lock](#acquire-the-lock) on the designated key in the KV store using the `acquire` parameter. |
||||
1. [Watch the KV key](#watch-the-kv-key-for-locks) to verify if the lock was released. If no lock is present, try to acquire a lock. |
||||
|
||||
- For the client that acquires the lock: |
||||
1. Periodically, [renew the session](#renew-a-session) to avoid expiration. |
||||
1. Optionally, [release the lock](#release-a-lock). |
||||
|
||||
- For other services: |
||||
1. [Watch the KV key](#watch-the-kv-key-for-locks) to verify there is at least one process holding the lock. |
||||
1. Use the values written under the KV path to identify the leader and update configurations accordingly. |
||||
|
||||
## Create a new session |
||||
|
||||
Create a configuration for the session. |
||||
The minimum viable configuration requires that you specify the session name. The following example demonstrates this configuration. |
||||
|
||||
<CodeBlockConfig hideClipboard> |
||||
|
||||
```json |
||||
{ |
||||
"Name": "session_name" |
||||
} |
||||
``` |
||||
|
||||
</CodeBlockConfig> |
||||
|
||||
Create a session using the [`/session` Consul HTTP API](/consul/api-docs/session) endpoint. In the following example, the node's `hostname` is the session name. |
||||
|
||||
```shell-session |
||||
$ curl --silent \ |
||||
--header "X-Consul-Token: $CONSUL_HTTP_TOKEN" \ |
||||
--data '{"Name": "'`hostname`'"}' \ |
||||
--request PUT \ |
||||
http://127.0.0.1:8500/v1/session/create | jq |
||||
``` |
||||
|
||||
|
||||
The command returns a JSON object containing the ID of the newly created session. |
||||
|
||||
```json |
||||
{ |
||||
"ID": "d21d60ad-c2d2-b32a-7432-ca4048a4a7d6" |
||||
} |
||||
``` |
||||
|
||||
### Verify session |
||||
|
||||
Use the `/v1/session/list` endpoint to retrieve existing sessions. |
||||
|
||||
```shell-session |
||||
$ curl --silent \ |
||||
--header "X-Consul-Token: $CONSUL_HTTP_TOKEN" \ |
||||
--request GET \ |
||||
http://127.0.0.1:8500/v1/session/list | jq |
||||
``` |
||||
|
||||
The command returns a JSON array containing all available sessions in the system. |
||||
|
||||
<CodeBlockConfig hideClipboard> |
||||
|
||||
```json |
||||
[ |
||||
{ |
||||
"ID": "d21d60ad-c2d2-b32a-7432-ca4048a4a7d6", |
||||
"Name": "hashicups-db-0", |
||||
"Node": "hashicups-db-0", |
||||
"LockDelay": 15000000000, |
||||
"Behavior": "release", |
||||
"TTL": "", |
||||
"NodeChecks": [ |
||||
"serfHealth" |
||||
], |
||||
"ServiceChecks": null, |
||||
"CreateIndex": 11956, |
||||
"ModifyIndex": 11956 |
||||
} |
||||
] |
||||
``` |
||||
|
||||
</CodeBlockConfig> |
||||
|
||||
You can verify from the output that the session is associated with the `hashicups-db-0` node, which is the client agent where the API request was made. |
||||
|
||||
With the exception of the `Name`, all parameters are set to their default values. The session is created without a `TTL` value, which means that it never expires and requires you to delete it explicitly. |
||||
|
||||
Depending on your needs you can create sessions specifying more parameters such as: |
||||
|
||||
- `TTL` - If provided, the session is invalidated and deleted if it is not renewed before the TTL expires. |
||||
- `ServiceChecks` - Specifies a list of service checks to monitor. The session is invalidated if the checks return a critical state. |
||||
|
||||
By setting these extra parameters, you can create a client-side leader election workflow that automatically releases the lock after a specified amount of time since the last renew, or that automatically releases locks when the service holding them fails. |
||||
|
||||
For a full list of parameters available refer to the [`/session/create` endpoint documentation](/consul/api-docs/session#create-session). |
||||
|
||||
## Acquire the lock |
||||
|
||||
Create the data object to associate to the lock request. |
||||
|
||||
The data of the request should be a JSON object representing the local instance. This value is opaque to Consul, but it should contain whatever information clients require to communicate with your application. For example, it could be a JSON object that contains the node's name and the application's port. |
||||
|
||||
<CodeBlockConfig hideClipboard> |
||||
|
||||
```json |
||||
{ |
||||
"Node": "node-name", |
||||
"Port": "8080" |
||||
} |
||||
``` |
||||
|
||||
</CodeBlockConfig> |
||||
|
||||
<Tabs> |
||||
<Tab heading="API" group="api"> |
||||
|
||||
Acquire a lock for a given key using the PUT method on a [KV entry](/consul/api-docs/kv) with the |
||||
`?acquire=<session>` query parameter. |
||||
|
||||
```shell-session |
||||
$ curl --silent \ |
||||
--header "X-Consul-Token: $CONSUL_HTTP_TOKEN" \ |
||||
--data '{"Node": "'`hostname`'"}' \ |
||||
--request PUT \ |
||||
http://localhost:8500/v1/kv/service/leader?acquire=d21d60ad-c2d2-b32a-7432-ca4048a4a7d6 | jq |
||||
``` |
||||
|
||||
This request returns either `true` or `false`. If `true`, the lock was acquired and |
||||
the local service instance is now the leader. If `false`, a different node acquired |
||||
the lock. |
||||
|
||||
|
||||
</Tab> |
||||
<Tab heading="CLI" group="cli"> |
||||
|
||||
```shell-session |
||||
$ consul kv put -acquire -session=d21d60ad-c2d2-b32a-7432-ca4048a4a7d6 /service/leader '{"Node": "'`hostname`'"}' |
||||
``` |
||||
|
||||
In case of success, the command exits with exit code `0` and outputs the following message. |
||||
|
||||
<CodeBlockConfig hideClipboard> |
||||
|
||||
```plaintext |
||||
Success! Lock acquired on: service/leader |
||||
``` |
||||
|
||||
</CodeBlockConfig> |
||||
|
||||
If the lock was already acquired by another node, the command exits with exit code `1` and outputs the following message. |
||||
|
||||
<CodeBlockConfig hideClipboard> |
||||
|
||||
```plaintext |
||||
Error! Did not acquire lock |
||||
``` |
||||
|
||||
</CodeBlockConfig> |
||||
|
||||
</Tab> |
||||
</Tabs> |
||||
|
||||
This example used the node's `hostname` as the key data. This data can be used by the other services to create configuration files. |
||||
|
||||
Be aware that this locking system has no enforcement mechanism that requires clients to acquire a lock before they perform an operation. Any client can read, write, and delete a key without owning the corresponding lock. |
||||
|
||||
## Watch the KV key for locks |
||||
|
||||
Existing locks need to be monitored by all nodes involved in the client-side leader elections, as well as by the other nodes that need to know the identity of the leader. |
||||
|
||||
- Lock holders need to monitor the lock because the session might get invalidated by an operator. |
||||
- Other services that want to acquire the lock need to monitor it to check if the lock is released so they can try acquire the lock. |
||||
- Other nodes need to monitor the lock to see if the value of the key changed and update their configuration accordingly. |
||||
|
||||
<Tabs> |
||||
<Tab heading="API" group="api"> |
||||
|
||||
Monitor the lock using the GET method on a [KV entry](/consul/api-docs/kv) with the blocking query enabled. |
||||
|
||||
First, verify the latest index for the current value. |
||||
|
||||
```shell-session |
||||
$ curl --silent \ |
||||
--header "X-Consul-Token: $CONSUL_HTTP_TOKEN" \ |
||||
--request GET \ |
||||
http://127.0.0.1:8500/v1/kv/service/leader?index=1 | jq |
||||
``` |
||||
|
||||
The command outputs the key data, including the `ModifyIndex` for the object. |
||||
|
||||
<CodeBlockConfig hideClipboard highlight="9"> |
||||
|
||||
```json |
||||
[ |
||||
{ |
||||
"LockIndex": 0, |
||||
"Key": "service/leader", |
||||
"Flags": 0, |
||||
"Value": "eyJOb2RlIjogImhhc2hpY3Vwcy1kYi0wIn0=", |
||||
"Session": "d21d60ad-c2d2-b32a-7432-ca4048a4a7d6", |
||||
"CreateIndex": 12399, |
||||
"ModifyIndex": 13061 |
||||
} |
||||
] |
||||
``` |
||||
|
||||
</CodeBlockConfig> |
||||
|
||||
Using the value of the `ModifyIndex`, run a [blocking query](/consul/api-docs/features/blocking) against the lock. |
||||
|
||||
```shell-session |
||||
$ curl --silent \ |
||||
--header "X-Consul-Token: $CONSUL_HTTP_TOKEN" \ |
||||
--request GET \ |
||||
http://127.0.0.1:8500/v1/kv/service/leader?index=13061 | jq |
||||
``` |
||||
The command hangs until a change is made on the KV path and after that the path data prints on the console. |
||||
|
||||
<CodeBlockConfig hideClipboard highlight="9"> |
||||
|
||||
```json |
||||
[ |
||||
{ |
||||
"LockIndex": 0, |
||||
"Key": "service/leader", |
||||
"Flags": 0, |
||||
"Value": "eyJOb2RlIjogImhhc2hpY3Vwcy1kYi0xIn0=", |
||||
"Session": "d21d60ad-c2d2-b32a-7432-ca4048a4a7d6", |
||||
"CreateIndex": 12399, |
||||
"ModifyIndex": 13329 |
||||
} |
||||
] |
||||
``` |
||||
|
||||
</CodeBlockConfig> |
||||
|
||||
For automation purposes, add logic to the blocking query mechanism to trigger a command every time a change is returned. |
||||
A better approach is to use the CLI command `consul watch`. |
||||
|
||||
</Tab> |
||||
<Tab heading="CLI" group="cli"> |
||||
|
||||
Monitor the lock using the [`consul watch`](/consul/commands/watch) command. |
||||
|
||||
```shell-session |
||||
$ consul watch -type=key -key=service/leader cat | jq |
||||
``` |
||||
|
||||
In this example, the command output prints to the shell. However, it is possible to pass more complex option to the command as well as a script that contains more complex logic to react to the lock data change. |
||||
|
||||
An example output for the command is: |
||||
|
||||
<CodeBlockConfig hideClipboard> |
||||
|
||||
```json |
||||
{ |
||||
"Key": "service/leader", |
||||
"CreateIndex": 12399, |
||||
"ModifyIndex": 13061, |
||||
"LockIndex": 0, |
||||
"Flags": 0, |
||||
"Value": "eyJOb2RlIjogImhhc2hpY3Vwcy1kYi0wIn0=", |
||||
"Session": "d21d60ad-c2d2-b32a-7432-ca4048a4a7d6" |
||||
} |
||||
``` |
||||
|
||||
</CodeBlockConfig> |
||||
|
||||
The `consul watch` command polls the KV path for changes and runs the specified command on the output when a change is made. |
||||
|
||||
</Tab> |
||||
</Tabs> |
||||
|
||||
From the output, notice that once the lock is acquired, the `Session` parameter contains the ID of the session that holds the lock. |
||||
|
||||
## Renew a session |
||||
|
||||
If a session is created with a `TTL` value set, you need to renew the session before the TTL expires. |
||||
|
||||
Use the [`/v1/session/renew`](/consul/api-docs/session#renew-session) endpoint to renew existing sessions. |
||||
|
||||
```shell-session |
||||
$ curl --silent \ |
||||
--header "X-Consul-Token: $CONSUL_HTTP_TOKEN" \ |
||||
--request PUT \ |
||||
http://127.0.0.1:8500/v1/session/renew/f027470f-2759-6b53-542d-066ae4185e67 | jq |
||||
``` |
||||
|
||||
If the command succeeds, the session information in JSON format is printed. |
||||
|
||||
<CodeBlockConfig hideClipboard> |
||||
|
||||
```json |
||||
[ |
||||
{ |
||||
"ID": "f027470f-2759-6b53-542d-066ae4185e67", |
||||
"Name": "test", |
||||
"Node": "consul-server-0", |
||||
"LockDelay": 15000000000, |
||||
"Behavior": "release", |
||||
"TTL": "30s", |
||||
"NodeChecks": [ |
||||
"serfHealth" |
||||
], |
||||
"ServiceChecks": null, |
||||
"CreateIndex": 11842, |
||||
"ModifyIndex": 11842 |
||||
} |
||||
] |
||||
``` |
||||
|
||||
</CodeBlockConfig> |
||||
|
||||
## Release a lock |
||||
|
||||
A lock associated with a session with no `TTL` value set might never be released, even when the service holding it fails. |
||||
|
||||
In such cases, you need to manually release the lock. |
||||
|
||||
<Tabs> |
||||
<Tab heading="API" group="api"> |
||||
|
||||
```shell-session |
||||
$ curl --silent \ |
||||
--header "X-Consul-Token: $CONSUL_HTTP_TOKEN" \ |
||||
--data '{"Node": "'`hostname`'"}' \ |
||||
--request PUT \ |
||||
http://localhost:8500/v1/kv/service/leader?release=d21d60ad-c2d2-b32a-7432-ca4048a4a7d6 | jq |
||||
``` |
||||
|
||||
The command prints `true` on success. |
||||
|
||||
</Tab> |
||||
<Tab heading="CLI" group="cli"> |
||||
|
||||
```shell-session |
||||
$ consul kv put -release -session=d21d60ad-c2d2-b32a-7432-ca4048a4a7d6 service/leader '{"Node": "'`hostname`'"}' |
||||
``` |
||||
|
||||
On success, the command outputs a success message. |
||||
|
||||
<CodeBlockConfig hideClipboard> |
||||
|
||||
```plaintext |
||||
Success! Lock released on: service/leader |
||||
``` |
||||
|
||||
</CodeBlockConfig> |
||||
|
||||
</Tab> |
||||
</Tabs> |
||||
|
||||
After a lock is released, the key data do not show a value for `Session` in the results. |
||||
Other clients can use this as a way to coordinate their lock requests. |
||||
|
Loading…
Reference in new issue