写点什么

组件容器化 Statefulset 与 Deployment 的选型与实践

  • 2022 年 4 月 20 日
  • 本文字数:4477 字

    阅读完需:约 15 分钟

本篇文章从 Pod 域名通信和存储两个方面对 Deployment 和 Statefulset 进行展开,并结合移动云消息队列 Pulsar 和 Kafka 产品的实际应用案例,帮助读者进一步了解二者的差异,以便根据不同的场景选择更为恰当的资源管理方式。

一、前言

Kubernetes 是一个开源的容器编排引擎,经常用于管理容器化应用,现在也被广泛用于大数据组件的容器化改造。由于组件的多样性,为了应对不同组件的不同需求,选择合适的容器管理方式就变得十分重要。通常情况下,k8s 自带的 Daemonset、Deployment 和 Statefulset 就可以涵盖绝大部分的使用场景。

其中 Daemonset 的使用场景很容易辨别,适合于每个 K8s 节点启动一个 Pod 副本的场景。而对于多 Pod 的集群类应用,在使用 Deployment 还是 Statefulset 上常常陷入犹豫,本文会从实际的角度列出二者在使用上常见的区别和实践经验。

二、一般区别

Deployment 和 Statefulset 的一般的区别相信大家或多或少都有一定了解,这里也先梳理一下。

1、Deployment

应用场景:无状态服务

特点:

  • Pod 名称随机,每次创建都会不一样

  • Pod 无顺序指定

  • 所有 Pod 共享存储

2、Statefulset

应用场景:有状态服务

特点:

  • Pod 按照固定规则命名,每次创建结果一样

  • Pod 有顺序指定,涉及创建、删除等操作

  • Pod 有稳定唯一的网络标识

  • 每个 Pod 可以有独自的存储

一般情况下,我们可以根据以上区别选择 Deployment 或 Statefulset。但是在实际使用中,为了实现功能我们需要更加精细地比较二者在某些情况下的不同,包括通信、存储以及对每个 Pod 管理的细粒度等,甚至有时需要跳出既定的使用框架,来达到我们的需求。

三、不同场景下的区别

3.1 Pod 域名和通信

3.1.1 Statefulset 和 Deployment 的域名

Deployment 的一个特点就是,Pod 名会带有特殊的随机字符串,一般来说,Deployment 的通信是通过 Service 进行的,先连接到 Service,再往下分发,然后访问到每个 Pod。由此带来的问题就是,无法精确访问到想要访问的那个特定的 Pod 上,对于需要相互通信的组件来说则一般不会选用 Deployment。

Statefulset 则不同,statefulset 的每个 Pod 都具有独立的域名,所以可以通过 Headless Service 精确访问到每一个 Pod,同时每个 Pod 之间也可以相互精确访问,我们只需要按照规则,拼写出每个 Pod 的特定的域名,就可以实现了。拼写规则为:[Hostname].[Domain].[namespace].svc.cluster.local。

  • [Hostname]:Pod 名,可以由 Statefulset 名加序号得到,也可以在容器内通过 hostname 命令得到。

  • [Domain]:一般是 Service 名,Statefulset 中 spec.serviceName 的值。

  • [Namespace]:命名空间的名字。

  • svc.cluster.local:固定后缀,也可以是其他值,这取决于 k8s 集群的配置。

其中[Domain].[namespace].svc.cluster.local 可以在容器内通过 hostname -d 命令得到。

示例如下:

apiVersion: apps/v1kind: Statefulsetmetadata:  name: sfs  namespace: devspec:  serviceName: sfs-svc
复制代码

提交这个 yaml 后,会创建一个名为 sfs-0 的 Pod,对于这个 Pod,其域名为:sfs-0.sfs-svc.dev.svc.cluster.local。

3.1.2 改变 Deployment 随机 Pod 名为固定 Pod 名的方法

在搭建可以构成集群且 Pod 需要相互通信的组件时,一般都会选用 Statefulset,不过,我们可以采用其他办法使得 Deployment 也可以实现这种通信方式。

Deployment 中可以通过 spec.template.spec.hostname 指定 Pod 名,且 spec.template.spec.subdomain 要设置为 Service 名,这样 Pod 就会有一个固定的域名,同时在 Pod 内部执行 hostname 和 hostname -d 命令也可以分别获得组成域名的值,这有利于编写脚本。

例如:

