# 高可用

在本节，将针对 Kubernetes 实现高可用进行介绍，主要包括两个方面的高可用：

1. 客户应用的高可用；
2. 集群自身高可用；

## 应用高可用

微服务应用在 Kubernetes 集群中运行可以充分的发挥优势，例如 Kubernetes 提供的 `Deployment` 应用可以维护应用的多个实例，并且若有实例挂掉，`Deployment` 控制器也会立刻进行恢复，以保证服务不间断工作。

针对无服务的应用，使用这种水平扩展的方式比较简单，因为它们可以同时运行，然而对于不能直接水平扩展的应用则需要采取 **领导选举机制**。

### 领导选举机制

针对有状态应用，不可以直接水平扩展，因为这有可能导致数据冲突等不可预测的问题。

**领导选举机制** 是指在多应用实例情况下，对谁是领导者达成一致的机制。例如，领导者要么是唯一执行任务的那个，其他所有节点都在等待领导者宕机，然后成为新的领导者；或者都是活跃的节点，只是领导者负责写操作，其他节点负责读操作（数据库的读写分离）。

#### 应用中领导选举机制的实现

幸运的是，在 Kubernetes 集群中，不需要我们自己的应用实现该机制，官方已经为我们提供了相关的容器可以替我们完成领导选举操作，镜像名为 `k8s.gcr.io/leader-elector`，由于 GFW 的原因，国内的用户可以下载 `fredrikjanssonse/leader-elector:0.6` 并重命名即可，同时加上参数 `--election={app_name}` 。

该容器完成的功能就是竞争领导者，当创建多个该容器时，这些容器会循环竞争，并选择领导者。我们先不考虑该容器是如何竞选领导的，我们只要知道通过向其发送 HTTP 请求的方式即能知道当前的领导者是谁即可：

```bash
curl localhost:4040
{"name":"leader-elector-6f5c59d88d-ttkbl"}%
```

> 从上面的输出中可以很容易的知道当前的领导者是名为 `leader-elector-6f5c59d88d-ttkbl` 的这个 Pod

由于一个 Pod 中可以包含多个容器，因此我们可以将我们的应用容器与该容器整合起来，将该容器以 sidecar 的形式与应用容器整合成一个 Pod，由于同一个 Pod 中的所有容器共享网络命名空间，因此在应用容器中同样可以使用上述方法知道哪个应用容器所在 Pod 是领导者。

**领导选举机制原理**

虽然可以直接使用官方提供的容器达到该功能，但是还是需要了解一下其中的原理的。

