# Kubernetes Deployment 源码分析（一）


## 概述

**Deployment** 是最常用的 Kubernetes 原生 **Workload** 资源之一，我们一开始尝试使用 Kubernetes 的时候大概率就是从运行一个 Deployment 类型的工作负载开始的。今天开始我们计划分成几讲来从 Deployment 的特性介绍、源码分析等方面深度剖析 Deployment 资源和其背后的 Deployment 控制器。

Deployment 的基础特性大家基本都熟悉，所以本文我们不计划赘述 Deployment 的所有功能细节，而是从滚动更新等不算太基础的特性入手，看下 Deployment 支持哪些玩法，为后面分析源码做准备。

《Kubernetes Deployment Controller 源码分析》分为两讲：

- [《Kubernetes Deployment 源码分析（一）》](../k8s-deployment-1/) - 功能特性
- [《Kubernetes Deployment 源码分析（二）》](../k8s-deployment-2/) - 源码流程

## Deployment 基础

我们创建一个简单的 Deployment，然后看下一些小细节。

### 创建 Deployment

以运行 nginx 为例，我们可以通过 Deployment 来拉起一个 3 副本的 nginx 负载：**nginx-dp**

- nginx-dp.yaml

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-dp
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
```

通过 `kubectl create -f nginx-dp.yaml` 我们可以创建这个 Deployment 资源。

```shell
# kubectl create -f nginx-dp.yaml
deployment.apps/nginx-dp created
# kubectl get deploy
NAME       READY   UP-TO-DATE   AVAILABLE   AGE
nginx-dp   1/3     3            1           3s
# kubectl get deploy
NAME       READY   UP-TO-DATE   AVAILABLE   AGE
nginx-dp   3/3     3            3           10s
```

等一会，就可以通过 `kubectl get deploy` 命令看到所有 Pod 都起来了，这里关注下输出字段含义（NAME 和 AGE 就不用说了）：

- **UP-TO-DATE**：多少副本已经更新到期望状态了
- **AVAILABLE**：多少副本已经可以提供服务了
- **READY**：可以提供服务的副本数/期望副本数

### ReplicaSet

- 查询 ReplicaSet

```shell
# kubectl get rs --selector=app=nginx
NAME                  DESIRED   CURRENT   READY   AGE
nginx-dp-66b6c48dd5   3         3         3       9m54s
```

前面创建了 Deployment 之后可以看到集群里多了一个 ReplicaSet 资源，也就是说 Deployment 管理的其实是 ReplicaSet 而不是直接管理 Pod，我们继续看下这个 ReplicaSet 的定义来验证下这个想法：

```shell
# kubectl get rs nginx-dp-66b6c48dd5 -o yaml
apiVersion: apps/v1
kind: ReplicaSet
// ……
  ownerReferences:
  - apiVersion: apps/v1
    blockOwnerDeletion: true
    controller: true
    kind: Deployment
    name: nginx-dp
    uid: 97736b65-0171-4916-bb18-feccc343ac14
  resourceVersion: "1099157"
  uid: 83ac5660-28eb-4d40-beb1-cb5ceb6928b6
// ……
```

这里可以看到这个 ReplicaSet 属于 Deployment 类型的 nginx-dp 资源。同样的方法可以看到对应 Pod 是属于 ReplicaSet 管理的。

到这里，我们可以猜下 **Deployment Controller** 的实现原理，大概可以想到其通过管理 ReplicaSet 的生命周期，借助 ReplicaSet Controller 提供的能力间接完成了 Pod 生命周期的管理；另外可以通过创建多个 ReplicaSet 资源，控制其副本数来实现滚动更新和回滚等操作。这样 Deployment Controller 的实现逻辑就相对“高层”了。

## 滚动更新

- 通过 `kubectl set` 命令来更新镜像：

```shell
# kubectl set image deployment/nginx-dp nginx=nginx:1.16.1
deployment.apps/nginx-dp image updated
```

- 查看 Event

```shell
# kubectl describe deploy nginx-dp
// ……

Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  26m   deployment-controller  Scaled up replica set nginx-dp-66b6c48dd5 to 3
  Normal  ScalingReplicaSet  88s   deployment-controller  Scaled up replica set nginx-dp-559d658b74 to 1
  Normal  ScalingReplicaSet  87s   deployment-controller  Scaled down replica set nginx-dp-66b6c48dd5 to 2
  Normal  ScalingReplicaSet  87s   deployment-controller  Scaled up replica set nginx-dp-559d658b74 to 2
  Normal  ScalingReplicaSet  86s   deployment-controller  Scaled down replica set nginx-dp-66b6c48dd5 to 1
  Normal  ScalingReplicaSet  86s   deployment-controller  Scaled up replica set nginx-dp-559d658b74 to 3
  Normal  ScalingReplicaSet  84s   deployment-controller  Scaled down replica set nginx-dp-66b6c48dd5 to 0
