接下来 2 篇解释我刚写出 Kubernetes 版本的 lyanna 配置文件,同时还要需要补充 2 个知识: DaemonSet 和 StatefulSet。

DaemonSet

通过资源对象的名字就能看出它的用法:用来部署 Daemon (守护进程) 的,DaemonSet 确保在全部 (或者一些) 节点上运行一个需要长期运行着的 Pod 的副本。主要场景如日志采集、监控等。

在 lyanna 的项目中,执行异步 arq 消息的任务进程使用了它 (k8s/arq.yaml):

kind: DaemonSet
apiVersion: apps/v1
metadata:
  name: lyanna-arq
  labels:
     app.kubernetes.io/name: lyanna-arq
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: lyanna-arq
  template:
    metadata:
      labels:
        app.kubernetes.io/name: lyanna-arq
    spec:
      containers:
      - image: dongweiming/lyanna:latest
        name: lyanna-web
        command: ['sh', '-c', './arq tasks.WorkerSettings']
        env:
        - name: REDIS_URL
          valueFrom:
            configMapKeyRef:
            name: lyanna-cfg
              key: redis_url
        - name: MEMCACHED_HOST
          valueFrom:
            configMapKeyRef:
              name: lyanna-cfg
              key: memcached_host
        - name: DB_URL
          valueFrom:
            configMapKeyRef:
              name: lyanna-cfg
              key: db_url
        - name: PYTHONPATH
          value: $PYTHONPATH:/usr/local/src/aiomcache:/usr/local/src/tortoise:/usr/local/src/arq:/usr/local/src

简单地说就是启动一个进程执行arq tasks.WorkerSettings,这里面有 4 个要说的地方

  • labels。lyanna 项目用的 Label 键的名字一般都是 app.kubernetes.io/name,表示应用程序的名字,这是官方推荐使用的标签,具体的可以看延伸阅读链接 1
  • image。由于是线上部署,所以不再使用 build 本地构建,而是用打包好的镜像,这里用的是 dongweiming/lyanna:latest,是我向 https://hub.docker.com/ 注册的账号下上传的镜像,其中配置了 Github 集成,每次 push 代码会按规则自动构建镜像。
  • env。设置环境变量,这里用到了 ConfigMap,之后会专门说,大家先略过,另外要注意使用了 PYTHONPATH,预先写好的。
  • command。sh -c ./arq tasks.WorkerSettings 是启动的命令,参数是一个列表,要求能找到第一个参数作为可执行命令,我这里常规的是用 sh -c 开头去执行,arq 这个文件是修改 Dockerfile 添加的:
❯ cat Dockerfile
...
WORKDIR /app
COPY . /app
COPY --from=build /usr/local/bin/gunicorn /app/gunicorn
COPY --from=build /usr/local/bin/arq /app/arq

其实就是在 build 阶段安装包之后把生成的可执行文件拷贝到/app下备用。

StatefulSet

先说「有状态」和「无状态」。在 Deployment 里面无论启动多少 Pod,它们的环境和做的事情都是一样的,请求到那个 Pod 上都可以正常被响应。在请求过程中不会对 Pod 产生额外的数据,例如持久化数据。这就是「无状态」。而 StatefulSet 这个资源对象针对的就是有状态的应用,比如 MySQL、Redis、Memcached 等,因为你在 Pod A 上写入数据 (例如添加了一个文件),如果没有数据同步,在另外一个 Pod B 里面是看不到这个数据的;而 Pod A 被销毁重建之后数据也不存在了。当然别担心,实际环境中会通过之前说的 PV/PVC 或者其他方法把这些需要持久化的数据存储到数据卷中,保证无论怎么操作 Pod 都不影响数据。

StatefulSet 另外的特点是它可以控制 Pod 的启动顺序,还能给每个 Pod 的状态设置唯一标识 (在 Pod 名字后加 0,1,2 这样的数字),当然对于部署、删除、滚动更新等操作也是有序的。

Memcached

在 lyanna 项目中 Memcached 和 Mariadb 使用了 StatefulSet,先说 Memcached (k8s/memcached.yaml):

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: lyanna-memcached
  labels:
    app.kubernetes.io/name: memcached
