天天看点

RPC框架的连接模型

一、RPC的客户端连接模型

1、连接成本: 使用TCP协议时,会在客户端和服务器之间建立一条虚拟的信道,这条虚拟信道就是指连接,而建议这条连接需要3次握手,拆毁这条连接需要4次挥手,可见,我们建立这条连接是有成本的,这个成本就是效率成本,简单点 说就是时间成本,你要想发送一段数据,必须先3次握手(来往3个包),然后才能发送数据,发送完了,你需要4次挥手(来往4个包)来断开这个连接。 其二,CPU资源成本,三次握手和4次挥手和发送数据都是从网卡里发送出去和接收的,还有其余的设备,比如防火墙,路由器等等,站在操作系统内核的角度来讲,如果我们是一个高并发系统的话,如果大量的数据包都经 历过这么一个过程,那是很耗CPU的。 其三,每个socket是需要耗费系统缓存的,比如系统提供了一些接口设置socket缓存。

1、短连接: client与server通过三次握手建立连接,client发送请求消息,server返回响应,一次连接就完成了。这时候双方任意都可以发起close操作,不过一般都是client先发起close操作。上述可知,短连接一般只会在 client/server间传递一次请求操作。短连接的优缺点:管理起来比较简单,存在的连接都是有用的连接,不需要额外的控制手段。所以对于并发量大,请求频率低的,建议使用短连接。

2、长连接:

  • TCP连接一旦建立后,是不是这个连接可以一直保持? 答案是否定的,操作系统在实现TCP协议的时候都做了一个限制,这个限制可 以参考配置:

    /proc/sys/net/ipv4/tcp_keepalive_time /proc/sys/net/ipv4/tcp_keepalive_intvl /proc/sys/net/ipv4/tcp_keepalive_probes

    .。我们看到默认这个tcp_keepalive_time的值为7200s,也就是2个小时,这个值代表如果TCP连接发送完最后一个ACK包后,如果 超过2个小时,没有数据往来,那么这个连接会断掉。那么我们如何才能保持住这个连接呢?实际上,这就是TCP的keepalive机 制,哦,说法不严谨,TCP协议并没有规定如此,但是很多的操作系统内核实现TCP协议时,都加上了这个keepalive机制,那么 这个功能默认是关闭的,那这个keepalive机制到底是如何的呢?也就是,如果TCP之间没有任何数据来往了在 tcp_keepalive_time(7200s,2h)后,服务器给客户端发送一个探测包,如果对方有回应,说明这个连接还存活,否则继续每 隔tcp_keepalive_intvl(默认为75s)给对方发送探测包,如果连续tcp_keepalive_probes(默认为9)次后,依然没有收到对端 的回复,那么则认为这个连接已经关闭。
  • 长连接适用于要进行大量数据传输的情况,如:数据库,redis,memcached等要求快速,数据量大的情况下。 长连接通过心跳机制(通信数据很少)来进行连接状态的监测,断后重新进行连接。 数据库的连接就是采用TCP长连接。RPC远程服务调用,在服务器,一个服务进程频繁调用另一个服务进程,可使用长连接,减少连接花费的时间。

3、单连接(单长连接): 同步方式下客户端所有请求共用同一连接,在获得连接后要对连接加锁在读写结束后才解锁释放连接,性能低下,基本很少采用,唯一优点是实现极其简单。异步方式下所有请求都带有消息ID,因此可以批量发送请求(发送到阻塞等待队列中),异步接收回复,所有请求和回复的消息都共享同一连接,信道得到最大化利用,因此吞吐量最大。这个时候接收端的处理能力也要求比较高,一般都是独立的一个(或者多个)收包线程(或者进程)防止内核缓冲区被填满影响网络吞吐量。缺点是实现复杂,需要异步状态机,需要增加负载均衡和连接健康度检测机制,等等。

