很多人觉得Java后端集群就是把war包往几台机器上一丢,配个Nginx就完事了。真正跑起来才发现,Session丢了、缓存乱了、定时任务重复执行、日志排查问题像大海捞针。集群部署不是单机部署的简单叠加,它是一套完全不同的技术体系。下面从项目架构改造、中间件选型、构建发布、流量调度到运维监控,完整走一遍生产级Java后端集群的落地流程。

项目架构的集群适配改造

单机应用要上集群,第一件事不是买服务器,而是改代码。Session共享是最先撞上的墙。如果项目里用到了HttpSession存登录态,集群环境下用户请求被负载均衡到不同机器,Session读不到,用户就会反复掉线。解决方案是把Session从内存抽离到Redis集中存储。引入spring-session-data-redis依赖后,在配置里加一行spring.session.store-type=redis就完成了切换,代码零侵入。但要注意,存进Session的对象必须实现Serializable接口,否则序列化会直接抛异常。

缓存一致性是第二个坑。单机用本地缓存Caffeine或者Guava Cache,数据存在JVM堆内,更新时直接改本地就行。集群里每台机器各自维护一份本地缓存,一台更新了数据,其他机器拿到的还是旧值。解决办法是把本地缓存改成分布式缓存Redis,所有节点读写同一个Redis集群。但全量切Redis会带来网络开销,热点数据访问延迟明显增加。更合理的方案是两级缓存架构:一级用本地缓存做极速读取,设置极短的过期时间比如3秒;二级用Redis做集群共享缓存,过期时间设长一些。数据更新时通过Redis的发布订阅或者Canal监听binlog,广播缓存失效通知,各节点收到通知后清空本地缓存。这样既保证了最终一致性,又扛住了高并发读。

定时任务在集群里会变成灾难。单机跑得好好的@Scheduled,到了集群环境每个节点都会触发,导致任务重复执行。如果任务是发短信扣款这类操作,后果很严重。解决思路是让定时任务在集群里只跑一次。可以引入分布式锁,比如用Redis的SETNX抢锁,抢到锁的节点执行任务,执行完释放锁。也可以直接用成熟的分布式任务调度框架,比如XXL-JOB或者Elastic-Job。XXL-JOB部署一个调度中心,所有业务节点注册为执行器,调度中心按策略把任务分发给其中一个执行器,天然避免了重复执行,还带了一个可视化的管理界面,任务日志和执行状态一目了然。

文件存储也需要抽离。单机把用户上传的文件存在本地磁盘,集群里用户上传到节点A的文件,下次请求被分到节点B就找不到。必须把文件存储独立出来,对接对象存储服务比如阿里云OSS、MinIO,或者自建FastDFS。改造时抽象一个文件服务接口,本地存储实现和OSS实现各一套,通过配置切换。这样开发环境还能用本地存储,生产环境切到OSS,互不影响。

中间件集群的搭建与配置

应用层集群化之后,中间件也必须集群化,否则中间件就成了单点故障源。Redis用哨兵模式或者Cluster模式部署。哨兵模式是一主多从加哨兵节点,主节点挂了哨兵自动把从节点提升为主,适合读多写少的场景。Cluster模式是数据分片,多组主从分摊数据,适合数据量大、写入压力高的场景。生产环境建议至少三主三从,保证高可用。连接配置要用JedisCluster或者Lettuce的集群客户端,把所有节点地址都配上,客户端自己发现节点拓扑。

数据库层面,单库扛不住就要读写分离加分库分表。读写分离用主从复制,写走主库,读走从库,从库可以水平扩展。中间件用ShardingSphere-JDBC,在配置里定义主从数据源和分片规则,对业务代码基本透明。分库分表按业务量预估,比如订单表按用户ID哈希分16个库,每个库16张表,能扛住上亿数据。ShardingSphere的配置文件里指定分片键和分片算法,SQL里带上分片键就能精确路由,不带分片键会全库扫描,性能很差,所以设计分片键要和业务查询模式对齐。

消息队列也需要集群化。RocketMQ部署双主双从或者Dledger集群,Kafka部署多Broker集群,保证消息不丢、服务不中断。生产者发送消息要做重试和容错,消费者要做幂等处理,因为集群重平衡时可能重复消费。消息幂等可以用数据库唯一键或者Redis记录消费过的消息ID来实现。

构建与持续集成流水线

代码改造完了,中间件也搭好了,接下来要把代码变成可运行的产物。Java项目一般用Maven或Gradle构建,产出jar包或者war包。现在主流是Spring Boot打fat jar,内嵌Tomcat,直接java -jar启动,省去部署到外部容器的麻烦。Docker镜像化是更进一步的做法,把jar包打进镜像,环境一致性有保障,扩容缩容也快。

Dockerfile写起来不复杂,选一个openjdk的基础镜像,把jar包拷进去,指定启动命令。多阶段构建能把构建环境和运行环境分开,最终镜像只保留运行时需要的文件,体积小很多。第一阶段的Maven镜像里执行mvn package,第二阶段把打好的jar包复制到jre镜像里。这样最终镜像可能只有两百多兆,推送和拉取都快。

