理论来源:https://blog.doorta.com/?p=57
回顾
在k8s集群中运行最基础的单元是Pod,其它资源都是围绕着Pod应用而实现,如Service用于给Pod提供一个固定端点,并且为动态Pod变动提供一个服务发现机构功能,运行在k8s之上服务指的就是Service,服务注册、服务发现都是借助于Service在k8s中运行的DNS服务来实现;
而DNS也是只一个附件,其本身也以Pod状态运行于k8s之上,其主要为Pod提供基础服务逻辑,我们称之为CoreDNS,Service注册在CoreDNS之上,从而为其它客户端完成服务发现(通过DNS解析服务名称时);
CoreDNS围绕着基础服务构建,其扮演着服务发现、服务注册功能,Pod为了提供更好的动态、自愈功能也包括了自动伸缩等功能则需要借助于Pod控制器,而Pod控制则归类为一组控制器,它们分别也有多种不同的实现方式,分别应用于不同的逻辑,
比如Deployment,它真正构建在ReplicaSet之上,完成真正意义上所谓无状态应用的各种运维管理工作,比如部署、变更、编辑等功能,
如果期望在每一个节点运行一个系统级应用,或者期望在集群的部分节点上运行一个Pod副本,则需要DaemonSet来实现;
statefulset
有状态应用副本集,有状态应用如redis,mysql,etcd,zk
对于有状态应用来说,应该使用的是StatefulSet控制器,引用程序一般有四种类型,它们分别是有状态有存储、无状态无存储、有状态无存储、有存储无状态,绝大多数的服务都是有状态有存储或无状态无存储的;
比如nginx,它就是无状态无存储,而对于用状态有存储,比如mysql来说,如果它必须用到存储,我们就必须给每一个实例起一个独有的标识,重建的Pod是随机的,在sts下就不能随机了,将来当集群中的某一个mysql挂掉时,控制器重建的mysql也会属于这个挂掉的mysql,并且同时会将原先挂掉的MYSQL的数据加载进来,进行一对一标识,不像ds一样为每一个重建的Pod名称后面跟一个随机字符串;
对于绝大部分有状态服务来说都是有存储的,如果Pod数据挂载放在本地,那么就会随着这个Pod生命周期的结束而结束,数据也就丢失了,为了避免这种情况,我们应该给有状态应用提供共享存储的能力,因此每一个实例都得有自己的专用存储,彼此之间是不能重叠的,而且每一个实例的名字应该也是固定的,所有实例应该各自使用各自的专有存储,
假设满足了如上条件,也无法满足于其它问题,比如存储集群,对于集群来讲,集群维护所谓的变更,假设这个变更是系统的扩缩容,对于mysql主从复制集群来说,随便加入的节点通常应该只能是一个从节点,这个从也应该位于其它从节点之后,而且不能与其它的节点同名,如果在一主多从的节点中,某一个节点挂了,那么主与从的替换方式也是不同的,如果是从,我们加入一个节点将把配置为主节点的从就行,但如果是主,需要将某一个从节点提升为主节点,同时还得确保其它的从节点能够正常连接主节点进行复制才可以,还需要检验数据均衡,并且还得确保主节点获取此前主节点的数据是正常的;
在比如Redis集群的扩缩容问题,我们所有数据集都是分散在多个节点上的,那么进行缩容就可能会产生各种各样的问题,所以操作就会很复杂,而这个问题就算能解决,redis的缩容与mysql主从缩容逻辑也是不一样的,对于有状态应用来讲,它所需的运维逻辑或操作步骤都不尽相同,所以缩容不是那么简单就能缩的,扩容也不是想扩就能扩的,如果是主从加个节点很简单,但如果是集群,它本来就是数据切分,或者是分片存储的,那么此时扩缩容就会很困难了;
因此,这些功能对其进行扩缩容时都需要考虑在内,而各种各样的或存储、或消息队列等一类的服务,基本上没有通用法则,因此没有任何一个机制能够通用语有状态的应用,逻辑极其复杂,发布变更处理扩缩容处理,基本上没有一个统一的办法来解决问题,所以我们的Kubernetes之上的StatefulSet能实现的是最多能帮你解决确保每一个实例的名字是固定的,可以为每一个实例分配一个固定存储,至于扩缩容,还是没有解决,Kubernetes也解决不了;
所以如果想使用Kubernetes的StatefulSet,你得在很大程序上,自己对特定应用程序的集群编写一大堆的代码来实现这个功能,也就是说,自己去写一个清单,去定义StatefulSet的Pod模版,以实现扩缩容,比如加一个节点或者减一个节点,你得把自己的成熟的运维操作逻辑或者过程,封装成程序,写在配置清单当中,以便于扩容、缩容不会出现故障才行;
但是,还是有很多人有可能会在使用StatefulSet来运行一个有状态的应用,但是StatefulSet又无法完全做到有状态的管理,所以有很多人,纷纷把自己写的专有的StatefulSet的配置清单,比如MySQL做了一个项目,开源到GITHUB,供人下载,但是这个配置清单也是极为复杂的,任何一个环节出现问题,都有可能会遇到致命灾难,所以早期Kubernetes在应用的时候,任然只会把无状态应用部署到Kubernetes上,把有状态应用依然留到Kubernetes之外,无状态应用对Kubernetes之外的有状态访问就通过所谓外部服务引入到集群内部的方式访问,但是这终究不是解决方案;
不同的分布式系统它们的管理、逻辑和运维操作都是不尽相同的,因此没有办法有一种控制器把每一种应用都操作起来,即便有了statefulset在定义不同的分布式系统时使用也是即其麻烦的,statefulset尽管在一定程序上能实现有状态应用的管理桥,但仍需要自行把我们对某一个应用的运维管理过程写成脚本写成脚本注入到statefulset的应用文件中才能使用
Operator
所以Kubernetes就提出了解决方案,后来由一家叫做CoreOS的公司提出了一个解决方法,它提供了一个接口,能够让用户自行的去开发一段代码,这个代码可以使用任何编程语言编写,比如Python、Java,C代码封装,而对于应用程序的解决方案需要成熟的运维人员手动操作所有运维步骤,只这些只针对一种特定的应用;
比如Redis主从复制集群,那么它就需要对redis在必要时可以初始化集群、在有可能必要的情况下进行动态扩展、动态缩容、以及销毁,就是将这些操作功能,用一个非常成熟的方式将运维人员所需要的操作,用代码封装起来,并将这个封装成一个应用程序;
而这个用户定义的这个redis-controller就不叫controller,CoreOS为了区别Kubernetes之上原来那个简单的Controller给它取名叫做Operator,它需要适用于每一种不同的有状态应用 ,开发了一个需要专门用到所有运维技能的封装,而这个应用程序,只需要在互联网上开源出来,人人都可以下载来,部署为Pod控制器,就像ingress一样,所以以后对这个集群的管理就可以委托给Operator就行了;
而这个Operator也需要运行为一个Pod,它也需要一个控制器来管理,所以我们可以使用Deployment控制它即可,它本身是无状态的,可以被替换的,所以使用Deployment来控制着这个Pod,这个Pod里面控制是另外一个Controller,控制着另外一组集群,每一组集群叫做一个实例,有了这样的项目,那我们就能够安全无虞的,将有状态服务跑在Kubernetes之上了;
而云原生是2018年的关键词之一,很多有项目的官方都开始自己去开发这么一个Operator,比如redis官方就有redis的Operator,可以借助这个Operator把它部署在Kubernetes之上用来管理redis,MySQL官方也就是Oracle,也专门开发了一款基于MySQL管理的Operator,zookeeper官方也有自己的Operator,所以在将来我们在Kubernetes之上去部署有状态应用,使用的不是StatefulSet而是Operator;
目前来讲这些Operator越来越多,Operator其实就是对StatefulSet进行的一些功能扩展,对我们用户来讲不应该使用StatefulSet而是Operator,但是Operator内部封装的还是StatefulSet,所以还是需要了解StatefulSet的运行机制;
自从出现了Operator以后,我们就需要开发Operator才能够更好的在Kubernetes之上去部署有状态应用,而不是在StatefulSet上去开发它的配置清单了,如何能够让用户更快的去开发Operator,那么CoreOS这个组织就在对应的Kubernetes之上额外引入了一个开发接口,就是,Operator的SDK,用户可以借助于SDK开发出来Operator控制器,这也就意味着,第三方程序员再去开发云原生应用不是直接针对于Kubernetes云原生API,而是针对这个SDK,CoreOS是属于RedHat旗下的产品,RedHat是IBM旗下的产品;
=所以对于Operator,只不过把StatefulSet的代码,结合某一个特有应用程序的特有运行逻辑,做了二次封装而已,Operator SDK只是让封装写起来更容易的一个开发工具箱和API;
目前主流工具的Operator列表:https://github.com/operator-framework/awesome-operators
StatefulSets要求
特点
- 每一个节点稳定且需要有唯一的网络标识符
- 稳定且持久的存储设备;
- 要求有序、平滑的部署和扩展; 如redis主从负载集群 (先主后从)
- 有序、平滑的终止和删除; 如8个从节点 R1-R8开, 那就R8-R1关
- 有序的、自动的滚动更新;
稳定意味着 Pod 调度或重调度的整个过程是有持久性的。如果应用程序不需要任何稳定的标识符或有序的部署、删除或伸缩,则应该使用由一组无状态的副本控制器提供的工作负载来部署应用程序,比如 Deployment 或者 ReplicaSet 可能更适用于您的无状态应用部署需要
限制
- 给定 Pod 的存储必须由 PersistentVolume 驱动 基于所请求的
storage class
来提供,或者由管理员预先提供 - 删除或者收缩 StatefulSet 并不会删除它关联的存储卷。这样做是为了保证数据安全,它通常比自动清除 StatefulSet 所有相关的资源更有价值
- StatefulSet 当前需要 headless 服务 来负责 Pod 的网络标识。您需要负责创建此服务。
- 当删除 StatefulSets 时,StatefulSet 不提供任何终止 Pod 的保证。为了实现 StatefulSet 中的 Pod 可以有序和优雅的终止,可以在删除之前将 StatefulSet 缩放为 0。
- 在默认 Pod 管理策略(
OrderedReady
) 时使用 滚动更新,可能进入需要 人工干预 才能修复的损坏状态。
组件与特性
**三个组件:**headless service、Statefulset控制器、volumeClaimTemplate(存储卷申请模板)
- StatefulSet里的每个pod都有稳定、唯一的网络标识,可以用来发现集群内的其它成员,假设Statefulset的名字叫kafka,那么第1个Pod叫kafka-0,第2个叫kafka-1,以此类推
- StatefulSet控制的Pod副本的启停顺序是受控的,操作第N个Pod时,前N-1个Pod已经是运行且准备好的状态
- StatefulSet里的Pod采用稳定的持久化存储卷,通过PV/PVC来实现,删除Pod时默认不会删除与StatefulSet相关的存储卷(为了保证数据的安全),每个节点应该有自己专用的存储卷,一定不能共享给其它节点
StatefulSet除了要与PV卷捆绑使用以存储Pod的状态数据,还要与Headless Service配合使用,即在每个StatefulSet的定义中要声明它属于哪个 Headless Service,Headless Service与普通Service的关键区别在于,它没有Cluster IP 无头服务,如果解析Headless Service的DNS域名,则返回的是 Service对应的全部Pod的Endpoint列表。
StatefulSet在Headless Service的基础上又为StatefulSet 控制的每个Pod实例创建了一个DNS域名,这个域名格式为: pod_name.service_name.ns_name.svc.cluster.local
如 kafka-0.kafka.default.svc.cluster.local
比如一个3节点的Kafka的StatefulSet集群,对应的Headless Service名字为kafka, StatefulSet的名字为kafka,则StatefulSet里面的3个Pod的DNS名称分别为 kafka-0.kafka、kafka-1.kafka、kafka-2.kafka,这些DNS名称可以直接在集群的配置文件中固定下来
Pod管理策略
一般来说,在创建StatefulSet时Pod是串行构建的,编号从小到大依次构建,缩容也是如此,StatefulSet的Pod管理就有如下几种策略,通过spec.podManagementPolicy定义
- OrderedReady Pod Management:ready之后才下一个
- Parallel Pod Management:可以同时创建
StatefulSets示例
初始化
- 搭建nfs
# 随便找台机器整个nfs做下测试
]# yum -y install nfs-utils
]# cat /etc/exports
/data 192.168.2.0/24(rw)
]# showmount -e
/data 192.168.2.0/24
-
创建pv
apiVersion: v1 kind: PersistentVolume metadata: name: sts-pv-1 # 每个名称不同即可 labels: name: sts-pv release: qa spec: accessModes: - ReadWriteOnce # 单路访问 capacity: storage: 1Gi nfs: server: 192.168.2.221 path: /data/nfs/v1 storageClassName: slow # 机械盘 slow volumeMode: Filesystem # 挂载卷的类型是文件系统 --- ... # 共创建5个
-
创建pvc
# 底层存储 --> pv --> pvc
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sts-pvc-1
labels:
name: sts-pvc
release: qa
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: slow # 类型
volumeMode: Filesystem
selector: # 标签选择器选择
matchLabels:
name: sts-pv
release: qa
--- # 这里也定义5个
创建sts
- 说明
Service 需要定义为无头服务
几个必要的参数
sts.spec
replicas: 定义几个副本
selector: 哪些pod副本可被管理
serviceName: 必须要关连到某一个无头服务上, 必须在创建sts之前,只有基于这个无头服务才能给每个pod分配一个唯一的持久的固定标识符
template: pod模板, 在此处关连某个存储卷(pvc) 容器挂载
volumeClaimTemplates: pvc应该由这个来生成
- 创建 StatefulSet
]# cat sts_test.yaml
apiVersion: v1
kind: Service # 注意,必须在sts之前先创建 无头service
metadata:
name: sts-server
labels:
app: sts-server
spec:
clusterIP: None
type: ClusterIP
ports:
- name: myapp
targetPort: 30080
port: 80
selector:
app: my-sts-nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: myapp
namespace: default
labels:
app: my-sts-nginx
annotations:
state/myapps: "v1"
spec:
replicas: 3
selector:
matchLabels:
app: my-sts-nginx
serviceName: sts-server
template:
metadata:
name: myapp
labels:
app: my-sts-nginx
spec:
containers:
- name: myapp
image: ikubernetes/myapp:v1
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
volumeMounts: # 挂载点
- name: myapp-pv
mountPath: /usr/share/nginx/html
volumeClaimTemplates: # 让sts自动创建pvc关连pv
- metadata:
name: myapp-pv
namespace: default
spec:
accessModes: ["ReadWriteOnce"] # 每次只能一个节点访问pv
resources:
requests:
storage: 1Gi
- 应用之后查看状态
]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES AGE
myapp-pv-myapp-0 Bound data-nfs5 1Gi RWO 3m51s
myapp-pv-myapp-1 Bound data-nfs2 1Gi RWO 3m35s
myapp-pv-myapp-2 Bound data-nfs1 1Gi RWO 3m33s
]# kubectl get pv
NAME CAPACITY 访问模式 回收策略 STATUS CLAIM AGE
data-nfs1 1Gi RWO Retain Bound default/myapp-pv-myapp-2 4m6s
data-nfs2 1Gi RWO Retain Bound default/myapp-pv-myapp-1 4m6s
data-nfs3 1Gi RWO Retain Available 4m6s
data-nfs4 1Gi RWO Retain Available 4m5s
data-nfs5 1Gi RWO Retain Bound default/myapp-pv-myapp-0 4m5s
# 创建过程 稳定、唯一的网络标识 往0后创建 pod_name_number
]# kubectl get pods -w 有序的从前往后一个一个创建
NAME READY STATUS RESTARTS AGE
myapp-0 1/1 Running 0 17s
myapp-1 0/1 ContainerCreating 0 1s
myapp-1 1/1 Running 0 2s
myapp-2 0/1 Pending 0 0s
myapp-2 0/1 Pending 0 0s
myapp-2 0/1 Pending 0 1s
myapp-2 0/1 ContainerCreating 0 1s
myapp-2 1/1 Running 0 3s
]# kubectl describe sts myapp
Name: myapp
Namespace: default
CreationTimestamp: Fri, 08 May 2020 15:14:07 +0800
Selector: app=my-sts-nginx
Labels: app=my-sts-nginx
Annotations: state/myapps: v1
Replicas: 4 desired | 4 total
Update Strategy: RollingUpdate
Partition: 0
Pods Status: 4 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
Labels: app=my-sts-nginx
Containers:
myapp:
Image: ikubernetes/myapp:v1
Port: 80/TCP
Host Port: 0/TCP
Environment: <none>
Mounts:
/usr/share/nginx/html from myapp-pv (rw)
Volumes: <none>
Volume Claims:
Name: myapp-pv
StorageClass:
Labels: <none>
Annotations: <none>
Capacity: 1Gi
Access Modes: [ReadWriteOnce]
Events: <none>
- 检验
]# kubectl exec -it myapp-0 -- /bin/sh
/ # wget -O - -q myapp-0/index.html
5 # 修改nfs中v5目录下的目录,访问是即时生效的
- 扩容一个副本
]# kubectl scale --replicas=4 statefulset myapp
~]# kubectl get pod -w
NAME READY STATUS RESTARTS AGE
myapp-0 1/1 Running 0 29m
myapp-1 1/1 Running 0 29m
myapp-2 1/1 Running 0 29m
myapp-3 0/1 Pending 0 1s
myapp-3 0/1 ContainerCreating 0 1s
myapp-3 1/1 Running 0 3s
]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES AGE
myapp-pv-myapp-3 Bound data-nfs3 1Gi RWO 52s
]# kubectl get pv
NAME CAPACITY 访问模式 回收策略 STATUS CLAIM AGE
data-nfs3 1Gi RWO Retain Bound default/myapp-pv-myapp-3 30m
sts更新策略
- 参数说明
~]# kubectl explain sts.spec.updateStrategy.rollingUpdate
updateStrategy: 自定义更新策略
rollingUpdate: 自定义更新策略
partition: 更新分区, 默认分区0, pod标识符
大于等于N的标签更新,如5个pod, N为5就只更新第5个,0就更新全部
type:
- 配置更新策略
# 与sts配置一样
spec: # 添加更新策略
updateStrategy:
rollingUpdate:
partition: 3 # 大于3的,从第4个节点开始金丝雀方式更新
# 或者 直接用裁剪patch
~]# kubectl patch statefulsets myapp -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":3}}}}'
]# kubectl describe sts myapp
Name: myapp
Namespace: default
CreationTimestamp: Sat, 09 May 2020 08:42:23 +0800
Selector: app=my-sts-nginx
Labels: app=my-sts-nginx
Annotations: state/myapps: v1
Replicas: 5 desired | 5 total
Update Strategy: RollingUpdate
Partition: 3
- 滚动更新
]# kubectl set image sts/myapp myapp=ikubernetes/myapp:v2
statefulset.apps/myapp image updated
~]# kubectl get pods -w 更新策略 从大于第3个开始更新 ,从最后往前推
NAME READY STATUS RESTARTS AGE
myapp-0 1/1 Running 0 14m
myapp-1 1/1 Running 0 14m
myapp-2 1/1 Running 0 14m
myapp-3 1/1 Running 0 14m
myapp-4 1/1 Running 0 14m
myapp-4 1/1 Terminating 0 15m
myapp-4 0/1 Pending 0 0s
myapp-4 0/1 ContainerCreating 0 1s
myapp-4 1/1 Running 0 3s
myapp-3 0/1 Terminating 0 15m
myapp-3 0/1 Pending 0 0s
myapp-3 0/1 ContainerCreating 0 0s
myapp-3 1/1 Running 0 2s