4、连接池(多长连接):

  • 连接池,连接池是将已经创建好的连接保存在池中,当有请求来时,直接使用已经创建好的连接对数据库进行访问。这样省略了创建连接和销毁连接的过程。这样性能上得到了提高。维护着一定数量Socket长连接的集合。它能自动检测Socket长连接的有效性,剔除无效的连接,补充连接池的长连接的数量。socket连接池 发出连接的client-port 一般是随机的,可以在客户端建立多个对目的服务端的连接池。每个请求单独占用一个连接,使用完以后把连接放回池中,给下一个请求使用。
  • 连接池的优点:

    ①资源重用:由于数据库连接得到重用,避免了频繁创建、释放连接引起的大量性能开销。在减少系统消耗的基础上,增进了系统环境的平稳性(减少内存碎片以级数据库临时进程、线程的数量)。

    ②更快的系统响应速度:数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于池内备用。此时连接池的初始化操作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而缩减了系统整体响应时间。

    ③新的资源分配手段:对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接的配置,实现数据库连接技术。

    ④统一的连接管理,避免数据库连接泄露: 在较为完备的数据库连接池实现中,可根据预先的连接占用超时设定,强制收回被占用的连接,从而避免了常规数据库连接操作中可能出现的资源泄露

  • 连接池的缺点:缺点还是网络利用率不高,因为在等待对端回复的时候,连接是空闲的。

5、连接模型比对: 小包复用链接才能提高吞吐和降低延时,简单说减少系统调用,纯服务,系统调用是最大的瓶颈。即使是复用的模式下,也是新建多个链接的。一般tars针对目标ip节点的socket是4个。并不都是一个,只是说这个配置在大多数的情况下够用,不是固定是1,特别是在发包是10mb以上的时候,还是需要增加connection来提升部分qps。这一部分结合网络基础的情况,一个Ip可发起的端口,最大只有65545,当调用后端有1万个服务的时候,这个socket是肯定不够用的。这一部分可以很大程度解决这一个带来的问题。第二个是大规模的集群环境下,流量特别大,大量的socket会给路由器带来负载。所以,只要包不要太大,其实一个连接也可以,关注socket饥饿效应就可以了。这一部分需要大量的理论知识,这个也是tars设计的基石,肯定会有各方面充足的理论基础给各个设计上面有支撑。

6、最大连接数量问题

单个客户端发出连接数量:在链接发起端,受端口号的限制理论上最多可以创建65535左右链接。端口号的理论值范围是从0到65535,系统端口的是0-1023 ,注册端口是1024-65535为用户端口。

/proc/sys/net/ipv4/ip_local_port_range

查询可用端口区间, 默认是:32768~61000,可用于定义网络连接可用作其源(本地)端口的最小和最大端口的限制,同时适用于TCP和UDP连接。说明这台机器本地能向外连接61000-32768=28232个连接,注意是本地向外连接。

单个服务端接收连接数量:TCP连接中Server IP + Server Port + Client IP + Client Port这个组合标识一个连接。服务端通常固定在某个本地端口上监听,等待客户端的连接请求。如果不考虑地址重用(SO_REUSEADDR选项)的情况下,即使服务端端有多个网卡ip,本地监听端口也是独占的,因此服务端TCP连接四元组中只有Client IP + Client Port是可变的,因此服务端TCP最大连接为Client IP 数×Client Port数,对IPV4,不考虑ip地址分类等因素,最大TCP连接数约为2的32次方(ip数)×2的16次方(port数),也就是服务端端单机理论最大TCP连接数约为2的48次方。但是如上所述最高的并发数量都要受到系统对用户单一进程同时可打开文件数量和内存占用的限制。这是因为系统为每个TCP连接被accept后都要创建一个连接套接字socket句柄,每个socket句柄同时也是一个文件句柄。

  • ulimit -n 输出的结果,说明对于一个进程而言最多能打开多少个文件,所以你要采用此默认配置最多也就可以并发上千个TCP连接。临时修改:ulimit -n 1000000,但是这种临时修改只对当前登录用户目前的使用环境有效,系统重启或用户退出后就会失效。永久修改:编辑/etc/rc.local,在其后添加如下内容:ulimit -SHn 1000000
  • 实际情况下,每创建一个链接需要消耗一定的内存,大概是4-10kb,所以链接数也受限于机器的总内存。

参数调优: 服务器为支持更大的连接数量,大内存、文件描述符限制足够大可以满足一定数量的连接。参数调优可以增加链接数量。一个Socket连接默认是有内存消耗的,可以按需调整tcp socket的参数如tcp发送\接收缓冲区的大小。

7、连接数过多的问题:

“Cannot assign requested address.”