spec:
  replicas: 3  # StatefulSet有3副本
  revisionHistoryLimit: 10  # 只保留最新10次部署记录,再远的就退不回去了
  selector:
    matchLabels:
      app.kubernetes.io/name: memcached
  serviceName: lyanna-memcached
  template:
    metadata:
      labels:
        app.kubernetes.io/name: memcached
    spec:
      containers:
      - command:  # 容器中启动Memcached的命令,值是一个列表,按照之前部署的参数来的
        - memcached
        - -o
        - modern
        - -v
        - -I
        - 20m
        image: memcached:latest  # 使用最新的官方memcached镜像
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 3
          initialDelaySeconds: 10
          periodSeconds: 10
          successThreshold: 1
          tcpSocket:
            port: memcache
          timeoutSeconds: 5
        name: lyanna-memcached
        ports:
        - containerPort: 11211
          name: memcache
          protocol: TCP
        readinessProbe:
          failureThreshold: 3
          initialDelaySeconds: 5
          periodSeconds: 10
          successThreshold: 1
          tcpSocket:
            port: memcache
          timeoutSeconds: 1
        resources:  # 限定Pod使用的CPU和MEM资源
          requests:
            cpu: 50m  # 1m = 1/1000CPU
            memory: 64Mi
        securityContext:  # 限定运行容器的用户,默认是root
          runAsUser: 1001
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      securityContext:
        fsGroup: 1001
      terminationGracePeriodSeconds: 30
  updateStrategy:
    type: RollingUpdate
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/name: memcached
  name: lyanna-memcached
spec:
  clusterIP: None
  ports:
  - name: memcache
    port: 11211
    protocol: TCP
    targetPort: memcache
  selector:
    app.kubernetes.io/name: memcached
  sessionAffinity: ClientIP

在配置文件中写了一些注释,每个服务大家可以理解他是一个「微服务」,包含一个 StatefulSet/Deployment 和一个 Service,应用通过访问 Service 域名的方式访问它。在一个 yaml 里面能写多个配置,中间用---隔开即可。

Memcached 是内存数据库,进程死掉缓存就丢失了,所以里面没有 mount 数据卷相关的配置,我使用 StatefulSet 它主要是考虑每个 Pod 内存中的数据是不一样的,另外注意服务定义中有一句sessionAffinity: ClientIP,让请求根据客户端的 IP 地址做会话关联:他每次都访问这个 Pod。

再重点说一下配置文件中用到的 2 种探针。探针是由 kubelet 对容器执行的定期诊断,它是 k8s 提供的应用程序健康检查方案:

  • livenessProbe。指示容器是否正在运行。如果存活探测失败,则 kubelet 会杀死容器,容器将按照重启策略 (restartPolicy) 重启。如果容器不提供存活探针,表示容器成功通过了诊断。
  • readinessProbe。指示容器是否准备好服务请求。如果就绪探测失败,Service 不会包含这个 Pod,请求也就不会发到这个 Pod 上来。初始延迟之前的就绪状态默认为失败,如果容器不提供就绪探针,则默认状态为 Success。

大家理解了吧?简单地说,livenessProbe 是看容器是否正常,readinessProbe 是看应用是否正常。

MariaDB

接着说数据库,首先说 MySQL 和 MariaDB 的区别:

MySQL 先后被 Sun 和 Oracle 收购,MySQL 之父 Ulf Michael Widenius 离开了 Sun 之后,由于对这种商业公司不信任等原因,新开了分支 (名字叫做 MariaDB) 发展 MySQL。MariaDB 跟 MySQL 在绝大多数方面是兼容的,对于开发者来说,几乎感觉不到任何不同。目前 MariaDB 是发展最快的 MySQL 分支版本,新版本发布速度已经超过了 Oracle 官方的 MySQL 版本。

MySQL 和 MariaDB 都有各自应用大户,所以目前不需要考虑 MariaDB 替代 MySQL 的问题,我这次选择「纯」开源版本的 MariaDB 主要是我瓣一直在用,而我用的云服务器上面只能选择 MySQL,正好借着 k8s 的机会使用 MariaDB。

MariaDB 显然是最适合用 StatefulSet 了,由于它要定义主从,配置文件 (k8s/optional/mariadb.yaml) 很长,所以分开来演示。先看一下 PV 部分:

kind: PersistentVolume
apiVersion: v1
metadata:
  name: mariadb-master
  labels:
    type: local
spec:
storageClassName: lyanna-mariadb-master
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /var/lib/mariadb
  persistentVolumeReclaimPolicy: Retain
---
kind: PersistentVolume
apiVersion: v1
metadata:
  name: mariadb-slave
  labels:
    type: local
