redis on kubernetes

1

缘起

线上有一个 redis 集群,因为当时 redis 自带的集群还不成熟,而且我们项目上的需求和应用的场景比较简单,redis 集群是通过 twemproxy + redis 进行搭建的。这个集群其实有很多的不足

  • 单节点虽然设置了持久化,但是没有使用主从模式,没有哨兵 sentinel 负责主从切换
  • twemproxy 没有 HA,存在单点故障问题
  • 集群的伸缩时(添加节点,删除节点),集群中的数据不能自动平衡

图片描述

如果需求放在现在,可以使用 reids 3.x 以后自带的集群特性,另外也可以选用 codis 这类开源方案。

正好最近在研究和实践 kubernetes,打算尝试将线上的这个集群迁移到 kubernetes,毕竟 kubernetes 能够保证集群的实际状态与用户的期望一致,特别是线上的环境是可能出现主机重启,多个 redis 实例宕掉的情况,利用 kubernetes 就能提高集群的可用性。

初步分析了一下,要迁移线上这个集群,需要使用 statefulset 来实现,因为这里面

  • 每个 redis 实例需要持久化,线上都是持久化到自己主机的某个目录,每个实例和持久化目录是紧密耦合的
  • twemproxy 的配置文件又和每个 redis 实例的 IP 是紧耦合的,要求 redis 的服务暴露在稳定的地址和端口

于是有了下面的实验。计划

  • 通过 pv/pvc 解决 redis 持久化的问题
  • 通过 statefulset 带起 N 个实例,它们将有稳定的主机名(线上一个部署单元是 108 个 redis 实例)
  • 通过 configmap 和 secret 注入配置文件和敏感信息
  • 因为线上系统的特性,我们底层的 redis 实例是不需要顺序启动或停止的,podManagementPolicy 将采用 Parallel

创建 kubernetes 集群

参考 setting-up-a-kubernetes-cluster-with-vagrant 文章,快速创建一个 kubernetes 集群。实际上,因为我在公司使用 windows 操作系统,实际使用的 Vagrantfile 我做了少量的修改。

创建 pv/pvc

简单起见,本次实验的目的主要是为了验证想法,所以简单地使用基于 nfs 的 PV 和 PVC。首先在 kubernetes 的集群的节点中搭建 nfs 服务。

# 每个节点
yum -y install nfs-server nfs-utils rpcbind

# 选 node1 提供服务
systemctl enable nfs rpcbind
systemctl start nfs rpcbind

# 其他节点开启 
systemctl enable rpcbind
systemctl start rpcbind

# node1 配置 nfs
mkdir /root/data
vi /etc/exports
/root/data    172.17.8.0/24(rw,sync,no_root_squash)

# node1 重启服务,使配置生效
systemctl restart nfs

# node1 检验
showmount -e localhost
/root/data 172.17.8.0/24

# nodex 检验
mount -t nfs 172.17.8.101:/root/data /mnt

然后创建 pv/pvc

# create pv
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs
spec:
  capacity:
    storage: 2Gi
  accessModes:
    - ReadWriteMany
  nfs:
    server: 172.17.8.101
    path: "/root/data"
    
# create pvc
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc-nfs
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 2Gi

创建 redis 镜像

本来没想自定义 redis 镜像,打算直接使用 hub 上的 redis 镜像,然后用 configmap 注入 redis 的配置 redis.conf,但是只能使用同一个 configmap,这样 pod 中 redis 持久化的位置会是同一个位置,不是期望的。后来想到可以让 redis.conf 中的 dir 和 hostname 关联,利用每个 pod 的 hostname 不同来实现持久化到不同的位置上,按照这个想法做了2个实验

  • 通过 spec 里通过 inti-container 执行个 shell 来修改注入的 redis.conf
  • 通过 sepc.lifecycle 通过 poststart 执行个 shell 来修改注入的 redis.conf

这两个想法都没有实验成功。于是打算还是自定义一个 redis 镜像吧,毕竟这样会通用很多。

参考文章 https://www.kubernetes.org.cn... 以及 文章中提到的 https://github.com/kubernetes... 。很受启发,但是相对我的实验目标都比较复杂,我只需要一个简单的 redis 镜像,于是做了一番改造:具体的内容放在了 https://github.com/arashicage...

这里主要讲一下 run.sh,脚本里通过 statefulset 中 pod 的 hostname 是稳定的特定,将其用在了持久化目录配置里