:是由于Linux分配的客户端连接端口用尽或本地可分配端口数比较少,无法建立socket连接所致。

cat /proc/sys/net/ipv4/ip_local_port_range

查询可用端口区间, 默认是:32768 61000,说明这台机器本地能向外连接61000-32768=28232个连接,注意是本地向外连接,不是这台机器的所有连接,不会影响这台机器的 80端口的对外连接数。参考链接 解决办法:

vi  /etc/sysctl.conf
net.ipv4.ip_local_port_range = 1024 65535
sysctl -p
           

netstat参数说明:

netstat -[atunlp]
-a :all,表示列出所有的连接,服务监听,Socket资料
-t :tcp,列出tcp协议的服务
-u :udp,列出udp协议的服务
-n :port number, 用端口号来显示
-l :listening,列出当前监听服务(显示作为服务端的连接,不加显示的是作为客户端的连接)
-p :program,列出服务程序的PID
           

netstat -n | awk '/^tcp/ {++state[$NF]} END {for(key in state) print key,"\t",state[key]}'

查看tcp的连接状态和数目。

RPC框架的连接模型

可临时重启(systemctl restart serviceName)相应的服务,使得连接数下降。0.0.0.0是一个特殊的IP地址,指的是本机的全部IP地址。如果一个应用绑定了0.0.0.0上的某个端口,意味着只要是发往这个端口的请求,不管是来自哪个IP地址,都会由这个应用处理。一般服务器都是多网卡的。

8、一次长连接的时长:

在Linux中/etc/sysctl.conf下有长连接的全局配置:

  • net.ipv4.tcp_keepalive_time=7200
  • net.ipv4.tcp_keepalive_intvl=75
  • net.ipv4.tcp_keepalive_probes=9

系统默认这个tcp_keepalive_time的值为7200s,也就是2个小时,这个值代表如果TCP连接发送完最后一个ACK包后,如果超过2个小时,没有数据往来,那么这个连接会断掉。

实际上TCP的keepalive机制会维持长连接状态,但是很多的操作系统内核实现TCP协议时,都加上了这个keepalive机制,那么这个功能默认是关闭的,如果TCP之间没有任何数据来往了在tcp_keepalive_time(7200s,2h)后,服务器给客户端发送一个探测包,如果对方有回应,说明这个连接还存活,否则继续每隔tcp_keepalive_intvl(默认为75s)给对方发送探测包,如果连续tcp_keepalive_probes(默认为9)次后,依然没有收到对端的回复,那么则认为这个连接已经关闭。如果收到了对端的回复,TCP为接下来的两小时复位存活定时器,如果在这两个小时到期之前,连接上发生应用程序的通信,则定时器重新为往下的两小时复位,并且接着交换数据。tcp keepalive默认不是开启的,如果想使用KeepAlive,需要在你的应用中设置SO_KEEPALIVE才可以生效。

当客户端端等待超过一定时间后自动给服务端发送一个空的报文,如果对方回复了这个报文证明连接还存活着,如果对方没有报文返回且进行了多次尝试都是一样,那么就认为连接已经丢失,客户端就没必要继续保持连接了。如果对端被关闭然后重启的情况,当系统被操作员关闭时,所有的应用程序进程(也就是客户端进程)都将被终止,客户端TCP会在连接上发送一个FIN。收到这个FIN后,服务器TCP向服务器进程报告一个文件结束,以允许服务器检测这种状态。

二、RPC中的长连接(tars举例)

Tars 客户端访问服务节点时默认采用长连接的形式,这样在并发请求大的情况下,单个连接处理不过就会导致超时了。主流的RPC框架都会追求性能选择使用长连接,所以如何保活连接就是一个重要的话题。保活一般采用TCP机制keep_alive和应用层心跳检测。

应用层心跳检测: 长连接实现,一般在应用层也有心跳检测机制,因为同一条连接上关联的请求太多,为了减小伤害面积,能够尽早发现异常连接状态越好,那么其实tcp的keepalive就应该够了,那么为啥要实现应用层的心跳呢?这里其实还有另外的考虑,tcp的保活定时器是系统层面上的东西,对于自己的代码其实并不能感知到,就算是对面的进程因为永久性的阻塞而无法服务了,其实保活定时器是无法处理这种情况的。简单应用层心跳检测的实现为设置一个定时器Timer, 每隔一段时间发送一个心跳检测包。比如微服务没隔一段时间向注册中心上报自己的心跳状态。