apiVersion: apps/v1kind: Deploymentmetadata:  name: pod-con-2  namespace: t1spec:  replicas: 1    spec:      hostname: pod-con-dem      subdomain: dem-domain
复制代码

可以查询其域名:

可以 ping 通:

不过对于所有 Pod 来说,都会有着同样的 Pod 名,这也体现使用对应域名通信的时候,流量也会负载均衡到不同的 Pod 上。所以,如果要使用 Deployment 精确通信,可以设置副本数 spec.replicas 为 1,启动多个 Deployment 来实现。在 Statefulset 的使用存在不便的场景,可以采用这种方法解决,相对于 Statefulset,这种方法可以更精确地管理每个 Pod,尤其是在创建删除的过程中(Statefulset 是根据顺序创建删除 Pod 的)。

3.1.3 Statefulset 对外暴露端口的通信问题

现有一个 Statefulset 有多个 Pod 副本,通过 Nodeport 方式将端口映射暴露到外部(loadbalancer 某些情况下也存在此类问题,此处不过多讨论),有一个 Nodeport 的 Service 绑定这个 Statefulset,在外部连接端口的时候,会发现网络流量负载到不同的 Pod 上。

例如,一个 Statefulset 启动了两个 Pod,其中一个监听了 32570 端口,映射到外部为 32578 端口,在外部使用 telnet 连接的时候,可能会出现连到另一个 Pod 副本的情况,这时会出现连接错误。如图:

造成这一问题的原因是由 Service 的负载均衡导致的,这对于需要精确访问某个 Pod 的组件来说非常致命。好在 Statefulset 对每个 Pod 都会生成一个标识其名字的标签,由于这个标签的值可以事先预知,所以可以在 Statefulset 部署前就可以创建绑定每个 Pod 的 Service,即每个 Pod 有自己专用的 Service,这样就可以解决上面遇到的问题。

3.2.存储

3.2.1 Deployment 和 Statefulset 的共享存储模式

Deployment 和 Statefulset 最常用的共享存储方式有两种常用的挂载方式:(1).hostpath。(2).PV/PVC。这两种挂载方式的写法大致相同,都是在 spec.template.spec.containers.volumeMounts 下描述在容器内的存储,再在 deployment.spec.template.spec.volumes 编写相对应的 volume 类型。

以 Deployment 为例:

apiVersion: apps/v1kind: Deploymentspec:  template:    spec:      containers:        - name: con          volumeMounts:            - name: vol-1             mountPath: /data/index1            - name: vol-2             mountPath: /data/index2      volumes:        - name: vol-1          hostPath:            path: /tmp/hostpath-1            type: Directory        - name: vol-2          persistentVolumeClaim:            claimName: dep-pvc
复制代码

使用这种写法的存储,所有 Pod 都会共同使用这个存储目录,也就是说,不同的 Pod 对存储目录的操作,对于其他 Pod 也是可以读写的,这就会导致 Pod 之间的操作可能会互相影响,存在一定的安全问题。如果不同的 Pod 读写的文件名称都有区分的话,理论上这种问题是可以容忍的,但还是建议不要这样使用。

3.2.2 Statefulset Pod 独享存储模式——volumeClaimTemplates

采用 volumeClaimTemplates 这种存储方式是 Statefulset 特有的。使用这种方式,无需手动创建 PV/PVC,只需要在 spec.volumeClaimTemplates 下描述存储即可。在提交 Statefulset 的时候,Statefulset 会自动给每个 Pod 创建各自专用的 PV/PVC,这样可以避免多 Pod 共用一个存储目录的问题,使每个 Pod 独自使用一个存储,管理和使用上对比 Deployment 有很大的优势。

示例如下:

apiVersion: apps/v1kind: StatefulSetspec:  template:    spec:      containers:        - name: con          volumeMounts:            - name: vol-1              mountPath: /tmp/pvc-path-1      volumes:        - name: vol-1          persistentVolumeClaim:            claimName: sfs-pvc  volumeClaimTemplates:    - metadata:        name: vol-1      spec:        accessModes: [ "ReadWriteOnce" ]        storageClassName: ceph-storageclass        resources:          requests:            storage: 10Gi
复制代码

Statefulset 使用 PC/PVC 是常用的存储挂载方式,但是有一个问题需要多加注意:现有一个 Statefulset 使用了 volumeClaimTemplates 存储且挂载了 Ceph 网盘,包含了多个分布在不同 K8s Node 上的 Pod,如果某个 Node 出现了意外宕机,此时这个 Node 上的 Pod 就会处于一种无法删除的中间状态,这就会导致有一个 Pod 无法正常使用。

