最近项目测试遇到一个奇怪的现象。在测试环境中,通过ApacheHTTPClient调用后端的HTTP服务,平均耗时接近39.2ms。图片来自Pexels。乍一看,您可能认为这不正常。有什么奇怪的?它不是。我先说一些基本信息。后端HTTP服务没有任何业务逻辑。它只是将字符串转换为大写并返回它。字符串长度只有100个字符,网络ping延迟只有1.9ms左右。因此,理论上调用应该耗时2-3ms左右,但为什么平均耗时39.2ms呢?CalldelayPingdelay由于工作原因,通话耗时问题对我来说并不奇怪,经常帮助业务解决内部RPC框架调用超时的相关问题,但HTTP调用耗时还是第一次。但是,故障排除例程是相同的。主要的方法论无非就是由外而内、自上而下等调查方法。我们先来看一些外围指标,看看能不能看出端倪。外设指标系统指标主要看一些外设系统指标(注意:主叫和被叫机器都要看)。例如负载、CPU。只需要一个Top命令。因此,确认CPU和负载都处于空闲状态。由于当时没有截图,这里就不放图片了。进程指标Java程序进程指标主要看GC和线程堆栈情况(注意:调用机器和被调用机器都要检查)。YoungGC很少见,不到10ms,所以没有长STW。因为平均调用时间是39.2ms,比较大,如果是代码导致的耗时,线程栈应该能查到点什么。看完之后,我一无所获。服务的相关线程栈主要显示线程池中的线程在等待任务,也就是说线程不忙。是不是觉得驴子已经穷途末路了,接下来怎么办?本地复现如果能在本地复现(本地是MAC系统),对于故障排除也是极好的。因此,我在本地使用ApacheHTTPClient写了一个简单的Test程序,直接调用后端HTTP服务,发现平均耗时55ms左右。哎,怎么和测试环境下39.2ms的结果有点不一样。主要原因是本地和测试环境后端的HTTP服务机器是跨地域的,Ping延迟在26ms左右,所以延迟增加。不过本地确实有问题,因为Ping延迟26ms,后端HTTP服务逻辑简单,几乎不耗时。因此,平均本地调用时间应该在26ms左右。为什么是55ms?是不是越来越迷茫了?迷茫,不知如何下手?期间一直在想是不是ApacheHTTPClient出了什么问题。因此,我利用JDK自带的HttpURLConnection写了一个简单的程序,测试了一下,结果是一样的。诊断定位其实从外围系统指标、过程指标、局部复发等方面来看,大致可以断定不是程序性原因。TCP协议层呢?有过网络编程经验的同学一定知道TCP的哪些参数会导致这种现象。是的,你猜对了,就是TCP_NODELAY。调用者和被调用者的哪个程序没有设置?调用方使用ApacheHttpClient,tcpNoDelay默认设置为True。再来看看被叫方,也就是我们后端的HTTP服务。这个HTTP服务使用了JDK自带的HttpServer:HttpServerserver=HttpServer.create(newInetSocketAddress(config.getPort()),BACKLOGS);直接设置tcpNoDelay接口,翻源码。哦,就是这样。ServerConfig类中有这个静态块,用于获取启动参数。默认ServerConfig.noDelay为false:static{AccessController.doPrivileged(newPrivilegedAction(){publicVoidrun(){ServerConfig.idleInterval=Long.getLong("sun.net.httpserver.idleInterval",30L)*1000L;ServerConfig。clockTick=Integer.getInteger("sun.net.httpserver.clockTick",10000);ServerConfig.maxIdleConnections=Integer.getInteger("sun.net.httpserver.maxIdleConnections",200);ServerConfig.drainAmount=Long.getLong("sun.net.httpserver.drainAmount",65536L);ServerConfig.maxReqHeaders=Integer.getInteger("sun.net.httpserver.maxReqHeaders",200);ServerConfig.maxReqTime=Long.getLong("sun.net.httpserver.maxReqTime",-1L);ServerConfig.maxRspTime=Long.getLong("sun.net.httpserver.maxRspTime",-1L);ServerConfig.timerMillis=Long.getLong("sun.net.httpserver.timerMillis",1000L);ServerConfig.debug=Boolean.getBoolean("sun.net.httpserver.debug");ServerConfig.noDelay=Boolean.getBoolean("sun.net.httpserver.nodelay");returnnull;}});}后端验证HTTP服务,添加"-Dsun.net.httpserver.nodelay=true"参数start测试效果明显,平均耗时从39.2ms下降到2.8ms:优化后调用延迟的问题解决了,但是如果就此打住,那就太便宜了。在这种情况下,简直就是浪费钱。因为还有很多疑惑等着你:为什么加了TCP_NODELAY后延迟从39.2ms降到了2.8ms?为什么本地测试Ping平均延迟是55ms而不是26ms?TCP协议是如何发送数据包的?来,趁热打铁吧。困惑①TCP_NODELAY是谁?在Socket编程中,TCP_NODELAY选项用于控制是否启用Nagle算法。在Java中,设置True关闭Nagle算法,设置False打开Nagle算法。你肯定会问什么是Nagle算法?②Nagle算法是什么鬼?Nagle算法是一种通过减少网络上发送的数据包数量来提高TCP/IP网络效率的方法。它以其发明者JohnNagle的名字命名,他于1984年首次使用该算法试图解决福特汽车公司的网络拥塞问题。试想一下,如果应用程序每次产生1个字节的数据,然后将这1个字节的数据以网络包的形式发送到远程服务器,那么很容易因为包太多而导致网络不堪重负。在这种典型情况下,发送一个只有1字节有效数据的数据包需要额外开销40字节的长头(即20字节的IP头+20字节的TCP头),这个payload的利用率为极低。Nagle算法的内容比较简单,下面是伪代码:ifthereisnewdatatosendifthewindowssize>=MSSandavailabledatais>=MSSsendcompleteMSSsegmentnowelseifthereisunconfirmeddatastillinthepipeenqueuedatainthebufferuntilanacknowledgeisreceivedelsesenddataimmediatelyendifendif具体方法是:如果发送的内容大于或等于1个MSS,则在没有包前被ACK,如果没有立即发送。如果有之前没有被ACK过的数据包,缓冲区就发送内容。如果收到ACK,则立即发送缓冲的内容。(MSS是一个TCP包每次可以传输的最大数据段)③什么是DelayedACK?我们都知道,为了保证传输的可靠性,TCP协议规定在收到数据包时需要向对方发送确认。简单地发送确认将花费很多(IP标头中的20个字节+TCP标头中的20个字节)。TCP延迟确认(delayedacknowledgement)就是试图通过提高网络性能来解决这个问题。它将几个ACK响应组合成一个响应,或者将ACK响应与响应数据一起发送给对方,从而减少协议开销。具体方法是:当有响应数据要发送时,立即将ACK连同响应数据一起发送给对方。如果没有响应数据,ACK将被延迟等待,看是否有响应数据可以随之发送。在Linux系统中,默认的延迟时间是40ms。如果在等待发送ACK时对方的第二个数据包到达,那么应该立即发送ACK。但是如果对方的三个数据包一个接一个到达,第三个数据段到达时是否立即发送ACK就取决于以上两者。④Nagle和DelayedACK一起作用会发生什么化学反应?Nagle和DelayedACK都可以提高网络传输的效率,但是放在一起就会好心办坏事。例如,在如下场景中,A和B进行数据传输:A运行Nagle算法,B运行DelayedACK算法。如果A向B发送数据包,由于DelayedACK,B不会立即响应。而A使用的是Nagle算法,A会等待B的ACK,如果ACK没有来就不发送第二个数据包。如果两个数据包响应同一个请求,那么这个请求会延迟40ms。⑤抓起一个小包来玩吧。让我们抓一个数据包并验证它。在后端HTTP服务上执行以下脚本即可轻松完成抓包过程。sudotcpdump-ieth0tcpandhost10.48.159.165-s0-wtraffic.pcap如下图,是使用Wireshark分析包内容的展示。红框是一个完整的POST请求处理流程。根据测试环境数据包分析,130序号和149序号相差40ms(0.1859-0.1448=0.0411s=41ms)。这是Nagle发送延迟ACK的化学反应。其中10.48.159.165运行DelayedACK,10.22.29.180运行Nagle算法。10.22.29.180在等待ACK,10.48.159.165触发了DelayedACK,所以傻等了40ms。这也解释了为什么测试环境需要39.2ms,因为大部分都被DelayedACK的40ms延迟了。但是在本地复现的时候,为什么Ping本地测试的平均延迟是55ms,而不是26ms?让我们也拿一个数据包。如下图,红框内是一个完整的POST请求处理流程。8和9序号相差25ms左右,网络延迟大约是Ping延迟的一半,也就是13ms。本地环境包分析,所以DelayedAck在12ms左右(由于本地MAC系统和Linux有些差异)。Linux使用系统配置/proc/sys/net/ipv4/tcp_delack_min来控制DelayedACK的时间,Linux默认为40ms;2、MAC通过net.inet.tcp.delayed_ack系统配置控制DelayedACK。delayed_ack=0respondsaftereverypacket(OFF)delayed_ack=1alwaysemploysdelayedack,6packetscanget1ackdelayed_ack=2immediateackafter2ndpacket,2packetsperack(CompatibilityMode)delayed_ack=3shouldautodetectwhentoemploysdelayedack,4packetsperack。(DEFAULT)设置为0表示总是延迟每个数据包回复ACK,设置为3表示系统自动检测回复ACK的时机。⑥为什么TCP_NODELAY可以解决问题?tcpNoDelay关闭Nagle算法。即使前一个数据包的ACK没有到达,也会发送下一个数据包,从而打破了DelayedACK的影响。一般在网络编程中,强烈建议开启tcpNoDelay以提高响应速度。当然也可以通过配置DelayedACK相关的系统来解决问题,但是修改机器配置不方便,所以不推荐这种方式。总结本文是一次简单的HTTP调用,延迟比较大导致的排查过程。首先由外而内分析相关问题,定位问题,验证解决方案。最后对TCP传输中的Nagle和DelayedACK进行了透彻的讲解,对问题案例的分析也比较透彻。