第一次被人当面问java分布式是什么的时候,我正盯着控制台刷出来一堆超时日志,脑子里面乱糟糟的,只知道眼下这套业务拆成了好几个服务,请求飘来飘去抓不住。那天下午本来打算改一个简单的查询接口,改完打包发布,自测的时候一切正常,一丢到测试环境,问题就冒出来了,同一个请求,有时候能拿到数据,有时候直接抛异常,刷新几次结果还不一样。
打包脚本跑的很快,日志页面不停的滚动,一开始以为只是缓存没清干净,反复清了几次redis,问题一点没变。抓包看请求地址,才发现调用链路并不固定,流量会分到不同的节点上,有的节点里面数据同步慢半拍,有的节点前一天发布的时候漏了一个配置项,返回的数据版本不对。
那会还分不清集群和分布式的差别,混在一起瞎猜,以为多部署几个实例就都叫分布式。同事路过扫了一眼日志,说现在这个场景就是java分布式带来的麻烦,代码拆成独立服务跑在不同服务器,相互之间靠远程调用通信,不是以前那种所有逻辑塞在一个项目里面,启动一个tomcat就搞定的模式。
改了一下负载均衡策略,暂时把流量都切到单台机器上,奇怪的报错马上消失。
切换回去之后,老问题又重新出现。这才慢慢意识到,麻烦不只是接口调用超时,数据一致性才是更磨人的地方。一笔订单的操作,要先后调用库存服务、支付服务、消息通知服务,几个服务不在同一个进程,不在同一台机器,其中任意一个环节卡住或者失败,别的服务已经执行完的动作不会自动回滚。那天卡了很久,一直在试手动补偿的办法,在代码里面加重试,重试次数写多了,又容易造成重复下单,写少了网络抖动的时候直接丢失请求。
有一次更糟,注册中心的心跳断了一小会,部分节点下线之后又重新注册上来,服务列表错乱,调用方还在往已经不可用的地址发请求,短时间内大量报错堆在告警面板上。运维那边忙着看服务器负载,我这边翻源码看服务发现的逻辑,到处找哪里写死了服务地址,其实根本没有硬编码,所有地址都是注册中心动态推送过来的,这也是java分布式里面很基础的一块,只是之前一直没有实实在在碰到故障,理解停留在纸面文字上。
也试过把相关的逻辑往一块合并,想退回到单体的写法,改动量巨大,上游还有别的服务依赖这个接口,牵一发而动全身,改动评审直接被打回来。拆分的初衷本来是为了方便团队并行开发,不同小组维护各自的服务,扩容的时候单独给压力大的模块加机器,不用整个项目一起扩容,好处别人讲过很多,只是落到自己手上处理故障的时候,感受不到这些好处,满脑子都是各种各样的隐性问题。
不同服务的日志分散在不同的地方,出问题之后追查链路特别费劲,要把traceId复制出来,挨个去每台机器的日志平台检索,链路长的时候,来回切换好几个页面,中间有一段日志丢失,整条链路就断了,很难定位到底是哪一步出的状况。单体项目的时候,打开一个日志文件顺着往下翻就行,对比起来落差很明显。
有段时间总在想,如果不拆分成分布式,是不是就不用应付这些额外的东西。后来和别的项目组的人聊,他们那边流量峰值很高,单体扛不住,只能拆,只是拆分的时机和粒度很难拿捏,拆的太碎,调用链路变长,维护成本会成倍往上走。我手上这套,拆分的时候规划的不算细致,边界划分模糊,很多本该放在一起的逻辑被拆开,后续埋了不少隐患。
这些都只代表我个人遇到的情况,别的项目里面做java分布式,规范做的完善,配套的中间件和监控补齐之后,体感会完全不一样。
下班收拾东西的时候,告警声停了,临时加的补偿脚本跑通了积压的异常订单。电脑屏幕留在监控页面,上面几条曲线慢慢回落。