DUBBO的调用过程 dubbo调用超时如何解决_客户端


简单介绍下:

dubbo是阿里开源出来的一款高性能远程调用框架,可以使开发者像使用本地服务一样调用远程服务,目前已经毕业为apache的顶级项目。


DUBBO的调用过程 dubbo调用超时如何解决_半连接_02


背景

目前生产环境发版可以简化为如下三个步骤: 假设服务A有10台机器A1~A10在提供服务:

  • 先针对机器A1~A5操作服务禁用,使调用请求不会发到即将部署的机器上。
  • 对机器A1~A5进行部署,检查应用启动情况。
  • 对机器A1~A5操作服务启用。
  • 对A6~A10重复以上3步。

问题来了:基本上每次进行发布的时候,都会有调用方反馈出现调用超时。通过查检查上下游日志总结出以下规律:

  • 超时调用发生在上线过程的第三步,也就是操作服务启用瞬间。
  • 调用方超时的请求在服务方没有对应日志。

猜想

  1. 或许是因为应用刚启动完成,代码还没有进行jit编译,导致调用超时;
  2. 与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;
    }


  1. 启用服务瞬间有大量消费者来建立tcp连接,造成服务端来不及响应;

tcp三次握手

在这里回顾下tcp的三次握手过程:


DUBBO的调用过程 dubbo调用超时如何解决_客户端_03


作为必考题之一的tcp三次握手,他的过程肯定已经再熟悉不过了:

  1. 客户端发生syn到服务端,客户端状态转为syn_send状态。
  2. 服务端接受到客户端发生的syn握手包,回复syn+ack,进入syn_rcvd状态。此时,服务端为半连接。
  3. 客户端收到服务端回传的syn+ack,并回传ack。客户端服务端进入established状态,变为全连接。

那么,问题来了

大家都知道网络是天然的并发环境,server端在收到第三次握手的ack后如何去校验传入的ack值是否合法呢?

答案是系统维护了两个队列,分别称为半连接队列,和全连接队列。

第一步server收到client的syn后,把相关信息放到半连接队列中,同时回复syn+ack给client

第三步server收到client的ack,从半连接队列拿出相关信息放入到全连接队列中。

所以,完整的tcp三次握手过程应该是下面这样:


DUBBO的调用过程 dubbo调用超时如何解决_半连接_04


所以,既然有队列存在,那么就一定会有长度限制。

dubbo连接模型

再回到dubbo上。这里只介绍两个与启用服务超时相关的点。

tcp连接数

阿里这样介绍dubbo:以少量提供者支持大量的消费者调用(原话我不记得了,反正是这个意思)。

我认为其原因之一是dubbo使用了共享连接:

简单来说,共享连接意思就是在同一组消费者,提供者机器上,只维护一个tcp长连接,即使该消费者需要调用提供者提供的多个服务。

当然,目前生产环境都是集群部署,针对单台提供者来看的话,他所建立的tcp连接应该是下图这样:


DUBBO的调用过程 dubbo调用超时如何解决_半连接_05


如果所示,如果应用customer1有3台机器,customer2有4台机器,且他们都需要调用provider的服务,那么prodiver就需要维护最多3+4=7条tcp连接。

(为什么是最多,因为dubbo后台有一个线程去关闭有段时间不用的tcp连接。如果qps效高,且负载均衡策略会落在每一台机器上的话,连接可能被关闭的不会很多)。

lazy 连接

当客户端与服务端创建代理时,暂不建立 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握手包,使服务端半连接队列溢出,从而无法提供服务。

如果怀疑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参数一同修改。

作者:一缕阳光同志88274