该容器的领导竞选相关原理可以从[这里](https://github.com/kubernetes-retired/contrib/tree/master/election)找到。

在 Kubernetes 中竞选领导需要用到锁机制，在集群中直接使用 `Endpoint` 资源或 `ConfigMap` 资源作为资源锁，因为这些资源是 Pod 通过 API 服务器可以共同接触到的，默认情况下使用 `Endpoint` 资源。

在这两个资源（或者说所有资源）中，竞选过程中需要使用到的有两个字段：

* `metadata.annotations` : 以键值对的形式存在，所有竞争者会尝试向 `control-plane.alpha.kubernetes.io/leader` 键中填入值，若填入成功，则称为领导；
* `metadata.resourceVersion` : 用于实现乐观锁，提高效率；

当使用 `k8s.gcr.io/leader-elector` 容器时，需要向其中传递参数，即上述的 `--election={app_name}`，在容器运行后，便会在当前 namespace 下创建一个名为 `{app_name}` 的 Endpoint 资源，并填充上面说的两个字段。

案例如下：

```yaml
apiVersion: v1
kind: Endpoints
metadata:
  annotations:
    control-plane.alpha.kubernetes.io/leader: '{"holderIdentity":"leader-elector-6f5c59d88d-ttkbl","leaseDurationSeconds":10,"acquireTime":"2020-03-11T12:58:17Z","renewTime":"2020-03-11T12:59:07Z","leaderTransitions":0}'
  creationTimestamp: "2020-03-11T12:37:06Z"
  name: example
  namespace: default
  resourceVersion: "787550"
  selfLink: /api/v1/namespaces/default/endpoints/example
  uid: 2a3897d9-386a-46a6-b3a6-725023a2ba97
```

`annotation` 中的 `control-plane.alpha.kubernetes.io/leader` 键对应的值中的个字段含义如下：

* `holderIdentity` : 当前资源锁的所有者
* `leaseDurationSeconds` : 资源锁租约时间是多长（资源锁的有效时长，所有者霸住该资源锁的时间超过这个限制，则会被其他竞争者抢占）
* `acquireTime` : 锁获得的时间
* `renewTime` : 续租的时间
* `leaderTransitions` : leader 进行切换的次数

`resourceVersion` 用于乐观锁，即当竞争者 A 尝试竞争时，首先读取版本号，假设为 1，在本地将信息更新后准备填充至 `annotation` 中之前，先再次读取版本号，若仍然为 1，则可以填充；若大于 1，则表示资源被更新了，因此需要重新读取版本号，并重新更新信息，再尝试抢占。

## Kubernetes 集群高可用

K8S 集群的高可用主要是指其控制平面的高可用，即下面几个组件：

* etcd
* API 服务器
* ControllerManager
* Scheduler

![三节点高可用集群](https://2906552408-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-M6Ub8CloS5kJszh6xSR%2Fsync%2Ffd8254e98e87be7d0730e5379ef67d119eca971b.png?generation=1588594610853194\&alt=media)

在上面这个高可用集群中，控制平面的每一个部分都做了高可用，包括用于接受请求的负载均衡器。

### 负载均衡器

请求的负载均衡器可以使用 **硬件** 或 **软件** 实现，在通常情况下使用 nginx, haproxy 等软件实现，其可以将请求分发至后端不同的 API 服务器中。

而负载均衡的高可用需要搭配虚拟 IP (vip) 以及 Keep-alive 软件实现，即所有来自客户端的请求都发送至虚拟 IP 中，即对用户而言可见的只有虚拟 IP，由软件决定将其交付给哪一个负载均衡器。

实际上不一定需要使用 keep-alive，直接使用 iptables 设定规则，将请求的目的 IP 进行更改同样能达到该效果。

### etcd

etcd 原生支持多实例运行，即原生支持高可用。如 [etcd 章节](https://yangsijie151104.gitbook.io/k8s-note/kubernetes-jie-shao/2.kubernetes-jie-shao/ji-qun-jia-gou/etcd-fen-bu-shi-cun-chu) 所述，部署大于等于 3 个的奇数个实例即可实现高可用。

etcd 会跨实例复制数据以保证彼此之间的数据一致性。

### API 服务器

API 服务器直接通过运行多实例的方式就可以实现高可用，因为它是无状态应用（所有数据均存放在 etcd 中，其本身不做缓存），它们彼此之间不会感知对方存在。

API 服务器 **只会与本地的 etcd 交互**，这样读写效率更高，因此在高可用的情况下，API 服务器和 etcd 实例是成对出现的。

多 API 服务器访问 etcd 时，会使用乐观锁，同样是看资源的 `metadata.resourceVersion` 的值，具体可见 [etcd 章节](https://yangsijie151104.gitbook.io/k8s-note/kubernetes-jie-shao/2.kubernetes-jie-shao/ji-qun-jia-gou/etcd-fen-bu-shi-cun-chu) 的描述。

### ControllerManager 与 Scheduler

ControllerManager 和 Scheduler 都不是无状态应用，它们都需要接收来自 API 服务器的通知，并执行操作。在多实例的情况下，若多个实例执行了同一个操作，将会造成不可预测的问题。

因此这两个组件的高可用不能仅仅通过水平扩展的方式解决，需要使用 **领导选举机制**，即同一时间内只能有一个实例执行任务，其他实例都等待成为领导者。

这两个组件原生支持 **领导者选举机制**，该机制的实现与上述的 `k8s.gcr.io/leader-elector` 容器一致，都是通过 Endpoint 资源或 ConfigMap 资源实现的，可以通过 `--leader-elect` 开启组件的选举机制，该选项默认为 `true`。

ControllerManager 与 Scheduler 可以与 API 服务器 和 etcd 在同一个节点上，此时直接与本地 API 服务器通信；也可以不在同一个节点上，此时需要通过负载均衡器与 API 服务器通信。