spec:
  storageClassName: lyanna-mariadb-slave
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /var/lib/redis-slave
  persistentVolumeReclaimPolicy: Retain

定义了 2 个 PersistentVolume 分别给 Master/Slave 用,它们都使用了 hostPath 挂载到宿主机 (其实就是 minikube 虚拟机),空间 5G,访问模式是 ReadWriteOnce,表示只能被单个节点以读 / 写模式挂载,这也是必然的,数据库文件被多个节点同时写会让文件损坏的。通过 persistentVolumeReclaimPolicy 制定回收策略,默认是 Delete(删除),我改成了 Retain(保留): 保留数据,需要管理员手工清理。

接着是 ConfigMap 部分,k8s 中通过 ConfigMap 方式配置数据库配置 (my.cnf 中的项):

apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app: mariadb
    app.kubernetes.io/component: master
  name: lyanna-mariadb-master
data:
  my.cnf: |-
    [mysqld]
    skip-name-resolve
    explicit_defaults_for_timestamp
    basedir=/data/mariadb
    port=3306
    socket=/data/mariadb/tmp/mysql.sock
    tmpdir=/data/mariadb/tmp
    max_allowed_packet=16M
    bind-address=0.0.0.0
    pid-file=/data/mariadb/tmp/mysqld.pid
    log-error=/data/mariadb/logs/mysqld.log
    character-set-server=UTF8
    collation-server=utf8_general_ci
    [client]
    port=3306
    socket=/data/mariadb/tmp/mysql.sock
    default-character-set=UTF8
    [manager]
    port=3306
    socket=/data/mariadb/tmp/mysql.sock
    pid-file=/data/mariadb/tmp/mysqld.pid
---
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app.kubernetes.io/name: mariadb
    app.kubernetes.io/component: slave
  name: lyanna-mariadb-slave
data:
  my.cnf: |-
    [mysqld]
    skip-name-resolve
    explicit_defaults_for_timestamp
    basedir=/data/mariadb
    port=3306
    socket=/data/mariadb/tmp/mysql.sock
    tmpdir=/data/mariadb/tmp
    max_allowed_packet=16M
    bind-address=0.0.0.0
    pid-file=/data/mariadb/tmp/mysqld.pid
    log-error=/data/mariadb/logs/mysqld.log
    character-set-server=UTF8
    collation-server=utf8_general_ci
    [client]
    port=3306
    socket=/data/mariadb/tmp/mysql.sock
    default-character-set=UTF8
    [manager]
    port=3306
    socket=/data/mariadb/tmp/mysql.sock
    pid-file=/data/mariadb/tmp/mysqld.pid

通过配置项可以感受到 Pod 会发生状态变化的文件都在/data/mariadb下。我对 MariaDB 配置没有什么经验,这部分主要是从 helm/charts/stable/mariadb 里找的。

我没有用官方 MariaDB 镜像,而是用了 bitnami/mariadb ,主要是为了容易地实现主从复制集群。先看 Matser 部分:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  labels:
    app.kubernetes.io/name: mariadb
    app.kubernetes.io/component: master
  name: lyanna-mariadb-master
spec:
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app.kubernetes.io/name: mariadb
      app.kubernetes.io/component: master
  serviceName: lyanna-mariadb-master
  template:
    metadata:
      labels:
        app.kubernetes.io/name: mariadb
        app.kubernetes.io/component: master
    spec:
      containers:
      - env:
        - name: MARIADB_USER
          valueFrom:
            configMapKeyRef:
              key: user
              name: lyanna-cfg
        - name: MARIADB_PASSWORD
          valueFrom:
            configMapKeyRef:
              key: password
              name: lyanna-cfg
        - name: MARIADB_DATABASE
          valueFrom:
            configMapKeyRef:
              key: database
              name: lyanna-cfg
        - name: MARIADB_REPLICATION_MODE
          value: master
        - name: MARIADB_REPLICATION_USER
          value: replicator
        - name: MARIADB_REPLICATION_PASSWORD
          valueFrom:
            configMapKeyRef:
              key: replication-password
              name: lyanna-cfg
        - name: MARIADB_ROOT_PASSWORD
          value: passwd
        image: bitnami/mariadb:latest
        imagePullPolicy: IfNotPresent
        livenessProbe:
          exec:
            command:
            - sh
            - -c
            - exec mysqladmin status -uroot -p$MARIADB_ROOT_PASSWORD
          failureThreshold: 3
          initialDelaySeconds: 120
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 1
        name: mariadb
        ports:
        - containerPort: 3306
          name: mysql
          protocol: TCP
        readinessProbe:
          exec:
            command:
            - sh
            - -c
            - exec mysqladmin status -uroot -p$MARIADB_ROOT_PASSWORD
          failureThreshold: 3
          initialDelaySeconds: 30
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 1
        volumeMounts:
        - mountPath: /data/mariadb
          name: data
      restartPolicy: Always
      securityContext:
        fsGroup: 1001
        runAsUser: 1001
      terminationGracePeriodSeconds: 30
      volumes:
      - configMap:
          defaultMode: 420
          name: lyanna-mariadb-master
        name: config
  updateStrategy:
    type: RollingUpdate
  volumeClaimTemplates:
  - metadata:
      labels:
        app.kubernetes.io/name: mariadb
        app.kubernetes.io/component: master
      name: data
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 5Gi
      volumeMode: Filesystem
      storageClassName: lyanna-mariadb-master

