learn-tech/专栏/Kubernetes入门实战课/26StatefulSet:怎么管理有状态的应用?.md
2024-10-16 06:37:41 +08:00

14 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        26 StatefulSet怎么管理有状态的应用
                        你好我是Chrono。

在中级篇里我们学习了Deployment和DaemonSet两种API对象它们是在Kubernetes集群里部署应用的重要工具不过它们也有一个缺点只能管理“无状态应用”Stateless Application不能管理“有状态应用”Stateful Application

“有状态应用”的处理比较复杂要考虑的事情很多但是这些问题我们其实可以通过组合之前学过的Deployment、Service、PersistentVolume等对象来解决。

今天我们就来研究一下什么是“有状态应用”然后看看Kubernetes为什么会设计一个新对象——StatefulSet来专门管理“有状态应用”。

什么是有状态的应用

我们先从PersistentVolume谈起它为Kubernetes带来了持久化存储的功能能够让应用把数据存放在本地或者远程的磁盘上。

那么你有没有想过,持久化存储,对应用来说,究竟意味着什么呢?

有了持久化存储应用就可以把一些运行时的关键数据落盘相当于有了一份“保险”如果Pod发生意外崩溃也只不过像是按下了暂停键等重启后挂载Volume再加载原数据就能够满血复活恢复之前的“状态”继续运行。

注意到了吗?这里有一个关键词——“状态”,应用保存的数据,实际上就是它某个时刻的“运行状态”。

所以从这个角度来说,理论上任何应用都是有状态的。

只是有的应用的状态信息不是很重要即使不恢复状态也能够正常运行这就是我们常说的“无状态应用”。“无状态应用”典型的例子就是Nginx这样的Web服务器它只是处理HTTP请求本身不生产数据日志除外不需要特意保存状态无论以什么状态重启都能很好地对外提供服务。

还有一些应用,运行状态信息就很重要了,如果因为重启而丢失了状态是绝对无法接受的,这样的应用就是“有状态应用”。

“有状态应用”的例子也有很多比如Redis、MySQL这样的数据库它们的“状态”就是在内存或者磁盘上产生的数据是应用的核心价值所在如果不能够把这些数据及时保存再恢复那绝对会是灾难性的后果。

理解了这一点我们结合目前学到的知识思考一下Deployment加上PersistentVolume在Kubernetes里是不是可以轻松管理有状态的应用了呢

的确用Deployment来保证高可用用PersistentVolume来存储数据确实可以部分达到管理“有状态应用”的目的你可以自己试着编写这样的YAML

但是Kubernetes的眼光则更加全面和长远它认为“状态”不仅仅是数据持久化在集群化、分布式的场景里还有多实例的依赖关系、启动顺序和网络标识等问题需要解决而这些问题恰恰是Deployment力所不及的。

因为只使用Deployment多个实例之间是无关的启动的顺序不固定Pod的名字、IP地址、域名也都是完全随机的这正是“无状态应用”的特点。

但对于“有状态应用”多个实例之间可能存在依赖关系比如master/slave、active/passive需要依次启动才能保证应用正常运行外界的客户端也可能要使用固定的网络标识来访问实例而且这些信息还必须要保证在Pod重启后不变。

所以Kubernetes就在Deployment的基础之上定义了一个新的API对象名字也很好理解就叫StatefulSet专门用来管理有状态的应用。

如何使用YAML描述StatefulSet

首先我们还是用命令 kubectl api-resources 来查看StatefulSet的基本信息可以知道它的简称是 stsYAML文件头信息是

apiVersion: apps/v1 kind: StatefulSet metadata: name: xxx-sts

和DaemonSet类似StatefulSet也可以看做是Deployment的一个特例它也不能直接用 kubectl create 创建样板文件但它的对象描述和Deployment差不多你同样可以把Deployment适当修改一下就变成了StatefulSet对象。

这里我给出了一个使用Redis的StatefulSet你来看看它与Deployment有什么差异

apiVersion: apps/v1 kind: StatefulSet metadata: name: redis-sts

spec: serviceName: redis-svc replicas: 2 selector: matchLabels: app: redis-sts

template: metadata: labels: app: redis-sts spec: containers: - image: redis:5-alpine name: redis ports: - containerPort: 6379

我们会发现YAML文件里除了 kind 必须是“StatefulSet”在 spec 里还多出了一个“serviceName”字段其余的部分和Deployment是一模一样的比如 replicas、selector、template 等等。

