This is the multi-page printable view of this section. .
Learning
1 - Data model
etcd is designed to reliably store infrequently updated data and provide reliable watch queries. etcd exposes previous versions of key-value pairs to support inexpensive snapshots and watch history events (“time travel queries”). A persistent, multi-version, concurrency-control data model is a good fit for these use cases.
etcd stores data in a multiversion persistent key-value store. The persistent key-value store preserves the previous version of a key-value pair when its value is superseded with new data. The key-value store is effectively immutable; its operations do not update the structure in-place, but instead always generate a new updated structure. All past versions of keys are still accessible and watchable after modification. To prevent the data store from growing indefinitely over time and from maintaining old versions, the store may be compacted to shed the oldest versions of superseded data.
Logical view
The store’s logical view is a flat binary key space. The key space has a lexically sorted index on byte string keys so range queries are inexpensive.
The key space maintains multiple revisions. When the store is created, the initial revision is 1. Each atomic mutative operation (e.g., a transaction operation may contain multiple operations) creates a new revision on the key space. All data held by previous revisions remains unchanged. Old versions of key can still be accessed through previous revisions. Likewise, revisions are indexed as well; ranging over revisions with watchers is efficient. If the store is compacted to save space, revisions before the compact revision will be removed. Revisions are monotonically increasing over the lifetime of a cluster.
A key’s life spans a generation, from creation to deletion. Each key may have one or multiple generations. Creating a key increments the version of that key, starting at 1 if the key does not exist at the current revision. Deleting a key generates a key tombstone, concluding the key’s current generation by resetting its version to 0. Each modification of a key increments its version; so, versions are monotonically increasing within a key’s generation. Once a compaction happens, any generation ended before the compaction revision will be removed, and values set before the compaction revision except the latest one will be removed.
Physical view
etcd stores the physical data as key-value pairs in a persistent b+tree . Each revision of the store’s state only contains the delta from its previous revision to be efficient. A single revision may correspond to multiple keys in the tree.
The key of key-value pair is a 3-tuple (major, sub, type). Major is the store revision holding the key. Sub differentiates among keys within the same revision. Type is an optional suffix for special value (e.g., t if the value contains a tombstone). The value of the key-value pair contains the modification from previous revision, thus one delta from previous revision. The b+tree is ordered by key in lexical byte-order. Ranged lookups over revision deltas are fast; this enables quickly finding modifications from one specific revision to another. Compaction removes out-of-date keys-value pairs.
etcd also keeps a secondary in-memory btree index to speed up range queries over keys. The keys in the btree index are the keys of the store exposed to user. The value is a pointer to the modification of the persistent b+tree. Compaction removes dead pointers.
Overall, etcd gets the revision information from btree and then uses the revision as key to fetch value from b+tree(As shown below).

2 - etcd client design
etcd Client Design
Gyuho Lee (github.com/gyuho, Amazon Web Services, Inc.), Joe Betz (github.com/jpbetz, Google Inc.)
Introduction
etcd server has proven its robustness with years of failure injection testing. Most complex application logic is already handled by etcd server and its data stores (e.g. cluster membership is transparent to clients, with Raft-layer forwarding proposals to leader). Although server components are correct, its composition with client requires a different set of intricate protocols to guarantee its correctness and high availability under faulty conditions. Ideally, etcd server provides one logical cluster view of many physical machines, and client implements automatic failover between replicas. This documents client architectural decisions and its implementation details.
Glossary
clientv3: etcd Official Go client for etcd v3 API.
clientv3-grpc1.0: Official client implementation, with grpc-go v1.0.x
, which is used in latest etcd v3.1.
clientv3-grpc1.7: Official client implementation, with grpc-go v1.7.x
, which is used in latest etcd v3.2 and v3.3.
clientv3-grpc1.23: Official client implementation, with grpc-go v1.23.x
, which is used in latest etcd v3.4.
Balancer: etcd client load balancer that implements retry and failover mechanism. etcd client should automatically balance loads between multiple endpoints.
Endpoints: A list of etcd server endpoints that clients can connect to. Typically, 3 or 5 client URLs of an etcd cluster.
Pinned endpoint: When configured with multiple endpoints, <= v3.3 client balancer chooses only one endpoint to establish a TCP connection, in order to conserve total open connections to etcd cluster. In v3.4, balancer round-robins pinned endpoints for every request, thus distributing loads more evenly.
Client Connection: TCP connection that has been established to an etcd server, via gRPC Dial.
Sub Connection: gRPC SubConn interface. Each sub-connection contains a list of addresses. Balancer creates a SubConn from a list of resolved addresses. gRPC ClientConn can map to multiple SubConn (e.g. example.com resolves to 10.10.10.1 and 10.10.10.2 of two sub-connections). etcd v3.4 balancer employs internal resolver to establish one sub-connection for each endpoint.
Transient disconnect: When gRPC server returns a status error of code Unavailable
.
Client Requirements
Correctness. Requests may fail in the presence of server faults. However, it never violates consistency guarantees: global ordering properties, never write corrupted data, at-most once semantics for mutable operations, watch never observes partial events, and so on.
Liveness. Servers may fail or disconnect briefly. Clients should make progress in either way. Clients should never deadlock waiting for a server to come back from offline, unless configured to do so. Ideally, clients detect unavailable servers with HTTP/2 ping and failover to other nodes with clear error messages.
Effectiveness. Clients should operate effectively with minimum resources: previous TCP connections should be gracefully closed after endpoint switch. Failover mechanism should effectively predict the next replica to connect, without wastefully retrying on failed nodes.
Portability. Official client should be clearly documented and its implementation be applicable to other language bindings. Error handling between different language bindings should be consistent. Since etcd is fully committed to gRPC, implementation should be closely aligned with gRPC long-term design goals (e.g. pluggable retry policy should be compatible with gRPC retry ). Upgrades between two client versions should be non-disruptive.
Client Overview
etcd client implements the following components:
- balancer that establishes gRPC connections to an etcd cluster,
- API client that sends RPCs to an etcd server, and
- error handler that decides whether to retry a failed request or switch endpoints.
Languages may differ in how to establish an initial connection (e.g. configure TLS), how to encode and send Protocol Buffer messages to server, how to handle stream RPCs, and so on. However, errors returned from etcd server will be the same. So should be error handling and retry policy.
For example, etcd server may return "rpc error: code = Unavailable desc = etcdserver: request timed out", which is transient error that expects retries. Or return rpc error: code = InvalidArgument desc = etcdserver: key is not provided, which means request was invalid and should not be retried. Go client can parse errors with google.golang.org/grpc/status.FromError, and Java client with io.grpc.Status.fromThrowable.
clientv3-grpc1.0: Balancer Overview
clientv3-grpc1.0 maintains multiple TCP connections when configured with multiple etcd endpoints. Then pick one address and use it to send all client requests. The pinned address is maintained until the client object is closed (see Figure 1). When the client receives an error, it randomly picks another and retries.

clientv3-grpc1.0: Balancer Limitation
clientv3-grpc1.0 opening multiple TCP connections may provide faster balancer failover but requires more resources. The balancer does not understand node’s health status or cluster membership. So, it is possible that balancer gets stuck with one failed or partitioned node.
clientv3-grpc1.7: Balancer Overview
clientv3-grpc1.7 maintains only one TCP connection to a chosen etcd server. When given multiple cluster endpoints, a client first tries to connect to them all. As soon as one connection is up, balancer pins the address, closing others (see Figure 2). The pinned address is to be maintained until the client object is closed. An error, from server or client network fault, is sent to client error handler (see Figure 3).


The client error handler takes an error from gRPC server, and decides whether to retry on the same endpoint, or to switch to other addresses, based on the error code and message (see Figure 4 and Figure 5).


Stream RPCs, such as Watch and KeepAlive, are often requested with no timeouts. Instead, client can send periodic HTTP/2 pings to check the status of a pinned endpoint; if the server does not respond to the ping, balancer switches to other endpoints (see Figure 6).

clientv3-grpc1.7: Balancer Limitation
clientv3-grpc1.7 balancer sends HTTP/2 keepalives to detect disconnects from streaming requests. It is a simple gRPC server ping mechanism and does not reason about cluster membership, thus unable to detect network partitions. Since partitioned gRPC server can still respond to client pings, balancer may get stuck with a partitioned node. Ideally, keepalive ping detects partition and triggers endpoint switch, before request time-out (see etcd#8673
and Figure 7).

clientv3-grpc1.7 balancer maintains a list of unhealthy endpoints. Disconnected addresses are added to “unhealthy” list, and considered unavailable until after wait duration, which is hard coded as dial timeout with default value 5-second. Balancer can have false positives on which endpoints are unhealthy. For instance, endpoint A may come back right after being blacklisted, but still unusable for next 5 seconds (see Figure 8).
clientv3-grpc1.0 suffered the same problems above.