持续集成用Jenkins或者GitLab CI。代码推到Git仓库,触发流水线自动编译、跑单元测试、构建镜像、推送到镜像仓库。镜像仓库用Harbor搭建私有仓库,安全可控。流水线里加上代码质量检查SonarQube,单元测试覆盖率不达标就阻断构建,把质量问题拦截在发布之前。

容器编排与集群部署

镜像有了,接下来要把它部署到服务器集群上跑起来。手工一台台登录服务器docker run不现实,必须上容器编排平台。Kubernetes是事实标准,但中小团队觉得它重,可以用Docker Swarm或者直接用云厂商的容器服务。K8s的核心概念是Deployment管理Pod副本数,Service提供稳定的访问入口,Ingress对外暴露服务。

部署时先写Deployment的yaml,指定镜像地址、副本数量、资源限制、健康检查。replicas设为3,K8s会保证集群里始终有3个Pod在运行,挂了一个自动拉起新的。存活探针livenessProbe用HTTP GET请求/actuator/health,就绪探针readinessProbe同样请求健康检查接口,只有就绪的Pod才会被Service加入流量分发。资源限制requests和limits要设好,不设的话一个Pod内存泄漏可能拖垮整个节点。

滚动更新是K8s的核心能力。修改镜像版本后,K8s按maxSurge和maxUnavailable策略逐步替换Pod,新Pod就绪后才停掉旧Pod,整个过程用户无感知。如果新版本有问题,kubectl rollout undo一键回滚到上一个版本。配置管理用ConfigMap和Secret,把数据库连接串、Redis地址这些环境相关的配置抽出来,不同环境用不同的ConfigMap,镜像保持不变就能跨环境复用。

流量入口与负载均衡

Pod都跑起来了,外部流量怎么进来?K8s里用Ingress Controller,比如Nginx Ingress,配置域名规则把请求转发到对应的Service。但Ingress只能做七层HTTP转发,性能有瓶颈。高并发场景可以在K8s前面再挂一层LVS或者云负载均衡,四层转发到各个Node节点的NodePort,再由Ingress做七层路由。这样流量路径是:DNS解析到云LB,云LB四层转发到K8s Node,Node上Ingress Controller七层匹配域名路径,转发到目标Service,最终到达某个Pod。

负载均衡算法选最小连接数或者一致性哈希。最小连接数把请求分给当前连接最少的节点,适合长连接场景。一致性哈希按请求特征比如用户ID哈希,同一个用户的请求始终落到同一台机器,对缓存友好。Nginx配置里用ip_hash或者hash $arg_user_id实现会话保持,但集群环境更推荐把Session集中存储,这样负载均衡策略可以更灵活,不用绑死在会话保持上。

灰度发布是生产环境必备能力。新版本先放一小部分流量进去验证,没问题再逐步放大。K8s里可以建两个Deployment,一个跑旧版本,一个跑新版本,共用一个Service,通过调整两个Deployment的副本数来控制流量比例。更精细的做法是用Istio这样的服务网格,按请求头里的用户标签把流量路由到不同版本,实现按用户灰度。

日志收集与监控告警

集群跑起来之后,日志分布在几十上百个Pod里,逐个Pod去kubectl logs查日志效率太低。需要统一日志收集方案,主流是EFK或者ELK栈。每个Pod里跑一个Filebeat边车容器,采集应用日志文件,发送到Kafka做缓冲,再由Logstash消费处理后存入Elasticsearch,最后用Kibana展示和查询。日志里要带上traceId,方便串联一次请求在多个服务间的完整链路。引入SkyWalking或者Jaeger做分布式链路追踪,哪个服务调用慢、哪里报错,拓扑图上一目了然。

监控方面,基础设施层用Prometheus采集Node Exporter的指标,容器层用cAdvisor采集Pod的CPU内存网络数据,应用层用Micrometer暴露JVM指标和业务指标。Prometheus配置告警规则,比如Pod重启次数超过阈值、接口响应时间P99超过3秒、JVM堆内存使用率超过80%,触发告警通过Alertmanager发送到钉钉、企业微信或者短信。Grafana搭一套大屏,把关键指标可视化,集群健康状态一眼能看出来。

压测与上线检查清单

正式上线前必须做全链路压测。压测工具用JMeter或者wrk,模拟真实用户行为,逐步加压直到找到系统瓶颈。压测过程中观察数据库连接数、Redis QPS、Pod CPU和内存、接口响应时间的变化。常见瓶颈在数据库连接池不够、Redis单节点带宽打满、Pod内存配置太小频繁GC。根据压测结果调整连接池大小、Redis集群节点数、Pod资源配额,再压一轮验证。

上线检查清单是最后一道防线。每条检查项过一遍:Session共享是否验证通过、定时任务是否只有单节点执行、文件上传是否走对象存储、数据库读写分离是否生效、消息队列是否集群部署、日志是否集中收集、监控告警是否配置、灰度发布流程是否演练过、回滚方案是否测试过。全部打勾才能切生产流量。

集群部署上线不是终点,而是新的起点。流量上来之后还会遇到新的问题,缓存热点、数据库慢查询、微服务间调用超时,这些都需要持续优化。但至少这套流程走下来,集群的基础是扎实的,出了问题也有排查手段和应对预案。