# 基于 Kubernetes 的应用开发注意点

## 预料到应用被杀死或重新调度

应用开发者必须允许应用可以被相对频繁地迁移。主要从下面两个方面进行考虑。

### 本地 IP 和主机名会发生变化

当一个 Pod 被重新创建时，它的 IP 和 主机名会发生变化（StatefulSet 管理的 Pod 的主机名不会变化）。

解决方法：使用 FQDN 或 Service 访问 Pod，而不依赖 IP 地址。

### 写入磁盘的数据会消失

Pod 的重新调度以及内部容器的重启都会导致写入容器可写层的数据消失。

解决方法：使用存储卷来实现跨容器持久化数据。

## 以固定顺序启动 Pod

有时候，可能多个 Pod 之间是有严格的启动顺序的。

解决方法：可以使用 [init 容器](https://yangsijie151104.gitbook.io/k8s-note/pod/4.pod/init-rong-qi)，在 init 容器中探测依赖的其他 Pod 是否已运行，在确定已运行后，再允许常规容器启动即可。

{% hint style="danger" %}
使用 init 容器只能保证启动时的正确性，不能应对该 Pod 在运行过程中，其依赖的服务崩溃的情况。

因此最好在开发应用时，构建一个不需要它所依赖的服务都准备好后才能启动的应用。并且可以使用 [Readiness探针](https://yangsijie151104.gitbook.io/k8s-note/pod/4.pod/tan-zhen) 间歇性的探测其所依赖的服务是否运行正常。
{% endhint %}

## 合理处理容器的关闭操作

在了解了 [容器关闭流程](https://yangsijie151104.gitbook.io/k8s-note/pod/4.pod/sheng-ming-zhou-qi-gou-zi) 之后，需要在 Pod 被关闭时进行合理的收尾操作。

应用应该通过 **启动关闭流程** 来响应 SIGTERM 信号，并且在 **关闭流程** 结束后终止运行。或者可以通过 **停止前钩子** 来收取关闭通知。

需要给 Pod 制定充足的 **终止宽限时间**，保证 Pod 能够正常的结束。

### 将重要的关闭流程替换为专门的 Pod 完成

在一些会有许多重要数据保存在存储卷中的情况中，可能在指定的终止宽限时间内，来不及将所有数据迁移至其他 Pod 中。

解决方法：针对这类业务场景，可以使用一个 CronJob 对象，周期性的运行数据迁移的 Pod，查看是否存在孤立的存储卷，若存在，则将其中的数据迁移至存活的 Pod 中。

## 确保所有客户端请求受到妥善处理

### Pod 启动时避免客户端连接断开

针对在 Pod 启动时，客户端连接断开的情况，可以为 Pod 设置 [Readiness探针](https://yangsijie151104.gitbook.io/k8s-note/pod/4.pod/tan-zhen)，只有当探针确认服务准备就绪时，才将 Pod 纳入 Service 的后端，为客户端提供服务。

### Pod 关闭时避免客户端连接断开

#### Pod 关闭时的事件

首先分析一下为何需要避免这种情况，这种情况是如何产生的。

下面这张图展示了在 Pod 被删除时发生的事件：

![Pod 删除时事件发生的时间线](https://2906552408-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-M6Ub8CloS5kJszh6xSR%2Fsync%2F25f211be58425af8abec6b32e59fa930689ff23c.png?generation=1592324945867809\&alt=media)

从 A 和 B 两条时间线中可以看出，**A 的操作少于 B 的操作**，因此在这种情况下，若 A 执行结束，即 Pod 中的容器已经被删除，但是 iptables 还存在，即还是会将客户端的请求转发至不存在的 Pod 中，这可能会导致用户收到如“连接被拒绝”之类的错误。

{% hint style="info" %}
🗣 如果是 B 事件先结束，是没关系的，因为移除 iptables 规则对已存在的连接没有影响，所以已存在的连接可以由容器优雅的关闭后，再关闭容器。
{% endhint %}

#### 解决方法

针对这种问题，唯一可以做的，就是在容器进程被结束前 **延长几秒钟**，让 **kube-proxy 更新完 iptables 后**，已经存在的连接被妥善处理后再退出。

妥善关闭应用步骤包括（即延长了 A 事件的完成时间，等到 B 事件完成后再结束）：

1. 等待几秒钟，然后停止接收新的连接（等待 kube-proxy 更新完 iptables 规则，即 B 事件完成）；
2. 关闭所有没有请求过来的长连接；
3. 等待所有的请求完成；
4. 完全关闭应用；

![在容器收到 SIGTERM 信号后妥善处理已存在和建立的连接](https://2906552408-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-M6Ub8CloS5kJszh6xSR%2Fsync%2Fd9d92fca387565d419e0ad17532d22ac167c8bd4.png?generation=1592324946041250\&alt=media)

至少可以通过使用一个 [停止前钩子](https://yangsijie151104.gitbook.io/k8s-note/pod/4.pod/sheng-ming-zhou-qi-gou-zi) 来等待几秒钟再退出（因为当存在 **停止前钩子** 时，只有当钩子执行结束，无论成功失败，才会发送 SIGTERM 信号）：

```yaml
lifecycle:
    preStop:    # 停止前钩子
        exec:    # Exec 机制；沉睡5s后，以15为状态码退出
            command: 
            - sh
            - -c
            - "sleep 5"
```

## 让应用方便运行和管理

### 构建可管理的容器镜像

使用的基础镜像能够小一点最好，可以除了包含业务代码外，再包含一些常用工具即可。

### 给镜像打标签，正确使用 ImagePullPolicy

给镜像打标签时，应当合理，可以根据版本进行区分，不要全部使用 latest。

使用 ImagePullPolicy 时，可以根据需要进行指定。

### 给资源使用多维度标签

给所有使用的资源都打上标签，可以有助于管理，使用多维度标签可以使得能够通过不同维度来选择他们。

标签可以包含如下的内容：

* 资源所属的应用（或微服务）名称
* 应用层级（前端、后端，等等）
* 运行环境（开发、测试、预发、生产，等等）
* 版本号
* 发布类型（稳定版、金丝雀、蓝绿开发中的绿色或蓝色，等等）
* 租户（如果你在每个租户中运行不同的 Pod 而不是使用命名空间）
* 分片（带分片的系统）

### 通过注解描述资源

资源至少应该包括一个 **描述资源的注解** 和一个 **描述资源负责人的注解**。

在微服务框架中，Pod 应该包含一个注解来描述 **该 Pod 依赖的其他服务名称**。

### 给进程终止提供更多信息

为了更加容易的调查容器的终止原因，可以在 `pod.spec.containers.terminationMessagePath` 中指定终止消息写入文件的路径，然后在容器中将终止时的消息写入该文件中。这样，当容器被终止时，可以通过 `kubectl describe` 查看对应容器的 **State** 或 **Last State** 中的 **Message** 就可以看到终止原因。

下面展示一个案例：

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-termination-message
spec:
  containers:
  - image: busybox
    name: main
    command:
    - sh
    - -c
    - 'echo "I''ve had enough" > /var/termination-reason ; exit 1'
    terminationMessagePath: /var/termination-reason    # 指定终止消息的写入文件路径
```

通过 `kubectl describe` 查看该容器的终止原因：

```bash
...
Containers:
  main:    # 对应容器
    Container ID:  docker://90b29aa2accd09bdb2fd5a2474f002f181e1bb961fd2e76de017109198b25db5
    Image:         busybox
    Image ID:      docker-pullable://busybox@sha256:95cf004f559831017cdf4628aaf1bb30133677be8702a8c5f2994629f637a209
    Port:          <none>
    Host Port:     <none>
    Command:
      sh
      -c
      echo "I've had enough" > /var/termination-reason ; exit 1
    State:       Running
      Started:   Wed, 17 Jun 2020 12:53:06 +0800
    Last State:  Terminated
      Reason:    Error
      Message:   I've had enough    # 看到终止原因

      Exit Code:    1
      Started:      Wed, 17 Jun 2020 12:51:23 +0800
      Finished:     Wed, 17 Jun 2020 12:51:23 +0800
...
```

{% hint style="info" %}
🐳 如果容器没有向任何文件写入消息，可以将 `pod.spec.containers.terminationMessagePolicy` 设置为 `FallbackToLogsOnError`，在容器 **启动失败** 的情况下，容器的最后几行日志会被当作终止消&#x606F;**（容器将消息输出至标准输出终端）**。

下面展示一个案例：

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-termination-message-policy
spec:
  containers:
  - image: busybox
    name: main
    command:
    - sh
    - -c
    - 'echo "I''ve had enough"; exit 1'    # 将消息输出至标准输出终端
    terminationMessagePolicy: FallbackToLogsOnError    # 修改终止消息输出策略
```

通过 `kubectl describe` 命令查看：

```bash
...
    State:      Terminated
      Reason:   Error
      Message:  I've had enough

      Exit Code:  1
      Started:    Wed, 17 Jun 2020 13:22:46 +0800
      Finished:   Wed, 17 Jun 2020 13:22:46 +0800
    Last State:   Terminated
      Reason:     Error
      Message:    I've had enough

      Exit Code:    1
      Started:      Wed, 17 Jun 2020 13:19:57 +0800
      Finished:     Wed, 17 Jun 2020 13:19:57 +0800
...
```

{% endhint %}

## 处理应用日志

当容器内应用将日志写到 **标准输出终端** 时，可以直接使用 `kubectl logs` 命令查看日志。

如果想查看当前容器之前的崩溃容器的日志，可以加上 `--previous` 参数查看日志。

如果应用比较庞大，也可以使用 EFK 栈去实现一套日志采集引擎 —— ElasticSearch、FluentD、Kibana。首先在每个宿主机中部署一个 FluentD 代理（通过 DaemonSet 作为 Pod 部署），负责从容器中采集日志，给日志打上和 Pod 相关的信息；然后将日志信息发送给 ElasticSearch，由 ES 持久化存储，并提供高级搜索功能；最后由 Kibana 提供前端可视化界面，对接 ES，为运维人员提供可视化操作。