```

从 Event 里可以看到 deployment-controller 通过调整 ReplicaSet 资源 nginx-dp-66b6c48dd5 和 nginx-dp-559d658b74 的副本数完成了这次滚动更新，先看下这两个 ReplicaSet：

```shell
# kubectl get rs --selector=app=nginx
NAME                  DESIRED   CURRENT   READY   AGE
nginx-dp-559d658b74   3         3         3       134m
nginx-dp-66b6c48dd5   0         0         0       159m
```

可以看到这时候新增了一个 nginx-dp-559d658b74，副本数是 3，同时老的 nginx-dp-66b6c48dd5 变成了 0 副本。这个过程大概是这样：

1. nginx-dp-66b6c48dd5 to 3 / replica set nginx-dp-559d658b74 to 1 -> 新 rs 增加一个副本到 1；合计 4 副本
2. Scaled down replica set nginx-dp-66b6c48dd5 to 2 -> 老 rs 减少一个副本到 2；合计 3 副本
3. Scaled up replica set nginx-dp-559d658b74 to 2 -> 新 rs 增加一个副本到 2；合计 4 副本
4. Scaled down replica set nginx-dp-66b6c48dd5 to 1 -> 老 rs 减少一个副本到 1；合计 3 副本
5. Scaled up replica set nginx-dp-559d658b74 to 3 -> 新 rs 增加一个副本到 3；合计 4 副本
6. Scaled down replica set nginx-dp-66b6c48dd5 to 0 -> 老 rs 减少一个副本到 0；合计 3 副本

## 失败回滚

### 历史版本

先看下怎么查询更新历史：

```shell
# kubectl rollout history deployments/nginx-dp
deployment.apps/nginx-dp
REVISION  CHANGE-CAUSE
1         <none>
2         <none>
```

这里可以看到一个细节，CHANGE-CAUSE 是空的，这个字段其实是从 *kubernetes.io/change-cause* 注解中拿的，我们加个注解试一下：

```shell
kubectl annotate deployment/nginx-dp kubernetes.io/change-cause="image updated to 1.16.1"
```

再查一次：

```shell
# kubectl rollout history deployments/nginx-dp
deployment.apps/nginx-dp
REVISION  CHANGE-CAUSE
1         <none>
2         image updated to 1.16.1
```

第一个版本怎么办呢？这里大概可以猜到要存储多个版本的 CHANGE-CAUSE 信息，这个注解应该是用的 ReplicaSet 里的，所以我们尝试这样补充第一个版本的注解：

```shell
kubectl annotate rs/nginx-dp-66b6c48dd5 kubernetes.io/change-cause="nginx deployment created"
```

再查一次：

```shell
# kubectl rollout history deployments/nginx-dp
deployment.apps/nginx-dp
REVISION  CHANGE-CAUSE
1         nginx deployment created
2         image updated to 1.16.1
```

现在就比较和谐了，需要指定版本回滚的时候不迷路。

### 回滚

- 设置一个不存在的镜像版本来模拟更新失败场景：

```shell
# kubectl set image deployment/nginx-dp nginx=nginx:1.161
deployment.apps/nginx-dp image updated
# kubectl get rs --selector=app=nginx
NAME                  DESIRED   CURRENT   READY   AGE
nginx-dp-559d658b74   3         3         3       168m
nginx-dp-66b6c48dd5   0         0         0       3h13m
nginx-dp-66bc5d6c8    1         1         0       6s
# kubectl get pod --selector=app=nginx
NAME                        READY   STATUS             RESTARTS   AGE
nginx-dp-559d658b74-l4bq7   1/1     Running            0          170m
nginx-dp-559d658b74-qhh8m   1/1     Running            0          170m
nginx-dp-559d658b74-vbtl5   1/1     Running            0          170m
nginx-dp-66bc5d6c8-tl848    0/1     ImagePullBackOff   0          2m2s
```

- 设置个注解：

```shell
# kubectl annotate deployment/nginx-dp kubernetes.io/change-cause="image updated to 1.161"
deployment.apps/nginx-dp annotated
# kubectl rollout history deployments/nginx-dp
deployment.apps/nginx-dp
REVISION  CHANGE-CAUSE
1         nginx deployment created
2         image updated to 1.16.1
3         image updated to 1.161
```

- 回滚到 revision 2：

```shell
# kubectl rollout undo deployment/nginx-dp
deployment.apps/nginx-dp rolled back
# kubectl rollout history deployments/nginx-dp
deployment.apps/nginx-dp
REVISION  CHANGE-CAUSE
1         nginx deployment created
3         image updated to 1.161
4         image updated to 1.16.1
```

这时候版本 2 变成了最新的版本：4

- 查看某个版本的详细配置：

```shell
# kubectl rollout history deployments/nginx-dp --revision=1
deployment.apps/nginx-dp with revision #1
Pod Template:
  Labels:	app=nginx
	pod-template-hash=66b6c48dd5
  Annotations:	kubernetes.io/change-cause: nginx deployment created
  Containers:
   nginx:
    Image:	nginx:1.14.2
    Port:	80/TCP
    Host Port:	0/TCP
    Environment:	<none>
    Mounts:	<none>
  Volumes:	<none>
