写在前面
本文一起看下k8s对于有状态应用部署提供的解决方案。
1:有状态应用和无状态应用
如果是一个应用每次重启时依赖环境都能和第一次启动时的完全一致,则就可以称这类应用是无状态应用用,反之,就是有状态应用,如下:
支付系统(无状态应用):
重启时只要其依赖的环境正常,则就可以正常启动对外提供服务,如其可能依赖MySQL,只要MySQL还是正常运行的,该服务就正常
MySQL(有状态应用):
运行期间,用户创建了user表,并插入了一些数据,如果重启时,user表被删除或者是表中数据被清除则MySQL将不能正常对外提供服务。
其实,状态
就是数据了,这样看来,我们只需要结合Deployment和PersistentVolume,就可以解决有状态应用的部署问题了,即只要解决了应用的数据持久化问题就行了,但k8s的眼光则显得更加的高瞻远瞩
,它认为应用的状态不仅仅是数据持久化,还应该包括启动顺序(服务之间存在依赖关系)
,网络标识(外部通过唯一标识访问POD)
等,如果将这些内容也考虑到状态的范畴的话
,不管是deployment还是DaemonSet都会显得力不从心,基于此,k8s对deployment进行升级提供了StatefulSet API对象,从其名称我们也能够看出一二,定义如下:
dongyunqi@mongodaddy:~$ kubectl api-resources | egrep -w 'StatefulSet|KIND'
NAME SHORTNAMES APIVERSION NAMESPACED KIND
statefulsets sts apps/v1 true StatefulSet
2:StatefulSet
sts的头信息如下:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-sts
定义如下yaml:
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
可以看到其和deployment是十分相似的,除了文件头的KIND不同外,在spec中还多了一个serviceName,其它的如replicas,selector等和Deployment是一样的,这也很好理解,因为都是用来维护一组POD嘛。接下来我们apply查看:
dongyunqi@mongodaddy:~/k8s$ kubectl apply -f redis-sts.yml
statefulset.apps/redis-sts created
dongyunqi@mongodaddy:~/k8s$ kubectl get pod
NAME READY STATUS RESTARTS AGE
redis-sts-0 1/1 Running 0 22s
redis-sts-1 1/1 Running 0 3s
注意NAME列,名称是sts的name-序号
,这里序号越小则说明创建的越早从AGE列也可以看出来
,这就解决了有状态应用中的启动顺序
问题,比如可以让redis-sts-0
作为redis的主节点,redis-sts-1
作为从节点,但POD之间如何知道彼此的关系呢?也比较简单,因为NAME使用的其实就是容器的hostname,如下:
dongyunqi@mongodaddy:~/k8s$ kubectl get pod -l app=redis-sts
NAME READY STATUS RESTARTS AGE
redis-sts-0 1/1 Running 0 15m
redis-sts-1 1/1 Running 0 15m
dongyunqi@mongodaddy:~/k8s$ kubectl exec -it redis-sts-0 -- hostname
redis-sts-0
dongyunqi@mongodaddy:~/k8s$ kubectl exec -it redis-sts-1 -- hostname
redis-sts-1
怎么让POD有固定的网络标识,即k8s环境中的域名呢?这就需要用到service 的内容,利用其为POD生成固定的网络标识,定义yaml如下:
apiVersion: v1
kind: Service
metadata:
name: redis-svc
spec:
selector:
app: redis-sts
ports:
- port: 6379
protocol: TCP
targetPort: 6379
注意这里的name: redis-svc
要和sts中的serviceName对应,这样service就知道POD是sts的pod,就会为其生成固定的网络标识了,应用后describe查看:
dongyunqi@mongodaddy:~/k8s$ kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 11d
redis-svc ClusterIP 10.102.97.104 <none> 6379/TCP 8s
dongyunqi@mongodaddy:~/k8s$ kubectl describe service redis-svc
Name: redis-svc
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=redis-sts
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.102.97.104
IPs: 10.102.97.104
Port: <unset> 6379/TCP
TargetPort: 6379/TCP
Endpoints: 10.10.4.5:6379,10.10.4.6:6379
Session Affinity: None
Events: <none>
和普通的service类似,也找到了后端的两个POD10.10.4.5:6379,10.10.4.6:6379
,但POD的域名就不再是IP 地址. 名字空间
,而是Pod 名. 服务名. 名字空间.svc.cluster.local
,比如pod 0的域名就是redis-sts-0.redis-svc.default.svc.cluster.local
,也可以简写为Pod 名.服务名
,如下测试:
dongyunqi@mongodaddy:~/k8s$ kubectl exec redis-sts-0 -it -- ping redis-sts-0.redis-svc
PING redis-sts-0.redis-svc (10.10.4.5): 56 data bytes
64 bytes from 10.10.4.5: seq=0 ttl=64 time=0.141 ms
64 bytes from 10.10.4.5: seq=1 ttl=64 time=0.043 ms
64 bytes from 10.10.4.5: seq=2 ttl=64 time=0.043 ms
64 bytes from 10.10.4.5: seq=3 ttl=64 time=0.050 ms
^C
--- redis-sts-0.redis-svc ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 0.043/0.069/0.141 ms
dongyunqi@mongodaddy:~/k8s$ kubectl exec redis-sts-0 -it -- ping redis-sts-0.redis-svc.default.svc.cluster.local
PING redis-sts-0.redis-svc.default.svc.cluster.local (10.10.4.5): 56 data bytes
64 bytes from 10.10.4.5: seq=0 ttl=64 time=0.029 ms
64 bytes from 10.10.4.5: seq=1 ttl=64 time=0.126 ms
^C
--- redis-sts-0.redis-svc.default.svc.cluster.local ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.029/0.077/0.126 ms
这里我们仅仅是使用Service为POD生成固定的网络标识,但是并不需要其负载均衡的功能,因此不需要额外为service生成IP地址,以减少不必须的开销,此时需要配置
clusterIP: None
。
然后,我们来看下service和sts之间的依赖关系,如下图:
接下来还有一个很重要的有状态应用的问题需要解决,那就是数据持久化
,也一起来看下,同样也是使用persistent volume,本文以NFS 为例来说明。sts为了强调和持久化存储的一对一绑定关系,增加了volumeClaimTemplates
属性来定义内嵌的PVC,yaml如下:
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
注意这里mountPath: /data
就是redis的rdb,aof 数据文件存储目录。apply后查看如下:
dongyunqi@mongodaddy:~/k8s$ kubectl get sts
NAME READY AGE
redis-pv-sts 2/2 13s
redis-sts 2/2 73m
dongyunqi@mongodaddy:~/k8s$ kubectl get pod -l app=redis-pv-sts
NAME READY STATUS RESTARTS AGE
redis-pv-sts-0 1/1 Running 0 45s
redis-pv-sts-1 1/1 Running 0 39s
在nfs server目录会自动创建持久化目录来存储redis的数据,如下:
dongyunqi@mongodaddy:/tmp/nfs$ ll | grep '100'
drwxrwxrwx 2 999 root 4096 1月 28 12:56 default-redis-100m-pvc-redis-pv-sts-0-pvc-de19a2a3-4b4e-400a-be87-95fd09ebb0e9/
drwxrwxrwx 2 999 root 4096 1月 28 12:56 default-redis-100m-pvc-redis-pv-sts-1-pvc-98dd5b76-904e-42b7-83ab-3a797796b57f/
PVC如下:
dongyunqi@mongodaddy:/tmp/nfs$ kubectl get pvc | egrep '100|CAPACITY'
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
redis-100m-pvc-redis-pv-sts-0 Bound pvc-de19a2a3-4b4e-400a-be87-95fd09ebb0e9 100Mi RWX nfs-client 17m
redis-100m-pvc-redis-pv-sts-1 Bound pvc-98dd5b76-904e-42b7-83ab-3a797796b57f 100Mi RWX nfs-client 17m
接下来测试下数据持久化功能,首先,模拟数据写入:
dongyunqi@mongodaddy:/tmp/nfs$ kubectl exec -it redis-pv-sts-0 -- redis-cli
127.0.0.1:6379> set name jack
OK
127.0.0.1:6379> get name
"jack"
127.0.0.1:6379>
然后模拟pod 0意外退出,如下:
dongyunqi@mongodaddy:/tmp/nfs$ kubectl get pod
NAME READY STATUS RESTARTS AGE
...
redis-pv-sts-0 1/1 Running 0 20m
redis-pv-sts-1 1/1 Running 0 20m
dongyunqi@mongodaddy:/tmp/nfs$ kubectl delete pod redis-pv-sts-0
pod "redis-pv-sts-0" deleted
dongyunqi@mongodaddy:/tmp/nfs$ kubectl get pod | grep 'redis-pv-sts'
redis-pv-sts-0 1/1 Running 0 34s
redis-pv-sts-1 1/1 Running 0 21m
可以看到pod 0很快就重新创建出来了,再进入看下数据是否还在:
dongyunqi@mongodaddy:/tmp/nfs$ kubectl exec -it redis-pv-sts-0 -- redis-cli
127.0.0.1:6379> get name
"jack"
成功!
写在后面
小结
本文一起看了如何通过k8s定义的StatefulSet对象来解决有状态应用的部署问题,并重点分析了数据持久化,启动顺序,依赖关系,网络标识问题。希望本文能够帮助到你。
参考文章列表
k8s之挂载NFS到POD中 。
k8s之Service 。