数据库主从是分别的 StatefulSet,每个 StatefulSet 都只有一个副本,这个配置中需要着重说的有 4 点:

  • env。主从都是 StatefulSet,那么 Pod 里面怎么知道自己要跑那种数据库实例呢?就靠环境变量,所以 Master 的环境变量包含 MARIADB_USERMARIADB_PASSWORDMARIADB_DATABASE、MARIADB_REPLICATION_MODEMARIADB_REPLICATION_USERMARIADB_REPLICATION_PASSWORDMARIADB_ROOT_PASSWORD,有些是要在我们自定义的 ConfigMap 中获取,有些是写死的常量
  • 探针。livenessProbe 和 readinessProbe 都用的是 mysqladmin status 来检查数据库状态
  • volumeMounts。数据库就是通过 volumeMounts 项找挂载到哪里,mountPath 表示要挂载到容器的路径,name 是使用的挂载 PVC 名字
  • volumes。配置的挂载,前面配置的数据库设置项都是由于他生效的
  • volumeClaimTemplates。PVC 的模板,基于 volumeClaimTemplates 数组会自动生成 PVC (PersistentVolumeClaim) 对象,它的名字要和 volumeMounts 里面的 name 一致才能对应上,由于访问模式是 ReadWriteOnce 的,所以 PVC 和 PV 是一一对应的。

接着看从 (Slave),其实它就是 Label、name 之类的值换个名字,限于篇幅问题只展示 env 这不同于 Master 的部分:

...
spec:
  containers:
  - env:
    - name: MARIADB_REPLICATION_MODE
      value: slave
    - name: MARIADB_MASTER_HOST
      value: lyanna-mariadb
    - name: MARIADB_MASTER_PORT_NUMBER
      valueFrom:
        configMapKeyRef:
          key: port
          name: lyanna-cfg
    - name: MARIADB_MASTER_USER
      valueFrom:
        configMapKeyRef:
          key: user
          name: lyanna-cfg
    - name: MARIADB_MASTER_PASSWORD
      valueFrom:
        configMapKeyRef:
          key: password
          name: lyanna-cfg
    - name: MARIADB_REPLICATION_USER
      value: replicator
    - name: MARIADB_REPLICATION_PASSWORD
      valueFrom:
        configMapKeyRef:
          key: replication-password
          name: lyanna-cfg
    - name: MARIADB_MASTER_ROOT_PASSWORD
      value: passwd
...

接着看一下名字是 lyanna-cfg 的 ConfigMap,这里面包含了数据库、Redis、Memcached 相关的设置项,这些想需要通过环境变量的方式传到对应容器中 (k8s/config.yaml):

apiVersion: v1
kind: ConfigMap
metadata:
  name: lyanna-cfg
data:
  port: "3306"
  database: test
  user: lyanna
  password: lyanna
  memcached_host: lyanna-memcached
  replication-password: lyanna
  redis_sentinel_host: redis-sentinel
  redis_sentinel_port: "26379"
  db_url: mysql://lyanna:lyanna@lyanna-mariadb:3306/test?charset=utf8

Redis Sentinel

类似部署 MariaDB 用的主从方案最大的问题是 Master 宕机了,不能实现自动主从切换,所有在实际的应用中还是直接连接的主服务器,从服务器更多的是数据备份的作用,如果真的 Master 出错了能手动调整 ConfigMap 让应用直接使用从服务器的数据。当然这部分可以优化,但我的博客实际上用的是云数据库,所以先跑起来再说。

