ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

K8S实战:从Zookeeper、MySQL到Dubbo微服务的容器化部署全解析

K8S实战:从Zookeeper、MySQL到Dubbo微服务的容器化部署全解析 前阵子帮某集团客户做容器化改造一个环境里同时上了 Zookeeper、Redis Cluster、MySQL 主从、Nginx 加 Tomcat 的 Java 应用、LNMP 一套、Dubbo 微服务还有 Ingress 统一网关整套搬进 Kubernetes 之后踩了不少坑也沉淀了不少经验。这套组合基本上覆盖了日常工作中绝大部分的 K8S 落地场景今天把整个实战过程梳理一遍从有状态应用部署到无状态应用编排再到流量入口管理一次讲透。1. 先把应用按有状态和无状态摊开部署顺序和策略的底层逻辑很多人拿到一堆应用就往 K8S 里塞结果发现 MySQL 数据丢了、Zookeeper 集群起不来、Dubbo 服务注册一堆乱码最后回头骂 K8S 不好用。其实问题不在于 K8S而在于没有先做应用分类。1.1 为什么这套方案里 Zookeeper 和 MySQL 必须单独对待无状态应用比如 Nginx、Tomcat、纯 Java 后端特点是随便杀、随便扩实例之间不需要互相认识数据不落在本地。这类应用用 Deployment 加 HPA 就能搞定。但有状态应用完全反过来——Zookeeper 需要维护节点之间的选举关系MySQL 需要保证数据不丢Redis Cluster 需要 Gossip 通信和槽位管理它们都要求稳定的身份和稳定的存储。所以我拿到这套需求后的第一件事就是把应用分成两个池子。第一个池子是无状态的Nginx、Tomcat、Java 应用、LNMP 里的 PHP 和 Nginx。第二个池子是有状态的Zookeeper、Redis、MySQL以及 Dubbo 微服务里的注册中心一般也挂在 Zookeeper 上。池子分清楚了后续的 StatefulSet、Headless Service、PVC 这些概念才派得上用场。1.2 实战里推荐的整体部署顺序先底座后业务再入口我的部署顺序是固定的这套路径跑通了无数次。先部署 Zookeeper 和 MySQL 这类底座型中间件因为它们会被上层应用依赖再部署 Redis 缓存层接着部署无状态的 Nginx 加 Tomcat 和 Java 业务应用然后处理 LNMP 组合和 Dubbo 微服务最后统一配置 Ingress 把流量收口。这个顺序不是随便定的。如果先部署应用后部署 Zookeeper应用启动时找不到注册中心Dubbo 接口暴露失败又要重启应用纯属浪费排障时间。K8S 虽然有 InitContainer 可以做依赖等待但能用正确的部署顺序解决的问题没必要引入额外的复杂度。2. Zookeeper 集群上 K8SHeadless Service 与选举机制的那些坑Zookeeper 是这套环境里第一个要落地的有状态应用。它要解决的核心问题是集群内每个节点必须知道彼此的存在并且节点的网络身份不能随着 Pod 重启而变化。2.1 为什么普通 Service 满足不了 Zookeeper 的集群发现先看一个反例。我见过有人用普通 Service 给 Zookeeper 做负载均衡结果集群永远选举不出 Leader。原因在于 Zookeeper 的节点之间需要直接通过 IP 或主机名通信而普通 Service 会把请求随机分发到任意一个后端 Pod 上客户端拿到的地址是虚拟的 ClusterIP节点之间无法建立真实的 TCP 连接选举直接失败。正确的做法是给 StatefulSet 配一个 Headless Service也就是 ClusterIP 为 None 的 Service。这种 Service 不做负载均衡DNS 解析直接返回所有 Pod 的真实 IP。配合 StatefulSet 的命名规则每个 Pod 会得到一个稳定的网络标识比如zk-0.zk-headless.namespace.svc.cluster.local。这样 Zookeeper 配置文件里的server.1zk-0.zk-headless...这类条目就能真正生效。2.2 配置 Zookeeper 集群时最容易漏掉的三个参数第一个是POD_IP环境变量。Zookeeper 的启动脚本需要知道自己当前 Pod 的 IP然后写入配置文件的quorumListenOnAllIPs。我用的是 downward API 把status.podIP注入到环境变量里启动时再通过脚本拼进配置文件。第二个是ZOO_MY_ID这个值直接对应 StatefulSet 的序号必须是稳定的不能随机生成。第三个很多人忽略——JVMFLAGS里的堆内存设置。Zookeeper 默认堆内存只有 256M在容器里跑线上集群明显不够。我一般设置为 1G 到 2G通过环境变量传进去。顺便提一嘴Zookeeper 是 CPU 密集型应用不建议给它设置 CPU Limit 限制得太死否则 GC 线程调度不过来选举超时的情况会频繁出现。2.3 实测中遇到的 Zookeeper 集群脑裂问题与解决思路我有一次在做滚动升级时把三个 Zookeeper 节点同时重启结果集群出现了短暂的脑裂现象。两个节点各自主导了一个伪集群客户端连接全部报错。排查下来发现是 StatefulSet 的podManagementPolicy默认值是OrderedReady理论上会逐个更新节点但我在配置清单里漏掉了这个字段。如果对可用性要求极高可以设置podManagementPolicy: Parallel配合 ReadinessProbe。但一般情况下建议保持默认的 OrderedReady 策略让 K8S 按照节点顺序逐个更新避免所有节点同时处于不可用状态。另外务必给每个 Zookeeper Pod 配置 PreStop 钩子在 Pod 终止前主动执行zkServer.sh stop让节点优雅退出否则会出现集群重选举业务侧感知到抖动。3. MySQL 主从与 Redis Cluster持久化存储和故障恢复的实战要诀中间件里 MySQL 和 Redis 是最考验持久化设计的两兄弟。MySQL 不能丢数据Redis 要保证分片和副本的高可用。这两类应用在 K8S 里的部署方式和踩坑路径完全不同。3.1 MySQL 主从复制在 K8S 里的存储方案选型先说存储。MySQL 的数据文件必须存放在 PV 里我这次用的是 NFS 动态供给配合 StorageClass 实现 PVC 自动创建。如果条件允许生产环境更推荐用云厂商的块存储性能比 NFS 好很多。但 NFS 有个致命问题需要提前规避——它不支持文件锁如果 MySQL 配置了skip-name-resolve之外还用了某些依赖锁的特性可能存在隐患。稳妥的做法是把 MySQL 数据目录挂载到 PVbinlog 也放在同一份存储里。StorageClass 的配置清单我会限制reclaimPolicy: Retain防止 PVC 删除时 PV 连带销毁。这个细节非常关键——一旦设置成 Delete节点宕机后重建 Pod 时旧数据可能直接被回收我见过有人因此在测试环境里把数据库搞丢过。3.2 主从复制初始化的一次完整排障链路用 Helm 或手工编排 MySQL 主从时最常见的问题是从库一直停在Waiting for master to send event状态。我当时部署完成后发现从库的Seconds_Behind_Master一直是 NULL主库状态正常但 binlog 没有传过来。排查链路是这样的先看从库的SHOW SLAVE STATUS发现Last_IO_Errno是 1236说明从库请求的 binlog 位置在主库已经被清理掉了。原因是主库的expire_logs_days设置太短而从库因为网络延迟或初始化时间过长追不上主库的 binlog 清理进度。解决办法是先停止从库的复制线程在主库上用mysqldump重新导一份全量备份记录好MASTER_LOG_FILE和MASTER_LOG_POS再从那个位置继续复制。整套操作下来从库才真正追平。这次经验告诉我MySQL 主从初始化不能无脑相信CHANGE MASTER TO配一次就行必须确认全量备份和 binlog 点位是对齐的。如果依赖的是mysqldump --master-data2需要严查导出的二进制日志位置坐标。3.3 Redis Cluster 的 Gossip 通信与 Kubernetes 网络模型适配Redis Cluster 在 K8S 里部署比单机 Redis 复杂得多。每个 Redis 节点都要监听两个端口一个是客户端通信端口 6379另一个是集群总线端口 16379用于节点间 Gossip 通信。K8S 网络模型默认情况下 Pod 之间的通信是通的但要小心 NetworkPolicy 把 16379 端口挡住否则集群节点之间无法交换心跳槽位迁移和故障转移全部失效。我这次的配置是给 Redis Cluster 单独建了一个命名空间然后放行该命名空间内所有 Pod 之间的 6379 和 16379 端口。不要在默认命名空间里和业务应用混部否则网络策略写起来又长又容易出错。存储方面用的是 local 卷或者云盘保证每个节点都有独立的持久化路径。3.4 Redis 故障转移时的一个高频坑DNS 缓存导致的路由错乱Redis Cluster 客户端在连接节点时会通过 Cluster 节点返回的 MOVED 重定向指令找到正确的分片。但在 K8S 环境里如果客户端使用了 Pod 的 IP 直连那么节点重启或者扩缩容后客户端缓存里的节点信息就是过期的导致大量CLUSTERDOWN报错。更稳的方案是给 Redis Cluster 的每个节点用 StatefulSet 加 Headless Service客户端通过 DNS 域名来连接。这样节点重启后 IP 变了域名不变客户端可以正常重连。我在实际项目中就是让 Java 应用配置的 Redis 地址从 IP 改成域名然后用 Redisson 或 Lettuce 的集群模式问题一下子少了很多。4. Nginx 负载 Tomcat 集群与 Java 应用部署镜像分层、探针与优雅上下线接下来是纯无状态应用的部署。Java 应用加 Nginx 入口是这套方案里最常见的组合。Nginx 负责流量分发和静态资源处理Tomcat 跑 Java Web 服务。4.1 Nginx 镜像里的 Tomcat 后端地址如何动态注入Nginx 的 upstream 配置在容器化环境里不能写死 IP。我采用的是环境变量加模板渲染的方式。镜像里的nginx.conf是模板文件启动脚本用envsubst把${TOMCAT_UPSTREAM}替换为真实的 Service 地址。比如 Tomcat 的 Service 叫tomcat-app命名空间是prod那 upstream 就是tomcat-app.prod.svc.cluster.local:8080。这种方式既避免了配置的硬编码也方便后续通过修改环境变量来切换上下游。需要特别留意的是envsubst默认会替换所有${...}格式的内容如果 nginx.conf 里有正则表达式包含$字符会被这工具误伤。我遇到过$host被替换成空字符串的问题最后把模板文件里的非法变量统一转义或者改用 Python 脚本做更精确的替换。4.2 Java 应用容器化时的堆内存配置和分层镜像优化Java 应用在容器里的第一道坎是 JVM 参数。老版本的 JVM 无法自动识别容器 CPU 和内存限制会把堆内存设置成宿主机内存大小导致 Pod 直接被 OOM Killer 杀掉。我这次用的 JDK 版本是 11 以上默认开启了容器感知。如果还在用 JDK 8务必手工加上-XX:MaxRAMPercentage75.0和-XX:InitialRAMPercentage50.0让 JVM 根据容器内存限制自动分配。镜像大小方面我的做法是采用多阶段构建。基础镜像用精简版 JDK然后在构建阶段编译打包运行阶段只拷贝最终的 JAR 文件和一个最小的基础镜像。实测下来镜像体积从 800 多 MB 缩小到 250 MB 左右推送到私有仓库的速度快了不少拉镜像占用的节点磁盘也少了。4.3 就绪探针和优雅停机配置的完整参数Java 应用启动比较慢如果 ReadinessProbe 的initialDelaySeconds设置得太短Pod 还没起来就被 K8S 判定为不健康然后被重启形成死循环。我一般设置为 20 秒配合periodSeconds: 5给应用留足启动时间。LivenessProbe 的判定阈值要放宽一点因为 JVM 在 GC 停顿或频繁 Full GC 时可能几秒钟没有响应容易误杀。优雅停机这里我用的参数是terminationGracePeriodSeconds: 60并在 Pod 里配置了 PreStop 钩子执行sleep 5配合应用本身的优雅停机机制。这样在发布滚动更新时旧 Pod 先停止接收新流量再等待已经进来的请求处理完毕最后才真正退出容器。实测下来线上请求的失败率从 5% 降到了 0。5. LNMP 组合上 K8S 的拆分思路与 Dubbo 微服务的注册发现LNMP 和微服务这两块是这套环境里架构最复杂的部分。LNMP 涉及三个组件的协作Dubbo 则是典型的注册中心加多服务调用链两者在 K8S 上的处理方式差异很大。5.1 LNMP 组合里 Nginx、PHP-FPM、MySQL 的容器协作方式LNMP 在传统服务器上是一个单体环境但容器化之后必须拆开。我的拆分方式是Nginx 容器负责静态资源和反向代理PHP-FPM 容器负责执行 PHP 代码MySQL 独立部署。Nginx 和 PHP-FPM 之间通过 FastCGI 协议通信Nginx 的fastcgi_pass配置指向 PHP-FPM 的 Service 地址。这中间有一个关键细节PHP 代码文件必须同时挂载到 Nginx 容器和 PHP-FPM 容器里。Nginx 需要读取静态文件和判断文件是否存在PHP-FPM 需要找到对应的.php文件执行。我用的是共享 PVC把代码目录挂载到两个 Pod 上保持路径一致。不然会出现 Nginx 能找到 PHP 文件但 PHP-FPM 报File not found的情况。5.2 PHP-FPM 容器遇到的一个经典权限问题PHP-FPM 容器里的执行用户默认是www-dataUID 是 33。如果代码目录挂载的是 NFS 存储文件属主和容器用户不一致PHP-FPM 就没法读取文件页面直接白屏报 403 或 500。我排查过一次最后发现是 PV 目录的属主是 1000而容器里跑的用户是 33。解决方式有两个一是在 PV 目录上直接用chown -R 33:33但这在共享存储上未必每次都生效二是在 PHP-FPM 镜像里修改用户 ID或者用initContainers在启动前把代码目录的属主改掉。我最后选了 initContainer 方案用一条chown -R 33:33 /var/www/html的命令把代码目录权限校正稳定且不需要手工介入。5.3 Dubbo 微服务迁移 K8S 的注册中心适配Dubbo 框架在 K8S 上部署最核心的变化是服务注册与发现。原来 Dubbo 服务通过 ZooKeeper 注册提供方启动后把 IP 和端口写入 ZK消费方监听 ZK 获取列表。在 K8S 里如果 Dubbo 注册的是 Pod IP那么 Pod 重建后 IP 变了消费方会拿到失效地址调用直接失败。解决方案是让 Dubbo 注册 K8S 的 Service 名称。具体做法是给每个 Dubbo 服务单独建一个无头 Service然后在启动参数里配置DUBBO_IP_TO_REGISTRY指向 Service 的 DNS 域名。这样消费方拿到的是稳定域名而不是易变的 Pod IP。我在实际操作中是把环境变量注入 Deployment通过dubbo.registry.address指向 ZK 的同时设置dubbo.protocol.host为 Service 域名。5.4 微服务滚动发布时的优雅上下线流程Dubbo 滚动发布时最怕的是服务下线瞬间消费方还在调用已经被删除的 Provider导致报错No provider available。K8S 默认的滚动更新策略是逐个重建 Pod但 Dubbo 的注册中心更新有延迟。我的经验是把滚动更新前的 PreStop 钩子用来做 Dubbo 主动下线调dubbo的qos端口发送offline指令然后等待几秒再退出容器。这个等待时间非常关键太短了下线指令没送达太长了拖慢发布节奏。我一般设置为 10 秒实测下来发布过程不会出现服务不可用的情况。消费方侧如果依赖了本地缓存的服务列表也要定期刷新避免长期持有失效地址。6. Ingress 统一入口多服务路由规则配置与流量治理的细节最后一块是入口流量管理。整套环境里有 Java 应用、LNMP、Dubbo 的网关层等多个 HTTP 或 gRPC 服务不可能每个服务都暴露一个 NodePortIngress 就是用来统一收口流量的。6.1 Ingress 路由规则的编写思路按域名、按路径分层我的配置策略是一个域名对应多个服务按路径进行区分。比如api.example.com域名下/java路径转发到 Java 应用的 Service/php路径转发到 LNMP 的 Nginx Service/dubbo路径转发到 Dubbo 网关 Service。这样只用一个域名就能覆盖所有业务入口证书管理也方便。路由规则里我用的是前缀匹配K8S 的 Ingress 规范从 1.22 版本开始推荐pathType: Prefix。同时要注意路径的斜杠问题——如果用/java作为路径且后端服务本身有 contextPath需要确认 Ingress 的 rewrite-target 注解行为避免路径被改写后后端找不到资源。6.2 Ingress Controller 选型和配置参数Ingress 只是一个资源对象真正干活的是 Ingress Controller。我用的方案是社区最主流的 ingress-nginx部署方式是用 Helm。核心配置参数我会打开这几个proxy-body-size调到 100m因为 LNMP 那边会上传文件keep-alive设置为 300 秒降低后端连接的频繁重建enable-real-ip打开让后端能拿到真实的客户端 IP而不是 Nginx 转发过来的内部地址。Helm 安装时建议把hostNetwork设为 true让 Ingress Controller 直接占用宿主机端口 80 和 443。这样比 NodePort 多一跳的转发更高效也方便直接在负载均衡器上把流量转到几个节点的对应端口。6.3 TLS 证书配置与路由兼容性的一次实测踩坑有一次我给域名配置 HTTPS 证书时发现客户端访问https://api.example.com/java报证书无效。排查了半天最后定位到问题不在证书本身而在 Ingress 规则里的 host 和后端 Service 的响应。Nginx Ingress Controller 默认会透传 SNI如果后端 Tomcat 启动了 HTTPS 连接器的证书校验Ingress 和后端之间的协商就对不上。解决方案是后端应用全部走 HTTP由 Ingress 统一终结 TLS不要在后端重复配置 HTTPS。Ingress 规则里用tls段绑定证书和 host然后后端 Service 只暴露 80 端口。这套架构清晰也避免了证书链嵌套导致的各类兼容性问题。6.4 Ingress 的灰度发布和流量分流能力扩展Ingress 天然支持按 header 或权重做流量分发我用它做了灰度发布。比如新版 Java 应用先部署一套新的后端 Service然后通过 Ingress 注解nginx.ingress.kubernetes.io/canary-weight: 10把 10% 的流量切到新版本。验证通过后把权重调到 100再清理旧副本。这套方案比直接改 Service 指向更平滑而且不需要停机。实际配置时要注意灰度规则只在同一 host 下生效且需要确保新旧两个 Service 都是可用的否则会把错误流量引入灰度版本影响线上用户体验。7. 从部署到稳定运行的运维视角这套架构在真实环境里还要盯什么前面把每一类应用的部署和踩坑都讲了一遍最后补充一些从整体架构视角出发的运维细节。很多项目部署完没问题但跑一段时间后就频繁故障问题基本都出在这些容易被忽略的环节。7.1 资源规划给每类应用预留多少资源才够用我把这套环境的资源分配做个参考Zookeeper 每个节点 1C2G三个节点MySQL 主节点 4C8G 加 500G 存储从节点 2C4G 加 200GRedis 每个分片 2C4G六个分片Java 应用平均 2C4G按 HPA 在 3 到 10 个副本之间弹性伸缩Nginx 入口 2C4G两个副本。资源不是越高越好但要保证核心中间件有足够的余量。MySQL 和 Kafka 这类 IO 密集应用建议把 CPU Request 和 Limit 设得接近一些避免 CPU 被抢占导致 IO 等待时间拉长。Java 应用则要关注堆内存和容器内存的比例避免容器设置了 4G Limit 但 JVM 堆只分配到 1G 的浪费。7.2 日志和监控的统一收集方案K8S 环境下日志收集不能再像传统服务器一样tail -f去翻文件。我为每个节点部署了 Filebeat以 DaemonSet 方式运行采集/var/log/containers/*.log下的容器日志输出到 Elasticsearch。这套方案的优点是无需改造应用镜像直接用标准输出日志即可。监控方面用的是 Prometheus 加 Grafana。Node Exporter 采集节点指标cAdvisor 采集容器指标MySQL Exporter 和 Redis Exporter 分别采集中间件指标。告警规则我会重点盯这几个Pod 频繁重启、PVC 使用率超过 85%、Redis 内存超过 80%、MySQL 主从延迟超过 30 秒。这些指标直接对应线上故障命中率高误报少。7.3 发布回滚预案如何快速恢复受损应用有状态应用的发布回滚比无状态应用风险高得多。MySQL 升级版本时万一失败不要直接重启因为自动恢复可能会把数据文件搞坏。我的习惯是升级前先全量备份备份文件放到集群外存储然后手动修改 StatefulSet 开启滚动更新。如果启动验证失败立即把镜像 tag 改回原版本执行回滚同时确认所有 PVC 没有被残留的定时任务清理掉。无状态应用的回滚相对简单Deployment 的rollout undo一条命令就能回到上一个版本。但需要注意如果同时改了 ConfigMap回滚不会自动恢复 ConfigMap 的版本需要额外处理配置文件的回退。我这次上线 Dubbo 版本时踩过这个坑最后总结的经验是凡是涉及配置变更的发布必须把 ConfigMap 版本和镜像版本一起放进发布单里管理。7.4 团队协作中的命名规范和环境隔离最后说一个团队协作层面的建议。K8S 的命名空间是最好的隔离工具我这次把开发、测试、生产三个环境用命名空间强制隔离资源配额也分别限制。命名空间内部再用标签区分应用和中间件比如appjava-app、appredis。这样无论是排查问题还是写网络策略目标都清晰。命名规范上Service 名、配置名、PVC 名统一加应用前缀比如mysql-prod-data、zk-headless、java-app-config。后期维护时一眼就能看出来对应的资源类型和用途。这套组合从部署到现在跑了两三个月整体稳定性是过关的。中间件和业务应用的日常操作没有出现明显故障弹性伸缩和灰度发布流程也已经沉淀成团队内部的标准化操作。希望这次完整的实战拆解能给正在做 K8S 落地的朋友一些参考少走一点我已经趟过的弯路。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表