if [[ ! -e /data/$(hostname) ]]; then
  echo "Redis data dir doesn't exist, data won't be persistent!"
  mkdir -p /data/$(hostname)
fi
echo dir /data/$(hostname) >> /usr/local/etc/redis/redis.conf
redis-server /usr/local/etc/redis/redis.conf --protected-mode no

创建 statefulset

---
apiVersion: v1
kind: Service
metadata:
  name: svc-redis
  labels:
    app: redis
spec:
  ports:
  - port: 6379
    name: redis
  clusterIP: None
  selector:
    app: redis
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: stateful-redis
spec:
  podManagementPolicy: Parallel
  serviceName: "redis"
  replicas: 4
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: arashicage/redis-glibc-slim
        ports:
        - containerPort: 6379
          name: redis
        volumeMounts:
        - name: nfs
          mountPath: "/data"
      volumes:
      - name: nfs
        persistentVolumeClaim:
          claimName: pvc-nfs

将上面的清单提交到 kubernetes 集群,等待其创建完成并验证(图如果看不清,拖到新的标签页里看大图)

图片描述

然后可以进 shell 里看看

图片描述

检查一下 nfs 目录,statefulset 成功创建了各个 pod 使用的持久化目录


[root@node1 ~]# ll /root/data
total 0
drwxr-xr-x. 2 root root 28 Apr 17 14:38 stateful-redis-0
drwxr-xr-x. 2 root root 28 Apr 17 14:38 stateful-redis-1
drwxr-xr-x. 2 root root 28 Apr 17 14:38 stateful-redis-2
drwxr-xr-x. 2 root root 28 Apr 17 14:38 stateful-redis-3

测试 redis pod

查看 stateful-redis-x 的 ip 并用 redis-cli 连接测试

图片描述

创建 twemproxy 的服务

