有状态服务 VS 无状态服务

一般来说,业务的服务类型可分为无状态服务和有状态服务。举个简单的例子,像打网络游戏这类的服务,就是有状态服务,而正常浏览网页这类服务一般都是无状态服务。其实判断两种请求的关键在于,两个来自相同发起者的请求在服务器端是否具备上下文关系。

  • 如果是有状态服务,其请求是状态化的,服务器端需要保存请求的相关信息,这样每个请求都可以默认地使用之前的请求上下文。
  • 而无状态服务就不需要这样,每次请求都包含了需要的所有信息,每次请求都和之前的没有任何关系。

有状态服务和无状态服务分别有各自擅长的业务类型和技术优势,在 Kubernetes 中,分别有不同的工作负载控制器来负责承载这两类服务。

Kubernetes 中的无状态工作负载

Kubernetes 中各个对象的 metadata 字段都有 label(标签)和 annotation(注解) 两个对象,可以用来标识一些元数据信息。不论是 label 还是 annotation,都是一组键值对,键值不允许相同。

  • annotation 主要用来记录一些非识别的信息,并不用于标识和选择对象。
  • label 主要用来标识一些有意义且和对象密切相关的信息,用来支持labelSelector(标签选择器)以及一些查询操作,还有选择对象。

为了让这种抽象的对象可以跟 Pod 关联起来,Kubernetes 使用了labelSelector来跟 label 进行选择匹配,从而达到这种松耦合的关联效果。

  1. $ kubectl get pod -l label1=value1,label2=value2 -n my-namespace

比如可以通过上述命令,查询出 my-namespace 这个命名空间下面,带有标签label1=value1和label2=value2的 pod。label 中的键值对在匹配的时候是“”的关系。

ReplicationController

Kubernetes 中有一系列的工作负载可以用来部署无状态服务。在最初,Kubernetes 中使用ReplicationController来做 Pod 的副本控制,即确保该服务的 Pod 数量维持在特定的数量。为了简洁并便于使用ReplicationController通常缩写为“rc”,并作为 kubectl 命令的快捷方式,例如:

  1. $ kubectl get rc -n my-namespace

如果副本数少于预定的值,则创建新的 Pod。如果副本数大于预定的值,就删除多余的副本。因此即使你的业务应用只需要一个 Pod,你也可以使用 rc 来自动帮你维护和创建 Pod。

ReplicaSet

随后社区开发了下一代的 Pod 控制器 ReplicaSet(可简写为 rs) 用来替代
ReplicaController。虽然 ReplicaController 目前依然可以使用,但是社区已经不推荐继续使用了。这两者的功能和目的完全相同,但是 ReplicaSet 具备更强大的基于集合的标签选择器,这样你可以通过一组值来进行标签匹配选择。目前支持三种操作符:in、notin和exists。
例如,你可以用environment in (production, qa)来匹配 label 中带有environment=production或environment=qa的 Pod。
同样你也可以使用tier notin (frontend,backend)来匹配 label 中不带有tier=frontend或tier=backend的 Pod。
或者你可以用 partition来匹配 label 中带有 partition 这个 key 的 Pod。
了解了标签选择器,我们就可以通过如下的 kubectl 命令查找 Pod:

  1. kubectl get pods -l environment=production,tier=frontend

或者使用:

  1. kubectl get pods -l 'environment in (production),tier in (frontend)'

虽然 Replicaset 可以独立使用,但是为了能够更好地协调 Pod 的创建、删除以及更新等操作,我们都是直接使用更高级的 Deployment来管理 Replicaset,社区也是一直这么定位和推荐的。比如一些业务升级的场景,使用单一的 ReplicaController 或者 Replicaset 是无法实现滚动升级的诉求,至少需要定义两个该对象才能实现,而且这两个对象使用的标签选择器中的 label 至少要有一个不相同。通过不断地对这两个对象的副本进行增减,也可以称为调和(Reconcile),才可以完成滚动升级。这样使用起来不方便,也增加了用户的使用门槛,极大地降低了业务发布的效率。

Deployment

