ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java项目从单体到微服务,我们踩过的那些坑

Java项目从单体到微服务,我们踩过的那些坑 代码仓库里还躺着上一次重构的遗迹生产环境告警群却已经炸了。凌晨两点我盯着屏幕上一串不知所云的NullPointerException终于承认一个事实微服务不是银弹而是霰弹枪扣动扳机时先倒下的往往是自己。我们用了十八个月把一个运行了七年的单体Java应用拆成了四十多个服务过程中踩遍了教科书不会写的坑。今天不聊架构图上的完美分层只聊那些让团队深夜崩溃的真实事故。第一步就没有退路工程拆分不是目录整理最初我们天真地认为微服务拆分就是把原来的包名从com.company.monolith改成com.company.order把Spring Boot启动类复制几份。结果第一个服务上线当天订单服务启动失败原因是Classpath里出现了两个版本的commons-lang3。单体项目里依赖冲突是编译器的事微服务里依赖冲突是要你从几十个服务里找出是谁偷偷塞进了老版本。更隐蔽的坑是隐式共享代码。我们把工具类放在common模块所有人都依赖它。当common更新时下游服务被迫全部重新构建部署。有一次改了个日志格式化方法直接导致支付回调的签名校验失败——因为那个方法里悄悄嵌入了业务密钥的读取逻辑。在单体里你可以说“这个类大家都用”在微服务里“大家都用”等于“谁都不敢改”。正确的做法是砍掉common拆成独立的小库每个服务只依赖真正需要的部分但代价是团队必须学会为粗糙的API设计负责。数据库拆分最疼的一刀应用拆了数据库还连着一个库这不算微服务只是把War包换成了Jar包。真正拆分订单表时我们锁了表整整三个小时。事务的ACID在分布式环境下变成了最终一致性而业务方根本不在乎你用什么分布式事务框架他们只问“为什么我的余额和订单对不上”。我们踩的坑是迷信Seata的可靠消息。实际运行中Seata的全局锁在高峰期成了性能瓶颈一个订单创建要锁定多个服务的数据源压测时TPS直接掉到原来的三分之一。后来被迫改为本地消息表加定时对账但新的问题出现了——消息重复消费。幂等性不是靠数据库唯一索引就能解决的因为两个请求可能来自不同服务落在不同库唯一索引只能防同一个库的重复。最后我们给每个业务请求生成全局ID在写入前先查询Redis里的执行记录。这个方案简单到有点丢人但它就是比任何花哨的分布式事务协议都可靠。服务发现和网关技术选型偏执狂的陷阱我们当时在Nacos和Consul之间纠结了半个月最后选了Nacos因为“阿里都在用”。选型时最容易忽略的事实你团队对某个组件的熟悉程度比组件本身的技术先进性重要十倍。Nacos的AP模式在注册中心场景下没问题但配置更新推送到所有服务有延迟。有一次配置变更导致订单服务获取了旧的服务列表调用积分服务时连接被拒重试机制又把请求堆到其他服务引发级联雪崩。网关也踩了大坑。我们用了Spring Cloud Gateway但默认的全局过滤器里没有处理超时时间。当下游服务变慢时网关线程全部阻塞CPU飙升到100%最后网关自己挂了。微服务架构里网关不是转发器而是你的防火墙。必须为每条路由配置独立的超时、重试和熔断策略而不是依赖“默认配置够用了”这种幻觉。配置管理那些看似优雅的魔法Spring Cloud Config Server配合Git存储配置看起来很美。可我们忘了配置文件里的密码和密钥再也不能像单体时代那样躺在application.yml里。有一次开发把数据库密码提交到了公共Git仓库两小时后就被扫描机器人盯上了导致生产库被拖库。配置中心最大的坑不是技术而是权限和审计流程。任何配置变更都可能瞬间影响所有实例所以必须像发布代码一样走评审和灰度。我们还遇到过配置刷新失败的诡异问题。用RefreshScope刷新Bean时如果Bean的构造方法里有复杂的初始化逻辑或者内部维护了线程池刷新会导致线程泄漏。RefreshScope不是万能药它只能作用于被代理的Bean而你内部管理的连接池、定时任务、状态机根本不会自动处理新旧实例的交接。最终我们限制刷新范围只允许热更新开关类配置其余配置强制走重启流程。这样反而更稳定。链路追踪你以为你有了其实没有我们接入了Sleuth和Zipkin但只追踪了HTTP调用。当消息队列成为服务间通信的主力后链路追踪的盲区出现了——生产消息和消费消息之间没有TraceId关联你查不到一个订单从创建到推送的完整链路。后来我们自己在消息头里塞TraceId但Kafka消费者消费时如果批量拉取并异步处理TraceId就乱套了。更让人崩溃的是日志聚合。我们的日志格式不统一有的服务用Logback有的用Log4j2有的把异常堆栈打到一行有的拆成多行。排查问题时Elasticsearch里搜索一个订单ID返回了几百条日志但根本没法按调用链拼接。链路追踪不是加个依赖就完事它要求所有服务统一日志规范、统一TraceId透传、统一异常格式化这更像是一场工程文化运动。团队协作比代码更硬的伤技术上的坑还可以填人的坑才是无底洞。我们把服务拆给不同小组结果每个组都按自己的理解定义接口。订单服务调用用户服务用户服务返回的地址格式是“省-市-区”订单服务却要求“省市区”为了对齐这个格式两个组开了三次会。微服务不仅仅拆分代码也拆分了知识。原本一个清晰的业务领域现在分散在几十个仓库里没人知道完整的故事。部署和测试也是重灾区。单体时代测试环境一个Tomcat搞定微服务时代测试环境要拉起几十个独立服务依赖关系错综复杂。我们建了Sprint Boot的Docker Compose但每次更新版本环境搭建都要花半天。如果微服务让开发本地调试变得困难那它就已经失败了——再漂亮的架构如果团队无法高效迭代就是负债。我们后来强制要求每个服务必须能在本地用Mock跑通虽然增加了工作量但拯救了生产效率。性能回归那些被忽视的慢调用拆分后一次用户下单原来单体里只需一次数据库事务现在要调用订单、库存、积分、通知四个服务。即使每个服务只花10ms网络开销和序列化/反序列化加起来也要200ms以上。微服务把原本的本地方法调用变成了远程调用每次调用都是潜在的定时炸弹。更可怕的是级联效应积分服务一个慢查询会拖垮网关线程池进而影响所有服务。我们踩过一个特别蠢的坑使用Feign时默认的connectTimeout是10秒readTimeout是60秒。这个配置在单体时代无所谓但在微服务环境下等于允许一次失败调用挂起60秒。超时配置必须根据依赖服务的P99延迟来设定而不是随便填个数字。我们后来给Feign加了全局超时和熔断但熔断器Hystrix或Sentinel自身又引入了线程池隔离的问题线程池大小设置不当直接导致内存溢出。测试策略端到端测试的伪安全感我们搭建了完整的测试金字塔单元测试多、集成测试少、端到端测试更少但现实是单元测试跑得欢集成测试全部假端到端测试像薛定谔的猫——你永远不知道它什么时候会挂。原因很简单微服务之间交互太多集成测试需要启动多个服务我们用Testcontainers模拟数据库和中间件但模拟的Kafka和真实Kafka的时序行为有差异导致测试通过、生产失败。端到端测试更是难以为继。一个完整流程涉及十几个服务测试环境资源有限经常超时。我们被迫采用自动化测试加契约测试的思路用Pact框架保证服务间接口兼容。但契约测试要求双方团队都付出额外维护成本如果你的团队连API文档都不想写契约测试也救不了你。运维监控别等你下线才发现没有监控最惨痛的一次事故我们上线了新版本订单服务一个线程池配置错误导致内存泄漏服务每隔三小时重启一次。生产告警群静默了整整两天直到用户投诉才被发现。为什么没监控因为我们的监控面板只有CPU、内存、JVM堆栈没有监控业务指标如订单成功率、平均响应时间、活跃用户数技术指标再健康业务已经烂掉了。我们后来引进了Prometheus和Grafana配置了详细告警但告警噪音又成了新问题。设定阈值这事儿太灵敏了半夜被叫醒处理误报太迟钝了又失去意义。最后我们狠心花了一周时间梳理每类告警的“symptom vs cause”把告警收敛到十几个核心指标。即便如此每周还是有几个凌晨的“未知异常”等着你。关于未来如果重来我们还会拆吗这个问题我们内部争论过无数次。答案是如果业务没有达到需要独立扩展和独立团队的规模微服务纯粹是自找麻烦。但如果你确实要拆请先意识到几个残酷的事实第一微服务让失败概率线性叠加你需要海量的自动化运维能力第二它把复杂性从运行期转移到了开发期要求每个团队成员都成为分布式系统专家第三别指望架构师能设计出完美方案架构是在踩坑中演化出来的而不是在开会中设计出来的。我们最终没有把所有服务都拆到底保留了几个“模块化单体”式的服务聚焦真正的核心业务拆分。那些踩过的坑现在都变成了团队的技术债清单。每次有人提议“这个系统太乱了我们要做微服务”我都会把这份清单拍在桌上——微服务是一场马拉松起跑时觉得风很轻跑到后半程才知道你的鞋子会磨脚你的腿会抽筋而终点线一直在移动。
返回列表