分享个人 Full-Stack JavaScript 项目开发经验
由 Redis 官方推出的 Redis Cluster Proxy 可以很方便地解决了部署在 Kubernetes 上的 Redis 集群节点 Pod IP 不固定而引起的问题。Redis 客户端连接到 Redis 代理就像连接到普通 Redis 主机一样方便,并不需要顾虑集群节点情况。 本文将会介绍 Redis 集群基本概念,以及如何在 Kubernetes 上搭建高安全级别的 Redis 集群和集群代理。
环境及版本说明:
内容列表:
Redis 群集化的好处主要还是提高可用性,自动将你的数据集分割在多个节点之间,当一部分节点出现故障或无法与集群的其余部分通信时,集群可以继续运行。另外一个好处就是可以提供不停机的横向伸缩能力。
每个 Redis 群集节点都需要打开两个 TCP 连接。
Redis 群集中有 16384 个哈希槽,每个键从概念上讲都是哈希槽的一部分。Redis 群集中的每个节点都负责哈希槽的子集。
因为将哈希槽从一个节点移动到另一个节点不需要停止操作,所以添加和删除节点或更改节点持有的哈希槽的百分比不需要任何停机时间。
另外,用户也可以通过使用 hash tag 来强制多个键成为同一个哈希槽的一部分。
Redis 集群使用 master-slave 模型来实现高可用性。每个 Redis 的 master 节点后会添加至少一个 slave 节点用于主从复制。例如,我们设置 3 个主节点 A、B、C,它们会拥有各自的哈希槽子集:
它们被创建后,会添加对应的 slave 节点 A1、B1、C1,并且会开始复制对应主节点的哈希槽。当 B 节点故障时,B1 节点会在节点超时时间后成为新的 master 节点继续工作。
但是如果 B 和 B1 同时故障,则 Redis 集群无法继续工作,直到 B 节点重新加入集群后,集群才能恢复可用。
Redis 群集无法保证强一致性。原因有两个:
Redis 集群在配置文件 redis.conf 文件中引入了一些特定的配置参数。
cluster-enabled<yes/no>
是否启用 Redis 集群支持。
cluster-config-file<filename>
它不是用户可编辑的配置文件,而是 Redis 集群实例在启动时生成,节点每次发生更改时进行更新。默认情况下为 nodes.conf。
cluster-node-timeout<milliseconds>
节点超时时间,在指定的时间内无法到达大多数主节点的每个节点都将停止接受查询。
cluster-slave-validity-factor<factor>
如果设置为 0,则从节点将始终尝试对主节点进行故障转移。
如果设置为正数,从节点会在该正数乘以 cluster-node-timeout 的时间内尝试对主节点进行故障转移。超过这时间,从节点就不会继续尝试对主节点进行故障转移。如果没有从节点可以进行故障转移,则集群变成不可用,直到主节点恢复正常。
cluster-migration-barrier<count>
主节点保持连接的最小从节点数量。这样,最小值以外的从节点可以迁移到没有从节点覆盖的其它主节点。
cluster-require-full-coverage<yes/no>
yes:如果某个节点未覆盖一定比例的密钥空间,集群将停止接受写入。(默认 yes,当有密钥空间缺失时,集群停止写入。)
no:即使仅可以处理有关密钥子集的请求,群集仍将提供操作。
cluster-allow-reads-when-down<yes/no>
当集群标记为失败,或者节点无法达到大多数主节点(大多数主节点故障了),或者密钥空间未全覆盖时,节点是否停止所有流量。
1、禁用 CONFIG 命令。在 redis.conf 中配置:
rename-command CONFIG ""
2、设置身份认证密码,这个密码最好足够长,以方式暴力破解。在 redis.conf 中配置:
requirepass your_long_enough_password
masterauth your_long_enough_password
3、使用非 Root 的 uid=999(redis) gid=999(redis) groups=999(redis) 用户运行容器。对应 Kubernetes 配置如下:
containers:
securityContext:
runAsNonRoot: true
runAsUser: 999
4、Kubernetes 中使用 NetworkPolicy 限制其它 Pod 对 Redis Pod 的访问。(前提是你的集群网络插件支持使用 NetworkPolicy。)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: redis-cluster-network-policy
spec:
podSelector:
matchLabels:
app: redis-cluster
ingress:
- from:
- podSelector:
matchLabels:
run: redis-client
namespaceSelector: {}
ports:
- port: 6379
- port: 16379
按预期工作的最小群集要求至少包含三个主节点(并且考虑"脑裂"情况,节点数目应该始终使用奇数)。下面是一个简洁的配置例子:
cluster-enabled yes
cluster-require-full-coverage no
cluster-node-timeout 5000
cluster-config-file /data/nodes.conf
cluster-migration-barrier 1
cluster-allow-reads-when-down no
requirepass your_long_enough_password
masterauth your_long_enough_password
appendonly yes
rename-command CONFIG ""
上面配置中,除了必要的常规集群配置外,还配置了:
下面是 Kubernetes 完整的集群配置例子:
---
apiVersion: v1
kind: Namespace
metadata:
name: redis-cluster
---
apiVersion: v1
kind: Service
metadata:
name: redis-cluster
namespace: redis-cluster
spec:
type: ClusterIP
ports:
- port: 6379
targetPort: 6379
name: client
- port: 16379
targetPort: 16379
name: gossip
selector:
app: redis-cluster
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: redis-cluster-network-policy
spec:
podSelector:
matchLabels:
app: redis-cluster
ingress:
- from:
- podSelector:
matchLabels:
run: redis-client
namespaceSelector: {}
ports:
- port: 6379
- port: 16379
---
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-cluster
namespace: redis-cluster
data:
# update-node.sh 用于启动时,更新 /data/nodes.conf 中自身的 Pod IP
update-node.sh: |
#!/bin/sh
REDIS_NODES="/data/nodes.conf"
sed -i -e "/myself/ s/[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/${POD_IP}/" ${REDIS_NODES}
exec "$@"
redis.conf: |
cluster-enabled yes
cluster-require-full-coverage no
cluster-node-timeout 5000
cluster-config-file /data/nodes.conf
cluster-migration-barrier 1
cluster-allow-reads-when-down no
requirepass your_long_enough_password
masterauth your_long_enough_password
appendonly yes
always-show-logo yes
rename-command CONFIG ""
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cluster
namespace: redis-cluster
spec:
serviceName: redis-cluster
replicas: 6
selector:
matchLabels:
app: redis-cluster
template:
metadata:
labels:
app: redis-cluster
run: redis-client
spec:
imagePullSecrets:
- name: aliyun-docker-registry
volumes:
- name: conf
configMap:
name: redis-cluster
defaultMode: 0755
- name: hugepage
hostPath:
path: /sys/kernel/mm/transparent_hugepage/enabled
initContainers:
- name: tcp-backlog
image: busybox
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "echo 511 > /proc/sys/net/core/somaxconn"]
securityContext:
privileged: true
- name: transparent-hugepage
image: busybox
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "echo never > /sys/kernel/mm/transparent_hugepage/enabled"]
securityContext:
privileged: true
volumeMounts:
- name: hugepage
mountPath: /sys/kernel/mm/transparent_hugepage/enabled
containers:
- name: redis
image: registry.cn-shenzhen.aliyuncs.com/leeguangxing/redis:6.0.6
imagePullPolicy: IfNotPresent
ports:
- containerPort: 6379
name: client
- containerPort: 16379
name: gossip
command: ["/conf/update-node.sh", "redis-server", "/conf/redis.conf"]
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
volumeMounts:
- name: conf
mountPath: /conf
readOnly: false
- name: data
mountPath: /data
readOnly: false
securityContext:
runAsNonRoot: true
runAsUser: 999
resources:
limits:
cpu: 1000m
memory: 1Gi
volumeClaimTemplates:
- metadata:
name: data
labels:
app: redis-cluster
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: nfs-dynamic
resources:
requests:
storage: 1Gi
因为每个集群节点需要保存自己集群配置文件,以及持久化数据文件,所以使用 StatefulSet 和 volumeClaim 部署。可以注意到,StatefulSet 的 template 中,使用了两个特权 initContainers,这是为了解决 Redis 给出的 TCP backlog 过低和没有禁用节点主机 THP 的警告。
根据上面配置初始化启动 Redis 实例后,会看到类似下面日志:
No cluster configuration found, I'm 25026b92dceb9b13ab6fc13994f1114e1588a117
由于 nodes.conf 文件不存在引起的,因此每个节点都会为其分配一个新的节点 ID,这个 ID 在整个生命周期中都不会变化。
kubectl exec -it redis-cluster-0 -n redis-cluster -- redis-cli -a 'your_long_enough_password' --cluster create --cluster-replicas 1 \
$(kubectl get pods -l app=redis-cluster -n redis-cluster -o jsonpath='{range.items[*]}{.status.podIP}:6379 ')
上面命令会在 StatefulSet 节点 0 中执行创建集群命令,Redis 集群节点 Pod IP 由 kubectl 命令动态获取。创建前会提示检查和确认,最后回复 yes 确认创建。
查询集群信息:
kubectl exec -it redis-cluster-0 -n redis-cluster -- redis-cli -a 'your_long_enough_password' cluster info
在命令行中输入密码存在风险,因为可能被系统所记录,存在隐患,所以执行此命令时候会有警告。官方建议使用环境变量方式设置密码,但是个人认为这种方式也有隐患,期待官方可以推出像 MySQL 一样的密码输入方式。
查询每个节点角色信息:
for x in $(seq 0 5); do echo "redis-cluster-$x"; kubectl exec redis-cluster-$x -n redis-cluster -- redis-cli -a 'your_long_enough_password' role; echo; done
亦可在节点容器内查看集群配置文件:
cat /data/nodes.conf
我们可以在 kubernetes 集群内部,使用 redis-cli 客户端访问 Redis 集群(但是所在 Pod 要在符合 NetworkPolicy):
redis-cli -a 'your_long_enough_password' -c -h redis-cluster.redis-cluster.svc.cluster.local
注意到使用参数 -c 启动客户端集群模式,和使用了完整了 DNS 名称。
当你删除其中一个主节点后,若超过 cluster-node-timeout 时间新节点 Pod 才创建完毕,它将会变成从节点,它原来的从节点会提升为主节点。可以使用 redis-cli 连接后,使用下面命令查看节点情况:
cluster nodes
到这里为止,我们已经部署好了三个主节点和三个从节点的 Redis 集群了。接下来,我们校验部署 Redis 集群带来。
由于 redis-cluster-proxy 还没有 stable 版本,所以官方也没有推出相关镜像。我们要构建自己的 redis-cluster-proxy 镜像,以下是 Docker Hub 社区开发者提供的一份 Dockerfile:
FROM alpine:3.11 AS build
RUN apk add --no-cache gcc musl-dev linux-headers openssl-dev make git
RUN addgroup -S app && adduser -S -G app app
RUN chown -R app:app /usr/local
WORKDIR /tmp
USER app
RUN git clone https://github.com/kornrunner/redis-cluster-proxy
RUN cd redis-cluster-proxy && make install
FROM alpine:3.11 AS runtime
RUN apk add --no-cache libstdc++
RUN apk add --no-cache strace
RUN apk add --no-cache python3
RUN apk add --no-cache redis
RUN addgroup -S app && adduser -S -G app app
COPY --chown=app:app --from=build /usr/local/bin/redis-cluster-proxy /usr/local/bin/redis-cluster-proxy
RUN chmod +x /usr/local/bin/redis-cluster-proxy
RUN ldd /usr/local/bin/redis-cluster-proxy
RUN mkdir -p /usr/local/etc/redis-cluster-proxy
RUN mkdir -p /usr/local/run/redis-cluster-proxy
RUN chown -R app:app /usr/local
VOLUME /usr/local/etc/redis-cluster-proxy
VOLUME /usr/local/run/redis-cluster-proxy
# Now run in usermode
USER app
WORKDIR /home/app
ENTRYPOINT ["/usr/local/bin/redis-cluster-proxy"]
EXPOSE 7777
CMD ["redis-cluster-proxy"]
它使用多阶段构建是 Docker 17.06 版本提出的,若果 Docker 版本过低,请参考官方安装升级说明先将 Docker 升级到 17.06 或以上版本。
部署 redis-cluster-proxy 相对简单,只需要将配置以 ConfigMap 方式挂载到 Pod 中,并重写启动参数即可。下面是使用 Deployment 部署的例子:
---
apiVersion: v1
kind: Service
metadata:
name: redis-cluster-proxy
spec:
ports:
- port: 7777
targetPort: 7777
selector:
app: redis-cluster-proxy
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: redis-cluster-proxy-network-policy
spec:
podSelector:
matchLabels:
app: redis-cluster-proxy
ingress:
- from:
- podSelector:
matchLabels:
proxy: redis-proxy-client
namespaceSelector: {}
ports:
- port: 7777
---
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-proxy-conf
data:
proxy.conf: |+
cluster redis-cluster.redis-cluster.svc.cluster.local:6379
auth 'your_long_enough_password'
bind 0.0.0.0
enable-cross-slot yes
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-cluster-proxy
labels:
app: redis-cluster-proxy
spec:
replicas: 1
minReadySeconds: 30
revisionHistoryLimit: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
progressDeadlineSeconds: 600
selector:
matchLabels:
app: redis-cluster-proxy
template:
metadata:
labels:
app: redis-cluster-proxy
run: redis-client
spec:
imagePullSecrets:
- name: aliyun-docker-registry
initContainers:
- name: tcp-backlog
image: busybox
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "echo 511 > /proc/sys/net/core/somaxconn"]
securityContext:
privileged: true
volumes:
- name: conf
configMap:
name: redis-proxy-conf
containers:
- name: redis-cluster-proxy
image: registry.cn-shenzhen.aliyuncs.com/leeguangxing/redis-cluster-proxy:6.0.6
imagePullPolicy: IfNotPresent
ports:
- containerPort: 7777
protocol: TCP
volumeMounts:
- name: conf
mountPath: /data
command: ["redis-cluster-proxy"]
args:
- -c
- /data/proxy.conf
resources:
limits:
cpu: 1000m
memory: 1Gi
上面配置中,enable-cross-slot 设置为 yes,以允许跨哈希插槽查询。这些查询会破坏很多 Redis 命令的原子性设计,并且跨插槽查询并不支持所有命令。配置中还指定了集群入口的 DNS 名称和端口。
通过 Kubernetes 部署 Redis 集群可以降低部署复杂度,并且提供了更好的高可用性等优点,配合 redis-cluster-proxy 更可降低客户端连接 Redis 集群的复杂性。虽然官方仍未推出稳定版本 redis-cluster-proxy,但经验证目前版本功能已经相对稳定,相信应该不久将来就推出稳定版本。
一篇来自 rancher.com 的 Redis 进群部署文章:
https://rancher.com/blog/2019/deploying-redis-cluster
官方 Redis 集群教程:
https://redis.io/topics/cluster-tutorial
官方 Redis 集群规范:
https://redis.io/topics/cluster-spec
官方 Redis 安全说明:
https://redis.io/topics/security
官方 Redis 配置:
http://download.redis.io/redis-stable/redis.conf
官方 redis-cluster-proxy 配置:
https://github.com/RedisLabs/redis-cluster-proxy/blob/unstable/proxy.conf
社区 redis-cluster-proxy Dockerfile:
https://hub.docker.com/r/kornrunner/redis-cluster-proxy/dockerfile
Docker CentOS 环境安装和升级说明:
https://docs.docker.com/engine/install/centos/