1. 项目缘起与整体设计思路1.1 为什么要在K8s体系里跑Dubbo服务先说说这个项目的来龙去脉。我所在的团队维护着一套微服务集群早期用的是虚拟机加脚本部署的方式服务数量一多环境差异、依赖冲突、扩容慢这些问题就全冒出来了。后来决定整体迁移到K8s而Dubbo作为核心的RPC框架自然也要跟着上流水线。这个项目的目标很明确把Dubbo服务的构建、镜像打包、部署到K8s这一整套流程用流水线串起来做到提交代码就能自动交付。Dubbo服务跟普通的Web服务不太一样它同时承担了服务提供者和消费者的角色注册中心是它的命脉。在K8s里跑Dubbo最大的挑战不是把进程启动起来而是解决服务注册与发现的地址问题、健康检查的探针设计、以及滚动更新时的流量平滑。这些点如果没处理好服务是起来了但调用方拿到的是一堆不可用的地址等于白干。这套方案适合谁参考我认为是那些已经有Dubbo服务在跑、准备往K8s迁移的团队或者是从零搭建微服务体系、想一步到位用云原生方式落地的开发者。如果你对Docker和K8s有基本概念但对怎么把Dubbo塞进这套体系还没头绪那这篇内容应该能帮你少走不少弯路。1.2 整体流水线的架构选型流水线这块我选的是业界比较通用的方案代码仓库触发构建构建工具负责编译打包然后走镜像构建最后通过声明式配置部署到K8s集群。具体来说编译用Maven镜像构建用Dockerfile多阶段构建部署用K8s的Deployment加Service注册中心沿用原有的Zookeeper集群也可以换成Nacos后面会讲差异。为什么用多阶段构建因为Dubbo服务的依赖包体积不小如果直接把Maven构建环境和产物打在一起镜像动辄七八百兆拉取慢、占用存储。多阶段构建的思路是第一阶段用完整的Maven镜像编译出jar包第二阶段只把jar包和精简的JRE基础镜像放一起最终镜像能压到两百兆以内。这个优化在集群规模大的时候收益非常明显。部署方式上我没有用Helm而是直接用K8s的原生YAML。原因很简单团队规模不大服务数量在可控范围内Helm的模板抽象反而增加了维护成本。等以后服务数量上来了再考虑用Helm或者Kustomize做配置管理也不迟。这个取舍要根据团队实际情况来不要为了用而用。1.3 核心链路的拆解整条链路可以拆成五个环节代码提交触发、编译打包、镜像构建与推送、K8s资源更新、服务注册与流量接入。每个环节都有它的坑。代码提交触发这块我用的是分支策略加Webhook的方式。开发分支提交触发构建和测试主干分支合并触发正式环境的镜像构建和部署。这样能保证正式环境的镜像一定来自经过验证的代码。编译打包环节Dubbo服务的接口包和实现包通常是分开的接口包会被消费者依赖。所以构建的时候要注意版本管理接口包的版本变更要谨慎避免消费者拿到不兼容的接口。镜像构建与推送重点是镜像标签的管理。我用的标签规则是“分支名-提交短哈希-时间戳”这样每个镜像都能追溯到具体的代码版本回滚的时候不会抓瞎。K8s资源更新Dubbo服务需要暴露端口给消费者调用同时可能需要暴露管理端口做健康检查。这里Service的配置和探针的设计是关键。服务注册与流量接入这是Dubbo在K8s里最特殊的地方。Dubbo默认会把Pod的IP注册到Zookeeper消费者直接连Pod IP。但Pod IP在K8s里是会变的滚动更新时旧Pod销毁、新Pod创建如果注册信息更新不及时消费者就会调用到已经销毁的Pod。解决办法后面详细讲。2. 核心细节解析与实操要点2.1 Dubbo服务在K8s中的注册地址问题这是整个项目里最核心的技术点我把它放在最前面讲。Dubbo默认的注册行为是把服务提供者的IP和端口写到注册中心消费者从注册中心拿到地址列表后直连。在虚拟机时代IP是固定的这套机制没问题。但在K8s里Pod的IP是动态分配的每次重启或重新调度都会变。如果直接用Pod IP注册会面临两个问题。第一Pod销毁后注册信息可能还没摘除消费者拿到失效地址调用报错。第二Pod重建后IP变了但注册中心里的旧地址可能还在新地址还没注册上去中间有个空窗期。解决思路有几种。一种是让Dubbo注册Service的ClusterIP而不是Pod IP但ClusterIP是虚拟IPDubbo直连会有问题而且负载均衡就交给K8s的Service了Dubbo自身的负载均衡策略就失效了。另一种是用Headless Service让Dubbo注册Pod的域名而不是IP但域名解析也有缓存和更新延迟的问题。我最终采用的方案是Dubbo正常注册Pod IP但在K8s层面做好优雅下线。具体做法是给Pod配置preStop钩子在Pod销毁前先调用Dubbo的离线接口让服务提供者主动从注册中心摘除自己然后再等待一段时间让消费者刷新地址列表最后才真正销毁Pod。同时配合K8s的terminationGracePeriodSeconds参数给足优雅下线的时间。这个方案的关键参数是preStop的执行时间和terminationGracePeriodSeconds的配合。我的设置是preStop里sleep 30秒terminationGracePeriodSeconds设为60秒。这样Pod收到销毁信号后先执行preStop里的离线操作然后等30秒再给30秒的缓冲总共60秒内完成所有清理。实测下来消费者基本能在10秒内感知到地址变化30秒的等待足够覆盖绝大多数场景。2.2 健康检查探针的设计K8s的探针分三种livenessProbe、readinessProbe和startupProbe。对于Dubbo服务这三种探针的设计直接关系到服务能否正常接流量。livenessProbe用来判断容器是否存活如果失败K8s会重启容器。Dubbo服务启动后如果注册中心连不上进程可能还在跑但服务已经不可用了。所以livenessProbe不能只检查端口是否监听最好能检查Dubbo的服务状态。我的做法是暴露一个HTTP的管理端口通过一个简单的健康检查接口返回服务状态livenessProbe去探测这个接口。readinessProbe用来判断容器是否准备好接流量。这个探针在Dubbo场景下特别重要因为服务启动后需要时间连接注册中心、注册自己、订阅其他服务。如果readinessProbe过早通过流量进来了但服务还没准备好就会报错。我的设置是initialDelaySeconds给30秒periodSeconds 10秒failureThreshold 3次。这样服务有足够的时间完成初始化。startupProbe是K8s 1.16之后引入的用来处理启动慢的容器。Dubbo服务如果依赖较多启动时间可能超过livenessProbe的initialDelaySeconds导致容器被误杀。startupProbe可以设置一个较长的探测周期成功一次后就交给livenessProbe接管。我一般给startupProbe设置failureThreshold 30次periodSeconds 5秒也就是最多等150秒足够大多数Dubbo服务启动完成。注意探针的端口不要和Dubbo的服务端口混用。Dubbo的端口是给RPC调用用的探针最好走独立的HTTP管理端口避免探针请求干扰正常的RPC流量。2.3 镜像构建的优化细节前面提到用多阶段构建这里展开说下Dockerfile的写法。第一阶段用Maven镜像把源码复制进去执行编译打包。这里有个技巧先把pom.xml复制进去执行dependency:go-offline下载依赖然后再复制源码。这样只要pom.xml没变依赖层就能命中缓存构建速度大幅提升。第二阶段用精简的JRE基础镜像比如eclipse-temurin的jre-alpine版本。把第一阶段target目录下的jar包复制过来设置好启动命令。启动命令里要加上JVM参数比如堆内存设置、GC策略选择。Dubbo服务通常是长连接、高并发的场景我一般用G1 GC堆内存根据服务规模设置小服务2G起步大服务4G到8G。镜像标签的管理也有讲究。我用的是“服务名-分支名-提交短哈希-时间戳”的格式比如“user-service-main-a1b2c3d-20240101120000”。这样从镜像标签就能看出是哪个服务、哪个分支、哪次提交、什么时候构建的。回滚的时候直接指定旧标签就行非常清晰。还有一个细节是时区问题。基础镜像默认是UTC时间如果服务里有依赖本地时间的逻辑会出现时间差。我一般在Dockerfile里设置TZ环境变量或者把时区文件复制进去。这个坑不大但排查起来很烦提前处理好省事。2.4 配置管理的方式选择Dubbo服务的配置分几块注册中心地址、服务端口、JVM参数、业务配置。这些配置在不同环境开发、测试、生产是不一样的。我用的方式是配置中心加环境变量结合。注册中心地址、服务端口这类跟环境强相关的配置通过K8s的ConfigMap注入环境变量Dubbo启动时从环境变量读取。业务配置如果变动不频繁也放ConfigMap如果变动频繁或者需要动态刷新就放配置中心Dubbo支持从配置中心动态拉取。这里有个经验不要把配置硬编码在镜像里。我见过有团队把生产环境的注册中心地址写死在代码里结果镜像没法复用到其他环境每次部署都要重新构建效率极低。配置和镜像分离是基本原则镜像一旦构建好就应该能在任何环境运行只是注入的配置不同。ConfigMap的更新有个坑K8s的ConfigMap更新后已经挂载为环境变量的Pod不会自动感知变化需要重启Pod才能生效。如果希望配置热更新得用volume挂载的方式并且应用要支持监听文件变化。Dubbo的配置中心方案天然支持热更新这也是我推荐用配置中心的原因之一。3. 实操过程与核心环节实现3.1 流水线的完整配置流水线的配置我以声明式的方式管理核心是几个阶段checkout、build、package、docker-build、docker-push、deploy。每个阶段的脚本我都放在代码仓库里跟源码一起版本管理这样流水线的变更也有记录可查。checkout阶段没什么好说的拉取代码切换到对应的分支。build阶段执行Maven编译这里我会加上单元测试测试不通过直接中断流水线避免有问题的代码进入后续环节。package阶段把编译产物打成jar包同时生成构建信息文件记录版本号、构建时间、提交哈希。docker-build阶段用前面说的多阶段Dockerfile构建镜像。这里有个优化点如果镜像仓库支持可以先用docker pull拉取基础镜像的最新版本利用层缓存加速构建。docker-push阶段把镜像推送到镜像仓库推送完成后记录镜像标签供后续部署使用。deploy阶段更新K8s的Deployment。我用的是kubectl set image的方式更新镜像然后kubectl rollout status等待滚动更新完成。如果更新失败流水线会自动回滚到上一个版本。这个回滚机制很重要我遇到过几次新版本有隐藏bug导致服务不可用的情况自动回滚能在几分钟内恢复服务把影响降到最低。3.2 K8s资源清单的关键配置Deployment的配置里有几个参数需要特别注意。replicas设置副本数我一般至少设2个保证一个Pod挂掉时服务不中断。strategy用RollingUpdatemaxSurge设1maxUnavailable设0这样滚动更新时先创建新Pod等新Pod就绪后再销毁旧Pod全程保证有足够的可用副本。Pod的配置里resources的requests和limits要设置合理。requests影响调度limits影响运行。Dubbo服务是内存敏感型的requests可以设小一点保证能调度limits要设够避免OOM。我一般requests设limits的50%到70%给突发流量留点余量。env里注入ConfigMap的配置包括注册中心地址、服务端口、JVM参数等。volumeMounts挂载日志目录方便日志收集。lifecycle里配置preStop钩子执行优雅下线脚本。Service的配置Dubbo服务需要暴露两个端口RPC端口和管理端口。RPC端口用ClusterIP类型的Service消费者通过Service名访问。管理端口可以暴露为NodePort或者通过Ingress访问方便运维排查。这里要注意Service的selector要匹配Pod的labels否则流量进不来。3.3 优雅下线的脚本实现优雅下线的脚本是整个方案里最核心的部分我把它单独拿出来讲。脚本的逻辑是先调用Dubbo的离线接口让服务提供者从注册中心摘除然后等待一段时间让消费者刷新地址列表最后再退出。Dubbo的离线接口可以通过telnet或者HTTP的方式调用。我用的是HTTP方式在服务里暴露一个管理接口接收下线指令后执行离线操作。脚本里用curl调用这个接口然后sleep一段时间。等待时间设多久合适这取决于消费者的地址刷新周期。Dubbo默认的消费者地址刷新是定时拉取间隔可以配置。我一般把消费者的刷新间隔设为5秒然后脚本里sleep 30秒确保所有消费者都刷新到了最新的地址列表。这个时间可以根据实际集群规模调整集群越大刷新传播的时间越长。脚本执行完后Pod才会真正收到SIGTERM信号然后JVM开始关闭流程。JVM关闭时Dubbo会执行自己的销毁逻辑包括关闭网络连接、注销注册信息等。所以preStop的sleep时间要足够长让脚本执行完同时也要给JVM关闭留时间。我的经验是preStop里sleep 30秒terminationGracePeriodSeconds设60秒这样脚本执行加上JVM关闭总共60秒内完成。提示优雅下线的脚本要幂等重复执行不能出问题。因为preStop在某些情况下可能被多次触发脚本里要做好状态判断已经离线过的服务再次调用离线接口应该直接返回成功。3.4 滚动更新的流量平滑滚动更新是K8s的默认更新策略但Dubbo服务的滚动更新需要额外注意。默认情况下K8s会先创建新Pod等新Pod就绪后销毁旧Pod。但新Pod就绪的标准是readinessProbe通过而readinessProbe通过不代表Dubbo服务已经注册到注册中心并且消费者已经感知到。所以我在readinessProbe里加了一个检查不仅要检查服务端口是否监听还要检查Dubbo是否已经成功注册到注册中心。这个检查可以通过查询注册中心的接口实现或者服务内部维护一个注册状态标志readinessProbe去读这个标志。只有注册成功了readinessProbe才返回成功K8s才会把新Pod加入Service的Endpoints。旧Pod的销毁也要配合优雅下线。K8s在销毁Pod时会先从Service的Endpoints里摘除然后发送SIGTERM信号。但Endpoints的摘除和Dubbo注册中心的摘除是两回事消费者可能还在从注册中心拿旧地址。所以preStop里的离线操作是必须的它保证了Dubbo层面的地址摘除。整个滚动更新的过程我实测下来从开始更新到全部完成2个副本的服务大概需要2到3分钟。这个时间包括新Pod启动、注册、就绪检查以及旧Pod的优雅下线。如果服务启动慢时间会更长。所以滚动更新的超时时间要设置合理避免更新被中断。4. 常见问题与排查技巧实录4.1 服务注册不上注册中心这是最常见的问题表现是服务启动后消费者调用报“No provider available”。排查思路分几步。先看Pod的日志确认Dubbo有没有尝试连接注册中心连接是否成功。如果连接失败检查注册中心地址配置是否正确网络是否通。K8s里的Pod访问外部注册中心需要确保网络策略允许DNS解析正常。如果连接成功但注册失败看注册中心的日志确认有没有收到注册请求。有时候是注册中心的权限问题或者注册的路径被占用。还有一种情况是Dubbo的版本和注册中心的版本不兼容比如Dubbo 2.7和Zookeeper 3.4的某些组合会有问题升级到兼容版本就好。如果注册成功但消费者拿不到检查消费者的订阅配置。Dubbo的消费者需要订阅对应的服务接口如果接口版本、分组不匹配订阅不到。这个在接口包升级时容易出问题接口版本变了但消费者没跟着升级就会订阅不到。4.2 Pod频繁重启Pod频繁重启通常是livenessProbe失败导致的。先看Pod的describe信息确认重启原因。如果是livenessProbe失败检查探针的配置是否合理。常见的问题是initialDelaySeconds设得太短服务还没启动完探针就开始探测连续失败几次后容器被重启。另一个原因是资源限制太紧Pod的CPU或内存达到limits被限制或OOMKilled。看Pod的监控指标确认资源使用情况。如果是内存问题调大limits或者优化JVM参数。如果是CPU问题调大limits或者优化代码。还有一种情况是节点资源不足Pod被驱逐。看节点的资源使用情况如果节点压力大K8s会驱逐一些Pod。解决办法是扩容节点或者调整Pod的资源requests让调度器把Pod调度到资源充足的节点。4.3 调用超时和连接拒绝调用超时和连接拒绝通常跟网络有关。先确认消费者和提供者是否在同一个K8s集群跨集群调用需要额外的网络配置。如果同集群检查Service的配置确认Endpoints里有正确的Pod IP和端口。Dubbo的连接是长连接如果连接建立后提供者Pod销毁连接会断开。消费者需要重连到新的提供者。如果重连不及时就会出现调用失败。这个跟优雅下线有关如果下线时没有正确摘除地址消费者会一直尝试连接已销毁的Pod。还有一种情况是网络策略限制了流量。K8s的NetworkPolicy可以控制Pod之间的流量如果配置了限制策略消费者可能访问不到提供者。检查NetworkPolicy的配置确认允许了Dubbo端口的流量。4.4 常见问题速查表问题现象可能原因排查方法解决方案服务注册不上注册中心地址错误检查ConfigMap配置修正注册中心地址服务注册不上网络不通在Pod内curl注册中心调整网络策略或DNS消费者拿不到地址接口版本不匹配对比接口包版本统一接口包版本Pod频繁重启探针配置不当查看Pod describe调整探针参数Pod频繁重启资源不足查看监控指标调大资源limits调用超时优雅下线未生效检查preStop脚本修正下线脚本调用超时网络策略限制检查NetworkPolicy放行Dubbo端口滚动更新卡住新Pod未就绪查看readinessProbe调整就绪检查逻辑4.5 几个踩过的坑和实操心得第一个坑是时区问题。有次服务里的定时任务执行时间不对排查了半天才发现是容器时区是UTC而业务逻辑依赖本地时间。后来在Dockerfile里统一设置了时区这个问题再没出现过。第二个坑是JVM参数。默认的JVM堆内存可能不适合容器环境容器里看到的CPU和内存跟宿主机不一样JVM如果按宿主机的资源来设置堆大小可能会超出容器的limits导致OOM。解决办法是设置JVM的UseContainerSupport参数让JVM感知容器的资源限制或者手动指定堆大小。第三个坑是日志收集。Dubbo服务的日志量不小如果直接输出到控制台K8s的日志收集会压力很大。我的做法是日志输出到文件然后通过sidecar容器或者DaemonSet收集。日志文件要配置轮转避免占满磁盘。第四个坑是镜像拉取策略。默认的IfNotPresent策略在镜像标签不变的情况下不会重新拉取如果镜像仓库里的镜像被覆盖了节点上还是旧镜像。我一般用Always策略确保每次部署都拉取最新镜像。但Always策略会增加拉取时间如果镜像没变就是浪费。折中方案是用唯一的镜像标签配合IfNotPresent策略这样标签变了就会拉取标签没变就用缓存。第五个坑是注册中心的会话超时。Dubbo跟注册中心之间有心跳如果心跳超时注册中心会认为提供者下线摘除地址。在K8s里如果Pod的网络有抖动心跳可能丢失导致误摘除。解决办法是调大心跳超时时间或者增加重试次数。这个参数在Dubbo的配置里可以调整具体值根据网络质量来定。5. 方案扩展与后续优化方向5.1 从Zookeeper迁移到NacosZookeeper作为注册中心在Dubbo体系里用了很多年稳定可靠。但它有个问题Zookeeper是CP系统强调一致性在网络分区时可能不可用。Nacos是AP系统强调可用性更适合云原生环境。而且Nacos自带配置中心功能可以替代Zookeeper加配置中心的组合。迁移到Nacos的改动不大主要是改注册中心地址和引入Nacos的依赖。Dubbo 2.7之后对Nacos的支持很完善配置方式跟Zookeeper类似。迁移过程中要注意的是Nacos的命名空间和分组概念跟Zookeeper的路径不一样需要重新规划服务的注册路径。5.2 接入服务网格服务网格是云原生微服务的另一个方向。Istio这类服务网格可以接管服务间的流量提供熔断、限流、链路追踪等能力。Dubbo服务接入服务网格后服务发现可以交给网格处理Dubbo自身的注册中心可以弱化甚至去掉。但服务网格也有代价Sidecar代理会增加网络延迟运维复杂度也更高。我的建议是如果团队规模不大服务数量不多先用Dubbo自身的服务治理能力就够了。等服务数量上来了治理需求复杂了再考虑引入服务网格。5.3 流水线的进一步优化现在的流水线是串行的从构建到部署一条龙。后续可以优化成并行加条件触发。比如单元测试和代码扫描可以并行执行镜像构建和部署可以分开部署环节加上人工审批重要环境需要审批后才能部署。还可以加入自动化测试环节部署到测试环境后自动跑集成测试测试通过才允许部署到生产环境。这样能进一步降低人为失误的风险。流水线的优化是个持续的过程根据团队的实际需求和痛点来调整不要一开始就追求大而全。5.4 监控和告警的完善Dubbo服务上K8s后监控体系也要跟上。基础的监控包括Pod的CPU、内存、网络指标这些K8s自带的监控就能覆盖。应用层的监控需要接入APM工具监控Dubbo的调用量、成功率、响应时间等指标。告警规则要根据业务特点来定。比如调用成功率低于99%告警响应时间超过阈值告警Pod重启次数超过阈值告警。告警要分级不同级别走不同的通知渠道避免告警风暴。我一般把告警分成P0到P3四级P0是服务不可用立即电话通知P1是服务降级短信通知P2是性能下降邮件通知P3是异常但影响不大记录到工单系统。监控和告警是保障服务稳定运行的基础上线前就要规划好不要等出了问题才补。我见过太多团队服务上线了但监控没跟上出了问题只能靠用户反馈排查起来非常被动。这套方案从设计到落地前后花了大概两个月时间中间踩了不少坑也积累了一些经验。K8s和Dubbo都是成熟的技术但组合在一起用还是有很多细节需要打磨。希望这些内容能给正在做类似事情的同行一些参考少走点弯路。
阅读完成 · 觉得有帮助?