连接超时机制: 客户端rpc的超时一定要有,另外服务端,如果是并发的,不管是线程还是协程,都一定要监控运行状态,防止永久性的阻塞造成内存泄露,设定一些阈值,在需要的时候直接终止正在执行的rpc请求。

1、tars客户端是用epoll组织的,tars客户端默认socket为长连接,启动TCP编程里的keepAlive机制。每个长连接有超时机制。

int NetworkUtil::createSocket(bool udp, bool isLocal/* = false*/, bool isIpv6/* = false*/)
{
    int domain = isLocal ? PF_LOCAL : (isIpv6 ? PF_INET6 : PF_INET);
    int type = udp ? SOCK_DGRAM : SOCK_STREAM;
    int protocol = udp ? IPPROTO_UDP : IPPROTO_TCP;
    int fd = socket(domain, type, protocol);
	...
    if(!udp)
    {
        setTcpNoDelay(fd);

        setKeepAlive(fd);
    }
    return fd;
}
           

2、连续两次tars调用可能不会复用tcp长连接,有两次负载均衡可能导致分配到的socket连接不同。但也有可能分配到同一个objectProxy的AdapterProxy对应的长连接中。 CommunicatorEpoll()会把该套接字放入epoll中进行监听数据的收发。

(1)第一层负载均衡:轮询选择ObjectProxy(CommunicatorEpoll)和与之相对应的ReqInfoQueue

ServantProxy::selectNetThreadInfo();

(2)第二层负载均衡: 通过EndpointManager选择AdapterProxy,负载均衡算法(Hash、权重、轮询)

配置文件netthread属性决定了客户端communicatorEpoll的数量,若配置文件没配置则默认为1.

void Communicator::initialize()
{
    TC_LockT<TC_ThreadRecMutex> lock(*this);
	...
    //客户端网络线程
    _clientThreadNum = TC_Common::strto<size_t>(getProperty("netthread","1"));

    if(0 == _clientThreadNum)
    {
        _clientThreadNum = 1;
    }
    else if(MAX_CLIENT_THREAD_NUM < _clientThreadNum)
    {
        _clientThreadNum = MAX_CLIENT_THREAD_NUM;
    }
	...
    for(size_t i = 0; i < _clientThreadNum; ++i)
    {
        _communicatorEpoll[i] = new CommunicatorEpoll(this, i);
        _communicatorEpoll[i]->start();
    }
    ...
}
           

3、tcp长连接也有超时,若超时超过一定比例或连接异常,会屏蔽节点关闭连接。若没有超时或网络异常,会维持一个长连接。

ServantProxy::ServantProxy(Communicator * pCommunicator, ObjectProxy ** ppObjectProxy, size_t iClientThreadNum)
: _communicator(pCommunicator)
, _objectProxy(ppObjectProxy)
, _objectProxyNum(iClientThreadNum)
, _syncTimeout(DEFAULT_SYNCTIMEOUT)
, _asyncTimeout(DEFAULT_ASYNCTIMEOUT)
, _id(0)
, _masterFlag(false)
, _queueSize(1000)
, _minTimeout(100)
    ...
}

/**
     * 缺省的同步调用超时时间
     * 超时后不保证消息不会被服务端处理
*/
enum { DEFAULT_SYNCTIMEOUT = 3000, DEFAULT_ASYNCTIMEOUT=5000};
msg->request.iTimeout     = (ReqMessage::SYNC_CALL == msg->eType)?_syncTimeout:_asyncTimeout;


//屏蔽结点
void AdapterProxy::setInactive()
{
    _activeStatus  = false;

    _nextRetryTime = TNOW + _objectProxy->checkTimeoutInfo().tryTimeInterval;

    _trans->close();

    TLOGINFO("[TARS][AdapterProxy::setInactive objname:" << _objectProxy->name() << ",desc:" << _endpoint.desc() << ",inactive" << endl);
}
           

4、在客户端第二层负载均衡:通过EndpointManager选择AdapterProxy,负载均衡算法(Hash、权重、轮询),会调用AdapterProxy::checkActive(bool bForceConnect)建立和目标server的tcp/Udp 连接。若后续客户端请求负载均衡到该objectProxy的AdapterProxy中,该连接可被向目标server发起请求的客户端复用。若重新向目标server发起连接socket 套接字会更新。

