前言
我们在部署服务的时候,有很多服务是master-slave模式,这些服务可能有自己的故障切换逻辑。
我们知道,我们可以通过一个负载均衡器(或额外的router服务)能够指向master服务,当故障发生的时候,在slave切换到master以后,负载均衡也能能切换到新的master上面。但是,这无疑会增加部署成本
那么,在K8S(或K3S)里面怎么最低成本的实现呢?
假设我有一个服务叫“OrgManager”服务,这是一个典型的master-slave主备模式的服务,它有一个能够获取当前集群主备信息的接口。
今天我们就以动态切换OrgManager主备节点为案例,讲解如何设置NodePort始终指向主节点的方案。
方案
我们知道,K8S的各种服务(Service),比如ClusterIP、NodePort,是通过标签选择器
来将流量导入到POD里面,那么,我们就可以通过修改POD的特定标签的形式,来让服务只指向该特定POD,来达成这个目标。
实现路线
- 构建一个主备的OrgManager服务
- 通过一个SideCar容器的脚本(不是一定要这样,反正得有个脚本来运行),定时的检测OrgManager服务的主备信息
- 当上述脚本检查到当前POD是master时,通过
kubectl api给当前POD打上role: master标签
,相应的,如果是slave,则打上role: slave标签 - Nodeport服务,其selector上增加role: master选择器
具体样例(所有命名空间都在myspace下)
1. 创建Headless服务
这个很重要,我们需要通过无头服务,即clusterIP: None
,让每一个StatefulSet POD能获取到稳定的集群地址。这个没得说,参考官网说明即可
apiVersion: v1
kind: Service
metadata:
name: OrgManager-headless-service
namespace: myspace
labels:
app: OrgManager
spec:
publishNotReadyAddresses: false
clusterIP: None
ports:
- port: 80
targetPort: webport
protocol: TCP
name: webport
selector:
app: OrgManager
2. 创建ServiceAccount
因为我们要在POD的SideCar容器里面调用kubectl api,所以我们需要配置一个具有特定权限的账户。关于ServiceAccount,自行参考官网学习即可。这里只是告诉大家,官网说明告诉了我们如何在一个POD内部访问kubectl api的服务。
在这个案例中,我们在POD中创建一个ServiceAccount,并且绑定ClusterRole,让其具备pod的查询和更新能力
apiVersion: v1
kind: ServiceAccount
metadata:
name: apiuser
namespace: myspace
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cluster-role
namespace: myspace
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: cluster-role-binding
namespace: myspace
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-role
subjects:
- kind: ServiceAccount
name: apiuser
namespace: myspace
3. 构建OrgManager主备服务,采用StatefuleSet模式
注意:下面的内容不是一个完整的部署文件
,
这里因为是自己的服务,我就不提供部署文件了。很多部分都用省略号代替了
这个服务有一个REST接口:/api/cluster/info
能够获取到集群信息。
信息如下:
{"master": "master-ip", "slaves": ["slave-ip-1", "slave-ip-2"......]}
下面的内容只是截取一段,说明它是“StatefuleSet”方式部署。因为StatefuleSet能够通过Headless Service获得稳定的集群IP地址,所以可以看到spec.serviceName指定了Headless Service的服务命名。
-
我们通过环境变量,将一些重要信息传入到POD的容器中
-
可以看到,我们指定了serviceAccountName
-
同时,我们可以看到,这里设置了一个名叫“side-car”的容器,这个容器可以是任意一个具备curl能力的容器,在这个案例中,我使用的是nginx,这个容器里面执行了一段脚本,这脚本就是这次的重点:通过获取主备信息,来动态设置POD的标签。
...
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: OrgManager
namespace: myspace
labels:
app: OrgManager
spec:
replicas: 2
serviceName: OrgManager-headless-service
template:
...
spec:
serviceAccountName: "apiuser"
containers:
- name: "OrgManager"
image: "OrgManager:1.3.6.14"
imagePullPolicy: "IfNotPresent"
....
- name: "side-car"
image: "nginx:1.6"
.......
env:
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: POD_HEADLESS_NAME
value: OrgManager-headless-service
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
command:
- bash
- -c
- |-
while true; do
value=$(curl http://127.0.0.1/api/cluster/info)
echo "----------集群信息---------"
echo ${value}
echo "--------------------------"
# 从返回信息中,截取master对应的ip地址
value=${value#*\"master\"\: \"}
value=${value%%\", \"*}
if [ "${value}" == "${POD_NAME}.${POD_HEADLESS_NAME}" ]; then
echo "当前POD为master,更新POD标签role: master"
echo -e '[{"op": "replace", "path": "/metadata/labels/role", "value": "master"}]' > /root/patch_lable.json
else
echo "当前POD为slave,更新POD标签role: slave"
echo -e '[{"op": "replace", "path": "/metadata/labels/role", "value": "slave"}]' > /root/patch_lable.json
fi
export APISERVER=https://kubernetes.default.svc
export SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount
export TOKEN=$(cat ${SERVICEACCOUNT}/token)
export CACERT=${SERVICEACCOUNT}/ca.crt
curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api/v1/namespaces/${POD_NAMESPACE}/pods/${POD_NAME} --data "$(cat /root/patch_lable.json)" --request PATCH -H "Content-Type:application/json-patch+json"
sleep 10
done
...
脚本解释:
- 通过api,获取到当前的集群信息
- 从信息中,通过字符串处理的方式,得到master的地址。(由于是statefulset,并且指定了headless服务,因此集群构建的时候能得到一个稳定的可声明的地址,脚本里也能获取到集群中这个地址)
- 将这个获取到的地址和当前POD的headless地址进行比较
- 根据比较结果,设置更新标签的
jsonpatch
请求体 - 根据官网说明的方式,调用接口更新POD的标签
通过上面的处理,就做到了给pod打role: master或role: slave标签的目标
4. 创建指向master的Nodeport
我们注意到,这个Nodeport的selector里面,设置了role: master
这个选择器
apiVersion: v1
kind: Service
metadata:
name: OrgManager-nodeport
namespace: myspace
labels:
app: OrgManager
spec:
type: NodePort
ports:
- port: 80
targetPort: webport
protocol: TCP
name: webport
nodePort: {{ .Values.nodePort.port }}
selector:
app: OrgManager
role: master
至此,我们就可以通过主机的80端口,访问OrgManager服务特定的master了。由于OrgManager具备主从切换的能力,当master挂掉以后,/api/cluster/info会获取到新的master的地址,这时候,脚本就会获取到这个变化,然后更新POD的标签,这样就对外无感的实现了主节点故障转移
最重要的是,我们没有额外部署其他负载均衡器或VIP服务,就做到了指定master的目标
总结
- 使用StatefulSet + HeadlessService的方式,在K8S中构建地址稳定的集群服务。且该服务具备接口能获取到master-slave信息
- 创建能够在POD中访问kubectl api的ServiceAccount
- 通过在POD中的容器执行检测集群状态的脚本,获取到master-slave信息,决定给当前POD打上什么标签
- K8S的服务,通过selector指定上述特定标签的方式,指向特定的POD
延生思路
上面我们是以OrgManager服务,和Nodeport来举例。
实际上,通过上述方法,我们就有了很灵活的方案。
比如:
Mysql主从模式,我们可以通过脚本监控Mysql主从的信息,获取到master pod或slave pod,然后给master或slave打上合适的标签。
在集群内部的其他服务,通过ClusterIP Service来访问Mysql,那么这个ClusterIP Service的selector只要指定了特定标签,就能够访问到特定的POD,比如Mysql的主节点。
由于每个脚本都在自己的POD内,因此该脚本实现了自动故障转移打标签,那么相应的Service也就是能动态指定到不同的POD了
最关键的是,这种方法没有引入任何第三方的额外部署,比如负载均衡、Router之类的东西。