而用 Redis 做 Master-Slave 也有这个问题,所以官方推荐 Redis Sentinel 这种高可用性 (HA) 解决方案: Sentinel 监控集群状态并能够实现自动切换,我们只要不断地从 Sentinel 哪里获得现在的 Master 是谁就可以了。

在学习 k8s 过程里面我发现 k8s 世界更多的是做基础支持,对于高可用、备份方案这类现实世界更真实的需求没什么官方成熟、完善的支持。我现在使用的是 k8s 官方例子中的 Redis Sentinel 集群用法,具体的可以看延伸阅读链接 2: 《Reliable, Scalable Redis on Kubernetes》,不过它的文档写的很简陋且不符合国情 (你懂得),且这个例子看起来比较古老,我对其做了一些调整。

构建镜像

例子中使用的镜像是k8s.gcr.io/redis:v1,但其实这个镜像是通过例子的 image 目录下的代码构建出来的,所以我针对国内源的问题修改了下具体的可以看 lyanna 项目下的 k8s/sentinel 目录下的内容,为此,我需要构建一个新的镜像 (dongweiming/redis-sentinel) 并上传到 hub.docker.com:

❯ docker build -t dongweiming/redis-sentinel:latest .
❯ docker push dongweiming/redis-sentinel

用 ReplicaSet 替代 ReplicationController

官方都这么推荐好久,可以这个例子还是使用 RC,所以为此我改进成了 ReplicaSet,不过为了省事我没有改成 StatefulSet,未来有时间再搞吧。

让 lyanna 支持 Redis Sentinel

原来在 lyanna 的代码中使用DB_URLREDIS_URL这样的设置项,而现在迁到容器里面,我的思路是用上面那个叫 lyanna-cfg 的 ConfigMap 把设置项通过环境变量传进容器,启动应用时会读取这些环境变量,另外也要支持 Redis Sentinel,所以改成这样 (config.py):

DB_URL = os.getenv('DB_URL', 'mysql://root:@localhost:3306/test?charset=utf8')
REDIS_URL = os.getenv('REDIS_URL', 'redis://localhost:6379')
DEBUG = os.getenv('DEBUG', '').lower() in ('true', 'y', 'yes', '1')
MEMCACHED_HOST = os.getenv('MEMCACHED_HOST', '127.0.0.1')

# Redis sentinel
REDIS_SENTINEL_SERVICE_HOST = None
REDIS_SENTINEL_SERVICE_PORT = 26379

try:
    from local_settings import *  # noqa
except ImportError:
    pass

# 这部分要加在`from local_settings import *`之后
redis_sentinel_host = os.getenv('REDIS_SENTINEL_SVC_HOST') or REDIS_SENTINEL_SERVICE_HOST  # noqa
if redis_sentinel_host:
    redis_sentinel_port = os.getenv('REDIS_SENTINEL_SVC_PORT',
                                    REDIS_SENTINEL_SERVICE_PORT)
    from redis.sentinel import Sentinel
    sentinel = Sentinel([(redis_sentinel_host, redis_sentinel_port)],
                        socket_timeout=0.1)
    redis_host, redis_port = sentinel.discover_master('mymaster')
    REDIS_URL = f'redis://{redis_host}:{redis_port}'

另外,lyanna 是一个 aio 项目,redis 驱动用的是 aioredis,它底层用的是 hiredis (Redis C 客户端的 Python 封装),它是不支持 sentinel 的,所以需要额外引入 redis-py 这个库 (requirements.txt)

看看代码

说了这么多,看看具体代码吧。架构分三步,首先是一个 Pod,里面有 2 个容器:一个 Master 和一个 Sentinel:

apiVersion: v1
kind: Pod
metadata:
  labels:
    name: redis
    redis-sentinel: "true"
    role: master
  name: redis-master
spec:
  containers:
    - name: master
      image: dongweiming/redis-sentinel:latest
      env:
        - name: MASTER
          value: "true"
      ports:
        - containerPort: 6379
      resources:
        limits:
          cpu: "0.1"
      volumeMounts:
        - mountPath: /redis-master-data
          name: data
    - name: sentinel
      image: dongweiming/redis-sentinel:latest
      env:
        - name: SENTINEL
          value: "true"
      ports:
        - containerPort: 26379
  volumes:
    - name: data
      hostPath:
        path: /var/lib/redis