一般情况下,我们都会尽快修复宕机 Node 节点使集群恢复正常,但如果在短时间内无法使得宕机 Node 恢复正常,转而采取重建受影响的 Pod 以解决问题,这个时候就要采取人工手段使 Statefulset 在其他正常 Node 上重建 Pod,但是会发现 PV/PVC 无法正常挂载。

报错举例: 

Warning  FailedMount         2m34s                  kubelet, node4           Unable to mount volumes for pod "web-1_default(5a734d94-a78f-11ea-b33d-384c4fcc7060)": timeout expired waiting for volumes to attach or mount for pod "default"/"web-1". list of unmounted volumes=[www]. list of unattached volumes=[www default-token-b2b49]
复制代码

解决的方法就是手动去 ceph 集群侧修改相关 watch,但是这种方式不仅繁琐,而且存在一定风险,不建议在生产环境操作。

出现这种无法挂载的情况是由 Statefulset 设计所导致的,有一定的合理性,不能算是一个 BUG,所以如果要部署的服务无法接受这个问题,建议不要使用 Statefulset。

四、在移动云产品的应用举例

移动云 Pulsar 是基于开源 Apache Pulsar 构建的企业级消息中间件服务,可应用于金融、物联网、互联网等领域。其内部采用了计算存储分离的架构,主要包含三个部分,底层存储服务 Bookkeeper 集群、协调服务 Zookeeper 集群、计算服务 Pulsar Broker 集群,其中 Bookkeeper 与 Zookeeper 是分别只有一个的共享集群,Pulsar Broker 是多个实例集群,Bookkeeper 和 Pulsar Broker 都依赖于 Zookeeper。在产品开发的过程中,根据服务的特性采取了不同的资源管理,如下图所示。

  • Bookkeeper 集群作为底层存储采用了 Daemonset 部署,在每个 K8s Node 上运行一个 Pod,使用本地存储,这样可以保证存储数据分布在所有节点上。

  • Zookeeper 集群采用 Statefulset 部署,这样易于管理 Pod 之间相互通信。存储方面使用 volumeClaimTemplates 并挂载 Ceph,使每个 Pod 都有自己的存储目录,至于存储挂载导致的 Pod 无法漂移问题,现阶段认为出现此问题并必须要手动修改 Ceph 侧 Watch 名单的概率很低,采用 Statefulset 并不会提高维护成本。为了保证 Pod 分布在不同的 Node 上,设置了节点反亲和性,由于 Zookeeper 集群的节点数少于 K8s Node 数量,故不采用 Daemonset。

  • Pulsar Broker 集群采用 Statefulset 部署,由于 Pulsar Broker 是计算节点,只需要考虑通信问题,使用 Statefulset 易于开发和管理。

五、总结及拓展

Deployment 和 Statefulset 都是官方给用户提供的较为方便的管理多 Pod 副本的方法,但在不同场景下的使用还是存在一些差异。

Statefulset 对 Pod 的管理在通信和存储上有很大方便性,但并不适合所有场景,其中按照顺序对创建的 Pod 副本编号有的时候就显得不太理想,管理精细度就是一个痛点。

这个时候可以采用一个 Deployment 管理一个 Pod 副本,启动多个 Deployment 副本的方式进行集群管控,从而做到精细管控每一个 Pod 副本,也不会出现 Statefulset 方式下使用 PV/PVC 存储,在节点宕机下的 Pod 无法漂移的问题。这种架构方式的优点是明显的,所以移动云 Kafka 开发的时候就采用了管理多 Deployment 的方式,这很适合计算存储一体的 Kafka。但是这样也会增加开发和维护的成本,例如 PV/PVC 就需要自己创建删除,管理上也从管理一个 Statefulset 变成管理多个 Deployment 等。


用户头像

移动云,5G时代你身边的智慧云 2019.02.13 加入

移动云大数据产品团队,在移动云上提供云原生大数据分析LakeHouse,消息队列Kafka/Pulsar,云数据库HBase,弹性MapReduce,数据集成与治理等PaaS服务。

评论

发布
暂无评论
组件容器化Statefulset与Deployment的选型与实践_Deployment_移动云大数据_InfoQ写作社区