bool AdapterProxy::checkActive(bool bForceConnect)
{
   ...
    //连接没有建立或者连接无效, 重新建立连接
    if(!_trans->isValid())
    {
        try
        {
            _trans->reconnect();
        }
        catch(exception &ex)
        {
            _activeStatus = false;

            _trans->close();

            TLOGERROR("[TARS][AdapterProxy::checkActive connect ex:" << ex.what() << endl);
        }
    }
...
    return (_trans->hasConnected() || _trans->isConnecting());
}

void Transceiver::connect()
{
...
        fd = NetworkUtil::createSocket(false, false, _ep.isIPv6());
        NetworkUtil::setBlock(fd, false);

        socklen_t len = _ep.isIPv6() ? sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in);
        bool bConnected = NetworkUtil::doConnect(fd, _ep.addrPtr(), len);
        if(bConnected)
        {
            setConnected();
        }
        else
        {
            _connStatus     = Transceiver::eConnecting;
            _conTimeoutTime = TNOWMS + _adapterProxy->getConTimeout();
        }
    }
	_fd = fd//重新建立套接字连接
 ...
}

           

5、若客户端负载均衡到一个adapterProxy中,会调用该adapterProxy的transcever向服务端发送数据,一个AdapterProxy内维持一个transceiver, 一个transceiver内维持一个EndpointInfo(连接的节点信息)信息和相应的客户端套接字socket。

