(这篇文章早就想写了,但苦于一直忙项目开发,就拖到了现在)

最近有两个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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException{
//......忽略
String serviceKey = serviceKey(port, path, inv.getAttachments().get(Constants.VERSION_KEY), inv.getAttachments().get(Constants.GROUP_KEY));

DubboExporter<?> exporter = (DubboExporter<?>) exporterMap.get(serviceKey);

if (exporter == null)
throw new RemotingException(channel, "Not found exported service: " + serviceKey + " in " + exporterMap.keySet() + ", may be version or group mismatch " + ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress() + ", message:" + inv);

return exporter.getInvoker();
}

protected static String serviceKey(int port, String serviceName, String serviceVersion, String serviceGroup) {
return ProtocolUtils.serviceKey(port, serviceName, serviceVersion, serviceGroup);
}

发现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
2
3
4
5
6
7
@Override
protected Result doInvoke(final Invocation invocation) throws Throwable {
RpcInvocation inv = (RpcInvocation) invocation;
final String methodName = RpcUtils.getMethodName(invocation);
inv.setAttachment(Constants.PATH_KEY, getUrl().getPath());
inv.setAttachment(Constants.VERSION_KEY, version);
// ...省略..

dubbo会把服务的类名作为path放到attachment里面,然后,

1
2
3
4
5
6
7
Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException{
boolean isCallBackServiceInvoke = false;
boolean isStubServiceInvoke = false;
int port = channel.getLocalAddress().getPort();
String path = inv.getAttachments().get(Constants.PATH_KEY);
//...省略...
String serviceKey = serviceKey(port, path, inv.getAttachments().get(Constants.VERSION_KEY), inv.getAttachments().get(Constants.GROUP_KEY));

找到具体的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,导致寻找服务时会发生版本错乱..