做为一个后端工程师,由于负责的大部分项目都是Web
服务这类的“无状态应用”,在平时工做中接触到的最经常使用的Kubernetes
控制器是Deployment
,可是Deployment
只适合于编排“无状态应用”,它会假设一个应用的全部 Pod
是彻底同样的,互相之间也没有顺序依赖,也无所谓运行在哪台宿主机上。正由于每一个Pod
都同样,在须要的时候能够水平扩/缩,增长和删除Pod
。html
可是并非全部应用都是无状态的,尤为是每一个实例之间有主从关系的应用和数据存储类应用,针对这类应用使用Deployment
控制器没法实现正确调度,因此Kubernetes
里采用了另一个控制器StatefulSet
负责调度有状态应用的Pod
,保持应用的当前状态始终等于应用定义的所需状态。node
和Deployment
同样StatefulSet
也是一种能够帮助你部署和扩展Kubernetes
Pod
的控制器,使用Deployment
时多数时候你不会在乎Pod
的调度方式。但当你须要关心Pod
的部署顺序、对应的持久化存储或者要求Pod
拥有固定的网络标识(即便重启或者从新调度后也不会变)时,StatefulSet
控制器会帮助你,完成调度目标。mysql
每一个由StatefulSet
建立出来的Pod
都拥有一个序号(从0开始)和一个固定的网络标识。你还能够在YAML
定义中添加VolumeClaimTemplate来声明Pod存储使用的PVC
。当StatefulSet
部署Pod
时,会从编号0到最终编号逐一部署每一个Pod
,只有前一个Pod
部署完成并处于运行状态后,下一个Pod
才会开始部署。nginx
StatefulSet
,是在Deployment
的基础上扩展出来的控制器,在1.9版本以后才加入Kubernetes
控制器家族,它把有状态应用须要保持的状态抽象分为了两种状况:web
拓扑状态。这种状况意味着,应用的多个实例之间不是彻底对等的关系。这些应用实例,必须按照某些顺序启动,好比应用的主节点 A 要先于从节点 B 启动。而若是你把 A 和 B 两个 Pod 删除掉,它们再次被建立出来时也必须严格按照这个顺序才行。而且,新建立出来的 Pod,必须和原来 Pod 的网络标识同样,这样原先的访问者才能使用一样的方法,访问到这个新 Pod。sql
存储状态。这种状况意味着,应用的多个实例分别绑定了不一样的存储数据。对于这些应用实例来讲,Pod A 第一次读取到的数据,和Pod A 被从新建立后再次读取到的数据,应该是同一份 。这种状况最典型的例子,就是一个数据库应用的多个存储实例。shell
因此,StatefulSet 的核心功能,就是经过某种方式记录这些状态,而后在 Pod 被从新建立时,可以为新 Pod 恢复这些状态。数据库
想要维护应用的拓扑状态,必须保证能用固定的网络标识访问到固定的Pod
实例,Kubernetes
是经过Headless Service
给每一个Endpoint(Pod)
添加固定网络标识的,因此接下来咱们花些时间了解下Headless Service
。编程
在文章学练结合,快速掌握Kubernetes Service 写过后端
Service
是在逻辑抽象层上定义了一组Pod
,为他们提供一个统一的固定IP和访问这组Pod
的负载均衡策略。对于
ClusterIP
模式的Service
来讲,它的 A 记录的格式是:serviceName.namespace.svc.cluster.local,当你访问这条 A 记录的时候,它解析到的就是该 Service 的 VIP 地址。
对于指定了 clusterIP=None 的 Headless Service来讲,它的A记录的格式跟上面同样,可是访问记录后返回的是Pod的IP地址集合。Pod 也会被分配对应的 DNS A 记录,格式为:podName.serviceName.namesapce.svc.cluster.local
普通的Service
都有ClusterIP
,它其实就是一个虚拟IP
,会把请求转发到该Service
所代理的某一个Pod
上。
仍是拿文章学练结合,快速掌握Kubernetes Service 里用过的例子来分析,使用的Service
和Deployment
的定义以下:
apiVersion: v1
kind: Service
metadata:
name: app-service
spec:
type: NodePort #建立NodePort类型Service时会先建立一个ClusterIp类型的Service
selector:
app: go-app
ports:
- name: http
protocol: TCP
nodePort: 30080
port: 80
targetPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-go-app
spec:
replicas: 2
selector:
matchLabels:
app: go-app
template:
metadata:
labels:
app: go-app
spec:
containers:
- name: go-app-container
image: kevinyan001/kube-go-app
ports:
- containerPort: 3000
复制代码
在Kubernetes里建立好上述资源后,能够进入其中一个Pod
查看Service的A记录
➜ kubectl exec -it my-go-app-69d6844c5c-gkb6z -- /bin/sh
/app # nslookup app-service.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: app-service.default.svc.cluster.local
Address: 10.108.26.155
复制代码
若是想让DNS经过刚才的Service
名直接解析出Pod
名对应的IP是不能够的:
/app # nslookup my-go-app-69d6844c5c-gkb6z.app-service.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
** server can't find my-go-app-69d6844c5c-gkb6z.app-service.default.svc.cluster.local: NXDOMAIN
复制代码
由于Service
有ClusterIp
,直接被DNS解析了,那怎么才能让DNS经过Service
解析Pod
的IP呢?因此就有了Headless Service
。
建立
Headless Service
跟建立普通Service
时惟一的不一样就是在YAML
定义里指定spec:clusterIP: None
,也就是不须要ClusterIP
的Service
。
下面我建立一个Headless Service
代理上面例子中的那两个应用Pod
实例,它的YAML
定义以下
# headless-service.yaml
apiVersion: v1
kind: Service
metadata:
name: app-headless-svc
spec:
clusterIP: None # <-- Don't forget!!
selector:
app: go-app
ports:
- protocol: TCP
port: 80
targetPort: 3000
复制代码
建立Service的命令
➜ kubectl apply -f headless-service.yaml service/app-headless-svc created
Headless Service
建立完后,咱们再来看一下这个Service
在DNS里对应的A记录
仍是在刚才进入的那个Pod里,记住Service的DNS记录的格式是 serviceName.namespace.svc.cluster.local
/app # nslookup app-headless-svc.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: app-headless-svc.default.svc.cluster.local
Address: 10.1.0.38
Name: app-headless-svc.default.svc.cluster.local
Address: 10.1.0.39
复制代码
DNS查询会返回HeadlessService
代理的两个Endpoint (Pod)
对应的IP,这样客户端就能经过Headless Service
拿到每一个EndPoint
的 IP,若是有须要能够本身在客户端作些负载均衡策略。Headless Service
还有一个重要用处(也是使用StatefulSet
时须要Headless Service
的真正缘由),它会为代理的每个StatefulSet
建立出来的Endpoint
也就是Pod
添加DNS域名解析,这样Pod
之间就能够相互访问。
划重点:
- 这个分配给
Pod
的DNS域名就是Pod
的固定惟一网络标识,即便发生重建和调度DNS域名也不会改变。- Deployment建立的
Pod
的名字是随机的,因此HeadlessService
不会为Deployment
建立出来的Pod单独添加域名解析。
咱们把上面的例子稍做修改,新增一个StatefulSet
对象用它建立Pod
来验证一下。
apiVersion: v1
kind: Service
metadata:
name: app-headless-svc
spec:
clusterIP: None # <-- Don't forget!!
selector:
app: stat-app
ports:
- protocol: TCP
port: 80
targetPort: 3000
---
apiVersion: apps/v1
kind: StatefulSet # <-- claim stateful set
metadata:
name: stat-go-app
spec:
serviceName: app-headless-svc # <-- Set headless service name
replicas: 2
selector:
matchLabels:
app: stat-app
template:
metadata:
labels:
app: stat-app
spec:
containers:
- name: go-app-container
image: kevinyan001/kube-go-app
resources:
limits:
memory: "64Mi"
cpu: "50m"
ports:
- containerPort: 3000
复制代码
这个YAML
文件,和咱们在前面用到的Deployment
的惟一区别,就是多了一个spec.serviceName
字段。
StatefulSet
给它所管理的全部 Pod
名字,进行了编号,编号规则是:StatefulSet名-序号。这些编号都是从 0 开始累加,与 StatefulSet 的每一个 Pod 实例一一对应,毫不重复。
➜ kubectl get pod
NAME READY STATUS RESTARTS AGE
stat-go-app-0 1/1 Running 0 9s
stat-go-app-1 0/1 ContainerCreating 0 1s
复制代码
咱们能够进入stat-go-app-0
这个Pod
查看一下这两个Pod
的DNS记录
提示: Headless Service给Pod添加的DNS的格式为podName.serviceName.namesapce.svc.cluster.local
/app # nslookup stat-go-app-0.app-headless-svc.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: stat-go-app-0.app-headless-svc.default.svc.cluster.local
Address: 10.1.0.46
/app # nslookup stat-go-app-1.app-headless-svc.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: stat-go-app-1.app-headless-svc.default.svc.cluster.local
Address: 10.1.0.47
复制代码
因而乎这样就保证了Pod
之间可以相互通讯,若是要用StatefulSet
编排一个有主从关系的应用,就能够经过DNS域名访问的方式保证相互之间的通讯,即便出现Pod
从新调度它在内部的DNS域名也不会改变。
经过上面名字叫stat-go-app
的StatefulSet
控制器建立Pod
的过程咱们能发现,StatefulSet
它所管理的全部 Pod
,名称的命名规则是:StatefulSet名-序号。序号都是从 0 开始累加,与 StatefulSet 的每一个 Pod 实例一一对应,毫不重复。
因此上面咱们经过kubectl get pod
命令看到了两个名字分别为stat-go-app-0
和stat-go-app-1
的Pod实例。
更重要的是,这些Pod
的建立,也是严格按照名称的编号顺序进行的。好比,在stat-go-app-0
进入到 Running 状态、而且细分状态(Conditions)成为 Ready 以前,stat-go-app-1
会一直处于 Pending 等待状态。
StatefulSet
会一直记录着这个拓扑状态,即便发生调谐,从新调度Pod
也是严格遵照这个顺序,编号在前面的Pod
建立完成而且进入Ready运行状态后,下一个编号的Pod
才会开始建立。
理解了Headless Service
真正的用途后,关于Kubernetes
内部如何让Pod
固定惟一网络标识这个问题的答案就是:Headless Service
为代理的每个StatefulSet
建立出来的Pod
添加DNS域名解析。因此在用StatefulSet
编排实例之间有主从关系这样的有状态应用时,Pod
相互之间就能以podName.serviceName.namesapce.svc.cluster.local 这个域名格式进行通讯,这样就不用在担忧Pod
被从新调度到其余的节点上后IP的变化。
前面的文章Kubernetes Pod入门指南在介绍Pod
使用的数据卷的时候,我曾提到过,要在一个Pod
里声明 Volume
,只须要在Pod
定义里加上spec.volumes
字段便可。而后,你就能够在这个字段里定义一个具体类型的Volume
了,好比:hostPath
类型。
......
spec:
volumes:
- name: app-volume
hostPath:
# 在宿主机上的目录位置
path: /data
containers:
- image: mysql:5.7
name: mysql
ports:
- containerPort: 3306
volumeMounts:
- mountPath: /usr/local/mysql/data
name: app-volume
......
复制代码
可是这种声明使用数据卷的方式,对于每一个Pod
实例都绑定了存储数据的数据存储类应用是不适用的。因为hostPath
类型的Volume是基于宿主机目录的,若是一旦Pod
发生从新调度,去了其余节点,就没有办法在新节点上把Pod
的存储数据恢复回来了。
既然在Pod
宿主机上的数据卷不适用,那么只能让Pod
去使用Kubernetes
的集群存储资源了。集群持久数据卷资源的配置和使用是经过PV
和PVC
完成的,咱们先来了解一下这两个概念。
持久卷(PersistentVolume,PV)是集群中的一块存储,能够由管理员事先供应,或者 使用存储类(Storage Class)来动态供应。 持久卷是集群资源,就像节点也是集群资源同样,它们拥有独立于任何使用PV
的Pod
的生命周期。
做为一个应用开发者,我可能对分布式存储项目(好比 Ceph、GFS、HDFS 等)一窍不通,天然不会编写它们对应的 Volume 定义文件,这不只超越了开发者的知识储备,还会有暴露公司基础设施敏感信息(秘钥、管理员密码等信息)的风险。因此Kubernetes
后来又引入了持久卷申领(PersistentVolumeClaim,PVC)。
PVC
表达的是Pod
对存储的请求。概念上与Pod
相似。 Pod
会耗用节点资源,而PVC
申领会耗用PV
资源。有了PVC
后,在须要使用持久卷的Pod
的定义里只须要声明使用这个PVC
便可,这为使用者隐去了不少关于存储的信息,举个例子来讲就是,我能够在彻底不知道远程存储的空间名、服务器地址、AccessKey之类的信息时直接把远程存储挂载到Pod
的容器里。好比像下面这样:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pv-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: pv-pod
spec:
containers:
- name: pv-container
image: nginx
ports:
- containerPort: 80
name: "http-server"
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: pv-storage
volumes:
- name: pv-storage
persistentVolumeClaim:
claimName: pv-claim
复制代码
能够看到,在这个Pod
的Volumes
定义中,咱们只须要声明它的类型是 persistentVolumeClaim
,而后指定 PVC
的名字,彻底不用关心持久卷自己的定义。
PVC
建立出来后须要和PV
完成绑定才能使用,不过对于使用者来讲咱们能够先不用关心这个细节。
能够用编程领域的接口和实现的关系来理解
PVC
和PV
的关系。
关于StatefulSet、Pod、PVC和PV之间的关系能够用下面这张图表示
在StatefulSet
的定义里咱们能够额外添加了一个spec.volumeClaimTemplates
字段。它跟 Pod
模板(spec.template
字段)的做用相似。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.9.1
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
复制代码
Note: StatefulSet和Deployment里都有Pod模板,他是控制器建立Pod实例的依据,关于这部分知识能够查看之前关于Deployment的文章详细了解。
也就是说,凡是被这个StatefulSet
管理的Pod
,都会声明一个对应的PVC
;而这个PVC
的定义,就来自于volumeClaimTemplates
这个模板字段。更重要的是,这个 PVC
的名字,会被分配一个与这个Pod
彻底一致的编号。
StatefulSet
建立的这些PVC
,都以**"PVC名-StatefulSet名-序号"**这个格式命名的。
对于上面这个StatefulSet
来讲,它建立出来的Pod
和PVC
的名称以下:
Pod: web-0, web-1
PVC: www-web-0, www-web-1
复制代码
假如发生从新调度web-0
这个Pod
被从新建立调度到了其余节点,在这个新的Pod
对象的定义里,因为volumeClaimTemplates
的存在,它声明使用的PVC
的名字,仍是叫做:www-web-0。因此,在这个新的web-0
被建立出来以后,Kubernetes
会为它查找绑定名叫www-web-0
的PVC
。因为PVC
的生命周期是独立于使用它的Pod
的,这样新Pod
就接管了之前旧Pod
留下的数据。
StatefulSet
就像是一种特殊的Deployment
,它使用Kubernetes
里的两个标准功能:Headless Service
和 PVC
,实现了对的拓扑状态和存储状态的维护。
StatefulSet
经过Headless Service
, 为它管控的每一个Pod
建立了一个固定保持不变的DNS域名,来做为Pod
在集群内的网络标识。加上为Pod
进行编号并严格按照编号顺序进行Pod
调度,这些机制保证了StatefulSet
对维护应用拓扑状态的支持。
而借由StatefulSet
定义文件中的volumeClaimTemplates
声明Pod
使用的PVC
,它建立出来的PVC
会以名称编号这些约定与它建立出来的Pod
进行绑定,借由PVC
独立于Pod
的生命周期和二者之间的绑定机制的帮助,StatefulSet
完成了应用存储状态的维护。
今天的文章就到这里,后面会继续分享学习Kuberntes的文章,力争打造一个适合工程师的Kubernetes学习教程,喜欢的能够在微信上关注公众号「网管叨bi叨」,每周都会推送技术进阶文章。