这两个不同之处其实就是StatefulSet与Deployment的关键区别。想要真正理解这一点我们得结合StatefulSet在Kubernetes里的使用方法来分析。

如何在Kubernetes里使用StatefulSet

让我们用 kubectl apply 创建StatefulSet对象用 kubectl get 先看看它是什么样的:

kubectl apply -f redis-sts.yml kubectl get sts kubectl get pod

从截图里你应该能够看到StatefulSet所管理的Pod不再是随机的名字了而是有了顺序编号从0开始分别被命名为 redis-sts-0、redis-sts-1Kubernetes也会按照这个顺序依次创建0号比1号的AGE要长一点这就解决了“有状态应用”的第一个问题启动顺序。

有了启动的先后顺序,应用该怎么知道自己的身份,进而确定互相之间的依赖关系呢?

Kubernetes给出的方法是使用hostname也就是每个Pod里的主机名让我们再用 kubectl exec 登录Pod内部看看

kubectl exec -it redis-sts-0 -- sh

在Pod里查看环境变量 $HOSTNAME 或者是执行命令 hostname都可以得到这个Pod的名字 redis-sts-0。

有了这个唯一的名字应用就可以自行决定依赖关系了比如在这个Redis例子里就可以让先启动的0号Pod是主实例后启动的1号Pod是从实例。

解决了启动顺序和依赖关系还剩下第三个问题网络标识这就需要用到Service对象。

不过这里又有一点奇怪的地方,我们不能用命令 kubectl expose 直接为StatefulSet生成Service只能手动编写YAML。但是这肯定难不倒你经过了这么多练习现在你应该能很轻松地写出一个Service对象。

因为不能自动生成你在写Service对象的时候要小心一些metadata.name 必须和StatefulSet里的 serviceName 相同selector 里的标签也必须和StatefulSet里的一致

apiVersion: v1 kind: Service metadata: name: redis-svc

spec: selector: app: redis-sts

ports:

  • port: 6379 protocol: TCP targetPort: 6379

写好Service之后还是用 kubectl apply 创建这个对象:

可以看到这个Service并没有什么特殊的地方也是用标签选择器找到StatefulSet管理的两个Pod然后找到它们的IP地址。

不过StatefulSet的奥秘就在它的域名上。

还记得在[第20讲]里我们说过的Service的域名用法吗Service自己会有一个域名格式是“对象名.名字空间”每个Pod也会有一个域名形式是“IP地址.名字空间”。但因为IP地址不稳定所以Pod的域名并不实用一般我们会使用稳定的Service域名。

当我们把Service对象应用于StatefulSet的时候情况就不一样了。

Service发现这些Pod不是一般的应用而是有状态应用需要有稳定的网络标识所以就会为Pod再多创建出一个新的域名格式是“Pod名.服务名.名字空间.svc.cluster.local”。当然这个域名也可以简写成“Pod名.服务名”。

我们还是用 kubectl exec 进入Pod内部用ping命令来验证一下

kubectl exec -it redis-sts-0 -- sh

显然在StatefulSet里的这两个Pod都有了各自的域名也就是稳定的网络标识。那么接下来外部的客户端只要知道了StatefulSet对象就可以用固定的编号去访问某个具体的实例了虽然Pod的IP地址可能会变但这个有编号的域名由Service对象维护是稳定不变的。

到这里通过StatefulSet和Service的联合使用Kubernetes就解决了“有状态应用”的依赖关系、启动顺序和网络标识这三个问题剩下的多实例之间内部沟通协调等事情就需要应用自己去想办法处理了。

关于Service有一点值得再多提一下。

Service原本的目的是负载均衡应该由它在Pod前面来转发流量但是对StatefulSet来说这项功能反而是不必要的因为Pod已经有了稳定的域名外界访问服务就不应该再通过Service这一层了。所以从安全和节约系统资源的角度考虑我们可以在Service里添加一个字段 clusterIP: None 告诉Kubernetes不必再为这个对象分配IP地址。

我画了一张图展示StatefulSet与Service对象的关系你可以参考一下它们字段之间的互相引用

如何实现StatefulSet的数据持久化

现在StatefulSet已经有了固定的名字、启动顺序和网络标识只要再给它加上数据持久化功能我们就可以实现对“有状态应用”的管理了。

这里就能用到上一节课里学的PersistentVolume和NFS的知识我们可以很容易地定义StorageClass然后编写PVC再给Pod挂载Volume。