Upstream gRPC Go had already migrated to new balancer interface. For example, clientv3-grpc1.7 underlying balancer implementation uses new gRPC balancer and tries to be consistent with old balancer behaviors. While its compatibility has been maintained reasonably well, etcd client still suffered from subtle breaking changes
. Furthermore, gRPC maintainer recommends to not rely on the old balancer interface
. In general, to get better support from upstream, it is best to be in sync with latest gRPC releases. And new features, such as retry policy, may not be backported to gRPC 1.7 branch. Thus, both etcd server and client must migrate to latest gRPC versions.
clientv3-grpc1.23: Balancer Overview
clientv3-grpc1.7 is so tightly coupled with old gRPC interface, that every single gRPC dependency upgrade broke client behavior. Majority of development and debugging efforts were devoted to fixing those client behavior changes. As a result, its implementation has become overly complicated with bad assumptions on server connectivities.
The primary goal of clientv3-grpc1.23 is to simplify balancer failover logic; rather than maintaining a list of unhealthy endpoints, which may be stale, simply roundrobin to the next endpoint whenever client gets disconnected from the current endpoint. It does not assume endpoint status. Thus, no more complicated status tracking is needed (see Figure 8 and above). Upgrading to clientv3-grpc1.23 should be no issue; all changes were internal while keeping all the backward compatibilities.
Internally, when given multiple endpoints, clientv3-grpc1.23 creates multiple sub-connections (one sub-connection per each endpoint), while clientv3-grpc1.7 creates only one connection to a pinned endpoint (see Figure 9). For instance, in 5-node cluster, clientv3-grpc1.23 balancer would require 5 TCP connections, while clientv3-grpc1.7 only requires one. By preserving the pool of TCP connections, clientv3-grpc1.23 may consume more resources but provide more flexible load balancer with better failover performance. The default balancing policy is round robin but can be easily extended to support other types of balancers (e.g. power of two, pick leader, etc.). clientv3-grpc1.23 uses gRPC resolver group and implements balancer picker policy, in order to delegate complex balancing work to upstream gRPC. On the other hand, clientv3-grpc1.7 manually handles each gRPC connection and balancer failover, which complicates the implementation. clientv3-grpc1.23 implements retry in the gRPC interceptor chain that automatically handles gRPC internal errors and enables more advanced retry policies like backoff, while clientv3-grpc1.7 manually interprets gRPC errors for retries.