通过 Deployment,我们就不需要再关心和操作 ReplicaSet 了。
📚无状态服务 - 图1
Deployment、ReplicaSet 和 Pod 这三者之间的关系见上图。通过 Deployment,我们可以管理多个 label 各不相同的 ReplicaSet,每个 ReplicaSet 负责保证对应数目的 Pod 在运行。
我们来看一个定义 Deployment 的例子:

  1. apiVersion: apps/v1
  2. kind: Deployment
  3. metadata:
  4. name: nginx-deployment-demo
  5. namespace: demo
  6. labels:
  7. app: nginx
  8. spec:
  9. replicas: 3
  10. selector:
  11. matchLabels:
  12. app: nginx
  13. template:
  14. metadata:
  15. labels:
  16. app: nginx
  17. version: v1
  18. spec:
  19. containers:
  20. - name: nginx
  21. image: nginx:1.14.2
  22. ports:
  23. - containerPort: 80

我们这里定义了副本数spec.replicas为 3,同时在spec.selector.matchLabels中设置了app=nginx的 label,用于匹配spec.template.metadata.labels的 label。我们还增加了version=v1的 label 用于后续滚动升级做比较。
我们将上述内容保存到deploy-demo.yaml文件中。
注意,spec.selector.matchLabels中写的 label 一定要能匹配得了spec.template.metadata.labels中的 label。否则该 Deployment 无法关联创建出来的 ReplicaSet。
然后我们通过下面这些命令,便可以在命名空间 demo 中创建名为 nginx-deployment-demo 的 Deployment。

  1. $ kubectl create ns demo
  2. $ kubectl create -f deploy-demo.yaml
  3. deployment.apps/nginx-deployment-demo created
  4. # 创建完成后,我们可以查看自动创建出来的 rs。
  5. $ kubectl get rs -n demo
  6. NAME DESIRED CURRENT READY AGE
  7. nginx-deployment-demo-5d65f98bd9 3 3 0 5s

接下来,我们可以通过如下的几个命令不断地查询系统,看看对应的 Pod 是否运行成功。

  1. $ kubectl get pod -n demo -l app=nginx,version=v1
  2. NAME READY STATUS RESTARTS AGE
  3. nginx-deployment-demo-5d65f98bd9-7w5gp 0/1 ContainerCreating 0 30s
  4. nginx-deployment-demo-5d65f98bd9-d78fx 0/1 ContainerCreating 0 30s
  5. nginx-deployment-demo-5d65f98bd9-ssstk 0/1 ContainerCreating 0 30s
  6. $ kubectl get pod -n demo -l app=nginx,version=v1
  7. NAME READY STATUS RESTARTS AGE
  8. nginx-deployment-demo-5d65f98bd9-7w5gp 1/1 Running 0 63s
  9. nginx-deployment-demo-5d65f98bd9-d78fx 1/1 Running 0 63s
  10. nginx-deployment-demo-5d65f98bd9-ssstk 1/1 Running 0 63s

我们可以看到,从 Deployment 到这里,最后的 Pod 已经创建成功了。
现在我们试着做下镜像更新看看。更改spec.template.metadata.labels中的version=v1为version=v2,同时更新镜像nginx:1.14.2为nginx:1.19.2。
你可以直接通过下述命令来直接更改:

  1. $ kubectl edit deploy nginx-deployment-demo -n demo
  2. # 然后运行这些命令:
  3. $ kubectl apply -f deploy-demo.yaml
  4. Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
  5. deployment.apps/nginx-deployment-demo configured
  6. # 这个时候,我们来看看 ReplicaSet 有什么变化:
  7. $ kubectl get rs -n demo
  8. NAME DESIRED CURRENT READY AGE
  9. nginx-deployment-demo-5d65f98bd9 3 3 3 4m10s
  10. nginx-deployment-demo-7594578db7 1 1 0 3s

