缘起
线上有一个 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 对象。