不过为了强调持久化存储与StatefulSet的一对一绑定关系Kubernetes为StatefulSet专门定义了一个字段“volumeClaimTemplates”直接把PVC定义嵌入StatefulSet的YAML文件里。这样能保证创建StatefulSet的同时就会为每个Pod自动创建PVC让StatefulSet的可用性更高。

“volumeClaimTemplates”这个字段好像有点难以理解你可以把它和Pod的 template、Job的 jobTemplate 对比起来学习它其实也是一个“套娃”的对象组合结构里面就是应用了StorageClass的普通PVC而已。

让我们把刚才的Redis StatefulSet对象稍微改造一下加上持久化存储功能

apiVersion: apps/v1 kind: StatefulSet metadata: name: redis-pv-sts

spec: serviceName: redis-pv-svc

volumeClaimTemplates:

  • metadata: name: redis-100m-pvc spec: storageClassName: nfs-client accessModes: - ReadWriteMany resources: requests: storage: 100Mi

replicas: 2 selector: matchLabels: app: redis-pv-sts

template: metadata: labels: app: redis-pv-sts spec: containers: - image: redis:5-alpine name: redis ports: - containerPort: 6379

    volumeMounts:
    - name: redis-100m-pvc
      mountPath: /data

这个YAML文件比较长内容比较多不过你只要有点耐心分功能模块逐个去看也能很快看明白。

首先StatefulSet对象的名字是 redis-pv-sts表示它使用了PV存储。然后“volumeClaimTemplates”里定义了一个PVC名字是 redis-100m-pvc申请了100MB的NFS存储。在Pod模板里用 volumeMounts 引用了这个PVC把网盘挂载到了 /data 目录也就是Redis的数据目录。

下面的这张图就是这个StatefulSet对象完整的关系图-

最后使用 kubectl apply 创建这些对象,一个带持久化功能的“有状态应用”就算是运行起来了:

kubectl apply -f redis-pv-sts.yml

你可以使用命令 kubectl get pvc 来查看StatefulSet关联的存储卷状态

看这两个PVC的命名不是随机的是有规律的用的是PVC名字加上StatefulSet的名字组合而成所以即使Pod被销毁因为它的名字不变还能够找到这个PVC再次绑定使用之前存储的数据。

那我们就来实地验证一下吧,用 kubectl exec 运行Redis的客户端在里面添加一些KV数据

kubectl exec -it redis-pv-sts-0 -- redis-cli

这里我设置了两个值,分别是 a=111 和 b=222。

现在我们模拟意外事故删除这个Pod

kubectl delete pod redis-pv-sts-0

由于StatefulSet和Deployment一样会监控Pod的实例发现Pod数量少了就会很快创建出新的Pod并且名字、网络标识也都会和之前的Pod一模一样

那Redis里存储的数据怎么样了呢是不是真的用到了持久化存储也完全恢复了呢

你可以再用Redis客户端登录去检查一下

kubectl exec -it redis-pv-sts-0 -- redis-cli

因为我们把NFS网络存储挂载到了Pod的 /data 目录Redis就会定期把数据落盘保存所以新创建的Pod再次挂载目录的时候会从备份文件里恢复数据内存里的数据就恢复原状了。

小结

好了今天我们学习了专门部署“有状态应用”的API对象StatefulSet它与Deployment非常相似区别是由它管理的Pod会有固定的名字、启动顺序和网络标识这些特性对于在集群里实施有主从、主备等关系的应用非常重要。

我再简单小结一下今天的内容:

StatefulSet的YAML描述和Deployment几乎完全相同只是多了一个关键字段 serviceName。 要为StatefulSet里的Pod生成稳定的域名需要定义Service对象它的名字必须和StatefulSet里的 serviceName 一致。 访问StatefulSet应该使用每个Pod的单独域名形式是“Pod名.服务名”不应该使用Service的负载均衡功能。 在StatefulSet里可以用字段“volumeClaimTemplates”直接定义PVC让Pod实现数据持久化存储。

课下作业

最后是课下作业时间,给你留两个思考题:

有了StatefulSet提供的固定名字和启动顺序应用还需要怎么做才能实现主从等依赖关系呢 是否可以不使用“volumeClaimTemplates”内嵌定义PVC呢会有什么样的后果呢

欢迎在留言区参与讨论,分享你的想法。我们下节课再见。