int AdapterProxy::invoke(ReqMessage * msg)
{
  ...
    //交给连接发送数据,连接连上,buffer不为空,直接发送数据成功
    if(_timeoutQueue->sendListEmpty() && _trans->sendRequest(msg->sReqData.c_str(),msg->sReqData.size()) != Transceiver::eRetError)
    {
        TLOGINFO("[TARS][AdapterProxy::invoke push (send) objname:" << _objectProxy->name() << ",desc:" << _endpoint.desc() << ",id:" << msg->request.iRequestId << endl);

        //请求发送成功了,单向调用直接返回
        if(msg->eType == ReqMessage::ONE_WAY)
        {
        #ifdef _USE_OPENTRACKING
            finishTrack(msg);
        #endif

            delete msg;
            msg = NULL;

            return 0;
        }

     ...
    return 0;
}
           

6、超时处理

void CommunicatorEpoll::run()
{
    ServantProxyThreadData * pSptd = ServantProxyThreadData::getData();
    assert(pSptd != NULL);
    pSptd->_netThreadSeq = (int)_netThreadSeq;
 while (!_terminate)
    {
        try
        {
            int iTimeout = ((_waitTimeout < _timeoutCheckInterval) ? _waitTimeout : _timeoutCheckInterval);
           int num = _ep.wait(iTimeout);
            if(_terminate)
            {
                break;
            }
            //先处理epoll的网络事件
            for (int i = 0; i < num; ++i)
            {
                const epoll_event& ev = _ep.get(i);
                uint64_t data = ev.data.u64;
                if(data == 0)
                {
                    continue; //data非指针, 退出循环
                }
                handle((FDInfo*)data, ev.events);
            }
            //处理超时请求
            doTimeout();
            //数据上报
            doStat();
        }
      ...
}

void CommunicatorEpoll::doTimeout()
{
    int64_t iNow = TNOWMS;
    if(_nextTime > iNow)
    {
        return;
    }

    //每_timeoutCheckInterval检查一次
    _nextTime = iNow + _timeoutCheckInterval;

    for(size_t i = 0; i < _objectProxyFactory->getObjNum(); ++i)
    {
        const vector<AdapterProxy*> & vAdapterProxy=_objectProxyFactory->getObjectProxy(i)->getAdapters();
        for(size_t iAdapter=0;iAdapter<vAdapterProxy.size();++iAdapter)
        {
            vAdapterProxy[iAdapter]->doTimeout();
        }
        _objectProxyFactory->getObjectProxy(i)->doTimeout();
    }
}
           

7、服务端关闭连接。当网络超时或者读取写入数据失败时会在服务端关闭连接connection。

void TC_EpollServer::NetThread::processNet(const epoll_event &ev)
{
    Connection *cPtr = getConnectionPtr(uid);
    ...
    if (ev.events & EPOLLERR || ev.events & EPOLLHUP)
    {
        delConnection(cPtr,true,EM_SERVER_CLOSE);

        return;
    }
    if(ev.events & EPOLLIN)               //有数据需要读取
    {
        recv_queue::queue_type vRecvData;

        int ret = recvBuffer(cPtr, vRecvData);

        if(ret < 0)
        {
            delConnection(cPtr,true,EM_CLIENT_CLOSE);

            return;
        }
        if(!vRecvData.empty())
        {
            cPtr->insertRecvQueue(vRecvData);
        }
    }

    if (ev.events & EPOLLOUT)              //有数据需要发送
    {
        int ret = sendBuffer(cPtr);

        if (ret < 0)
        {
            delConnection(cPtr,true,(ret==-1)?EM_CLIENT_CLOSE:EM_SERVER_CLOSE);

            return;
        }
    }

    _list.refresh(uid, cPtr->getTimeout() + TNOW);
}
           

三、Tars 异步长连接的实现:

传统的 HTTP 服务多是基于同步的线程模型,由于 HTTP 协议本身无状态,所以在协议层面就不支持异步,所以当在客户端发起一次 HTTP 调用时主调线程必须挂起等待被调响应请求,这个时候主调线程的资源则被浪费了,因为线程资源是有限的,大量线程被挂起等待白白浪费了主调方的运算资源。相比于使用HTTP协议的常规方案,TARS首先提供的特性就是异步长连接的RPC调用方式:

发起一个异步调用之后,当前线程并不会被阻塞而是继续执行,当收到服务端响应之后在回调线程池中通过回调函数来执行结果的处理。这样所有的处理线程都一直处于工作的状态中,而不会挂起导致线程资源的浪费。整体上提升了服务的处理能力。

TARS的异步能力主要是通过两个部分的异步来实现的,首先是网络首发包的异步,TARS的网络层实现采用了Reactor模型,通过nio提供的事件IO实现基于事件的异步网络IO。第二是线程模型的异步,我们从线程模型上来看TARS如何是做到异步调用的:

RPC框架的连接模型

TARS的主要通过上图的过程来完成异步调用,首先主调线程发起异步调用,主调线程将请求内容加入网络线程池的发送队列中,之后该线程继续执行。网络线程池使用Reactor模型实现,通过nio提供的Selecter实现事件IO,所以所有网络线程均是事件驱动的异步IO,当监听到对应连接的写事件后将请求发送,等待监听到读事件后读取响应并交给回调线程处理响应。这样所有的线程都避免了IO阻塞达到了更高的利用效率。

同步异步和长短连接:

按照长短连接、同异步做笛卡尔积的结果一共四种,但是平时我们只说其中三种:同步短连接、同步长连接、异步长连接,异步短连接一般不会提,是因为异步短连接是比同步短连接效率还要低,实现却比同步短连接复杂很多。

​ 异步短连接:client、server各开一个端口监听,用于接收数据;client连接server端口,发送请求,关闭连接;server处理完请求,依据请求来源,建立与客户端的连接,发送应答,关闭连接;比较复杂。一般不会采用。

​ 同步短连接: server开端口监听,client连接server端口,发送请求,服务端处理请求,返回应答数据,关闭连接;因为实现简单,而且在不稳定网络状况下有一些优势,经常被采用;如果短连接被用在了交易频繁的系统上,会产生很多待释放的连接,会引发一些问题。

​ 同步长连接:server开端口,client连接server端口,发送请求,服务端处理请求,返回应答数据,server继续接收请求……;实现复杂度排名第三,吞吐效率排名第二,时延指标排名第一;其实和同步短连接实现起来差不多,多写个循环,但是效率能改善很多,复杂或不稳定网络环境下需要进行很多异常判断;适合于对时延要求很严格,但是吞吐效率其次的场景。

​ 异步长连接: client采用异步调用的形式发起长连接,发送数据后不会一直等待数据结果,而是自己返回,当服务端数据返回时会有epoll事件监听机制处理。复杂度较高,时延和吞吐较好;适合与对吞吐效率、时延指标要求比较平衡的场景。