这 2 个容器都有对应的环境变量 MASTER 和 SENTINEL,但是注意监听端口不同 (master 6379/sentinel 26379),而且 Master 会把容器的 /redis-master-data (Redis 数据存储目录,具体逻辑可以看 k8s/sentinel 目录下的代码) 挂载到本地 /var/lib/redis,让数据持久化。

然后是 Sentinel 服务:

apiVersion: v1
kind: Service
metadata:
  labels:
    name: sentinel
    role: service
  name: redis-sentinel
spec:
  ports:
    - port: 26379
      targetPort: 26379
  selector:
    redis-sentinel: "true"

服务并不直接提供 Redis 服务,这是一个 Sentinel 服务,lyanna 请求它获得现在的 Master IP 和端口,然后拼REDIS_URL访问,具体的可以看前面提的 config.py 中的改动。

然后是 2 个 ReplicaSet,先看 Master 的:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: redis
spec:
  replicas: 2
  selector:
    matchLabels:
      name: redis
  template:
    metadata:
      labels:
        name: redis
        role: master
    spec:
      containers:
      - name: redis
        image: dongweiming/redis-sentinel:latest
        ports:
        - containerPort: 6379
        volumeMounts:
        - mountPath: /redis-master-data
          name: data
      volumes:
        - name: data
          hostPath:
            path: /var/lib/redis

总体和前面的 name 为 redis-master 的 Pod 中 master 部分一样,唯一不同的是:这个 ReplicaSet 中的 2 个副本都没有环境变量 MASTER,所以可以理解它们是 Slave!

再看 Sentinel 的 ReplicaSet:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: redis-sentinel
spec:
  replicas: 2
  selector:
    matchLabels:
      redis-sentinel: "true"
  template:
    metadata:
      labels:
        name: redis-sentinel
        redis-sentinel: "true"
        role: sentinel
    spec:
      containers:
      - name: sentinel
        image: dongweiming/redis-sentinel:latest
        env:
          - name: SENTINEL
            value: "true"
        ports:
          - containerPort: 26379

Service 的后端 Pod (服务的 selector 为 redis-sentinel: "true") 包含这个 ReplicaSet 里面 2 个 Pod,以及前面的 name 为 redis-master 的 Pod 中的 sentinel,这三个 Pod 都有 SENTINEL 变量但是没有放在同一个 ReplicaSet 的设计是为了在初始化时让 Sentinel 服务先生效再启动 ReplicaSet 里面的 2 个 Pod(这部分逻辑在 k8s/sentinel/run.sh 里面)。

我再深入的解释下这个问题吧。Replica 里的 2 个 Pod 是靠 svc/redis-sentinel 获取 IP 和端口的,但问题是这个服务就是靠这些 Pod 才能接受请求,这就有了「没有鸡就下不了蛋,没有蛋生不了鸡」的问题。那么 svc 中久需要有一 (多) 个用另外的方法获得 IP 和端口才可以。svc 是 Pod 之间的通信,另外一种方法就是让 Pod 内部 2 个容器内部直接通信,所以在 run.sh 里面会尝试redis-cli -h $(hostname -i) INFO,那么 name 为 redis-master 的 Pod 中的 sentinel 就能和 Master 容器直接通信了。其实看 Sentinel Pod 日志也能看到这个过程:

❯ kubectl logs redis-sentinel-5p84q |head -5
Could not connect to Redis at 10.101.31.21:26379: Connection refused
Could not connect to Redis at 172.17.0.7:6379: Connection refused
Connecting to master failed.  Waiting...
# Server
redis_version:4.0.14
# 👆 首先尝试从服务10.101.31.21:26379获取失败,由于容器所在的Pod的网络是共享的,所以尝试了访问自己这个IP的6379端口也失败

# 👇先从服务10.101.31.21:26379获取失败,再连自己连成功了,就没第二个Connection refused
❯ kubectl logs redis-master -c sentinel |head -5
Could not connect to Redis at 10.101.31.21:26379: Connection refused
# Server
redis_version:4.0.14

现在为什么这么搞了吧?

后记

贴了好长的配置,大家慢慢理解吧~

全部 k8s 配置可以看 lyanna 项目下的 k8s 目录

延伸阅读

  1. https://kubernetes.io/zh/docs/concepts/overview/working-with-objects/common-labels/
  2. https://github.com/kubernetes/examples/tree/master/staging/storage/redis