问题排查避免陷入局部解
(这篇文章早就想写了,但苦于一直忙项目开发,就拖到了现在)
最近有两个case对我影响比较深,
一个case
一个是帮公司某个业务排查dubbo的问题, 现象是:业务发现他们调用某一个服务的时候,总是调用不到想要的版本:比如,他们本来想调用服务A的1.0版本,但实际上总是掉到2.0版本。
拿到问题后,我就开始梳理dubbo的代码,以及查看zk上的数据。 发现,服务A一个奇怪的名字:A2,按理说应该是服务A名字自然是A啊?继续看dubbo的代码,发现如果dubbo服务如果有两个的名字一样的话,就会依赖spring的bean的命名机制,给另一个服务命名为A2,A3等(序号自增)。
然后,我去查看业务的代码,发现确实他们定义了两个服务名一样的bean,只是版本不一样。我又查看了dubbo处理服务提供方的代码,发现dubbo在获取invoker的时候有这样的逻辑:
1 | Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException{ |
发现dubbo再获取invoker的时候,会根据serviceKey从exporterMap拿到具体的invoker,而serviceKey是通过serviceName等信息构造的。
OK了,看到这,我就跟业务说了我的结论:
- 服务名字后面有个2是因为spring bean的命名机制导致的
- dubbo对同一个服务多个版本不支持,因为serverName一样的话,会在exporterMap里面被覆盖,这样,本来是想调用版本为1的服务,结果路由到provider的时候,找到的却是版本为2的服务(因为版本为2的服务把版本1给覆盖了)
因为当时在开发新的项目,就把这个问题放下了。直到后面又有业务找过来,让我评估他们的服务升级方案,他们听说了我这边的结论,就问题dubbo服务升级怎么搞,我当时认真一想,发现如果dubbo不支持一个服务多个版本的话,那dubbo也太菜了。于是又花时间仔细看了下代码。
继续排查
梳理下dubbo从调用方发起到走到provider的逻辑,发现调用服务的时候:
1 | @Override |
dubbo会把服务的类名作为path放到attachment里面,然后,
1 | Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException{ |
找到具体的invoker的时候,会使用这个path,虽然serviceKey的第二个参数的函数签名是serverName(这也就是我第一次排查误解的地方),但是,具体调用的时候却是传入的path,也就是服务bean的名字
到这里,整个链路就清楚了,dubbo是支持一个服务(interface)多个版本的,实现思路上是把实现inerface的bean的name作为URL的path记录下来,调用的时候,先根据interface筛选provider列表,然后根据path找到具体的bean
(PS:由于过去较长时间了,可能有出入,但这不是重点)
又一个case
这个case就是当时排查我们的mq server fgc的问题,netty-entry-fgc.
这个问题最开始也是给出了一个错误的结论, 误以为是内存泄漏造成的。
结论
上面两个case让我感触颇深,想来想去,觉的得总结一下,如何避免后面排查问题的时候又得出一个错误的结论
不难看出,两个case还是有共性的:
- 出现问题
- 排查问题
- 找到一个(代码)片段,发现这个片段刚好能够解释问题的原因
- 给出结论
其实,基于片段所给出的结论,类似于算法里的局部最优解,跳出来从整体看,往往发现是错误的。所以,为了跳出来,我觉得我们在排查问题的时候,如果找到了基于某个片段做出的结论,这个时候,不应该停下来,而是要在整体上,把整个正常流程梳理出来。 简单点说,就是,找出了你认为导致错误的问题的时候,别急,把正确的流程梳理出来看看。
把结论应用于上面两个case看看:
- case1: 如果觉得exporterMap会把serviceName相同的覆盖掉,那么,正常的服务是怎么找到的? 这个时候,去梳理这个流程,就会发现原来dubbo处理服务名称的时候,已经做了处理来支持多版本
- case2: 如果觉得是内存泄漏,那么,正常的内存时候是什么时候呢?看代码,就会发现,如果entry被释放,就没有内存堆积了,就不会得出上层内存泄漏的结论了
题外话
case1的原因,后面经过排查,发现是业务用了一个错误的dubbo版本,这个dubbo是公司另外一个团队维护的,他们引入了一个bug,导致寻找服务时会发生版本错乱..