k8s中部署有状态应用
一、场景描述
一般来讲,不建议在 k8s 中部署 Redis、Kafka 或者 MySQL 等需要持久化数据等有状态应用,因为数据卷数据安全问题、IO 性能问题、非最佳实践导致隔离不彻底、资源争用问题等问题的存在,导致容器化部署的可靠性不如非容器化部署;其次,中间件、数据库类应用通常也不会很频繁的版本变动或者弹性伸缩。但也不是说数据库一定不要部署在容器里,在安全、性能、SLA 要求不高的场景下(如数据丢失不敏感的业务或者环境),容器化部署也是节省成本、提高效率的好办法。
二、可选方案
- 根据官方提供或者自编写的 dockerfile 文件,自行实现 redis 集群方案、设计 k8s 编排文件
- 优:可定制强
- 劣:繁复,成本高
- 使用开源 helm 仓库中的成品部署,如 stable 或者 Bitnami 版本
- 优:简单定制,开箱即用
- 劣:可能存在功能局限,如 stable 仓库于 2020.11 放弃维护,Bitnami 版本的 cluster 集群只支持单个 database;
- 三方开源的 redis-operator,如 ucloud 开源产品
- 优:开箱即用,国人开发,高可用
- 劣:偏黑盒,较多 CRD,如果不熟悉代码的话不好排查问题;
- 企业级产品,如 Kubedb
- 优:简单定制,开箱即用,稳定性好
- 劣:企业版需付费,社区版存在部分功能局限
三、实践
选型说明
目的是容器化非生产环境的 Redis 部署,结合自身需求对比以上几种方案。如果不差钱,可以用 Ucloud 开源的 operator,这个简便快捷,但是最小规格的集群拓扑仍需启动 6 个 pod(3master 节点 + 3slave),比较耗资源;暂定使用 Bitnami 指定版本的 redis(非 HA 版)部署,说明如下:
- SLA 要求不高,预算吃紧,使用非 HA 版;
- 我们目前在使用的 5.0 版本的 Redis,Chart 10.6.12 版本对应 Redis 5.0.9;
- 业务方已经完全剔除哨兵模式 Redis 集群的客户端连接配置,无需兼容 Sentinel;
- 集群拓扑使用一主一从形式,支持读写分离,主节点崩溃的情况下不进行主从切换,等待 k8s Manager 重新拉起主节点即可,会有 30s 左右的闪断;(如果客户端没有读写分离需求,不要 slave 节点也可以)
- 只读 slave 节点支持水平扩容,master 节点只支持垂直扩容,如遇性能瓶颈,对 master 节点变配即可;
操作步骤
- 配置 Chart 仓库:
helm repo add bitnami https://charts.bitnami.com/bitnami
- 下载指定版本 redis Chart:
helm pull bitnami/redis --version=10.6.12
- 解压:
tar -zxf redis-10.5.7.tgz
- 编辑 redis/value.yaml 文件内容进行定制,改动点如下:
- 更换镜像为私库镜像
- 使用本地存储(已有的动态存储类),开启持久化,使故障自愈后不丢失数据
- 启用密码认证、禁用监控、禁用 sentinel
- 增大 databases 数目为 256,和阿里云 redis 服务保持一致
- 设置资源限额(requests 和 limits)
- 示例文件:
global:
imageRegistry: registry-vpc.cn-hangzhou.aliyuncs.com
storageClass: alicloud-nas-subpath
redis:
password: "cx5swxbr05MhIXcj"
image:
registry: registry-vpc.cn-hangzhou.aliyuncs.com
repository: mars-xxx/redis
tag: 5.0.9-debian-10-r4
pullPolicy: IfNotPresent
cluster:
enabled: true
slaveCount: 1
sentinel:
enabled: false
usePassword: true
image:
registry: registry-vpc.cn-hangzhou.aliyuncs.com
repository: mars-xxx/redis-sentinel
tag: 5.0.9-debian-10-r2
pullPolicy: IfNotPresent
masterSet: mymaster
initialCheckTimeout: 5
quorum: 2
downAfterMilliseconds: 60000
failoverTimeout: 18000
parallelSyncs: 1
port: 26379
configmap:
staticID: false
livenessProbe:
enabled: true
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 5
readinessProbe:
enabled: true
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 1
successThreshold: 1
failureThreshold: 5
customLivenessProbe: {}
customReadinessProbe: {}
service:
type: ClusterIP
sentinelPort: 26379
redisPort: 6379
annotations: {}
labels: {}
loadBalancerIP:
clusterDomain: cluster.local
networkPolicy:
enabled: false
ingressNSMatchLabels: {}
ingressNSPodMatchLabels: {}
serviceAccount:
create: false
name:
rbac:
create: false
role:
rules: []
securityContext:
enabled: true
fsGroup: 1001
runAsUser: 1001
usePassword: true
password: ""
usePasswordFile: false
persistence:
existingClaim:
redisPort: 6379
master:
command: "/run.sh"
configmap:
extraFlags: []
disableCommands:
- FLUSHDB
- FLUSHALL
podLabels: {}
podAnnotations: {}
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 1024Mi
cpu: 500m
livenessProbe:
enabled: true
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 5
readinessProbe:
enabled: true
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 1
successThreshold: 1
failureThreshold: 5
customLivenessProbe: {}
customReadinessProbe: {}
affinity: {}
service:
type: ClusterIP
port: 6379
annotations: {}
labels: {}
loadBalancerIP:
persistence:
enabled: true
path: /data
subPath: ""
accessModes:
- ReadWriteOnce
size: 10Gi
matchLabels: {}
matchExpressions: {}
statefulset:
updateStrategy: RollingUpdate
priorityClassName: {}
slave:
service:
type: ClusterIP
port: 6379
annotations: {}
labels: {}
loadBalancerIP:
port: 6379
command: "/run.sh"
configmap:
extraFlags: []
disableCommands:
- FLUSHDB
- FLUSHALL
affinity: {}
livenessProbe:
enabled: true
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 5
readinessProbe:
enabled: true
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 10
successThreshold: 1
failureThreshold: 5
customLivenessProbe: {}
customReadinessProbe: {}
podLabels: {}
podAnnotations: {}
persistence:
enabled: true
path: /data
subPath: ""
accessModes:
- ReadWriteOnce
size: 10Gi
matchLabels: {}
matchExpressions: {}
statefulset:
updateStrategy: RollingUpdate
metrics:
enabled: false
image:
registry: registry-vpc.cn-hangzhou.aliyuncs.com
repository: mars-xxx/redis-exporter
tag: 1.5.3-debian-10-r21
pullPolicy: IfNotPresent
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9121"
serviceMonitor:
enabled: false
selector:
prometheus: kube-prometheus
prometheusRule:
enabled: false
additionalLabels: {}
namespace: ""
rules: []
service:
type: ClusterIP
annotations: {}
labels: {}
volumePermissions:
enabled: false
image:
registry: registry-vpc.cn-hangzhou.aliyuncs.com
repository: mars-xxx/minideb
tag: buster
pullPolicy: Always
resources: {}
configmap: |-
appendonly yes
save ""
databases 256
sysctlImage:
enabled: false
command: []
registry: registry-vpc.cn-hangzhou.aliyuncs.com
repository: mars-xxx/minideb
tag: buster
pullPolicy: Always
mountHostSys: false
resources: {}
podSecurityPolicy:
create: false
- 编辑Chart.yaml文件内容,定制 Chart 详情,如:修改 name 为 redis-ms
- 在 default namespace 部署:
helm install dev-cache -n default ./redis
- 检查部署情况:
- 查看 Chart 概览:
helm status dev-cache
- 查看关键资源:
kubectl get all -l app=redis-ms
- 功能验证:
- 访问 redis 集群任意 pod,如 slave 节点容器:
kubectl exec -it dev-cache-redis-ms-slave-0 -- bash
- 验证 master 节点可读写:
- 使用命令行工具连接主节点:
redis-cli -h dev-cache-redis-ms-master -p 6379 -a <your password>
- 写入 string 类型的 key:
set masterkey 123456
,输出”OK”表示写入成功;输入quit
退出命令行工具;
- 使用命令行工具连接主节点:
- 验证 slave 节点不可写:
- 使用命令行工具连接主节点:
redis-cli -h dev-cache-redis-ms-slave -p 6379 -a <your password>
- 写入 string 类型的 key:
set slavekey 123456
,写入失败,报错提示:(error) READONLY You can’t write against a read only replica.,输入quit
退出命令行工具;
- 使用命令行工具连接主节点:
- 验证节点垂直扩容、master 节点异常终止并恢复后不丢失数据:
- 修改config/values.yaml的 resources 配置并执行更新操作:
helm upgrade -f redis/values.yaml dev-cache ./redis
- 更新成功后使用命令行工具验证数据未丢失
- 修改config/values.yaml的 resources 配置并执行更新操作:
- 手动删除 master 节点容器模拟强制调度场景:
kubectl delete pod dev-cache-redis-ms-master-0
- master 节点恢复后使用命令行工具验证数据未丢失
补充说明
该集群会默认创建一个无头服务:dev-cache-redis-ms-headless,本意是用于分布式高可用集群场景下集群内节点交互、master 选取等场景;不要将该无头服务用于业务访问 redis 的连接地址,因为无头服务不像普通 Service,不使用集群内置负载均衡功能、访问 pod 不经过 service 转发;而是直接通过 Endpoints 控制器创建 Endpoints 记录,并在 CoreDNS 服务器中为该服务创建两条 A 型解析记录,使用服务名访问无头服务时会从 DNS 服务器中获取其中一条记录对应的 IP 使用(可能对应 master 节点的 pod、也可能对应 slave 节点的 pod),而不同的节点又对应不同的读写权限,故而不可控。
四、总结
以上是以 Redis 部署为例说明各开源应用工具在 k8s 集群中部署方案;要部署其他应用的思路可以参考这个来,都差不多~~