简单介绍下:
dubbo是阿里开源出来的一款高性能远程调用框架,可以使开发者像使用本地服务一样调用远程服务,目前已经毕业为apache的顶级项目。
目前生产环境发版可以简化为如下三个步骤: 假设服务A有10台机器A 1 ~A 10 在提供服务:
问题来了:基本上每次进行发布的时候,都会有调用方反馈出现调用超时。通过查检查上下游日志总结出以下规律:
或许是因为应用刚启动完成,代码还没有进行jit编译,导致调用超时;
与dubbo的lazy连接有关?阅读源码可知,在开启lazy连接的情况下,消费端并不会着急与服务端建立tcp连接,而是在有调用发生时在去建立;
DubboProtocol.java ------------------ private ExchangeClient initClient(URL url) { ... ExchangeClient client; try { // connection should be lazy if (url.getParameter(Constants.LAZY_CONNECT_KEY, false)) { client = new LazyConnectExchangeClient(url, requestHandler); } else { client = Exchangers.connect(url, requestHandler); } } catch (RemotingException e) { throw new RpcException("Fail to create remoting client for service(" + url + "): " + e.getMessage(), e); } return client; } 复制代码
在这里回顾下tcp的三次握手过程:
作为必考题之一的tcp三次握手,他的过程肯定已经再熟悉不过了:
那么,问题来了
大家都知道网络是天然的并发环境,server端在收到第三次握手的ack后如何去校验传入的ack值是否合法呢?
答案是系统维护了两个队列,分别称为半连接队列,和全连接队列。
第一步server收到client的syn后,把相关信息放到半连接队列中,同时回复syn+ack给client
第三步server收到client的ack,从半连接队列拿出相关信息放入到全连接队列中。
所以,完整的tcp三次握手过程应该是下面这样:
(图来自这里: www.cnxct.com/something-a…再回到dubbo上。这里只介绍两个与启用服务超时相关的点。
阿里这样介绍dubbo:以少量提供者支持大量的消费者调用(原话我不记得了,反正是这个意思)。
我认为其原因之一是dubbo使用了共享连接:
简单来说,共享连接意思就是在同一组消费者,提供者机器上,只维护一个tcp长连接,即使该消费者需要调用提供者提供的多个服务。
当然,目前生产环境都是集群部署,针对单台提供者来看的话,他所建立的tcp连接应该是下图这样:
如果所示,如果应用customer1有3台机器,customer2有4台机器,且他们都需要调用provider的服务,那么prodiver就需要维护最多3+4=7条tcp连接。
(为什么是最多,因为dubbo后台有一个线程去关闭有段时间不用的tcp连接。如果qps效高,且负载均衡策略会落在每一台机器上的话,连接可能被关闭的不会很多)。
当客户端与服务端创建代理时,暂不建立 tcp长连接,当有数据请求时再做连接初始化。
有了上面这些背景知识,再看最初的超时问题就比较容易理解了:
首先,dubbo服务端需要和每一天客户端建立一个tcp连接,如果调用方部署非常多,那么每个服务端需要建立的连接也是非常多的。 tcp连接的个数可以用这个命令来估算一下:
netstat -ant | wc -l 复制代码
其次,由于我们开启了lazy连接,那么在启用服务后,消费者收到zk事件,开始分配调用到新的提供者上,在qps较高的情况下,服务端受到的压力情况可以用下面这段伪代码来描述:
CountDownLatch latch = new CountDownLatch(NUMBER_OF_TCP_CONN); int i = 0; while (i++< NUMBER_OF_TCP_CONN){ new Thread(()->{ latch.countDown(); latch.await(); //发送syn握手包 send_syn(); }).start(); } 复制代码
可以想到,大家都在几乎同一时间来建立tcp连接,这种情况是不是让你想起了一种古老的拒绝服务攻击?
没错,就是syn flood攻击。简单来说就是通过发送大量的syn握手包,使服务端半连接队列溢出,从而无法提供服务。具体可以参考[ zh.wikipedia.org/wiki/SYN_fl… ](SYN flood)。
如果怀疑tcp连接队列溢出,可以使用以下命令确认:
netstat -s | egrep "listen|LISTEN" 复制代码
知道了原因,那么问题就解决了一半。
最直观的做法就是调整tcp的半连接队列和全连接队列。
全连接队列的大小取决于:min(backlog, somaxconn) 。 backlog是在socket创建的时候传入的,somaxconn是一个os级别的系统参数。
半连接队列的大小取决于:max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)。
其中值得注意的是,在jdk中,backlog默认的设置的为50。
java.net.ServerSocket --------------------- public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException { setImpl(); if (port < 0 || port > 0xFFFF) throw new IllegalArgumentException( "Port value out of range: " + port); if (backlog < 1) backlog = 50; try { bind(new InetSocketAddress(bindAddr, port), backlog); } catch(SecurityException e) { close(); throw e; } catch(IOException e) { close(); throw e; } } public ServerSocket(int port) throws IOException { this(port, 50, null); } 复制代码
所以,对于backlog参数,仅修改系统参数是不起作用的,需要将创建ServerSocket是传入的backlog参数一同修改。