clientv3-grpc1.23: Balancer Limitation
Improvements can be made by caching the status of each endpoint. For instance, balancer can ping each server in advance to maintain a list of healthy candidates, and use this information when doing round-robin. Or when disconnected, balancer can prioritize healthy endpoints. This may complicate the balancer implementation, thus can be addressed in later versions.
Client-side keepalive ping still does not reason about network partitions. Streaming request may get stuck with a partitioned node. Advanced health checking service need to be implemented to understand the cluster membership (see etcd#8673 for more detail).

Currently, retry logic is handled manually as an interceptor. This may be simplified via official gRPC retries .
3 - etcd learner design
etcd Learner
Gyuho Lee (github.com/gyuho, Amazon Web Services, Inc.), Joe Betz (github.com/jpbetz, Google Inc.)
Background
Membership reconfiguration has been one of the biggest operational challenges. Let’s review common challenges.
1. New Cluster member overloads Leader
A newly joined etcd member starts with no data, thus demanding more updates from leader until it catches up with leader’s logs. Then leader’s network is more likely to be overloaded, blocking or dropping leader heartbeats to followers. In such case, a follower may election-timeout to start a new leader election. That is, a cluster with a new member is more vulnerable to leader election. Both leader election and the subsequent update propagation to the new member are prone to causing periods of cluster unavailability (see Figure 1).

2. Network Partitions scenarios
What if network partition happens? It depends on leader partition. If the leader still maintains the active quorum, the cluster would continue to operate (see Figure 2).

2.1 Leader isolation
What if the leader becomes isolated from the rest of the cluster? Leader monitors progress of each follower. When leader loses connectivity from the quorum, it reverts back to follower which will affect the cluster availability (see Figure 3).

When a new node is added to 3 node cluster, the cluster size becomes 4 and the quorum size becomes 3. What if a new node had joined the cluster, and then network partition happens? It depends on which partition the new member gets located after partition.
2.2 Cluster Split 3+1
If the new node happens to be located in the same partition as leader’s, the leader still maintains the active quorum of 3. No leadership election happens, and no cluster availability gets affected (see Figure 4).

2.3 Cluster Split 2+2
If the cluster is 2-and-2 partitioned, then neither of partition maintains the quorum of 3. In this case, leadership election happens (see Figure 5).

2.4 Quorum Lost
What if network partition happens first, and then a new member gets added? A partitioned 3-node cluster already has one disconnected follower. When a new member is added, the quorum changes from 2 to 3. Now, this cluster has only 2 active nodes out 4, thus losing quorum and starting a new leadership election (see Figure 6).

Since member add operation can change the size of quorum, it is always recommended to “member remove” first to replace an unhealthy node.
Adding a new member to a 1-node cluster changes the quorum size to 2, immediately causing a leader election when the previous leader finds out quorum is not active. This is because “member add” operation is a 2-step process where user needs to apply “member add” command first, and then starts the new node process (see Figure 7).

3. Cluster Misconfigurations
An even worse case is when an added member is misconfigured. Membership reconfiguration is a two-step process: “etcdctl member add” and starting an etcd server process with the given peer URL. That is, “member add” command is applied regardless of URL, even when the URL value is invalid. If the first step is applied with invalid URLs, the second step cannot even start the new etcd. Once the cluster loses quorum, there is no way to revert the membership change (see Figure 8).

Same applies to a multi-node cluster. For example, the cluster has two members down (one is failed, the other is misconfigured) and two members up, but now it requires at least 3 votes to change the cluster membership (see Figure 9).

As seen above, a simple misconfiguration can fail the whole cluster into an inoperative state. In such case, an operator need manually recreate the cluster with etcd --force-new-cluster flag. As etcd has become a mission-critical service for Kubernetes, even the slightest outage may have significant impact on users. What can we better to make etcd such operations easier? Among other things, leader election is most critical to cluster availability: Can we make membership reconfiguration less disruptive by not changing the size of quorum? Can a new node be idle, only requesting the minimum updates from leader, until it catches up? Can membership misconfiguration be always reversible and handled in a more secure way (wrong member add command run should never fail the cluster)? Should an user worry about network topology when adding a new member? Can member add API work regardless of the location of nodes and ongoing network partitions?
Raft Learner
In order to mitigate such availability gaps in the previous section, Raft §4.2.1 introduces a new node state “Learner”, which joins the cluster as a non-voting member until it catches up to leader’s logs.
Features in v3.4
An operator should do the minimum amount of work possible to add a new learner node. member add --learner command to add a new learner, which joins cluster as a non-voting member but still receives all data from leader (see Figure 10).

When a learner has caught up with leader’s progress, the learner can be promoted to a voting member using member promote API, which then counts towards the quorum (see Figure 11).

etcd server validates promote request to ensure its operational safety. Only after its log has caught up to leader’s can learner be promoted to a voting member (see Figure 12).

Learner only serves as a standby node until promoted: Leadership cannot be transferred to learner. Learner rejects client reads and writes (client balancer should not route requests to learner). Which means learner does not need issue Read Index requests to leader. Such limitation simplifies the initial learner implementation in v3.4 release (see Figure 13).

In addition, etcd limits the total number of learners that a cluster can have, and avoids overloading the leader with log replication. Learner never promotes itself. While etcd provides learner status information and safety checks, cluster operator must make the final decision whether to promote learner or not.
Proposed features for future releases
Make learner state only and default: Defaulting a new member state to learner will greatly improve membership reconfiguration safety, because learner does not change the size of quorum. Misconfiguration will always be reversible without losing the quorum.
Make voting-member promotion fully automatic: Once a learner catches up to leader’s logs, a cluster can automatically promote the learner. etcd requires certain thresholds to be defined by the user, and once the requirements are satisfied, learner promotes itself to a voting member. From a user’s perspective, “member add” command would work the same way as today but with greater safety provided by learner feature.
Make learner standby failover node: A learner joins as a standby node, and gets automatically promoted when the cluster availability is affected.
Make learner read-only: A learner can serve as a read-only node that never gets promoted. In a weak consistency mode, learner only receives data from leader and never process writes. Serving reads locally without consensus overhead would greatly decrease the workloads to leader but may serve stale data. In a strong consistency mode, learner requests read index from leader to serve latest data, but still rejects writes.
Learner vs. Mirror Maker
etcd implements “mirror maker” using watch API to continuously relay key creates and updates to a separate cluster. Mirroring usually has low latency overhead once it completes initial synchronization. Learner and mirroring overlap in that both can be used to replicate existing data for read-only. However, mirroring does not guarantee linearizability. During network disconnects, previous key-values might have been discarded, and clients are expected to verify watch responses for correct ordering. Thus, there is no ordering guarantee in mirror. Use mirror for minimum latency (e.g. cross data center) at the costs of consistency. Use learner to retain all historical data and its ordering.
Appendix: Learner Implementation in v3.4
Expose “Learner” node type to “MemberAdd” API.
etcd client adds a flag to “MemberAdd” API for learner node. And etcd server handler applies membership change entry with pb.ConfChangeAddLearnerNode type. Once the command has been applied, a server joins the cluster with etcd --initial-cluster-state=existing flag. This learner node can neither vote nor count as quorum.
etcd server must not transfer leadership to learner, since it may still lag behind and does not count as quorum. etcd server limits the number of learners that cluster can have to one: the more learners we have, the more data the leader has to propagate. Clients may talk to learner node, but learner rejects all requests other than serializable read and member status API. This is for simplicity of initial implementation. In the future, learner can be extended as a read-only server that continuously mirrors cluster data. Client balancer must provide helper function to exclude learner node endpoint. Otherwise, request sent to learner may fail. Client sync member call should factor into learner node type. So should client endpoints update call.
MemberList and MemberStatus responses should indicate which node is learner.
Add “MemberPromote” API.
Internally in Raft, second MemberAdd call to learner node promotes it to a voting member. Leader maintains the progress of each follower and learner. If learner has not completed its snapshot message, reject promote request. Only accept promote request if and only if: The learner node is in a healthy state. The learner is in sync with leader or the delta is within the threshold (e.g. the number of entries to replicate to learner is less than 1/10 of snapshot count, which means it is less likely that even after promotion leader would not need send snapshot to the learner). All these logic are hard-coded in etcdserver package and not configurable.
Reference
- Original github issue: etcd#9161
- Use case: etcd#3715
- Use case: etcd#8888
- Use case: etcd#10114
4 - etcd v3 authentication design
Why not reuse the v2 auth system?
The v3 protocol uses gRPC as its transport instead of a RESTful interface like v2. This new protocol provides an opportunity to iterate on and improve the v2 design. For example, v3 auth has connection based authentication, rather than v2’s slower per-request authentication. Additionally, v2 auth’s semantics tend to be unwieldy in practice with respect to reasoning about consistency, which will be described in the next sections. For v3, there is a well-defined description and implementation of the authentication mechanism which fixes the deficiencies in the v2 auth system.
Functionality requirements
- Per connection authentication, not per request
- User ID + password based authentication implemented for the gRPC API
- Authentication must be refreshed after auth policy changes
- Its functionality should be as simple and useful as v2
- v3 provides a flat key space, unlike the directory structure of v2. Permission checking will be provided as interval matching.
- It should have stronger consistency guarantees than v2 auth
Main required changes
- A client must create a dedicated connection only for authentication before sending authenticated requests
- Add permission information (user ID and authorized revision) to the Raft commands (
etcdserverpb.InternalRaftRequest) - Every request is permission checked in the state machine layer, rather than API layer
Permission metadata consistency
The metadata for auth should also be stored and managed in the storage controlled by etcd’s Raft protocol like other data stored in etcd. It is required for not sacrificing availability and consistency of the entire etcd cluster. If reading or writing the metadata (e.g. permission information) needs an agreement of every node (more than quorum), single node failure can stop the entire cluster. Requiring all nodes to agree at once means that checking ordinary read/write requests cannot be completed if any cluster member is down, even if the cluster has an available quorum. This unanimous scheme ultimately degrades cluster availability; quorum based consensus from raft should suffice since agreement follows from consistent ordering.
The authentication mechanism in the etcd v2 protocol has a tricky part because the metadata consistency should work as in the above, but does not: each permission check is processed by the etcd member that receives the client request (server/etcdserver/api/v2http/client.go), including follower members. Therefore, it’s possible the check may be based on stale metadata.
This staleness means that auth configuration cannot be reflected as soon as operators execute etcdctl. Therefore there is no way to know how long the stale metadata is active. Practically, the configuration change is reflected immediately after the command execution. However, in some cases of heavy load, the inconsistent state can be prolonged and it might result in counter-intuitive situations for users and developers. It requires a workaround like this .
Inconsistent permissions are unsafe for linearized requests
Inconsistent authentication state is most serious for writes. Even if an operator disables write on a user, if the write is only ordered with respect to the key value store but not the authentication system, it’s possible the write will complete successfully. Without ordering on both the auth store and the key-value store, the system will be susceptible to stale permission attacks.
Therefore, the permission checking logic should be added to the state machine of etcd. Each state machine should check the requests based on its permission information in the apply phase (so the auth information must not be stale).
Design and implementation
Authentication
At first, a client must create a gRPC connection only to authenticate its user ID and password. An etcd server will respond with an authentication reply. The response will be an authentication token on success or an error on failure. The client can use its authentication token to present its credentials to etcd when making API requests.
The client connection used to request the authentication token is typically thrown away; it cannot carry the new token’s credentials. This is because gRPC doesn’t provide a way for adding per RPC credential after creation of the connection (calling grpc.Dial()). Therefore, a client cannot assign a token to its connection that is obtained through the connection. The client needs a new connection for using the token.
Notes on the implementation of Authenticate() RPC
Authenticate() RPC generates an authentication token based on a given user name and password. etcd saves and checks a configured password and a given password using Go’s bcrypt package. By design, bcrypt’s password checking mechanism is computationally expensive, taking nearly 100ms on an ordinary x64 server. Therefore, performing this check in the state machine apply phase would cause performance trouble: the entire etcd cluster can only serve almost 10 Authenticate() requests per second.
For good performance, the v3 auth mechanism checks passwords in etcd’s API layer, where it can be parallelized outside of raft. However, this can lead to potential time-of-check/time-of-use (TOCTOU) permission lapses:
- client A sends a request
Authenticate() - the API layer processes the password checking part of
Authenticate() - another client B sends a request of
ChangePassword()and the server completes it - the state machine layer processes the part of getting a revision number for the
Authenticate()from A - the server returns a success to A
- now A is authenticated on an obsolete password
For avoiding such a situation, the API layer performs version number validation based on the revision number of the auth store. During password checking, the API layer saves the revision number of auth store. After successful password checking, the API layer compares the saved revision number and the latest revision number. If the numbers differ, it means someone else updated the auth metadata. So it retries the checking. With this mechanism, the successful password checking based on the obsolete password can be avoided.
Resolving a token in the API layer
After authenticating with Authenticate(), a client can create a gRPC connection as it would without auth. In addition to the existing initialization process, the client must associate the token with the newly created connection. grpc.WithPerRPCCredentials() provides the functionality for this purpose.
Every authenticated request from the client has a token. The token can be obtained with grpc.metadata.FromIncomingContext() in the server side. The server can obtain who is issuing the request and when the user was authorized. The information will be filled by the API layer in the header (etcdserverpb.RequestHeader.Username and etcdserverpb.RequestHeader.AuthRevision) of a raft log entry (etcdserverpb.InternalRaftRequest).
Checking permission in the state machine
The auth info in etcdserverpb.RequestHeader is checked in the apply phase of the state machine. This step checks the user is granted permission to requested keys on the latest revision of auth store.
Two types of tokens: simple and JWT
There are two kinds of token types: simple and JWT. The simple token isn’t designed for production use cases. Its tokens aren’t cryptographically signed and servers must statefully track token-user correspondence; it is meant for development testing. JWT tokens should be used for production deployments since it is cryptographically signed and verified. From the implementation perspective, JWT is stateless. Its token can include metadata including username and revision, so servers don’t need to remember correspondence between tokens and the metadata.
There is a known issue #18437 with simple tokens. Within etcd servers, tokens are resolved at the API layer and simple tokens are stateful. The process is not protected by a linearizable check, meaning an etcd member may not have completed processing a previous authentication request before receiving the next one. In such cases, the member might return an “invalid auth token” error to the client. This issue is usually rare on a node with good network conditions but can occur if there is significant latency. As a workaround, applications can implement a retry mechanism to handle this error.
Directly setting JWT tokens
In addition to the standard Authenticate() RPC flow, etcd supports setting JWT tokens directly at the client level. This allows applications to manage the complete lifecycle of JWT tokens outside of etcd, including token generation, validation, and rotation.
Use case and workflow
This approach is useful when:
- A separate token management system (outside of etcd) handles JWT token generation and lifecycle
- Applications receive pre-signed JWT tokens through an external mechanism (e.g., environment variables, configuration service)
- Token lifecycle must be managed entirely by the client application rather than by etcd’s automatic token generation
The typical workflow is:
- An external authority (not etcd) generates a signed JWT token that includes the username and other claims
- The application receives the pre-signed token and configures the etcd client with it
- The client submits the JWT token directly with requests (without calling
Authenticate()) - The etcd server validates the token signature using its configured public key and grants access based on the username in the token
- Before the token expires, the application obtains a new token from the external authority
- The application creates a new client with the updated token (token updates require client recreation)
How it differs from standard authentication
When using the standard Authenticate() flow:
- The client calls
Authenticate()with username and password - etcd generates and returns a token
- The client automatically uses this token for subsequent requests
- Token refresh requires calling
Authenticate()again
When setting JWT tokens directly:
- The client is initialized with a pre-signed JWT token
- The client does not call
Authenticate() - The token is used directly in all requests
- The client application is responsible for obtaining new tokens before expiration and managing client lifecycle
AuthStatus without valid token
To support applications that manage their own JWT tokens, the AuthStatus RPC is designed to allow clients to determine whether authentication is enabled and retrieve the current authRevision. This is important for recovery scenarios where a token has expired and the client needs the latest revision in order to obtain a new valid token from its external token provider.
Without this capability, an expired token could prevent a client from learning the current authRevision, leading to a deadlock where no new token can be generated.
Notes on the difference between KVS models and file system models
etcd v3 is a KVS, not a file system. So the permissions can be granted to the users in form of an exact key name or a key range like ["start key", "end key"). It means that granting a permission of a nonexistent key is possible. Users should care about unintended permission granting. In a case of file system like system (e.g. Chubby or ZooKeeper), an inode like data structure can include the permission information. So granting permission to a nonexist key won’t be possible (except the case of sticky bits).
The etcd v3 model requires multiple lookup of the metadata unlike the file system like systems. The worst case lookup cost will be sum the user’s total granted keys and intervals. The cost cannot be avoided because v3’s flat key space is completely different from Unix’s file system model (every inode includes permission metadata). Practically the cost won’t be a serious problem because the metadata is small enough to benefit from caching.
5 - etcd API
This document is meant to give an overview of the v3 etcd APIs central design. This should not be mistaken with etcd v2 API, deprecated in etcd v3.5. It is by no means all encompassing, but intended to focus on the basic ideas needed to understand etcd without the distraction of less common API calls. All etcd APIs are defined in gRPC services , which categorize remote procedure calls (RPCs) understood by the etcd server. A full listing of all etcd RPCs are documented in markdown in the gRPC API listing .
gRPC Services
Every API request sent to an etcd server is a gRPC remote procedure call. RPCs in etcd are categorized based on functionality into services.
Services important for dealing with etcd’s key space include:
- KV - Creates, updates, fetches, and deletes key-value pairs.
- Watch - Monitors changes to keys.
- Lease - Primitives for consuming client keep-alive messages.
Services which manage the cluster itself include:
- Auth - Role based authentication mechanism for authenticating users.
- Cluster - Provides membership information and configuration facilities.
- Maintenance - Takes recovery snapshots, defragments the store, and returns per-member status information.
Requests and Responses
All RPCs in etcd follow the same format. Each RPC has a function Name which takes NameRequest as an argument and returns NameResponse as a response. For example, here is the Range RPC description:
Response header
All Responses from etcd API have an attached response header which includes cluster metadata for the response:
- Cluster_ID - the ID of the cluster generating the response.
- Member_ID - the ID of the member generating the response.
- Revision - the revision of the key-value store when generating the response.
- Raft_Term - the Raft term of the member when generating the response.
An application may read the Cluster_ID or Member_ID field to ensure it is communicating with the intended cluster (member).
Applications can use the Revision field to know the latest revision of the key-value store. This is especially useful when applications specify a historical revision to make a time travel query and wish to know the latest revision at the time of the request.
Applications can use Raft_Term to detect when the cluster completes a new leader election.
Key-Value API
The Key-Value API manipulates key-value pairs stored inside etcd. The majority of requests made to etcd are usually key-value requests.
System primitives
Key-Value pair
A key-value pair is the smallest unit that the key-value API can manipulate. Each key-value pair has a number of fields, defined in protobuf format :
- Key - key in bytes. An empty key is not allowed.
- Value - value in bytes.
- Version - version is the version of the key. A deletion resets the version to zero and any modification of the key increases its version.
- Create_Revision - revision of the last creation on the key.
- Mod_Revision - revision of the last modification on the key.
- Lease - the ID of the lease attached to the key. If lease is 0, then no lease is attached to the key.
In addition to just the key and value, etcd attaches additional revision metadata as part of the key message. This revision information orders keys by time of creation and modification, which is useful for managing concurrency for distributed synchronization. The etcd client’s distributed shared locks use the creation revision to wait for lock ownership. Similarly, the modification revision is used for detecting software transactional memory read set conflicts and waiting on leader election updates.
Revisions
etcd maintains a 64-bit cluster-wide counter, the store revision, that is incremented each time the key space is modified. The revision serves as a global logical clock, sequentially ordering all updates to the store. The change represented by a new revision is incremental; the data associated with a revision is the data that changed the store. Internally, a new revision means writing the changes to the backend’s B+tree, keyed by the incremented revision.
Revisions become more valuable when considering etcd’s multi-version concurrency control backend. The MVCC model means that the key-value store can be viewed from past revisions since historical key revisions are retained. The retention policy for this history can be configured by cluster administrators for fine-grained storage management; usually etcd discards old revisions of keys on a timer. A typical etcd cluster retains superseded key data for hours. This also provides reliable handling for long client disconnection, not just transient network disruptions: watchers simply resume from the last observed historical revision. Similarly, to read from the store at a particular point-in-time, read requests can be tagged with a revision to return keys from a view of the key space at the point-in-time that revision was committed.
Key ranges
The etcd data model indexes all keys over a flat binary key space. This differs from other key-value store systems that use a hierarchical system of organizing keys into directories. Instead of listing keys by directory, keys are listed by key intervals [a, b).
These intervals are often referred to as “ranges” in etcd. Operations over ranges are more powerful than operations on directories. Like a hierarchical store, intervals support single key lookups via [a, a+1) (e.g., [‘a’, ‘a\x00’) looks up ‘a’) and directory lookups by encoding keys by directory depth. In addition to those operations, intervals can also encode prefixes; for example the interval ['a', 'b') looks up all keys prefixed by the string ‘a’.
By convention, ranges for a request are denoted by the fields key and range_end. The key field is the first key of the range and should be non-empty. The range_end is the key following the last key of the range. If range_end is not given or empty, the range is defined to contain only the key argument. If range_end is key plus one (e.g., “aa”+1 == “ab”, “a\xff”+1 == “b”), then the range represents all keys prefixed with key. If both key and range_end are ‘\0’, then range represents all keys. If range_end is ‘\0’, the range is all keys greater than or equal to the key argument.
Range
Keys are fetched from the key-value store using the Range API call, which takes a RangeRequest:
- Key, Range_End - The key range to fetch.
- Limit - the maximum number of keys returned for the request. When limit is set to 0, it is treated as no limit.
- Revision - the point-in-time of the key-value store to use for the range. If revision is less or equal to zero, the range is over the latest key-value store. If the revision is compacted, ErrCompacted is returned as a response.
- Sort_Order - the ordering for sorted requests.
- Sort_Target - the key-value field to sort.
- Serializable - sets the range request to use serializable member-local reads. By default, Range is linearizable; it reflects the current consensus of the cluster. For better performance and availability, in exchange for possible stale reads, a serializable range request is served locally without needing to reach consensus with other nodes in the cluster.
- Keys_Only - return only the keys and not the values.
- Count_Only - return only the count of the keys in the range.
- Min_Mod_Revision - the lower bound for key mod revisions; filters out lesser mod revisions.
- Max_Mod_Revision - the upper bound for key mod revisions; filters out greater mod revisions.
- Min_Create_Revision - the lower bound for key create revisions; filters out lesser create revisions.
- Max_Create_Revision - the upper bound for key create revisions; filters out greater create revisions.
The client receives a RangeResponse message from the Range call:
- Kvs - the list of key-value pairs matched by the range request. When
Count_Onlyis set,Kvsis empty. - More - indicates if there are more keys to return in the requested range if
limitis set. - Count - the total number of keys satisfying the range request.
For large key ranges where buffering the full response is undesirable, see RangeStream .
RangeStream
RangeStream returns the same result set as Range, but the server splits the response into a sequence of chunks and streams them to the client. This avoids buffering large ranges entirely in memory on either side. RangeStream accepts the same RangeRequest as Range.
The client receives a stream of RangeStreamResponse messages from the RangeStream call:
Field population across chunks:
- Kvs - each chunk carries a disjoint slice of the result. Concatenating the
kvsfrom every chunk in the order they arrive yields the same key set as a singleRangecall. - Header, More, Count - populated only on the final chunk, and only when the stream completes without error. Earlier chunks leave these fields zero-valued. Applying
proto.Mergeover every chunk’srange_responseyields aRangeResponseequivalent to whatRangewould have returned.
If the stream ends in error, no chunk carries a valid header, more, or count.
Every chunk in the stream is served against the same revision. If the request does not set Revision, the server captures the latest committed revision when the stream starts and reuses it for the rest of the stream.
RangeStream does not support custom sort orders or revision filters (min_mod_revision, max_mod_revision, min_create_revision, max_create_revision). Requests that use either return Unimplemented. RangeStream is also not supported by the etcd gRPC proxy.
There are two common ways to consume a RangeStream:
- Process each chunk independently. Suitable for high-performance scenarios where the client wants to decode and act on keys as they arrive rather than collecting the whole result first. The client iterates chunks and handles
kvsfrom each one, then readsheader,more, orcountfrom the last chunk after the stream ends cleanly. - Assemble a single response. Suitable when the client wants a result equivalent to a unary
Range. The client merges every chunk’srange_responseinto oneRangeResponse(e.g., viaproto.Merge). The merged result has the fullkvs, plusheader,more, andcountfrom the final chunk. The Go client providesclientv3.GetStreamToGetResponseas a helper for this pattern.
Put
Keys are saved into the key-value store by issuing a Put call, which takes a PutRequest:
- Key - the name of the key to put into the key-value store.
- Value - the value, in bytes, to associate with the key in the key-value store.
- Lease - the lease ID to associate with the key in the key-value store. A lease value of 0 indicates no lease.
- Prev_Kv - when set, responds with the key-value pair data before the update from this
Putrequest. - Ignore_Value - when set, update the key without changing its current value. Returns an error if the key does not exist.
- Ignore_Lease - when set, update the key without changing its current lease. Returns an error if the key does not exist.
The client receives a PutResponse message from the Put call:
- Prev_Kv - the key-value pair overwritten by the
Put, ifPrev_Kvwas set in thePutRequest.
Delete Range
Ranges of keys are deleted using the DeleteRange call, which takes a DeleteRangeRequest:
- Key, Range_End - The key range to delete.
- Prev_Kv - when set, return the contents of the deleted key-value pairs.
The client receives a DeleteRangeResponse message from the DeleteRange call:
- Deleted - number of keys deleted.
- Prev_Kv - a list of all key-value pairs deleted by the
DeleteRangeoperation.
Transaction
A transaction is an atomic If/Then/Else construct over the key-value store. It provides a primitive for grouping requests together in atomic blocks (i.e., then/else) whose execution is guarded (i.e., if) based on the contents of the key-value store. Transactions can be used for protecting keys from unintended concurrent updates, building compare-and-swap operations, and developing higher-level concurrency control.
A transaction can atomically process multiple requests in a single request. For modifications to the key-value store, this means the store’s revision is incremented only once for the transaction and all events generated by the transaction will have the same revision. However, modifications to the same key multiple times within a single transaction are forbidden.
All transactions are guarded by a conjunction of comparisons, similar to an If statement. Each comparison checks a single key in the store. It may check for the absence or presence of a value, compare with a given value, or check a key’s revision or version. Two different comparisons may apply to the same or different keys. All comparisons are applied atomically; if all comparisons are true, the transaction is said to succeed and etcd applies the transaction’s then / success request block, otherwise it is said to fail and applies the else / failure request block.
Each comparison is encoded as a Compare message:
- Result - the kind of logical comparison operation (e.g., equal, less than, etc).
- Target - the key-value field to be compared. Either the key’s version, create revision, modification revision, or value.
- Key - the key for the comparison.
- Target_Union - the user-specified data for the comparison.
After processing the comparison block, the transaction applies a block of requests. A block is a list of RequestOp messages:
- Request_Range - a
RangeRequest. - Request_Put - a
PutRequest. The keys must be unique. It may not share keys with any other Puts or Deletes. - Request_Delete_Range - a
DeleteRangeRequest. It may not share keys with any Puts or Deletes requests.
All together, a transaction is issued with a Txn API call, which takes a TxnRequest:
- Compare - A list of predicates representing a conjunction of terms for guarding the transaction.
- Success - A list of requests to process if all compare tests evaluate to true.
- Failure - A list of requests to process if any compare test evaluates to false.
The client receives a TxnResponse message from the Txn call:
- Succeeded - Whether
Compareevaluated to true or false. - Responses - A list of responses corresponding to the results from applying the
Successblock if succeeded is true or theFailureif succeeded is false.
The Responses list corresponds to the results from the applied RequestOp list, with each response encoded as a ResponseOp:
The ResponseHeader included in each inner response shouldn’t be interpreted in any way.
If clients need to get the latest revision, then they should always check the top level ResponseHeader in TxnResponse.
Watch API
The Watch API provides an event-based interface for asynchronously monitoring changes to keys. An etcd watch waits for changes to keys by continuously watching from a given revision, either current or historical, and streams key updates back to the client.
Events
Every change to every key is represented with Event messages. An Event message provides both the update’s data and the type of update:
- Type - The kind of event. A PUT type indicates new data has been stored to the key. A DELETE indicates the key was deleted.
- KV - The KeyValue associated with the event. A PUT event contains current kv pair. A PUT event with kv.Version=1 indicates the creation of a key. A DELETE event contains the deleted key with its modification revision set to the revision of deletion.
- Prev_KV - The key-value pair for the key from the revision immediately before the event. To save bandwidth, it is only filled out if the watch has explicitly enabled it.
Watch streams
Watches are long-running requests and use gRPC streams to stream event data. A watch stream is bi-directional; the client writes to the stream to establish watches and reads to receive watch events. A single watch stream can multiplex many distinct watches by tagging events with per-watch identifiers. This multiplexing helps reducing the memory footprint and connection overhead on the core etcd cluster.
To read about guarantees made about watch events, please read etcd api guarantees .
A client creates a watch by sending a WatchCreateRequest over a stream returned by Watch:
- Key, Range_End - The key range to watch.
- Start_Revision - An optional revision for where to inclusively begin watching. If not given, it will stream events following the revision of the watch creation response header revision. The entire available event history can be watched starting from the last compaction revision.
- Progress_Notify - When set, the watch will periodically receive a WatchResponse with no events, if there are no recent events. It is useful when clients wish to recover a disconnected watcher starting from a recent known revision. The etcd server decides how often to send notifications based on current server load.
- Filters - A list of event types to filter away at server side.
- Prev_Kv - When set, the watch receives the key-value data from before the event happens. This is useful for knowing what data has been overwritten.
In response to a WatchCreateRequest or if there is a new event for some established watch, the client receives a WatchResponse:
- Watch_ID - the ID of the watch that corresponds to the response.
- Created - set to true if the response is for a create watch request. The client should store the ID and expect to receive events for the watch on the stream. All events sent to the created watcher will have the same watch_id.
- Canceled - set to true if the response is for a cancel watch request. No further events will be sent to the canceled watcher.
- Compact_Revision - set to the minimum historical revision available to etcd if a watcher tries watching at a compacted revision. This happens when creating a watcher at a compacted revision or the watcher cannot catch up with the progress of the key-value store. The watcher will be canceled; creating new watches with the same start_revision will fail.
- Events - a list of new events in sequence corresponding to the given watch ID.
If the client wishes to stop receiving events for a watch, it issues a WatchCancelRequest:
- Watch_ID - the ID of the watch to cancel so that no more events are transmitted.
Lease API
Leases are a mechanism for detecting client liveness. The cluster grants leases with a time-to-live. A lease expires if the etcd cluster does not receive a keepAlive within a given TTL period.
To tie leases into the key-value store, each key may be attached to at most one lease. When a lease expires or is revoked, all keys attached to that lease will be deleted. Each expired key generates a delete event in the event history.
Obtaining leases
Leases are obtained through the LeaseGrant API call, which takes a LeaseGrantRequest:
- TTL - the advisory time-to-live, in seconds.
- ID - the requested ID for the lease. If ID is set to 0, etcd will choose an ID.
The client receives a LeaseGrantResponse from the LeaseGrant call:
- ID - the lease ID for the granted lease.
- TTL - is the server selected time-to-live, in seconds, for the lease.
- ID - the lease ID to revoke. When the lease is revoked, all attached keys are deleted.
Keep alives
Leases are refreshed using a bi-directional stream created with the LeaseKeepAlive API call. When the client wishes to refresh a lease, it sends a LeaseKeepAliveRequest over the stream:
- ID - the lease ID for the lease to keep alive.
The keep alive stream responds with a LeaseKeepAliveResponse:
- ID - the lease that was refreshed with a new TTL.
- TTL - the new time-to-live, in seconds, that the lease has remaining.
6 - etcd persistent storage files
This document explains the etcd persistent storage format: naming, content and tools that allow developers to inspect them. Going forward the document should be extended with changes to the storage model. This document is targeted at etcd developers to help with their data recovery needs.
Prerequisites
The following articles provide helpful background information for this document:
- etcd data model overview
- Raft overview (especially “5.3 Log replication” section).
Overview
Long leaving files
| File name | High level purpose |
|---|---|
./member/snap/db | bbolt b+tree that stores all the applied data, membership authorization information & metadata. It’s aware of what's the last applied WAL log index ("consistent_index"). |
./member/snap/0000000000000002-0000000000049425.snap ./member/snap/0000000000000002-0000000000061ace.snap | Periodic snapshots of legacy v2 store, containing:
As of etcd v3, the content is redundant to the content of /snap/db files. Periodically (30s) these files are purged, and the last |
/member/snap/000000000007a178.snap.db | A complete bbolt snapshot downloaded from the etcd leader if the replica was lagging too much. Has the same type of content as ( The file is used in 2 scenarios:
The file is not being deleted when the recovery is over (so whole content is populated to ./member/snap/db file). Periodically (30s) the files are purged.
Here also |
./member/wal/000000000000000f-00000000000b38c7.wal ./member/wal/000000000000000e-00000000000a7fe3.wal ./member/wal/000000000000000d-000000000009c70c.wal | Raft’s Write Ahead Logs, containing recent transactions accepted by Raft, periodic snapshots or CRC records. Recent If the snapshots are too infrequent, there can be more than |
./member/wal/0.tmp (or .../1.tmp) | Preallocated space for the next write ahead log file. Used to avoid Raft being stuck by a lack of WAL logs capacity without the possibility to raise an alarm. |
Temporary files
During etcd internal processing, it is possible that several short living files might be encountered:
| File | High level purpose |
|---|---|
./member/snap/0000000000000002-000000000007a178.snap.broken | Snapshot files are renamed as ‘broken’ when they cannot be loaded. The attempt to load the newest file happens when etcd is being started. Or during backup/migrate commands of etcdctl. |
./member/snap/tmp071677638 (random suffix) | Temporary (bbolt) file created on replicas in response to the msgSnap leaders request, so to the demand from the leader to recover storage from the given snapshot. After successful (complete) retrieval of content the file is renamed to: See etcd/issues/12837. Fixed in etcd 3.5. |
/member/snap/db.tmp.071677638 (random suffix) | A temporary file that contains a copy of the backend content (/member/snap/db), during the process of defragmentation. After the successful process the file is renamed to /member/snap/db, replacing the original backend. On etcd server startup these files get pruned. |
bbolt b+tree: member/snap/db
This file contains the main etcd content, applied to a specific point of the Raft log (see consistent_index ).
Physical organization
The better bolt storage is physically organized as a b+tree
. The physical pages of b-tree are never modified in-place1. Instead, the content is copied to a new page (reclaimed from the freepages list) and the old page is added to the free-pages list as soon as there is no open transaction that might access it. Thanks to this process, an open RO transaction sees a consistent historical state of the storage. The RW transaction is exclusive and blocking all other RW transactions.
Big values are stored on multiple continuous pages. The process of page reclamation combined with a need to allocate contiguous areas of pages of different sizes might lead to growing fragmentation of the bbolt storage.
The bbolt file never shrinks on its own. Only in the defragmentation process, the file can be rewritten to a new one that has some buffer of free pages on its end and has truncated size.
Logical organization
The bbolt storage is divided into buckets. In each bucket there are stored keys (byte[]->value byte[] pairs), in lexicographical order. The list below represents buckets used by etcd (as of version 3.5) and the keys in use.
| Bucket | Key | Exemplar value | Description |
|---|---|---|---|
| alarm | rpcpb.Alarm:
{MemberID, Alarm: NONE|NOSPACE|CORRUPT} | nil | Indicates problems have been diagnosed in one of the members. |
| auth | "authRevision" | "" (empty) or BigEndian.PutUint64 | Any change of Roles or Users increments this field on transaction commit. The value is used only for optimistic locking during the authorization process. |
| authRoles | [roleName] as string | authpb.Role marshalled | |
| authUsers | [userName] as string | authpb.User marshalled | |
| cluster | "clusterVersion" | "3.5.0" (string) | minor version of consensus-agreed common storage version. |
| "downgrade" | JSON:{
"target-version": "3.4.0"
"enabled": true/false
} | Persists intent configured by the most recent: Since v3.5 | |
| key | [revisionId] encoded using bytesToRev{main,sub} The key-value deletes are marshalled with 't' at the end (as a "Tombstone") | mvccpb.KeyValue marshalled proto (key, create_rev, mod_rev, version, value, lease id) | |
| lease | leasepb.Lease marshalled proto (ID, TTL, RemainingTTL) | Note: LeaseCheckpoint is extending only RemainingTTL. Just TTL is from the original Grant. Note2: We persist TTLs in seconds (from the undefined 'now'). Crash-looping server does not release leases!!! | |
| members | [memberId] in hex as string: "8e9e05c52164694d" | JSON as string serialized Member structure:{
"id":10276657743932975437,
"peerURLs":[
"http://localhost:2380"],
"name":"default",
"clientURLs": ["http://localhost:2379"]
} | Agreed cluster membership information. |
| members_removed | [memberId] in hex as string: "8e9e05c52164694d" | []byte("removed") | Ids of all removed members. Used to validate that a removed member is never added again under the same id. The field is currently (3.4) read from store V2 and never from V3. See https://github.com/etcd-io/etcd/pull/12820 |
| meta | "consistent_index" | uint64 bytes (BigEndian) | Represents the offset of the last applied WAL entry to the bolt DB storage. |
| "scheduledCompactRev" | bytesToRev{main,sub} encoded. (16 bytes) | Used to reinitialize compaction if a crash happened after a compaction request. | |
| "finishedCompactRev" | bytesToRev{main,sub} encoded. (16 bytes) | Revision at which store was recently successfully compacted (https://github.com/etcd-io/etcd/blob/ae7862e8bc8007eb396099db4e0e04ac026c8df5/server/mvcc/kvstore_compaction.go#L54) | |
| "confState" | Since etcd 3.5 | ||
| "term" | Since etcd 3.5 | ||
| "storage-version" |
Tools
bbolt
bbolt has a command line tool that enables inspecting the file content.
Examples of use:
List all buckets in given bbolt file:
Read a particular key/value pair:
etcd-dump-db
etcd-dump-db can be used to list content of v3 etcd backend (bbolt).
See more examples in: https://github.com/etcd-io/etcd/tree/master/tools/etcd-dump-db
WAL: Write ahead log
Write ahead log is a Raft persistent storage that is used to store proposals. First the leader stores the proposal in its log and then (concurrently) replicates it using Raft protocol to followers. Each follower persists the proposal in its WAL before confirming back replication to the leader.
The WAL log used in etcd differs from canonical Raft model 2-fold:
- It does persist not only indexed entries, but also Raft snapshots (lightweight) & hard-state. So the entire Raft state of the member can be recovered from the WAL log alone.
- It is append-only. Entries are not overridden in place, but an entry appended later in the file (with the same index) is superseding the previous one.
File names
The WAL log files are named using following pattern:
Example: ./member/wal/0000000000000010-00000000000bf1e6.wal
So the file names contains hex-encoded:
- Sequential number of the WAL log file
- Index of the first entry or snapshot in the file. In particular the first file “0000000000000000-0000000000000000.wal” has the initial snapshot record with index=0.
Physical content
The WAL log file contains a sequence of “Frames ”. Each frame contains:
- LittleEndian 2 encoded uint64 that contains the length of the marshalled walpb.Record (3).
- Padding: Some number of 0 bytes, such that whole frame has aligned (mod 8) size
- Marshalled walpb.Record
data:
- type - int encoded enum driving interpretation of the data-field below
- data - depending on type, usually marshalled proto
- crc - RC-32 checksum of all “data” fields combined (no type) in all the log records on this particular replica since WAL log creation. Please note that CRC takes in consideration ALL records (even if they didn’t get committedcomitted by Raft).
The files are “cut” (new file is started) when the current file is exceeding 64*10^6 bytes.
Logical content
Write ahead log files in the logical layer contains:
Raftpb.Entry:recent proposals replicated by Raft leader. Some of these proposals are considered ‘committed’ and the others are subject to be logically overridden.Raftpb.HardState(term,commit,vote):periodic (very frequent) information about the index of a log entry that is ‘committed’ (replicated to the majority of servers), so guaranteed to be not changed/overridden and that can be applied to the backends (v2, v3). It also contains a “term” (indicator whether there were any election related changes) and a vote - a member the current replica voted for in the current term.walpb.Snapshot(term, index):periodic snapshots of Raft state (no DB content, just snapshot log index and Raft term)- V2 store content is stored in a separate *.store files.
- V3 store content is maintained in the bbolt file, and it’s becoming an implicit snapshot as soon as entries are applied there.
- crc32 checksum record (at the beginning of each file), used to resume CRC checking for the remainder of the file.
etcdserverpb.Metadata(node_id, cluster_id)- identifying the cluster & replica the log represents.
Each WAL-log file is build from (in order):
CRC-32 frame (running crc from all previous files, 0 for the first file).
Metadata frame (cluster & replica IDs)
For the initial WAL file only:
- Empty Snapshot frame (Index:0, Term: 0). The purpose of this frame is to hold an invariant that all entries are ‘preceded’ by a snapshot.
For not initial (2nd+) WAL file:
- HardState frame.
Mix of entry, hard-state & snapshot records
The WAL log can contain multiple entries for the same index. Such a situation can happen in cases described in figure 7. of the Raft paper . The etcd WAL log is appended only, so the entries are getting overridden, by appending a new entry with the same index.
In particular during the WAL reading, the logic is overriding old entries with newer entries . Thus only the last version of entries with entry.index <= HardState.commit can be considered as final. Entries with index > HardState.commit are subject to change.
The “terms” in the WAL log are expected to be monotonic.
The “indexes” in the WAL log are expected to:
- start from some snapshot
- be sequentially growing after that snapshot as long as they stay in the same ‘term’
- if the term changes, the index can decrease, but to a new value that is higher than the latest HardState.commit.
- a new snapshot might happen with any index >= HardState.commit, that opens a new sequence for indexes.

Tools
etcd-dump-logs
etcd WAL logs can be read using etcd-dump-logs tool:
Be aware that:
- The tool shows only Entries, and not all the WAL records (Snapshots, HardStates) that are in the WAL log files.
- The tool automatically applies ‘overrides’ on the entries. If an entry got overridden (by a fresher entry under the same index), the tool will print only the final value.
- The tool also prints uncommitted entries (from the tail of the LOG), without information about HardState.commitIndex, so it’s not known whether entries are final or not.
Snapshots of (Store V2): member/snap/{term}-{index}.snap
File names:
member/snap/{term}-{index}.snap
The filenames are generated here
("%016x-%016x.snap") and are using 2 hex-encoded compounds:
- term -> Raft term (period between elections) at the time snapshot is emitted
- index -> of last applied proposal at the time snapshot is emitted
Creation
The *.snap files are created by Snapshotter.SaveSnap method.
There are 2 triggers controlling creation of these files:
- A new file is created every (approximately) –snapshotCount=(by default 100'000) applied proposals. It’s an approximation as we might receive proposals in batches and we consider snapshotting only at the end of batch, finally the snapshotting process is asynchronously scheduled. The flag name (–snapshotCount) is pretty misleading as it drives differences in index value between last snapshot index and last applied proposal index.
- Raft requests the replica to restore from the snapshot. As a replica is receiving the snapshot over wire (msgSnap) message, it also checkpoints (lightweight) it into WAL log. This guarantees that in the WAL logs tail there is always a valid snapshot followed by entries. So it suppresses potential lack of continuity in the WAL logs.
Currently the files are roughly3 associated 1-1 with WAL logs Snapshot entries. With store v2 decommissioning we expect the files to stop being written at all (opt-in: 3.5.x, mandatory 3.6.x).
Content
The file contains marshalled snapdb.snapshot proto
(uint32 crc, bytes data),
that in the ‘data’ field holds Raftpb.Snapshot :
(bytes data, SnapshotMetadata{index, term, conf } metadata),
Finally the nested data holds a JSON serialized store v2 content .
In particular there is:
- Term
- Index
- Membership data:
/0/members/8e9e05c52164694d/attributes -> {"name":"default","clientURLs":["http://localhost:2379"]}/0/members/8e9e05c52164694d/RaftAttributes -> "{"peerURLs":["http://localhost:2380"]}"
- Storage version: /0/version-> 3.5.0
Tools
protoc
Following command allows you to see the file content when executed from etcd root directory:
Analogously you can extract ‘data’ field and decode as ‘Raftpb.Snapshot
'
Exemplar JSON serialized store v2 content in etcd 3.4 *.snap files:
Changes
This section is reserved to describe changes to the file formats introduces between different etcd versions.
7 - etcd API guarantees
etcd is a consistent and durable key value store. The key value store is exposed through gRPC Services . etcd ensures the strongest consistency and durability guarantees for a distributed system. This specification enumerates the API guarantees made by etcd.
APIs to consider
- KV APIs
- Watch APIs
- Lease APIs
- Grant
- [Revoke]
- Keep alive
KV API allows for direct reading and manipulation of key value store. Watch API allows subscribing to key value store changes. Lease API allows assigning a time to live to a key.
Both KV and Watch APIs allow access to not only the latest versions of keys, but also previous versions are accessible within a continuous history window, limited by a compaction operation.
Calling KV API will take an immediate effect, while Watch API will return with some unbounded delay. In correctly working etcd cluster you should expect to see watch events to appear with 10ms delay after them happening. However, there is no limit and events in unhealthy clusters might never arrive.
KV APIs
etcd ensures durability and strict serializability for all KV api calls. Those are the strongest isolation guarantee of distributed transactional database systems.
Durability
Any completed operations are durable. All accessible data is also durable data. A read will never return data that has not been made durable.
Strict serializability
KV Service operations are atomic and occur in a total order, consistent with real-time order of those operations. Total order is implied through revision . Read more about strict serializability .
For transactions without nested TXNs, the order of execution of operations is guaranteed to be the same as in its list of operations, which means stable GET responses within the transaction. For transactions with nested TXNs, the order of execution is not specified.
Strict serializability implies other weaker guarantees that might be easier to understand:
Atomicity
All API requests are atomic; an operation either completes entirely or not at all. For watch requests, all events generated by one operation will be in one watch response. Watch never observes partial events for a single operation.
Linearizability
From the perspective of client, linearizability provides useful properties which
make reasoning easily. This is a clean description quoted from
the original paper
: Linearizability provides the illusion that each operation applied by concurrent processes takes effect instantaneously at some point between its invocation and its response.
For example, consider a client completing a write at time point 1 (t1). A client issuing a read at t2 (for t2 > t1) should receive a value at least as recent as the previous write, completed at t1. However, the read might actually complete only by t3. Linearizability guarantees the read returns the most current value. Without linearizability guarantee, the returned value, current at t2 when the read began, might be “stale” by t3 because a concurrent write might happen between t2 and t3.
etcd ensures linearizability for all other operations by default.
Linearizability comes with a cost, however, because linearized requests must go
through the Raft consensus process. To obtain lower latencies and higher
throughput for read requests, clients can configure a request’s consistency mode
to serializable, which may access stale data with respect to quorum, but
removes the performance penalty of linearized accesses’ reliance on live consensus.
Watch APIs
Watches make guarantees about events:
- Ordered - events are ordered by revision. An event will never appear on a watch if it precedes an event in time that has already been posted. For transactions without nested TXNs, the order of generated events is guaranteed to be the same as in its list of operations. For transactions with nested TXNs, the order of generated events is not specified.
- Unique - an event will never appear on a watch twice.
- Reliable - a sequence of events will never drop any subsequence of events within the available history window. If there are events ordered in time as a < b < c, then if the watch receives events a and c, it is guaranteed to receive b as long b is in the available history window.
- Atomic - a list of events is guaranteed to encompass complete revisions. Updates in the same revision over multiple keys will not be split over several lists of events.
- Resumable - A broken watch can be resumed by establishing a new watch starting after the last revision received in a watch event before the break, so long as the revision is in the history window.
- Bookmarkable - Progress notification events guarantee that all events up to a revision have been already delivered.
etcd does not ensure linearizability for watch operations. Users are expected to verify the revision of watch events to ensure correct ordering with other operations.
Lease APIs
etcd provides a lease mechanism . The primary use case of a lease is implementing distributed coordination mechanisms like distributed locks. The lease mechanism itself is simple: a lease can be created with the grant API, attached to a key with the put API, revoked with the revoke API, and will be expired by the wall clock time to live (TTL). However, users need to be aware about the important properties of the APIs and usage for implementing correct distributed coordination mechanisms.
etcd specific definitions
Operation completed
An etcd operation is considered complete when it is committed through consensus,
and therefore “executed” – permanently stored – by the etcd storage engine.
The client knows an operation is completed when it receives a response from the
etcd server. Note that the client may be uncertain about the status of an
operation if it times out, or there is a network disruption between the client
and the etcd member. etcd may also abort operations when there is a leader
election. etcd does not send abort responses to clients’ outstanding requests
in this event.
Revision
An etcd operation that modifies the key value store is assigned a single increasing revision. A transaction operation might modify the key value store multiple times, but only one revision is assigned. The revision attribute of a key value pair that was modified by the operation has the same value as the revision of the operation. The revision can be used as a logical clock for key value store. A key value pair that has a larger revision is modified after a key value pair with a smaller revision. Two key value pairs that have the same revision are modified by an operation “concurrently”.
8 - etcd versus other key-value stores
The name “etcd” originated from two ideas, the unix “/etc” folder and “d"istributed systems. The “/etc” folder is a place to store configuration data for a single system whereas etcd stores configuration information for large scale distributed systems. Hence, a “d"istributed “/etc” is “etcd”.
etcd is designed as a general substrate for large scale distributed systems. These are systems that will never tolerate split-brain operation and are willing to sacrifice availability to achieve this end. etcd stores metadata in a consistent and fault-tolerant way. An etcd cluster is meant to provide key-value storage with best of class stability, reliability, scalability and performance.
Distributed systems use etcd as a consistent key-value store for configuration management, service discovery, and coordinating distributed work. Many organizations use etcd to implement production systems such as container schedulers, service discovery services, and distributed data storage. Common distributed patterns using etcd include leader election , distributed locks , and monitoring machine liveness.
Use cases
- Container Linux by CoreOS: Applications running on Container Linux get automatic, zero-downtime Linux kernel updates. Container Linux uses locksmith to coordinate updates. Locksmith implements a distributed semaphore over etcd to ensure only a subset of a cluster is rebooting at any given time.
- Kubernetes stores configuration data into etcd for service discovery and cluster management; etcd’s consistency is crucial for correctly scheduling and operating services. The Kubernetes API server persists cluster state into etcd. It uses etcd’s watch API to monitor the cluster and roll out critical configuration changes.
Comparison chart
Perhaps etcd already seems like a good fit, but as with all technological decisions, proceed with caution. Please note this documentation is written by the etcd team. Although the ideal is a disinterested comparison of technology and features, the authors’ expertise and biases obviously favor etcd. Use only as directed.
The table below is a handy quick reference for spotting the differences among etcd and its most popular alternatives at a glance. Further commentary and details for each column are in the sections following the table.
| etcd | ZooKeeper | Consul | NewSQL (Cloud Spanner, CockroachDB, TiDB) | |
|---|---|---|---|---|
| Concurrency Primitives | Lock RPCs , Election RPCs , command line locks , command line elections , recipes in go | External curator recipes in Java | Native lock API | Rare , if any |
| Linearizable Reads | Yes | No | Yes | Sometimes |
| Multi-version Concurrency Control | Yes | No | No | Sometimes |
| Transactions | Field compares, Read, Write | Version checks, Write | Field compare, Lock, Read, Write | SQL-style |
| Change Notification | Historical and current key intervals | Current keys and directories | Current keys and prefixes | Triggers (sometimes) |
| User permissions | Role based | ACLs | ACLs | Varies (per-table GRANT , per-database roles ) |
| HTTP/JSON API | Yes | No | Yes | Rarely |
| Membership Reconfiguration | Yes | >3.5.0 | Yes | Yes |
| Maximum reliable database size | Several gigabytes | Hundreds of megabytes (sometimes several gigabytes) | Hundreds of MBs | Terabytes+ |
| Minimum read linearization latency | Network RTT | No read linearization | RTT + fsync | Clock barriers (atomic, NTP) |
ZooKeeper
ZooKeeper solves the same problem as etcd: distributed system coordination and metadata storage. However, etcd has the luxury of hindsight taken from engineering and operational experience with ZooKeeper’s design and implementation. The lessons learned from Zookeeper certainly informed etcd’s design, helping it support large scale systems like Kubernetes. The improvements etcd made over Zookeeper include:
- Dynamic cluster membership reconfiguration
- Stable read/write under high load
- A multi-version concurrency control data model
- Reliable key monitoring which never silently drop events
- Lease primitives decoupling connections from sessions
- APIs for safe distributed shared locks
Furthermore, etcd supports a wide range of languages and frameworks out of the box. Whereas Zookeeper has its own custom Jute RPC protocol, which is totally unique to Zookeeper and limits its supported language bindings
, etcd’s client protocol is built from gRPC
, a popular RPC framework with language bindings for go, C++, Java, and more. Likewise, gRPC can be serialized into JSON over HTTP, so even general command line utilities like curl can talk to it. Since systems can select from a variety of choices, they are built on etcd with native tooling rather than around etcd with a single fixed set of technologies.
When considering features, support, and stability, new applications planning to use Zookeeper for a consistent key value store would do well to choose etcd instead.
Consul
Consul is an end-to-end service discovery framework. It provides built-in health checking, failure detection, and DNS services. In addition, Consul exposes a key value store with RESTful HTTP APIs. As it stands in Consul 1.0 , the storage system does not scale as well as other systems like etcd or Zookeeper in key-value operations; systems requiring millions of keys will suffer from high latencies and memory pressure. The key value API is missing, most notably, multi-version keys, conditional transactions, and reliable streaming watches.
etcd and Consul solve different problems. If looking for a distributed consistent key value store, etcd is a better choice over Consul. If looking for end-to-end cluster service discovery, etcd will not have enough features; choose Kubernetes, Consul, or SmartStack.
NewSQL (Cloud Spanner, CockroachDB, TiDB)
Both etcd and NewSQL databases (e.g., Cockroach , TiDB , Google Spanner ) provide strong data consistency guarantees with high availability. However, the significantly different system design parameters lead to significantly different client APIs and performance characteristics.
NewSQL databases are meant to horizontally scale across data centers. These systems typically partition data across multiple consistent replication groups (shards), potentially distant, storing data sets on the order of terabytes and above. This sort of scaling makes them poor candidates for distributed coordination as they have long latencies from waiting on clocks and expect updates with mostly localized dependency graphs. The data is organized into tables, including SQL-style query facilities with richer semantics than etcd, but at the cost of additional complexity for processing, planning, and optimizing queries.
In short, choose etcd for storing metadata or coordinating distributed applications. If storing more than a few GB of data or if full SQL queries are needed, choose a NewSQL database.
Using etcd for metadata
etcd replicates all data within a single consistent replication group. For storing up to a few GB of data with consistent ordering, this is the most efficient approach. Each modification of cluster state, which may change multiple keys, is assigned a global unique ID, called a revision in etcd, from a monotonically increasing counter for reasoning over ordering. Since there’s only a single replication group, the modification request only needs to go through the raft protocol to commit. By limiting consensus to one replication group, etcd gets distributed consistency with a simple protocol while achieving low latency and high throughput.
The replication behind etcd cannot horizontally scale because it lacks data sharding. In contrast, NewSQL databases usually shard data across multiple consistent replication groups, storing data sets on the order of terabytes and above. However, to assign each modification a global unique and increasing ID, each request must go through an additional coordination protocol among replication groups. This extra coordination step may potentially conflict on the global ID, forcing ordered requests to retry. The result is a more complicated approach with typically worse performance than etcd for strict ordering.
If an application reasons primarily about metadata or metadata ordering, such as to coordinate processes, choose etcd. If the application needs a large data store spanning multiple data centers and does not heavily depend on strong global ordering properties, choose a NewSQL database.
Using etcd for distributed coordination
etcd has distributed coordination primitives such as event watches, leases, elections, and distributed shared locks out of the box (Note that in the case of the distributed shared lock, users need to be aware about its non obvious properties. The details are described below). These primitives are both maintained and supported by the etcd developers; leaving these primitives to external libraries shirks the responsibility of developing foundational distributed software, essentially leaving the system incomplete. NewSQL databases usually expect these distributed coordination primitives to be authored by third parties. Likewise, ZooKeeper famously has a separate and independent library of coordination recipes. Consul, which provides a native locking API, goes so far as to apologize that it’s “not a bulletproof method ”.
In theory, it’s possible to build these primitives atop any storage systems providing strong consistency. However, the algorithms tend to be subtle; it is easy to develop a locking algorithm that appears to work, only to suddenly break due to thundering herd and timing skew. Furthermore, other primitives supported by etcd, such as transactional memory depend on etcd’s MVCC data model; simple strong consistency is not enough.
For distributed coordination, choosing etcd can help prevent operational headaches and save engineering effort.
Notes on the usage of lock and lease
etcd provides lock APIs which are based on the lease mechanism and its implementation in etcd . The basic idea of the lease mechanism is: a server grants a token, which is called a lease, to a requesting client. When the server grants a lease, it associates a TTL with the lease. When the server detects the passage of time longer than the TTL, it revokes the lease. While the client holds a non revoked lease it can claim that it owns access to a resource associated with the lease. In the case of etcd, the resource is a key in the etcd keyspace. etcd provides lock APIs with this scheme. However, the lock APIs cannot be used as mutual exclusion mechanism by themselves. The APIs are called lock because for historical reasons . The lock APIs can, however, be used as an optimization mechanism of mutual exclusion as described below.
The most important aspect of the lease mechanism is that TTL is defined as a physical time interval. Both of the server and client measures passing of time with their own clocks. It allows a situation that the server revokes the lease but the client still claims it owns the lease.
Then how does the lease mechanism guarantees mutual exclusion of the locking mechanism? Actually, the lease mechanism itself doesn’t guarantee mutual exclusion. Owning a lease cannot guarantee the owner holds a lock of the resource.
In the case of controlling mutual accesses to keys of etcd itself with etcd lock, mutual exclusion is implemented based on the mechanism of version number validation (it is sometimes called compare and swap in other systems like Consul). In etcd’s RPCs like Put or Txn, we can specify required conditions about revision number and lease ID for the operations. If the conditions are not satisfied, the operation can fail. With this mechanism, etcd provides distributed locking for clients. It means that a client knows that it is acquiring a lock of a key when its requests are completed by etcd cluster successfully.
In distributed locking literature similar designs are described:
- In the paper of Chubby , the concept of sequencer is introduced. We interpret that sequencer is an almost same to the combination of revision number and lease ID of etcd.
- In How to do distributed locking , Martin Kleppmann introduced the idea of fencing token. The authors interpret that fencing token is revision number in the case of etcd.
- In Practical Uses of Synchronized Clocks in Distributed Systems , we can find a description that Thor implements a distributed locking mechanism based on version number validation and lease.
Why do etcd and other systems provide lease if they provide mutual exclusion based on version number validation? Well, leases provide an optimization mechanism for reducing a number of aborted requests.
Note that in the case of etcd keys, it can be locked efficiently because of the mechanisms of lease and version number validation. If users need to protect resources which aren’t related to etcd, the resources must provide the version number validation mechanism and consistency of replicas like keys of etcd. The lock feature of etcd itself cannot be used for protecting external resources.
9 - Glossary
This document defines the various terms used in etcd documentation, command line and source code.
Alarm
The etcd server raises an alarm whenever the cluster needs operator intervention to remain reliable.
Authentication
Authentication manages user access permissions for etcd resources.
Client
A client connects to the etcd cluster to issue service requests such as fetching key-value pairs, writing data, or watching for updates.
Cluster
Cluster consists of several members.
The node in each member follows raft consensus protocol to replicate logs. Cluster receives proposals from members, commits them and apply to local store.
Compaction
Compaction discards all etcd event history and superseded keys prior to a given revision. It is used to reclaim storage space in the etcd backend database.
Election
The etcd cluster holds elections among its members to choose a leader as part of the raft consensus protocol.
Endpoint
A URL pointing to an etcd service or resource.
Key
A user-defined identifier for storing and retrieving user-defined values in etcd.
Key range
A set of keys containing either an individual key, a lexical interval for all x such that a < x <= b, or all keys greater than a given key.
Keyspace
The set of all keys in an etcd cluster.
Lease
A short-lived renewable contract that deletes keys associated with it on its expiry.
Member
A logical etcd server that participates in serving an etcd cluster.
Modification Revision
The first revision to hold the last write to a given key.
Peer
Peer is another member of the same cluster.
Proposal
A proposal is a request (for example a write request, a configuration change request) that needs to go through raft protocol.
Quorum
The number of active members needed for consensus to modify the cluster state. etcd requires a member majority to reach quorum.
Revision
A 64-bit cluster-wide counter that starts at 1 and is incremented each time the keyspace is modified.
Role
A unit of permissions over a set of key ranges which may be granted to a set of users for access control.
Snapshot
A point-in-time backup of the etcd cluster state.
Store
The physical storage backing the cluster keyspace.
Term
A term is a monotonically increasing integer that is associated with each leader election in the Raft algorithm. For a term there can only be one elected leader and term is incremented on leader change.
Transaction
An atomically executed set of operations. All modified keys in a transaction share the same modification revision.
Key Version
The number of writes to a key since it was created, starting at 1. The version of a nonexistent or deleted key is 0.
Watcher
A client opens a watcher to observe updates on a given key range.