37

Python项目容器化实践(七) - lyanna的Kubernetes配置文件

 4 years ago
source link: http://www.dongwm.com/post/use-kubernetes-4/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

接下来 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
# :point_up_2: 首先尝试从服务10.101.31.21:26379获取失败,由于容器所在的Pod的网络是共享的,所以尝试了访问自己这个IP的6379端口也失败

# :point_down:先从服务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 目录

延伸阅读


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK