15、StatefulSet与Operator
回顾
StatefulSet
Operator
StatefulSet环境要求
限制
Pod标识符
Pod管理策略
更新操作
StatefulSet示例
SpringCloud要点
SpringBoot获取环境变量
StatefulSet基础要点
StatefulSet
Operator
StatefulSet环境要求
限制
Pod标识符
Pod管理策略
更新操作
StatefulSet示例
SpringCloud要点
SpringBoot获取环境变量
StatefulSet基础要点
回顾
在Kubernetes之上最基础运行的单元是Pod,其他资源基本都是围绕着丰富Pod本身的应用而实现的,比如Service用来给Pod提供一个固定的访问接口,并且为动态Pod变动提供一个服务发现机制基础单元,在Kubernetes之上服务就是指的Service,服务注册、服务发现都是借助于Service在Kubernetes所运行的DNS服务来实现的,只不过DNS是一个附件,它本身也是以所谓Pod的方式运行在Kubernetes之上,但是它比普通的Pod功能要强大,主要为Pod提供基础服务逻辑我们称之为CoreDNS,Service注册在CoreDNS之上,从而能够为其他客户端在通过DNS解析服务名称时,完成服务发现,因此CoreDNS扮演了服务和发现的总线,CoreDNS也是围绕着基础服务构建的,Pod本身为了能够提供更好的动态、自愈包括伸缩等功能需要借助于Pod控制器,而Pod控制器是归类成为的一组控制器,他们分别有多种实现,分别应用不同的逻辑,比如Deployment,Deployment能够真正建构在ReplicaSet之上,完成真正意义上的所谓无状态应用的各种运维管理工作,比如部署、变更等功能,对于Deployment只能维护那些不固定数量的,所谓不固定指的是用户可以按需去调整数量,我们也无法去界定在一个节点资源上运行了多少这个Pod资源的这么一种控制器;
如果希望运行系统级应用,比如只在每一个节点运行一个Pod副本,或者在集群的部分节点上运行一个Pod副本,那就需要DaemonSet来实现,无论是DaemonSet、Deployment他们都是控制守护进程的,非守护进程的作业可以使用Job,而周期性作业可以使用CronJob,CronJob是Job更加高级的抽象功能,Job是作业,CronJob是用于实现调度周期性作业的一种逻辑,所以说Deployment建构在ReplicaSet之上,而CronJob则是建构在Job之上;
StatefulSet
以上描述的主要的是一些无状态应用,对于有状态应用来讲,应该使用的是StatefulSet控制器,为什么有状态应用要单独使用一个控制器呢,因此引用程序分为四种类型,他们分别是有状态有存储、无状态无存储、有状态无存储、有存储无状态,绝大多数的服务都是有状态有存储或无状态无存储的,比如Nginx,它就是无状态无存储的,这样的应用管理起来就极为便捷的,因为它的每一个实例可以不加区别的被其他实例所取代,而对于有状态有存储的,大多数有状态的也就是有存储的,这类应用一旦有存储这就有点麻烦,比如MySQL,我们有三个MySQL实例,但前面做了一个负载均衡器,如果说这个MySQL是允许写入的,用户的请求进来了,如果不加区别的使用MySQL会出现,写的时候写在了第一个MySQL服务器上,读的时候在第二个MySQL服务器上读,那显然读取不到数据,所以就会遇到这样的问题的,在这种情况执行,因此这种服务器出现固定我们就不能做到任何一个MySQL服务器出现故障之后随后任意重构一个取代掉,那很显然是做不到的,如果数据在本地存储,那如果一取代数据都没了;
即便我们在外部存储存放数据,实例蹦了,那还必须要确保这个存储挂载到这个存储上来,只有这样我们对于有状态的应用才能管理好的,所以我们使用Deployment来管理有状态服务很显然是不合适的,比如我们现在有三个MySQL实例,在前端做了一个Service,如果使用的是ipvs的调度类型,默认使用的RR算法,所有写操作,分别分布在三个MySQL实例上,将来MySQL蹦了,那么崩掉的MySQL实例的数据就丢掉了,所以对于有状态应用来讲,如果它必须用到存储,我们就得给每一个实例必须有一个独有的标识,重建的Pod是随机的,而在这种情况下就不能随机了,将来一个MySQL蹦了,控制器将其重建可以确定这个重建的MySQL属于原来的哪一个MySQL,并且将原来崩掉的MySQL的数据加载进来,进行一对一标识,不能向Deployment控制器一样,下面的Pod重建之后每一个Pod名称后面会有随机字符;
还有大多数有状态的服务都是有存储的,如果一个Pod蹦了,没了,如果把数据放在本地,那么就随着这个Pod的生命周期的结束,数据也就丢失了,为了避免这个情形,我们应该给有状态应用提供共享存储的能力,由于这是有状态应用,调度器很可能调度给每一个Pod实例的请求是不一样的,那么这个三个Pod能不能共享同一个存储呢,如果共享存储第一个Pod得到了一个name=luce的数据,第二个Pod上得到了一个name=alex的数据,那这个时候name到底是luce还是alex,我们现在要新建这个数据,它不知道此前是有这个数据的,把它直接覆盖了,那这就乱套了;
因此每一个实例都得有自己的专用存储,彼此之间是不能重叠的,而且每一个实例的名字应该也是固定的,所有实例各自使用各自的专有存储,就比如MySQL的一主多从,如果这多个从使用同一个数据集,那多个副本就没什么意义了,副本所在的存储系统一崩溃,或者说存储系统崩溃那这多个MySQL从节点就都没有数据了,那冗余在架构层面的意义就没有了,所以对于数据来说,就算本身是一样的,我们也不能使用同一个存储,那我们就不得不为每一个Pod挂载一个专有存储才可以,这还只是基础要求,我们的Deployment控制器是无法满足的,固定表示,专有存储;
但是即便满足了这两项条件,也无法满足其他问题,比如有一个存储集群,对于集群来讲,集群维护所谓的变更,也很常见,这个变更假如就是系统的扩缩容,对于MySQL主从复制集群来讲,随便加一个节点,这个加进来的节点通常应该只能是一个从,这个从也应该位于此前的其他从节点之后才对,而且还不能和前面两个从同名,如果现在一主三从的节点当中,某一个从节点蹦了,那蹦的是从和蹦的是主他们的替换方式也是不一样的,如果蹦了一个从,我们加入一个节点把它配置为主节点的从就完了,那如果蹦的是一个主,要把它替换成为主节点,同时还要确保余下的三个从节点能够正常的连接主节点进行复制才可以,还需要校验数据均衡,还要确保主节点获取此前的主节点的数据没有问题;
还有,扩缩容的问题,比如我们Redis集群,我们的所有数据集都是分散到三个节点的,那此时要进行缩容就可能有问题了,所以操作复杂了很多的,即便这个问题能够解决,但是redis的缩容和mysql主从缩容他们的逻辑也是不一样的,对于有状态的应用来讲,它的运维逻辑,或者所需要运维的操作步骤都不尽相同,所以缩容不是那么简单的就能缩的,扩容也不是简简单单的就你扩容的,如果是主从加个节点很容器,但是如果是集群,它本来是数据切分的,或者说数据是分片存储的,那这样的扩缩容可能不能那么简单的事;
因此,这些功能对其进行扩缩容时都需要考虑在内,而各种各样的或存储、或消息队列等一类的服务,基本上没有通用法则,因此没有任何一个机制能够通用语有状态的应用,逻辑极其复杂,发布变更处理扩缩容处理,基本上没有一个统一的办法来解决问题,所以我们的Kubernetes之上的StatefulSet能实现的是最多能帮你解决确保每一个实例的名字是固定的,可以为每一个实例分配一个固定存储,至于扩缩容,还是没有解决,Kubernetes也解决不了;
所以如果想使用Kubernetes的StatefulSet,你得在很大程序上,自己对特定应用程序的集群编写一大堆的代码来实现这个功能,也就是说,自己去写一个清单,去定义StatefulSet的Pod模版,以实现扩缩容,比如加一个节点或者减一个节点,你得把自己的成熟的运维操作逻辑或者过程,封装成程序,写在配置清单当中,以便于扩容、缩容不会出现故障才行;
但是,还是有很多人有可能会在使用StatefulSet来运行一个有状态的应用,但是StatefulSet又无法完全做到有状态的管理,所以有很多人,纷纷把自己写的专有的StatefulSet的配置清单,比如MySQL做了一个项目,开源到GITHUB,供人下载,但是这个配置清单也是极为复杂的,任何一个环节出现问题,都有可能会遇到致命灾难,所以早期Kubernetes在应用的时候,任然只会把无状态应用部署到Kubernetes上,把有状态应用依然留到Kubernetes之外,无状态应用对Kubernetes之外的有状态访问就通过所谓外部服务引入到集群内部的方式访问,但是这终究不是解决方案;
Operator
所以Kubernetes就提出了解决方案,后来有一家叫做CoreOS的公司提出了一个解决方法,它提供了一个接口,能够让用户自行的去开发一段代码,这个代码可以使用任何编程语言编写比如Python、Java,C这段代码封装了,对某一种应用程序的需要成熟的运维人员的手动操作所有运维步骤内容,但只对一种特定应用,比如Redis主从复制集群,那么它就需要对redis在必要时能初始化集群、有可能必要的时候进行动态扩展、动态缩容、必要时销毁,就是把这些操作功能,此前需要写入配置清单的代码,经由充分测试之后,用一个非常成熟的方式把运维人员所需要的操作,用代码封装起来,并把这个封装成一个应用程序,比如这个程序我们自定为redis-controller,这个程序就像ingress一样,把它以Pod的形式运行在Kubernetes之上,将来想去创建一个redis集群只需要向用户定义的这个redis-controller发送信息,告诉redis-controller需要新建一个集群,你只需要告诉它有几个副本,比如一主两从,这个一主两从的redis集群就叫做redis-cluster,它被当做一个单独的实体进行管理,比如将来可以对它进行扩容、缩容,因为扩容、缩容都在用户定义的这个redis-controller封装有代码,各种各样的操作;
而这个用户定义的这个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列表:<a href="https://github.com/operator-framework/awesome-operators">https://github.com/operator-framework/awesome-operators</a>
StatefulSet环境要求
StatefulSet要求应用程序有几个特点;
1、每个实例都有固定,唯一的网络标记符;
2、每个实例都有固定,唯一的持久存储;
3、每个实例都有有序优雅的部署和扩展;
4、每个实例都有有序优雅的删除或者终止;
5、每个实例都有有序自动执行滚动更新;
限制
对于StatefulSet要想真正应用起来还有几个硬性条件;
1、各Pod用到的存储卷必须使用由StorageClass动态供给或由管理员事先创建好的PV;
2、删除StatefulSet或缩减其规模导致Pod删除时,不应该自动删除其存储卷以确保数据安全;
3、StatefulSet控制器依赖于一个事先存在的Headless Service对象实现Pod对象的持久、唯一的标识符配置,此Headless Service需要由用户手动配置,每一个Pod都应该有自己的唯一标识,就算这个Pod重建标识也不应该变更,StatefulSet自己做不到这个功能需要借助于Headless Service;
Pod标识符
在StatefulSet上每一个Pod标识符应该是固定且唯一的, 但是Pod还是由StatefulSet控制器所控制的,所以事先无法做好命名,没法事先做好命令就只能动态生成,动态生成就无法固定的,因此,为了确保它能够固定,StatefulSet使用有序索引来构建,比如StatefulSet叫做web,如果的deployment后面可能有一个随机字符串,而对于StatefulSet来讲是按顺序索引来构建的,第一个叫web-0、第二个叫web-1以此类推,如果web-0宕机了,随后重建出来的还是web-0,它被称之为Ordinal Index,有三个副本的,它的索引就是0-2;
对于有状态应用到的控制器的名字格式是:$(statefulset_name)-$(ordinal);
一个每一个Pod在使用完整的FQDN格式主机名访问时,格式是:$(service_name).$(namespace).svc.cluster.local,因为它是无头服务,所以在解析时是得到的是Pod的IP;
Pod名称也是固定的,他的格式是:$(pod_name).$(service_name).$(namespace).svc.cluster.local;
Pod管理策略
如果一个StatefulSet下面有很多Pod,这些Pod默认是串行构建的,先构建第一个,等他Running且ready之后再构建下一个,缩容也是如此,从最大编号开始缩,因此StatefulSet的Pod管理就有了几种策略,spec.podManagementPolicy字段;
OrderedReady Pod Management:ready之后才下一个;
Parallel Pod Management:可以同时创建;
更新操作
比如,MySQL5.5希望升级到MySQL5.6使用StatefulSet来控制,也和Deployment一样,先把其中的一部分进行完成更新,更新的时候更新策略可以是灰度还可以指定最多比用户期望的副本多出几个,或者能少几个,来控制更新粒度,还能实现金丝雀更新逻辑;
On Delete:默认不更新,只有当用户手动删除现有Pod对象才会触发更新;
Rolling Updates:默认策略,它通过自动更新机制完成更新过程,启动更新过程时,它自动删除每个Pod对象并以新配合进行重建,更新顺序同删除StatefulSet时的逆向操作机制,一次删除并更新一个Pod对象;
StatefulSet示例
利用无状态服务模拟有状态服务器,测试StatefulSet控制器;
# 各节点安装NFS
[root@node1 ~]# yum install -y nfs-utils
# 创建共享目录
[root@node1 ~]# mkdir -p /data/volumes/v{0..6}
# 导出nfs目录
[root@node1 ~]# cat /etc/exports
/data/volumes/v0 172.16.0.0/16(rw)
/data/volumes/v1 172.16.0.0/16(rw)
/data/volumes/v2 172.16.0.0/16(rw)
/data/volumes/v3 172.16.0.0/16(rw)
/data/volumes/v4 172.16.0.0/16(rw)
/data/volumes/v5 172.16.0.0/16(rw)
/data/volumes/v6 172.16.0.0/16(rw)
[root@node3 ~]# for i in {0..6};do echo "v${i}" > /data/volumes/v${i}/index.html;done
# 创建pv,和名称空间
[root@node1 ~]# cat pv.yaml
apiVersion: v1
kind: Namespace
metadata:
name: sts
---
# 创建pv
apiVersion: v1
kind: PersistentVolume
metadata:
name: sts-pv-v0
namespace: sts
labels:
contorller: statefulset
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 5G
nfs:
path: /data/volumes/v0
server: 172.16.1.2
---
# 创建pv
apiVersion: v1
kind: PersistentVolume
metadata:
name: sts-pv-v1
namespace: sts
labels:
contorller: statefulset
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 5G
nfs:
path: /data/volumes/v1
server: 172.16.1.2
---
# 创建pv
apiVersion: v1
kind: PersistentVolume
metadata:
name: sts-pv-v2
namespace: sts
labels:
contorller: statefulset
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 5G
nfs:
path: /data/volumes/v2
server: 172.16.1.2
---
# 创建pv
apiVersion: v1
kind: PersistentVolume
metadata:
name: sts-pv-v3
namespace: sts
labels:
contorller: statefulset
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 5G
nfs:
path: /data/volumes/v3
server: 172.16.1.2
---
# 创建pv
apiVersion: v1
kind: PersistentVolume
metadata:
name: sts-pv-v4
namespace: sts
labels:
contorller: statefulset
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 5G
nfs:
path: /data/volumes/v4
server: 172.16.1.2
---
# 创建pv
apiVersion: v1
kind: PersistentVolume
metadata:
name: sts-pv-v5
namespace: sts
labels:
contorller: statefulset
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 5G
nfs:
path: /data/volumes/v5
server: 172.16.1.2
---
# 创建pv
apiVersion: v1
kind: PersistentVolume
metadata:
name: sts-pv-v6
namespace: sts
labels:
contorller: statefulset
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 5G
nfs:
path: /data/volumes/v6
server: 172.16.1.2
[root@node1 ~]# kubectl get pv -n sts
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
sts-pv-v0 5G RWO Retain Available 8s
sts-pv-v1 5G RWO Retain Available 8s
sts-pv-v2 5G RWO Retain Available 8s
sts-pv-v3 5G RWO Retain Available 8s
sts-pv-v4 5G RWO Retain Available 8s
sts-pv-v5 5G RWO Retain Available 8s
sts-pv-v6 5G RWO Retain Available 8s
# 创建一个StatefulSet应用
[root@node1 ~]# cat sts.yaml
# 创建一个无头Service
apiVersion: v1
kind: Service
metadata:
name: sts-service
namespace: sts
labels:
contorller: statefulset
spec:
clusterIP: None # 无头Service的标志
ports:
- name: http
targetPort: 80
port: 80
protocol: TCP
selector:
app_name: pod
contorller: statefulset
# 创建StatefulSet,创建方法其实和Deployment没什么两样,无非就是多了个volumeClaimTemplates
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: sts-pod-contoller
namespace: sts
spec:
replicas: 2
selector:
matchLabels:
app_name: pod
contorller: statefulset
serviceName: sts-service
template:
metadata:
name: sts-pod
namespace: sts
labels:
app_name: pod
contorller: statefulset
spec:
containers:
- name: containers
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
image: ikubernetes/myapp:v1
volumeMounts:
- name: sts-pvc # 挂载pvc
mountPath: /usr/share/nginx/html
volumeClaimTemplates: # 动态卷模版,用户自动创建pvc并且关联到pv
- metadata:
name: sts-pvc # pvc名称前缀,会动态生成,pvc的名字是$(pvc-prefix)-$(pod_name)-pod名最后的数字1、2、3....
namespace: sts # pvc的位于的名称空间
spec:
accessModes: # pvc为单路读写
- ReadWriteOnce
resources: # 请求资源,期望的存储空间有多大
requests:
storage: 2G
[root@node2 ~]# kubectl get pods -n sts
NAME READY STATUS RESTARTS AGE
sts-pod-contoller-0 1/1 Running 0 24m
sts-pod-contoller-1 1/1 Running 0 20m
# 查看pv是否Bound
[root@node1 ~]# kubectl get pv -n sts
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
sts-pv-v0 5G RWO Retain Bound sts/sts-pvc-sts-pod-contoller-1 32m
sts-pv-v1 5G RWO Retain Bound sts/sts-pvc-sts-pod-contoller-0 32m
sts-pv-v2 5G RWO Retain Available 32m
sts-pv-v3 5G RWO Retain Available 32m
sts-pv-v4 5G RWO Retain Available 32m
sts-pv-v5 5G RWO Retain Available 32m
sts-pv-v6 5G RWO Retain Available 32m
# 随便进入一个pod,测试访问是否正常
[root@node2 ~]# kubectl exec -it -n sts sts-pod-contoller-0 /bin/sh
/ # wget -O - -q sts-pod-contoller-0.sts-service
v1
/ # wget -O - -q sts-pod-contoller-1.sts-service
v0
# 手动重建sts-pod-contoller-1,重建完成之后查看Pod名是否还是sts-pod-contoller-1
[root@node1 ~]# kubectl delete pod -n sts sts-pod-contoller-1
[root@node1 ~]# kubectl get pods -n sts
NAME READY STATUS RESTARTS AGE
sts-pod-contoller-0 1/1 Running 0 28m
sts-pod-contoller-1 1/1 Running 0 108s # 名称一致
# 查看数据卷是否会自动挂载,挂载完成之后数据和之前的是否一致
[root@node2 ~]# kubectl exec -it -n sts sts-pod-contoller-0 /bin/sh
/ # wget -O - -q sts-pod-contoller-1.sts-service
v0 # 数据一致
# 手动扩展到四个节点
[root@node1 ~]# kubectl scale --replicas=4 -n sts statefulset sts-pod-contoller
[root@node3 ~]# kubectl get pvc -n sts
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
sts-pvc-sts-pod-contoller-0 Bound sts-pv-v1 5G RWO 32m
sts-pvc-sts-pod-contoller-1 Bound sts-pv-v0 5G RWO 29m
sts-pvc-sts-pod-contoller-2 Bound sts-pv-v6 5G RWO 11s
sts-pvc-sts-pod-contoller-3 Bound sts-pv-v2 5G RWO 7s
# 查看数据
[root@node2 ~]# kubectl exec -it -n sts sts-pod-contoller-0 /bin/sh
/ # wget -O - -q sts-pod-contoller-0.sts-service
v1
/ # wget -O - -q sts-pod-contoller-1.sts-service
v0
/ # wget -O - -q sts-pod-contoller-2.sts-service
v6
/ # wget -O - -q sts-pod-contoller-3.sts-service
v2
# 测试版本更新
[root@node1 ~]# kubectl set image sts -n sts sts-pod-contoller containers=ikubernetes/myapp:v2
# 查看更新过程,默认是一个一个来,首先更新1等1running并ready之后再更新2,经过测试,更新是无序的
[root@node1 ~]# kubectl get pods -n sts -w
NAME READY STATUS RESTARTS AGE
sts-pod-contoller-0 1/1 Running 0 12m
sts-pod-contoller-1 1/1 Running 0 12m
sts-pod-contoller-1 1/1 Terminating 0 12m # 开始移除第1个
sts-pod-contoller-1 0/1 Terminating 0 12m
sts-pod-contoller-1 0/1 Terminating 0 12m
sts-pod-contoller-1 0/1 Terminating 0 12m
sts-pod-contoller-1 0/1 Pending 0 0s
sts-pod-contoller-1 0/1 Pending 0 0s
sts-pod-contoller-1 0/1 ContainerCreating 0 0s
sts-pod-contoller-1 1/1 Running 0 5s # 第1个运行成功
sts-pod-contoller-0 1/1 Terminating 0 12m # 开始移除第0个
sts-pod-contoller-0 0/1 Terminating 0 12m
sts-pod-contoller-0 0/1 Terminating 0 12m
sts-pod-contoller-0 0/1 Terminating 0 12m
sts-pod-contoller-0 0/1 Pending 0 0s
sts-pod-contoller-0 0/1 Pending 0 0s
sts-pod-contoller-0 0/1 ContainerCreating 0 0s
sts-pod-contoller-0 1/1 Running 0 5s # 第0个运行成功
# 修改更新策略实现金丝雀发布
[root@node1 ~]# cat sts.yaml
# 创建一个无头Service
apiVersion: v1
kind: Service
metadata:
name: sts-service
namespace: sts
labels:
contorller: statefulset
spec:
clusterIP: None # 无头Service的标志
ports:
- name: http
targetPort: 80
port: 80
protocol: TCP
selector:
app_name: pod
contorller: statefulset
# 创建StatefulSet,创建方法其实和Deployment没什么两样,无非就是多了个volumeClaimTemplates
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: sts-pod-contoller
namespace: sts
spec:
replicas: 2
updateStrategy:
rollingUpdate:
partition: 1 # 默认从那个Pod索引号开始更新
selector:
matchLabels:
app_name: pod
contorller: statefulset
serviceName: sts-service
template:
metadata:
name: sts-pod
namespace: sts
labels:
app_name: pod
contorller: statefulset
spec:
containers:
- name: containers
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
image: ikubernetes/myapp:v1
volumeMounts:
- name: sts-pvc # 挂载pvc
mountPath: /usr/share/nginx/html
volumeClaimTemplates: # 动态卷模版,用户自动创建pvc并且关联到pv
- metadata:
name: sts-pvc # pvc名称前缀,会动态生成,pvc的名字是$(pvc-prefix)-$(pod_name)-pod名最后的数字1、2、3....
namespace: sts # pvc的位于的名称空间
spec:
accessModes: # pvc为单路读写
- ReadWriteOnce
resources: # 请求资源,期望的存储空间有多大
requests:
storage: 2G
# 查看更新状态,会发现只从索引号为2开始更新0和1并没有更新
[root@node1 ~]# kubectl get pods -n sts -w
NAME READY STATUS RESTARTS AGE
sts-pod-contoller-0 1/1 Running 0 2m18s
sts-pod-contoller-1 1/1 Running 0 2m35s
sts-pod-contoller-2 1/1 Running 0 23s
sts-pod-contoller-3 1/1 Running 0 30s
sts-pod-contoller-3 1/1 Terminating 0 39s
sts-pod-contoller-3 0/1 Terminating 0 40s
sts-pod-contoller-3 0/1 Terminating 0 52s
sts-pod-contoller-3 0/1 Terminating 0 52s
sts-pod-contoller-3 0/1 Pending 0 0s
sts-pod-contoller-3 0/1 Pending 0 0s
sts-pod-contoller-3 0/1 ContainerCreating 0 0s
sts-pod-contoller-3 1/1 Running 0 2s
sts-pod-contoller-2 1/1 Terminating 0 47s
sts-pod-contoller-2 0/1 Terminating 0 48s
sts-pod-contoller-2 0/1 Terminating 0 49s
sts-pod-contoller-2 0/1 Terminating 0 49s
sts-pod-contoller-2 0/1 Pending 0 0s
sts-pod-contoller-2 0/1 Pending 0 0s
sts-pod-contoller-2 0/1 ContainerCreating 0 0s
sts-pod-contoller-2 1/1 Running 0 2s
SpringCloud要点
对于springcloud架构,除了eureka之外,全部可以使用Deployment,erueka则需要使用statefulset,因为statefulset的主机名才会被我们的dns解析,这样其他的Pod才可以直接联系到我们的zuul进行注册,对于其他微服务还有一个要求,就是需要使用ip注册到eureka,因为主机名在Pod之间是无法解析的,这样会造成服务调用之间的通信问题,在springboot配置文件里面配置eureka.instance.prefer-ip-address=true;
示例代码以及配置如下:https://github.com/caichangen/kubernetes_springCloud.git
SpringBoot获取环境变量
对于配置文件的问题,向开发提出需求,将各种配置定义为环境变量,因为经过测试java使用yaml语法的时候可以直接使用${var_name}的方式读取系统环境变量,当docker启动SpringBoot打包的服务时,且一些参数需要从外界获取而非写死在properties文件里,通过以下两步完成此需求;
在配置文件中配置环境变量
spring.redis.host=${REDIS_HOST:127.0.0.1}
spring.redis.port=6379
spring.redis.timeout=30000
# 以上表是REDIS_HOST在系统环境变量中获取,如果获取不到默认值为127.0.0.1
在启动docker容器时传入环境参数
[root@node1 ~]# docker run -d --name test2 {镜像名} -e REDIS_HOST=192.168.0.1
StatefulSet基础要点
对于statefulset来讲,我们定义的Pod名称是没意义的,其就是一个占位符,因为在template.metadata.name这个字段是必须的,所以它只是一个占位符,而真正的Pod名称是控制器的名称后面跟一个数字,比如spring-cloud-controller-0;
对于任何控制器下的Pod来讲,容器名称只是一个标识符,比如我们的set image的时候需要使用-c指定容器,那么这个时候就需要用到容器名称来指定修改哪一个容器的镜像,因为一个Pod内部可以有多个容器;