可以看到,这个时候自动创建了一个新的 rs 出来:nginx-deployment-demo-7594578db7。一般 Deployment 的默认更新策略是 RollingUpdate,即先创建一个新的 Pod,并待其成功运行以后,再删除旧的。
还有一个更新策略是Recreate,即先删除现有的 Pod,再创建新的。关于 Deployment,我们还可以设置最大不可用(maxUnavailable)和最大增量(maxSurge),来更精确地控制滚动更新操作,具体配置可参照这个文档
我建议你使用默认的策略来保证可用性。后面 Deployment 控制器会不断对这两个 rs 进行调和,直到新的 rs 副本数为 3,老的 rs 副本数为 0。
我们可以通过如下命令,能够观察到 Deployment 的调和过程。

  1. $ kubectl get pod -n demo -l app=nginx,version=v1
  2. NAME READY STATUS RESTARTS AGE
  3. nginx-deployment-demo-5d65f98bd9-7w5gp 1/1 Running 0 4m15s
  4. nginx-deployment-demo-5d65f98bd9-d78fx 1/1 Running 0 4m15s
  5. nginx-deployment-demo-5d65f98bd9-ssstk 1/1 Running 0 4m15s
  6. $ kubectl get pod -n demo -l app=nginx
  7. NAME READY STATUS RESTARTS AGE
  8. nginx-deployment-demo-5d65f98bd9-7w5gp 1/1 Running 0 4m22s
  9. nginx-deployment-demo-5d65f98bd9-d78fx 1/1 Running 0 4m22s
  10. nginx-deployment-demo-5d65f98bd9-ssstk 1/1 Running 0 4m22s
  11. nginx-deployment-demo-7594578db7-zk8jq 0/1 ContainerCreating 0 15s
  12. $ kubectl get pod -n demo -l app=nginx,version=v2
  13. NAME READY STATUS RESTARTS AGE
  14. nginx-deployment-demo-7594578db7-zk8jq 0/1 ContainerCreating 0 19s
  15. $ kubectl get pod -n demo -l app=nginx,version=v2
  16. NAME READY STATUS RESTARTS AGE
  17. nginx-deployment-demo-7594578db7-4g4fk 0/1 ContainerCreating 0 1s
  18. nginx-deployment-demo-7594578db7-zk8jq 1/1 Running 0 31s
  19. $ kubectl get pod -n demo -l app=nginx,version=v1
  20. NAME READY STATUS RESTARTS AGE
  21. nginx-deployment-demo-5d65f98bd9-7w5gp 1/1 Running 0 4m40s
  22. nginx-deployment-demo-5d65f98bd9-d78fx 1/1 Running 0 4m40s
  23. nginx-deployment-demo-5d65f98bd9-ssstk 1/1 Terminating 0 4m40s
  24. $ kubectl get pod -n demo -l app=nginx,version=v2
  25. NAME READY STATUS RESTARTS AGE
  26. nginx-deployment-demo-7594578db7-4g4fk 1/1 Running 0 5s
  27. nginx-deployment-demo-7594578db7-ftzmf 0/1 ContainerCreating 0 2s
  28. nginx-deployment-demo-7594578db7-zk8jq 1/1 Running 0 35s
  29. $ kubectl get pod -n demo -l app=nginx,version=v1
  30. NAME READY STATUS RESTARTS AGE
  31. nginx-deployment-demo-5d65f98bd9-7w5gp 0/1 Terminating 0 4m52s
  32. nginx-deployment-demo-5d65f98bd9-ssstk 0/1 Terminating 0 4m52s
  33. $ kubectl get pod -n demo -l app=nginx,version=v2
  34. NAME READY STATUS RESTARTS AGE
  35. nginx-deployment-demo-7594578db7-4g4fk 1/1 Running 0 17s
  36. nginx-deployment-demo-7594578db7-ftzmf 1/1 Running 0 14s
  37. nginx-deployment-demo-7594578db7-zk8jq 1/1 Running 0 47s
  38. $ kubectl get rs -n demo
  39. NAME DESIRED CURRENT READY AGE
  40. nginx-deployment-demo-5d65f98bd9 0 0 0 5m5s
  41. nginx-deployment-demo-7594578db7 3 3 3 58s
  42. $ kubectl get pod -n demo -l app=nginx,version=v1
  43. No resources found in demo namespace.

或者可以通过 kubectl 的 watch 功能:
$ kubectl get pod -n demo -l app=nginx -w

至此,我们完成了 Deployment 的升级过程。