这一步,因为 twemproxy 是无状态的打算创建一个 deployment 和一个 service,在 hub 上找了一下,拉取数量比较多的都从 twemproxy 都从外部的 etcd 中通过 confd 来获取 twemproxy 的配置,(我第一次听说 confd 是从我青云的一个朋友哪里,他们在 confd 上做了些改造,很不错的软件),想法很不错,但是对于我目前的实验加大了难度,我还是找一个纯粹点的 twemproxy 吧。最后选择了 zapier/twemproxy ,不过也是4年前的了,使用的 twemproxy 是v0.3.0,最目前最新 v0.4.1 支持 Authentication,而且是用在 aws 云上的,影响实验,本想需要改造一下(去掉了 python 相关的,去掉了 memcached 相关的)。后来找到一个 fblgit/twemproxy-nutcracker 比较贴合自己的需求,但是这个镜像也是有问题的(Dockerfile 里的 chmod 755 实际上没起作用,运行的时候报 Permission deny,https://github.com/moby/moby/... 另外这个镜像将 nutcracker 的配置文件和二进制文件都放在了一起 /scripts,我这需要在运行的时候挂载或在 kubernetes 中通过configmap 注入,也修改了配置文件的位置)。修改后是这样的

# ref https://hub.docker.com/r/zapier/twemproxy/~/dockerfile/
# ref https://hub.docker.com/r/jgoodall/twemproxy/~/dockerfile/
# ref https://hub.docker.com/r/fblgit/twemproxy-nutcracker/~/dockerfile/

FROM ubuntu:16.04

MAINTAINER arashicage@yeah.net

ENV DEBIAN_FRONTEND=noninteractive

ENV VERSION=v0.4.1

RUN apt-get update && DEBIAN_FRONTEND=noninteractive && apt-get install -qy gcc autoconf make libtool binutils wget

RUN cd /root && wget https://github.com/twitter/twemproxy/archive/${VERSION}.tar.gz && tar zxf ${VERSION}.tar.gz && cd twemproxy-* && \
    autoreconf -fvi && ./configure --prefix=/usr && make -j4 && make install

ADD start.sh /start.sh
RUN chmod 755 /start.sh

CMD ["/start.sh"]

将文件上传到 github,通过 hub.docker 的自动构建,最后拉取下来进行了测试:

# /root/config/ 包含了 nutcracker.yml 文件,内容见后面
docker run -it --name xxx -d -v /root/config/:/usr/local/etc/nutcracker/ docker.io/arashicage/twemproxy:0.4.1

查找容器的 IP 并检测服务是否可用

# 查找 ip
docker inspect xxx |grep IPAddress
172.33.96.3

# 检测 nutcracker 服务
curl 172.33.96.3:22222
{"service":"nutcracker", "source":"34a2f6582378", "version":"0.4.1", "uptime":61, "timestamp":1524019442, "total_connections":2, "curr_connections":1, "alpha": {"client_eof":0, "client_err":0, "client_connections":1, "server_ejects":0, "forward_error":0, "fragments":0, "server0": {"server_eof":0, "server_err":0, "server_timedout":0, "server_connections":0, "server_ejected_at":0, "requests":0, "request_bytes":0, "responses":0, "response_bytes":0, "in_queue":0, "in_queue_bytes":0, "out_queue":0, "out_queue_bytes":0},"server1": {"server_eof":0, "server_err":0, "server_timedout":0, "server_connections":0, "server_ejected_at":0, "requests":0, "request_bytes":0, "responses":0, "response_bytes":0, "in_queue":0, "in_queue_bytes":0, "out_queue":0, "out_queue_bytes":0},"server2": {"server_eof":0, "server_err":0, "server_timedout":0, "server_connections":0, "server_ejected_at":0, "requests":0, "request_bytes":0, "responses":0, "response_bytes":0, "in_queue":0, "in_queue_bytes":0, "out_queue":0, "out_queue_bytes":0},"server3": {"server_eof":0, "server_err":0, "server_timedout":0, "server_connections":0, "server_ejected_at":0, "requests":0, "request_bytes":0, "responses":0, "response_bytes":0, "in_queue":0, "in_queue_bytes":0, "out_queue":0, "out_queue_bytes":0}}}

上面这个镜像 nutcracker 的配置文件(参考 nutcracker)路径是 /usr/local/etc/nutcracker/nutcracker.yml,通过 configmap 来注入

# nutcracker.yml
alpha:
  listen: 0.0.0.0:22121
  hash: fnv1a_64
  hash_tag: "{}"
  distribution: ketama
  auto_eject_hosts: false
  timeout: 400
  redis: true
  redis_auth: foobar
  servers:
   - stateful-redis-0:6379:1 server0
   - stateful-redis-1:6379:1 server1
   - stateful-redis-2:6379:1 server2
   - stateful-redis-3:6379:1 server3
2018-05-03 补充:上面的 nutcracker.yml 中,应当使用 svc-redis.stateful-redis-0 的形式。

创建 configmap

mv nutcracker.yml /root/config
kubectl create configmap twemproxy-config --from-file=config/

# 结果
key=nutcracker.yml
val=文件内容

然后,创建 deployment

---
kind: Service
apiVersion: v1
metadata:
  name: svc-twemproxy
spec:
  selector:
    app: twemproxy
  ports:
  - name: proxy
    protocol: TCP
    port: 22121
    targetPort: 22121
  - name: state
    protocol: TCP
    port: 22122
    targetPort: 22122
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: twemproxy-deployment
  labels:
    app: twemproxy
spec:
  replicas: 2
  selector:
    matchLabels:
      app: twemproxy
  template:
    metadata:
      labels:
        app: twemproxy
    spec:
      containers:
      - name: twemproxy
        image: arashicage/twemproxy:0.4.1
        ports:
        - containerPort: 22121
          name: proxy
        - containerPort: 22122
          name: state
        volumeMounts:
        - name: config-volume
          mountPath: "/usr/local/etc/nutcracker"
      volumes:
        - name: config-volume
          configMap:
            name: twemproxy-config
            items:
            - key: nutcracker.yml
              path: nutcracker.yml

测试 twemproxy 到 stateful-redis-x

图片描述

没通啊,根据以往的经验,说明 nutcracker 不能连到后面的 redis 实例(可能 redis 宕掉,可能主机宕掉,但现在情况不是这样),估计是 nutcracker Pod 的不能通过 stateful-redis-x 解析到正确的地址,验证一下(从 dashboard 的exec 进去):

root@twemproxy-deployment-545c7dcbfd-k2h52:/# ping stateful-redis-0
bash: ping: command not found

可惜镜像里缺少 ping,nlslookup 等实用工具。只好通过其他方式了:

# 先拉个 busybox
docker pull busybox
# 再查一下 twemproxy pod 的容器 id(在stateful-redis-0 的节点上查,根据 pod 名称判断,找 pause 的 id)
docker ps -a 
# 找到 df4af96008ed

# 启动 busybox 连入 pause 的网络空间
docker run -it --name busybox --rm --network:container:df4af96008ed busybox

# ping 主机名不同,ping ip 是通的,ping svc-redis 也是通的
/ # ping stateful-redis-0
ping: bad address 'stateful-redis-0'
/ # ping 172.33.57.3
PING 172.33.57.3 (172.33.57.3): 56 data bytes
64 bytes from 172.33.57.3: seq=0 ttl=62 time=1.274 ms
/ # ping svc-redis
PING svc-redis (172.33.57.2): 56 data bytes
64 bytes from 172.33.57.2: seq=0 ttl=62 time=0.965 ms

也就是说,这里 nutcracker.yml 里不能直接使用 statefulset 的主机名,因为无法进行域名解析(ping ip 或 svc-redis 能通是因为 dns 的缘故)。要解决这个问题,需要修改 nutcracker.yml 将它改为 ip 地址。虽然statefulset 的 ip 地址是不变的,但是显式的设定感觉还是不够通用,回头通过 confd 来解决吧。

configmap 修改为

# nutcracker.yml
alpha:
  listen: 0.0.0.0:22121
  hash: fnv1a_64
  hash_tag: "{}"
  distribution: ketama
  auto_eject_hosts: false
  timeout: 400
  redis: true
  redis_auth: foobar
  servers:
   - 172.33.57.3:6379:1 server0
   - 172.33.57.2:6379:1 server1
   - 172.33.96.2:6379:1 server2
   - 172.33.92.4:6379:1 server3

重建 configmap,deployment,svc,检验

[root@node1 ~]# kubectl get pods -o wide
NAME                                    READY     STATUS    RESTARTS   AGE       IP            NODE
stateful-redis-0                        1/1       Running   0          20h       172.33.57.3   node2
stateful-redis-1                        1/1       Running   0          22h       172.33.57.2   node2
stateful-redis-2                        1/1       Running   0          22h       172.33.96.2   node1
stateful-redis-3                        1/1       Running   0          22h       172.33.92.4   node3
twemproxy-deployment-545c7dcbfd-5k2xh   1/1       Running   0          35s       172.33.92.3   node3
twemproxy-deployment-545c7dcbfd-r7d6h   1/1       Running   0          35s       172.33.96.4   node1
[root@node1 ~]# 
[root@node1 ~]# 
[root@node1 ~]# 
[root@node1 ~]# redis-cli -h 172.33.92.3 -p 22121 -a foobar
172.33.92.3:22121> set a b
OK
172.33.92.3:22121> exit
[root@node1 ~]# redis-cli -h 172.33.96.4 -p 22121 -a foobar
172.33.96.4:22121> get a
"b"
172.33.96.4:22121> 

[root@node1 ~]# kubectl get svc -o wide
NAME            TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)               AGE       SELECTOR
kubernetes      ClusterIP   10.254.0.1     <none>        443/TCP               1d        <none>
svc-redis       ClusterIP   None           <none>        6379/TCP              23h       app=redis
svc-twemproxy   ClusterIP   10.254.68.39   <none>        22121/TCP,22122/TCP   4m        app=twemproxy
[root@node1 ~]# redis-cli -h 10.254.68.39 -p 22121 -a foobar
10.254.68.39:22121> get a
"b"
10.254.68.39:22121> 


# twemproxy 也自带 HA 了,通过 服务也能访问。服务随便宕还能自愈,厉害了。

无头服务 headless service

有时不需要或不想要负载均衡,以及单独的 Service IP。 遇到这种情况,可以通过指定 Cluster IP(spec.clusterIP)的值为 "None" 来创建 Headless Service。

这个选项允许开发人员自由地寻找他们想要的方式,从而降低与 Kubernetes 系统的耦合性。 应用仍然可以使用一种自注册的模式和适配器,对其它需要发现机制的系统能够很容易地基于这个 API 来构建。

对这类 Service 并不会分配 Cluster IP,kube-proxy 不会处理它们,而且平台也不会为它们进行负载均衡和路由。 DNS 如何实现自动配置,依赖于 Service 是否定义了 selector。

有 selector 创建 Endpoints; 无 selector 不会 Endpoints 对象。

你可能感兴趣的

载入中...