K8S学习笔录 - Headless与StatefulSet简单应用

3,204 阅读2分钟

原文链接

将以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 ServicePersistentVolume

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节点便会少一个。