```

- 指定版本回滚：

```shell
# kubectl rollout undo deployment/nginx-dp --to-revision=1
deployment.apps/nginx-dp rolled back
# kubectl rollout history deployments/nginx-dp
deployment.apps/nginx-dp
REVISION  CHANGE-CAUSE
3         image updated to 1.161
4         image updated to 1.16.1
5         nginx deployment created
```

## 其他特性

最后我们看下 Deployment 类型 spec 的全部属性：

- **minReadySeconds**：默认值为 0，表示一个 pod reday 之后多长时间可以提供服务；换句话说配置成1就是 pod ready 之后 1s 才对外提供服务；
- **paused**：挂起；
- **progressDeadlineSeconds**：默认 600，表示处理一个 Deployment 任务的超时时间，比如 10 分钟到了还没有升级成功，则标记为 failed 状态；
- **replicas**：副本数；
- **revisionHistoryLimit**：默认是 10，表示保留的历史版本数量；
- **selector**：标签选择器；
- **strategy**：表示 Deployment 更新 pod 时的替换策略；
- **template：Pod** 模板；

这里的 **strategy** 有两个属性，分别是：**type** 和 **rollingUpdate**。type 可选值是："Recreate" 和 "RollingUpdate"，默认为 "RollingUpdate"。strategy.rollingUpdate 有两个属性：

- **maxSurge**： 表示滚动更新的时候最多可以比期望副本数多几个，数字或者百分比配置都行；比如 1 表示更新过程中最多同时新增 1 个副本，然后等一个老副本删掉之后才能继续增加 1 个新副本；百分比计算的时候向上取整；
- **maxUnavailable**：表示滚动更新的时候可以有多少副本不可用，同样是数字或者百分比配置；比如期望副本数是 3，1 则表示最多删除副本到剩下 2，然后要等新副本创建才能继续删除；百分比计算的时候向下取整；

## 小结

本文我们的目的是知道 Deployment 的全部特性，进而为后面的源码分析做准备。在这个过程中我们没有赘述 Deployment 的基础特性，而是主要介绍“滚动更新”和“回滚”等主要功能，另外简单过一下 Deployment 的 spec 包含的全量配置项，从而心中有个概念，知道 Deployment 的能力边界在那里，从而后面看源码时更有针对性。

