将以Redis为例子,学习使用Headless Service与StatefulSet完成Redis集群的创建。 以实现一个无单点故障、高可用、可动态扩展的Redis集群。
什么是Headless服务
Headless服务是一种特殊的服务,其clusterIP值为None。 这样设置在运行时不会被分配ClusterIP,而在访问过程中,将会返回包含了其label指定的全部Pod列表,然后客户端程序可以自定义如何处理这个Pod列表。
通常情况下,Service如果有一个集群IP,则在DNS查找Service时会返回该IP的记录。 但是如果Service不需要集群IP,则DNS将会返回Pod的IP列表。
Headless服务示例
当前拥有一个Deployment资源,它管理了3个带有 app: nginx 标签的副本。
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
publishNotReadyAddresses: true # 是否发布未就绪的Pod
ports:
- port: 80
targetPort: http
clusterIP: None # Headless服务需要clusterIP为None
selector:
app: nginx
通过DNS发现Pod。 查看该服务在DNS上的对应记录,会发现有对应的三个Pod的IP。
$ nslookup nginx.default.svc.cluster.local 10.96.0.10
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: nginx.default.svc.cluster.local
Address: 172.40.0.2
Name: nginx.default.svc.cluster.local
Address: 172.32.0.3
Name: nginx.default.svc.cluster.local
Address: 172.32.0.4
另外会发现,这个Service上使用了一个 publishNotReadyAddresses 的属性。
正常情况下Pod只有就绪后才能被DNS解析。而publishNotReadyAddresses为true时,即使Pod未到就绪状态,也能被DNS所解析。 Pod的就绪状态由Pod的就绪探针决定。
为什么需要StatefulSet
在Kubernetes中RC、Deployment、ReplicaSet、DaemonSet等等都是面向无状态服务的。 他们管理的Pod的IP、名字等都是随机的,而在一些情况下,这是不可行的。
StatefulSet顾名思义所以有状态的集合。它主要是为了解决有状态服务的问题。 与Deployment类似,StatefulSet管理了基于相同容器定义的一组Pod。
但和Deployment不同的是,StatefulSet为它们的每个Pod维护了一个固定的ID。 这些Pod是基于相同的声明来创建的,但是不能相互替换,无论怎么调度,每个Pod都有一个永久不变的ID。
StatefulSet的配置结构
type StatefulSet struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// StatefulSet定义
Spec StatefulSetSpec `json:"spec,omitempty"`
// StatefulSet状态,可过时
Status StatefulSetStatus `json:"status,omitempty"`
}
type StatefulSetSpec struct {
// 副本数量
Replicas *int32 `json:"replicas,omitempty"`
// Pod的选择器
Selector *metav1.LabelSelector `json:"selector"`
// Pod模板
Template v1.PodTemplateSpec `json:"template"`
// 一组存储卷申请模板
// StatefulSet会参考模板为每个Pod分配一个专属的存储卷。
// 此字段的每个模板项必须在Pod模板的容器配置中至少有一个匹配的volumeMount(名称一样即可)。
// 在模板中卷在同名称的情况下,该字段对应的卷优先于其他任何卷。
VolumeClaimTemplates []v1.PersistentVolumeClaim `json:"volumeClaimTemplates,omitempty"`
// 简单的理解为Headless Service名称,用来为StatefulSet提供可靠的网络标识,需要在StatefulSet存在之前就存在
// Pod需要在创建后获得一个DNS/hostnames,格式为podName.serviceName.default.svc.cluster.local
// podName则由StatefulSet进行管理
ServiceName string `json:"serviceName"`
// podManagementPolicy控制Pod再创建和扩/缩容时的方案。
// 该字段有两个值OrderedReady(默认)和Parallel。
// OrderedReady在Pod创建时名字由0开始依次递增,例如pod-0、pod-1。控制器会依次创建每个Pod。
// 在缩容情况下,Pod会按照名字从大到小依次删除。
// Parallet会一次性创建/删除所有的Pod,而不会等待上一个完成。
PodManagementPolicy PodManagementPolicyType `json:"podManagementPolicy,omitempty"`
// Pod更新策略
UpdateStrategy StatefulSetUpdateStrategy `json:"updateStrategy,omitempty"`
// 保存的历史版本数量,用于回滚Pod,默认值为10
RevisionHistoryLimit *int32 `json:"revisionHistoryLimit,omitempty"`
}
// PodManagementPolicyType定义了在StatefulSet中Pod的创建规则
type PodManagementPolicyType string
const (
// 按照严格的顺序依次处理扩/缩容的情况。同一时间最多只处理一个Pod,处理完一个再处理下一个。
OrderedReadyPodManagement PodManagementPolicyType = "OrderedReady"
// 同时处理所有的Pod。
ParallelPodManagement PodManagementPolicyType = "Parallel"
)
创建Redis集群
使用StatefulSet需要使用 Headless Service 与 PersistentVolume 。
Headless服务用来帮助StatefulSet识别Pod的网络标识。
PersistenVolumn负责实现持久化存储,例如下面例子中的redis node id。
NFS服务
搭建过程不展开了,网上很多。
持久存储卷需要使用网络文件服务来支持。NFS搭建起来比较简单,所以使用了NFS。
由于我有的集群节点是分布在两个不同机房的物理机,所以设置了IP1和IP2两个权限。
$ cat /etc/exports
# /etc/exports: the access control list for filesystems which may be exported
# to NFS clients. See exports(5).
#
# Example for NFSv2 and NFSv3:
# /srv/homes hostname1(rw,sync,no_subtree_check) hostname2(ro,sync,no_subtree_check)
#
# Example for NFSv4:
# /srv/nfs4 gss/krb5i(rw,sync,fsid=0,crossmnt,no_subtree_check)
# /srv/nfs4/homes gss/krb5i(rw,sync,no_subtree_check)
#
/var/nfs/redis/pv1 IP1/0(rw,all_squash) IP2/0(rw,insecure,all_squash)
/var/nfs/redis/pv2 IP1/0(rw,all_squash) IP2/0(rw,insecure,all_squash)
/var/nfs/redis/pv3 IP1/0(rw,all_squash) IP2/0(rw,insecure,all_squash)
/var/nfs/redis/pv4 IP1/0(rw,all_squash) IP2/0(rw,insecure,all_squash)
/var/nfs/redis/pv5 IP1/0(rw,all_squash) IP2/0(rw,insecure,all_squash)
/var/nfs/redis/pv6 IP1/0(rw,all_squash) IP2/0(rw,insecure,all_squash)
最后重启nfs服务
创建Redis通用配置类
为方便收敛集群中redis的配置。创建文件redis.conf,并创建一个ConfigMap。
appendonly yes
cluster-enabled yes
cluster-config-file /var/redis/node.conf
cluster-node-timeout 5000
dir /var/redis
port 6379
$ kubectl describe cm redis-conf
Name: redis-conf
Namespace: default
Labels: <none>
Annotations: <none>
Data
====
redis.conf:
----
appendonly yes
cluster-enabled yes
cluster-config-file /var/redis/node.conf
cluster-node-timeout 5000
dir /var/redis
port 6379
Events: <none>
创建PersistentVolumn作持久存储卷
创建持久存储卷资源,目前对这一块儿不是很了解,只知道配置中将NFS中对应的path挂载到节点上用作持久存储。
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv1 # pv1 pv2 ... pv6
spec:
capacity:
storage: 200M
accessModes:
- ReadWriteMany
nfs:
server: NFS_SERVER
path: /var/nfs/redis/pv1 # pv1 pv2 ... pv 6
---
...省略
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv6
spec:
capacity:
storage: 200M
accessModes:
- ReadWriteMany
nfs:
server: NFS_SERVER
path: /var/nfs/redis/pv6
创建Headless Service
创建无头服务,向StatefulSet提供Pod的列表。
apiVersion: v1
kind: Service
metadata:
name: redis-service
labels:
app: redis
spec:
ports:
- name: redis-port
port: 6379
clusterIP: None
selector:
app: redis
appCluster: redis-cluster
创建StatefulSet
创建有状态集合资源。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-app
spec:
serviceName: redis-service
replicas: 6
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
appCluster: redis-cluster
spec:
containers:
- name: redis
image: redis
command:
- "redis-server"
args:
- "/etc/redis/redis.conf"
- "--protected-mode no"
ports:
- name: redis
containerPort: 6379
protocol: "TCP"
volumeMounts:
- name: "redis-conf"
mountPath: "/etc/redis"
- name: "redis-data"
mountPath: "/var/redis"
volumes:
- name: "redis-conf"
configMap:
name: "redis-conf"
items:
- key: "redis.conf"
path: "redis.conf"
volumeClaimTemplates:
- metadata:
name: redis-data
spec:
accessModes: ["ReadWriteMany"]
resources:
requests:
storage: 200M
初始化Redis集群
创建一个Redis集群的管理Pod,并依次将所有的Redis节点加入集群。
$ redis-cli --cluster create \
172.32.0.3:6379 \
172.32.0.4:6379 \
172.32.0.5:6379 \
172.40.0.2:6379 \
172.40.0.3:6379 \
172.40.0.4:6379 \
--cluster-replicas 1
>>> Performing hash slots allocation on 6 nodes...
(省略大量信息...)
在初始化完成之后,便可以通过 redis-cli -c -h redis-service 来连接Redis集群了。
另外我也尝试重启了使某个master节点,观察到了其slave变为了master,而当原来的master重新上线后,变为了slave。
需要注意的是,如果没有使用 PersistentVolume 资源或者其他支持持久化存储的手段,则在某个Pod被杀死之后,其中Redis的节点信息便会丢失。
虽然后面会新建一个Pod,但是新的Pod会新创建一份节点数据,也不会自动加入到集群中,集群中Redis节点便会少一个。