资讯动态

K8s上MySQL、Kafka、Redis高可用实践:从配置到踩坑

发布时间:2026/10/21 13:37:41 来源:尧图企业网站定制
把中间件搬上 Kubernetes最怕的不是服务起不来而是起来之后才发现“高可用”只是纸面功夫。MySQL 主从切换失败、Kafka 分区因为节点重启迟迟无法恢复、Redis 哨兵误判主节点导致脑裂这些我都实打实踩过。2.4 这一节要聊的就是我在 K8s 上部署 MySQL、Kafka、Redis 三件套高可用方案时的完整思路、具体配置和踩坑记录适合正在规划中间件容器化、或者已经被生产环境问题折磨过的运维和架构同学参考。下面内容不涉及任何线上敏感数据所有示例配置都是可复现的最小化方案。1. 整体设计思路为什么在 K8s 上跑有状态中间件以及高可用的本质1.1 先从“状态”说起K8s 能解决什么不能解决什么很多人脑海里第一反应是K8s 天生适合无状态应用中间件这种带数据的家伙还是别碰了。这个说法有一定道理但不完全对。无状态应用随便调度容器挂了就重启副本多开几个就行有状态中间件的难点在于三件事数据持久化、服务身份稳定、故障恢复有序。数据持久化对应 K8s 里的存储卷也就是 PVC 和 PV。服务身份稳定对应 StatefulSet 的稳定网络标识Pod 名称和 Headless Service 的 DNS 记录比如mysql-0.mysql-headless.namespace.svc.cluster.local。故障恢复有序则靠探针、Operator、控制器之间的协作保证“谁是主、谁是备”不混乱。K8s 能统一调度、自动重启、滚动升级这是它最大的价值。但它不负责解决“MySQL 主库挂了之后从库能否自动提升”这类数据库原生高可用问题。K8s 只是舞台高可用剧本还是要中间件自己配合 Controller 去跑。这是我认为最重要的一点不要指望把中间件塞进 K8s 就万事大吉高可用本质上取决于你有没有把状态管理、服务发现、故障切换机制想清楚。1.2 三种主流部署方式的取舍裸 Pod、Helm Chart、Operator我在初期选择部署方式时对比过三条路线部署方式优点缺点适用场景裸 Pod StatefulSet 自写 YAML完全可控没有额外依赖故障切换逻辑需要自己实现工作量大测试环境或有专人长期维护Helm Chart 直接部署参数化配置一条命令拉起升级和故障切换仍依赖 chart 内部的脚本中小规模、人少事多的团队Operator 方式把故障切换、备份、扩缩容都封装起来引入额外控制面组件学习成本高生产环境长期运维我个人最终选择 Operator 方式。原因很实际MySQL、Redis 这类中间件的高可用不是简单“多起几个副本”就能实现主从拓扑、选举、故障恢复这些逻辑写起来太复杂用 Operator 能省掉大量重复工作。但它不是银弹Operator 本身也是跑在集群里的服务你要确保它有足够的权限、稳定的网络、以及和存储系统配合好否则控制面一出问题工作负载也跟着遭殃。1.3 高可用的本质数据不丢、服务不宕、恢复自动高可用不是一个口号拆解开其实是三个可量化的目标数据不丢持久化存储必须可靠并且要配置合理的备份策略。MySQL 至少要有 binlog 和全量备份Kafka 要关注副本同步和 ack 机制Redis 要启用 AOF 并考虑 RDB 与 AOF 配合。服务不宕无论故障发生在进程、节点、还是流量层面都要有冗余机制。不可用的时间取决于探针发现、负载均衡切换、以及重新调度的速度。恢复自动故障发生后系统应在无人干预的情况下完成切换并收敛。例如 Kafka 的控制器自动选举、MySQL Operator 自动选主、Redis Sentinel 自动故障转移。如果这三个目标做不到那部署得再花哨也只是“看起来高可用”。我在后续每一节里都会围绕这三条线讲配置细节。2. MySQL 在 K8s 上的高可用实现细节2.1 架构选型主从复制 半同步 自动故障转移MySQL 高可用方案很多比如 MHA、MySQL Router、半同步复制、组复制等。在 K8s 上我倾向用“基于主从复制做自动故障转移”的方案核心组件是现成的不用自己维护一堆脚本。推荐组合一个主实例、至少一个从实例开启半同步复制lossless semi-synchronous replication由 Operator 监听主库状态一旦主库不可用自动提升一个从库为新的主库并同步更新服务发现。这里需要注意半同步复制并不能完全保证零丢失但它能在主库崩溃时尽量减少数据丢失窗口。为什么要用半同步而不是异步异步复制下主库崩溃时从库可能缺少最新事务丢数据非常明显。半同步要求至少一个从库确认收到 binlog 后主库才提交事务这能显著降低主备切换时的数据差异。代价是写入延迟增加尤其跨地域部署时更明显。所以在生产环境中建议在同机房或同可用区部署主从避免延迟过高。2.2 核心配置StatefulSet、Headless Service 与自定义配置StatefulSet 是 MySQL 部署的地基。它保证 Pod 名称稳定、启动顺序可控。Headless Service 是关键点它让每个 Pod 拥有独立 DNS 记录客户端可以按名字访问指定实例而不是像 ClusterIP 那样做负载均衡轮询。一个最小化的 MySQL StatefulSet 需要关注以下字段serviceName指定 Headless Service 的名称。volumeClaimTemplates声明 PVC 模板每个 Pod 获取独立存储卷。podManagementPolicy如果是OrderedReady则按顺序逐个启动适合做集群初始化如果不想被依赖关系卡住可以用Parallel但 MySQL 主从初始化一般还是有序启动比较稳。env注入时区、字符集、root 密码等信息。livenessProbe和readinessProbe要区分清楚readiness 探针挂在真正能接受流量之前liveness 探针用于判断进程是否存活配置不当会把正常在恢复的实例误杀。下面是简化版的 YAML 片段我故意省略了 Operator 相关部分便于理解底层对象apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql spec: serviceName: mysql-headless replicas: 3 podManagementPolicy: OrderedReady selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: mysql:8.0 args: - --server-id$(POD_NAME_SUFFIX) - --gtid_modeON - --enforce_gtid_consistencyON - --log-binmysql-bin - --binlog_formatROW - --log_slave_updatesON env: - name: POD_NAME_SUFFIX valueFrom: fieldRef: fieldPath: metadata.name ports: - containerPort: 3306 name: mysql volumeMounts: - name: data mountPath: /var/lib/mysql readinessProbe: exec: command: - sh - -c - mysqladmin ping -uroot -p$$MYSQL_ROOT_PASSWORD initialDelaySeconds: 10 periodSeconds: 10 volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] resources: requests: storage: 100Gi这里有个我踩过的坑server-id如果不唯一主从复制会异常。直接用 Pod 名称作为后缀生成server-id可以确保唯一性但要注意 Pod 名里的数字和斜杠问题实际生产中我还是建议用脚本把 Pod 名解析成整数再拼到配置里。2.3 主从初始化与故障切换逻辑主从初始化不能靠人工登录容器执行 SQL必须做成自动化的部分。Operator 会负责在集群初始化时指定主实例创建复制账号启动复制线程。如果你不想用 Operator自己写 initContainer 或 Job 来实现也是可行的但工作量和排错成本会上升。故障切换逻辑更复杂至少要考虑怎么判断主库真的挂了不能只看进程还要看网络分区、磁盘 IO 挂起等情况。我建议结合mysqladmin ping、SELECT 1的返回值以及存储层的健康状态综合判断。从库的数据追到哪一步如果从库延迟很大直接提升会导致大量数据丢失应该设定半同步复制的最小确认数并监控复制延迟阈值。切换后其他从库怎么重新指向新主需要自动执行CHANGE MASTER TO这些操作在 Operator 里都有现成逻辑自研方案要自己保证幂等。这里还要提醒一件事不要在容器启动命令里写死主从关系。我以前见过有人把MASTER_HOST写死在启动参数里一旦故障切换后从库不会自动重定向整个高可用直接失效。更好的方式是让 Operator 通过探针或者 Discovery Service 获取当前主库地址动态写入配置。这是初学者最容易忽略的坑。2.4 存储选型与备份恢复的细节MySQL 的存储分两条路线用云厂商提供的持久化卷或者自建存储系统。性能敏感的业务可以用本地 SSD WaitForFirstConsumer 延迟绑定减少卷调度距离。存储类型选择和 PVC 的StorageClassName直接相关底层若是网络存储要关注 IOPS 和带宽。生产环境建议至少配置自动扩容或监控磁盘使用率防止磁盘写满引起只读故障。备份恢复是很多人容易忽略的一环。K8s 上跑 MySQL卷快照和逻辑备份要同时做。卷快照适合秒级恢复但不能保证事务一致性尤其在高并发写入时可能不一致逻辑备份如mysqldump更可靠但速度慢。建议定期做逻辑备份并周期性测试恢复流程。不要等真正出事才发现备份文件已经损坏这个教训我吃过一次之后养成了“每两周演练一次恢复”的习惯。3. Kafka 在 K8s 上的高可用方案3.1 架构选型KRaft 模式取代 ZooKeeperKafka 早期依赖 ZooKeeper 做元数据管理但引入一堆运维负担。好在现在 Kafka 3.x 已经支持 KRaft 模式直接把元数据放进 Kafka 自身的 controller 节点不需要再单独维护一套协调器。虽然我理解不少人还在用老版本但新项目我建议直接上 KRaft 模式省去一个组件也少一类故障源。在 K8s 上我选择部署 3 个 controller/broker 合并节点也就是把 controller 角色和 broker 角色跑在同一个 pod 里。生产环境如果规模大可以拆分成单独的 controller 节点但中小规模合并节点足够成本也低。Kafka 的高可用关键配置参数包括replication.factor3Topic 必须有 3 个副本保证单节点故障时可读可写。min.insync.replicas2至少两个副本同步完成才返回 ack防止写丢数据。unclean.leader.election.enablefalse绝不允许不同步的副本竞选 leader宁可短暂不可用也不丢数据。retention.ms和log.segment.bytes根据数据量调整避免段文件过大影响恢复时间。3.2 分布策略反亲和性、PodDisruptionBudget 与 TopologySpreadKafka 是典型的多副本分布式系统如果多个副本被调度到同一台物理机上那这台机器宕机就全完蛋。在 K8s 里必须配置 Pod 反亲和性podAntiAffinity让同一集群的 broker 尽量分布在不同节点。更细一点还要考虑 zone 维度用topologySpreadConstraints把副本分散到不同可用区。之前遇到过一个问题集群有 3 台节点但某个 broker 被调度到同一节点导致另外两台节点的磁盘利用率不均衡整个集群的流量都压在少数 broker 上。后来加了topologyKey: kubernetes.io/hostname的反亲和规则然后把maxSkew1的 spread 约束加上才真正把分布打散。PodDisruptionBudgetPDB也很关键它保证主动运维如节点维护、滚动升级时同一时间最多只有1个副本不可用。否则驱逐 Pod 时如果多个副本同时从集群掉线分区就不可用了。配置示例apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: kafka-pdb spec: minAvailable: 2 selector: matchLabels: app: kafka上面这个 PDB 表示任何情况下至少保留 2 个 Kafka 实例可用。3.3 持久化与性能存储、JVM 和网络参数Kafka 对磁盘性能非常敏感顺序写和面对消费读写都要大量使用页缓存。不推荐用普通网络存储跑关键业务延迟和吞吐都容易出问题。我建议本地 SSD 卷配合 K8s 的本地 PV或者使用 NVMe 云盘并设置足够的 IOPS。存储卷大小要提前规划Kafka 不会自动把数据均匀分配到不同 broker 之外的地方Topic 分区增加不会自动均衡历史数据所以扩磁盘比扩实例更常用。JVM 参数要根据实际堆大小来调。Kafka 的堆不需要给很大关键的内存分配在于页缓存所以我倾向于把堆限制在 4GB 左右预留更多内存给操作系统缓存。容器内存 limits 要留出系统缓存和堆外内存的空间否则容易触发 OOM。我通常把limits.memory设置为 JVM 堆的 1.5 倍左右比如堆 4Glimits 给 6G同时在 Kafka 启动脚本里设置KAFKA_HEAP_OPTS-Xmx4g -Xms4g。网络方面broker 的advertised.listeners必须配置成客户端能访问到的地址。如果用 ClusterIP客户端在集群内没问题但如果在集群外访问就要用 NodePort 或 Ingress同时修改 advertised 地址否则会看到一连串的connection refused。3.4 扩缩容与节点替换时的踩坑记录Kafka 扩缩容不像无状态服务那样加副本就完事。新增 broker 后不会自动搬移分区要手动执行分区重新分配工具。如果直接删掉一个 broker所有 leader 副本会切换到其他节点但旧节点上的数据副本被标记为停用需要后续重新加载。我实际遇到过一个很尴尬的场景某个 broker 的本地磁盘快满了我临时增加副本数量来分摊数据结果分区均衡过程非常慢最后不得不停机扩容磁盘。后来我总结的经验是不要让 Kafka 的存储利用率超过 60%留够应对故障转移和再平衡的空间。如果必须替换某个 broker流程应该是先把该 broker 的副本迁移到其他节点确认分区都处于正常状态再删除对应的 StatefulSet 副本。直接删 PVC 再重建是我见过最危险的操作数据没做迁移就全部丢失救都救不回来。4. Redis 在 K8s 上的高可用部署4.1 高可用模式选择Sentinel 还是 ClusterRedis 在 K8s 上最容易做但也最容易做错。容易做是因为单节点 Redis 的开箱体验很好做错是因为很多人直接部署一个 ClusterIP 服务然后寄希望于 K8s 自动切换后端 Pod但 Redis 主从切换和客户端的连接重定向并不是 K8s 能自动解决的。哨兵模式和集群模式是两条主流路线特性Sentinel 模式Redis Cluster 模式数据分片不支持所有节点存全量数据支持数据自动分片到 16384 个 slot故障转移Sentinel 自动提升从节点为主节点集群自动选举迁移槽位客户端要求需要感知主从切换支持哨兵请求需要支持 Cluster 重定向容量扩展只能增加副本或升级单机规格可横向增加分片对大多数中小规模业务我推荐 Redis Sentinel 模式。容量不够可以通过规范架构或增加本地缓存来缓解运维相对简单。如果数据量巨大且要求自动分片再考虑集群模式但集群模式下 key 分布、批量操作、事务都有不少限制。4.2 哨兵模式在 K8s 上的具体配置与注意点哨兵模式最少需要 3 个 Sentinel 实例它们之间通过 Pub/Sub 发现彼此并协商主节点状态。Redis 客户端要先和 Sentinel 通信获取当前主库地址再建立连接。在 K8s 里我会部署三个哨兵实例分别用appredis-sentinel的标签管理并且妥善设置sentinel monitor的配置。有一个很关键的参数是sentinel down-after-milliseconds它控制 Sentinel 判定主节点不健康的时间阈值。这个参数设置得太大故障转移会很慢设置得太小网络瞬时抖动就会导致误判和脑裂。经验上建议设置在 3000-5000ms 左右并且配合quorum2代表至少需要两个 Sentinel 同意才能触发故障转移。哨兵还依赖服务发现。如果某个 Pod 重建了Redis 实例 IP 变了Sentinel 需要通过 K8s Service 或 DNS 找到新地址。这里如果直接使用 Headless ServiceSentinel 可以按 Pod 名访问IP 变化不影响连接。如果使用普通 Service多个 Pod 共享一个 VIP但可能把流量负载均衡到当前主节点如果主节点变了还好如果均衡到了从节点写请求就失败了。因此我坚持在 Redis 和 Sentinel 之间也使用 Headless Service。4.3 Redis 持久化与数据安全AOF、RDB 和 PVCRedis 默认开启 RDB 快照但 RDB 快照间隔较长高并发下丢失数据范围比较大。为了减少数据丢失我建议开启 AOF 持久化并设置appendfsync everysec。这个设置可以保证每秒最多丢 1 秒的数据同时性能影响可控。如果你无论如何都不能接受数据丢失可以设置appendfsync always但每次写命令都要刷盘性能下降非常明显不适合高 QPS 场景。AOF 文件会不断增长所以必须开启重写机制比如auto-aof-rewrite-percentage 100、auto-aof-rewrite-min-size 64mb。重写耗时较长尤其是在数据量大时要和故障转移、备份窗口错开否则可能阻塞主线程。PVC 配置建议为每个 Redis 实例单独分配持久化卷。Redis Sentinel 模式里所有节点存储相同数据PVC 大小按全量数据估算并预留扩容空间。我在生产环境遇到过一次 Redis 内存飙高导致 AOF 重写超时后来把maxmemory设置为容器内存 limits 的 80%并启用了maxmemory-policy allkeys-lru避免内存打满。4.4 频繁出现的客户端重定向问题Redis Cluster 模式下客户端如果连接了错误的节点会收到MOVED重定向。K8s 里如果用 ClusterIP Service客户端第一次连接到任意 Pod当 key 对应的 slot 不在该节点时会收到 MOVED 并重新建立连接到正确节点。这个过程本身没问题但如果客户端库不支持自动重定向应用就会报错。即使是 Sentinel 模式在故障转移后应用使用的旧连接也会失效客户端库应该能自动重新从 Sentinel 获取新的主节点地址。所以我强烈建议使用有重连和重试机制的客户端库比如较新版本的官方客户端并测试故障切换时的应用日志确认没有大面积报错。5. 常见问题与排查技巧实录5.1 故障速查表现象可能原因排查与解决MySQL 主从不同步Seconds_Behind_Master持续增长从库磁盘慢、半同步复制网络延迟高、binlog 量过大检查从库 IO 和磁盘压力增加并行复制线程减少单事务过大Kafka 分区 leader 频繁切换某 broker 网络抖动或磁盘繁忙导致 follower 心跳超时查看 broker 日志和 GC 停顿检查节点负载和磁盘 IO调整session.timeout.msRedis 哨兵误切换主节点频繁变化哨兵网络分区down-after设置过短或 quorum 配置不当增加监控超时调大 quorum确保哨兵节点在不同可用区分布Redis 内存高但数据没增长AOF 文件膨胀未开启 AOF 重写或重写触发阈值过高调整auto-aof-rewrite-percentage手动执行BGREWRITEAOFK8s 驱逐后中间件 Pod 长时间无法调度节点资源不足或 PVC 限制在旧节点的可用区检查events增加反亲和约束确保 PVC 支持跨节点迁移5.2 探针误杀与故障恢复的实战心得探针是把双刃剑。livenessProbe配置过于激进在瞬时负载升高或 GC 停顿期间可能把正在正常工作的中间件杀掉重启。我见过一次 MySQL 节点在高并发写入时 CPU 打满liveness 探针超时被连续杀掉 3 次最终触发数据恢复流程造成不必要的抖动。正确做法是把 liveness 探针配置得宽松一些重点依赖 readiness 探针来控制流量。liveness 只需要保证进程真的卡死时能重启readiness 才是业务可用性的判官。我常用的配置是livenessProbe: exec: command: - sh - -c - mysqladmin ping -uroot -p$$MYSQL_ROOT_PASSWORD initialDelaySeconds: 30 periodSeconds: 20 timeoutSeconds: 10 readinessProbe: exec: command: - sh - -c - | mysql -uroot -p$$MYSQL_ROOT_PASSWORD -e SELECT 1 mysql -uroot -p$$MYSQL_ROOT_PASSWORD -e SHOW REPLICA STATUS\G | grep -q Slave_IO_Running: Yes initialDelaySeconds: 10 periodSeconds: 10readiness 探针必须能区分“实例存活”和“实例可用”。如果只是mysqladmin ping在从库断掉复制链路时依然返回 OK流量打到从库上虽然读没问题但如果是写请求就会直接失败。这需要让探针检查复制状态和只读设置确保从库在提升为主库之前不会接收写流量。5.3 存储与节点故障的容灾边界K8s 上的中间件高可用有一个很现实的边界如果整个可用区断电你能恢复吗如果三个 MySQL 副本都跑到同一个可用区可用区一挂全挂。所以跨可用区部署是必须考虑的事哪怕做不到跨地区至少也要跨可用区。但跨可用区部署要付出代价网络延迟升高数据一致性变慢存储卷类型可能变受限。合理的做法是设计两级故障转移策略同可用区内做主从快速切换跨可用区放最后一个副本做灾备。Kafka 的min.insync.replicas2配合跨可用区副本可以容忍单可用区故障但acksall的写入延迟会增加需要业务侧接受这种权衡。我提醒过很多同事不要把所有中间件都塞进同一个默认的 namespace 和节点池那样虽然后期管理方便但故障爆炸半径会非常大。5.4 还有哪些 K8s 原生机制是隐藏的救星除了上面聊的 StatefulSet、PVC 和 PDB还有几个 K8s 原生机制能帮你提升高可用水平Pod 拓扑分布约束topologySpreadConstraints控制 Pod 在节点和可用区中的分布均匀性比单纯的podAntiAffinity更灵活。Pod 优先级为关键中间件设置PriorityClass避免资源紧张时被先驱逐。花式调度通过nodeSelector、taint和tolerations把中间件调度到性能隔离的节点池减少计算争抢。资源配额避免某个团队把集群资源打满影响其他人的工作负载。Kubernetes Events排障时第一个看kubectl describe pod最后的 Events比翻日志快得多。这些机制单独看不惊艳但组合在一起能把你排障的时间从小时级缩短到分钟级。6. 一些想留给你的经验这套 MySQL、Kafka、Redis 高可用方案我前前后后调整过很多版。现在回头看最值钱的不是某个 YAML 片段而是从一次次故障里提炼出来的几个原则。先说我自己的经验不要在业务低谷期盲目验证高可用要专门挑流量高峰前做故障演练。有一次我们在半夜压测时手动杀了一个 Kafka broker结果因为客户端重试逻辑没调好全链路延迟飙升了十几倍。从此我要求每次演练前先检查客户端参数再看向导方案最后才动手。第二个经验是高可用方案要尽量简化依赖链条。如果你把 MySQL、Redis、Kafka 都依赖同一个底层存储池那这个存储池一旦抖动三套中间件同时出问题。我的习惯是把不同中间件放在不同的 StorageClass 或节点池上让故障面隔离。第三个经验是监控和告警必须闭环。部署完高可用不等于故障会自动通知你要监控主从延迟、ISR 集合、分片覆盖、Redis 内存碎片率等指标。告警规则不要太敏感否则天天误报到最后没人看消息。我一般会设置三个级别WARNING 只通知值班人CRITICAL 才会调起中断响应。最后分享一个小技巧每次修改中间件配置后不要直接对着生产环境改先在测试环境跑一遍“故障注入”。在 K8s 里模拟故障很简单用kubectl delete pod --grace-period0强制删除 Pod或者临时给节点打NotReady标签就能看出集群是否真的能自愈。这套演练做完你对高可用的信心会强很多。中间件容器化的道路上困难不少但只要把“数据不丢、服务不宕、恢复自动”这三件事做到位就真的可以放心让它跑在 K8s 上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