# etcd分布式存储

etcd是一种响应快、分布式、一致的key-value存储。因为它是分布式的，因此可以运行多个etcd示例来获取高可用性和更好的性能。

etcd是存储Kubernetes**集群状态**和**元数据**的唯一的地方。

在集群中，所有组件都需要通过API服务器与其他组件通信，因此*唯一*与etcd通信的是API服务器。

## 乐观锁

API服务器并发的与etcd通信时，由于API服务器支持多实例并发，因此在访问资源时需要使用**锁机制**。

在这里，采用的是**乐观锁**的方式。

多实例使用乐观锁机制更新数据的过程如下：

1. 从etcd中读取资源数据，每个资源都包含一个**版本号**，即`metadata.resourceVersion`，假设为A；
2. 更新资源数据；
3. 提交更新数据时，再次读取该资源，并查看其**版本号**是否为A，若为A，则表示资源未被更新过，直接提交即可；否则，重新进入第1步；

## 资源存储在etcd中

现在新的Kubernetes版本已经开始使用etcd v3处理数据的存储。

之前提到过，etcd v3使用key-value存储，在Kubernetes中使用的是包含斜杠格式的key，类似于目录的形式，但是注意，etcd v3不支持目录形式的key，只是看起来像而已。

集群的数据的key都是以`/registry`为前缀的，如下查看以`/registry`为前缀的所有的key：

```bash
$ ETCDCTL_API=3 etcdctl --endpoints https://127.0.0.1:2379 --cacert ca.crt --cert server.crt --key server.key get /registry --prefix --keys-only
/registry/apiregistration.k8s.io/apiservices/v1.
/registry/apiregistration.k8s.io/apiservices/v1.admissionregistration.k8s.io
/registry/apiregistration.k8s.io/apiservices/v1.apiextensions.k8s.io
/registry/apiregistration.k8s.io/apiservices/v1.apps
/registry/apiregistration.k8s.io/apiservices/v1.authentication.k8s.io
...
```

从打印出的结果可以看到，key的组织结构为`/registry/{资源类型}/{命名空间}/{资源实例名}`。

{% hint style="info" %}
`ETCDCTL_API=3`：指定使用etcd v3；

`--endpoints`：指定etcd服务器的IP地址即端口；

`--cacert`：用于验证是否是真的etcd服务器的CA文件路径；

`--cert`：用于HTTPS通信的，验证客户端安全的TLS证书文件路径；

`--key`：用于HTTPS通信的，验证客户端安全的TLS密钥文件路径；
{% endhint %}

Kubernetes集群在将数据保存至etcd中时，会先将数据使用protobuf编码，再存入etcd中，从而压缩数据量，读取时再将数据进行解码，可以看下面这个例子，查看`default`命名空间中，名为`curl`的Pod：

```bash
$ ETCDCTL_API=3 etcdctl --endpoints https://127.0.0.1:2379 --cacert ca.crt --cert server.crt --key server.key get /registry/pods/default/curl
/registry/pods/default/curl
k8s

v1Pod�

�
curldefault"*$4c0d9790-d5ec-4a59-9de1-3a0d924a24822����b�
0kubectl.kubernetes.io/last-applied-configuration�{"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"curl","namespace":"default"},"spec":{"containers":[{"command":["sleep","9999999"],"image":"tutum/curl","name":"main"}]}}
z�
1
default-token-67tb62
default-token-67tb6��
main
tutum/curlsleep9999999*BJJ
default-token-67tb6-/var/run/secrets/kubernetes.io/serviceaccount"2j/dev/termination-logrAlways����FileAlways 2
                     ClusterFirstBdefaultJdefaultminikubeX`hr���default-scheduler�6
node.kubernetes.io/not-readyExists"    NoExecute(��8
node.kubernetes.io/unreachableExists"    NoExecute(�����
Running#

InitializedTru����*2
ReadyTru����*2'
ContainersReadyTru����*2$

PodScheduledTru����*2"*192.168.99.1062
172.17.0.����B�
main


����o�Error�������:Idocker://dc7751dcdce4bcc3a85dd37ab32915fade2e4020a8acebf029322c15408f453e (
2tutum/curl:latest:ddocker-pullable://tutum/curl@sha256:b6f16e88387acd4e6326176b212b3dae63f5b2134e69560d0b0673cfb0fb976fBIdocker://dcb335aa117717980bfa42fb512e86a27a445a078afa1caea52e5a45abd2335bHJ
BestEffortZb


172.17.0.2"
```

## 确保etcd集群一致性

为了保持高性能，往往会运行多个etcd实例，多个etcd实例中的数据需要保持一致。

etcd使用RAFT一致性算法，确保在任何时间点，每个节点的状态要么是大部分（法定数量，通常是一半）节点的状态，要么是之前确认过的状态。

一致性算法，要求etcd集群大部分（法定数量，通常是一半）节点参与，才能进行到下一个状态。

假设由于某些原因，etcd集群分为两个不互联的节点组（这种情况称为**脑裂**），当有访问集群的请求时，有两种可能：

1. 请求访问的是大部分节点组中的节点：由于节点组中有过半的节点，因此可以更改etcd集群的状态；
2. 请求访问的是小部分节点组中的节点：由于节点组中的节点数量不过半，则无法参与状态变更；

当两个节点组之间恢复连接时，第二个组的节点会更新为第一个组的节点的状态。

{% hint style="info" %}
从上面的分析中也可以看出，集群的节点数量一定要是**奇数**。
{% endhint %}

![脑裂情况下，只有拥有大部分（法定数量）节点的组会接受状态变更](https://2906552408-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-M6Ub8CloS5kJszh6xSR%2Fsync%2F6873aaefa9f95152ddc60d99bc400ae2e0f6d7d6.png?generation=1588594615735866\&alt=media)
