1。背景最近项目测试遇到了一个奇怪的现象。在测试环境中,通过ApacheHttpClient调用后端HTTP服务的平均耗时接近39.2ms。可能你第一眼觉得这不正常,有什么好奇怪的?其实不是,先说一些基本信息,后台的HTTP服务没有任何业务逻辑,只是把一个字符串转成大写返回,字符串长度只有100个字符,网络ping延迟仅为1.9ms左右。因此,理论上调用应该耗时2-3ms左右,但为什么平均耗时39.2ms呢?由于工作原因,通话费时的问题我并不感到奇怪,而且往往会帮助业务解决。内部RPC框架调用超时的问题,但首次遇到HTTP调用耗时问题。但是,故障排除例程是相同的。主要的方法论无非就是由外而内、自上而下等调查方法。我们先来看一些外围指标,看看能不能看出端倪。2.外围指标2.1系统指标主要是指一些外围系统指标(注意:主叫机和被叫机都要勾选)。例如负载、CPU。只需要一个top命令。因此,确认CPU和负载都处于空闲状态。由于当时没有截图,这里就不放图片了。2.2进程指标Java程序进程指标主要看GC和线程堆栈情况(注意:调用机器和被调用机器都要检查)。YoungGC很少见,不到10ms,所以没有长STW。因为平均调用时间是39.2ms,比较大,如果是代码导致的耗时,线程栈应该能查到点什么。看完之后,我一无所获。服务的相关线程栈主要显示线程池中的线程在等待任务,也就是说线程不忙。您是否觉得自己已无路可走,下一步应该做什么?3、本地复现如果能在本地复现(本地是MAC系统),对于排错也是很好的。因此,我在本地使用ApacheHttpClient写了一个简单的Test程序,直接调用后端HTTP服务,发现平均耗时55ms左右。哎,怎么和测试环境下39.2ms的结果有点不一样。主要原因是本地和测试环境后端的HTTP服务机器是跨地域的,ping延迟在26ms左右,所以延迟增加。不过本地确实有问题,因为ping延迟是26ms,加上后端HTTP服务的逻辑简单,几乎是耗时的,所以本地调用平均时间应该在26ms左右。为什么是55ms?是不是越来越迷茫了?迷茫,不知如何下手?期间怀疑是Apache的HttpClient有问题,于是用JDK自带的HttpURLConnection写了一个简单的程序,测试了一下,结果还是一样。4、诊断4.1定位其实从外围系统指标、进程指标、局部复发等方面来看,大致可以断定不是程序的原因。TCP协议层呢?有过网络编程经验的同学一定知道TCP的哪些参数会导致这种现象。是的,你猜对了,就是TCP_NODELAY。调用者和被调用者的哪个程序没有设置?调用方使用ApacheHttpClient,tcpNoDelay的默认设置为true。让我们看一下被调用者,它是我们的后端HTTP服务。这个HTTP服务使用了JDK自带的HttpServerHttpServerserver=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");returnull;}});}4.2验证HTTP服务在后端,添加“-Dsun.net.httpserver.nodelay=true”参数,再试一次,效果明显,平均时间-消耗从39.2毫秒下降到2.8毫秒。问题解决了,但是如果就此打住,那就太便宜了。这种情况简直就是浪费。因为还有很多疑问等着你?为什么加了TCP_NODELAY后延迟从39.2ms降到了2.8ms?为什么本地测试的平均延迟是55ms,而不是ping26ms?TCP协议是如何发送Packet的?来吧,我们继续趁热打铁。5.困惑5.1谁是TCP_NODELAY?在Socket编程中,TCP_NODELAY选项用于控制是否启用Nagle算法。在Java中,设置true关闭Nagle算法,false打开Nagle算法。你肯定会问什么是Nagle算法?5.2Nagle算法是什么鬼?Nagle算法是一种通过减少网络上发送的数据包数量来提高TCP/IP网络效率的方法。它以其发明者JohnNagle的名字命名,他于1984年首次使用该算法试图解决福特汽车公司的网络拥塞问题。试想一下,如果应用程序每次产生1个字节的数据,然后将这1个字节的数据以网络包的形式发送到远程服务器,那么很容易因为包太多而导致网络不堪重负。在这种典型情况下,传输一个只有1字节有效数据的数据包需要额外开销40字节的长头(即20字节的IP头+20字节的TCP头),这个payload的利用率为极低。Nagle算法的内容比较简单,下面是伪代码:ifthereisnewdatatosendifthewindowsize>=MSSandavailabledatais>=MSSsendcompleteMSSsegmentnowelseifthereisunconfirmeddatastillinthepipeenqueuedatainthebufferuntilanacknowledgeisreceivedelsesenddataimmediatelyendifendifendif具体方法是:如果发送的内容大于等于1MSS之前没有被打包,如果ACK发送缓存内容;如果收到ACK,则立即发送缓存的内容。(MSS是每次一个TCP数据包可以传输的最大数据段)5.3什么是DelayedACK?大家都知道,为了保证传输的可靠性,TCP协议规定在收到一个数据包的时候,需要向对方发送一个消息。确认。简单地发送确认将花费更多(IP标头20字节+TCP标头20字节)。TCP延迟确认(delayedacknowledgement)就是为了提高网络性能而努力解决的这个问题。它将几个ACK响应合并为一个响应,或者将ACK响应和响应数据发送给对方,从而减少协议开销。.具体方法是:当有响应数据要发送时,立即将ACK连同响应数据一起发送给对方;如果没有响应数据,会延迟发送ACK等待看是否有响应数据可以一起发送。在Linux系统中,默认延迟时间为40ms;如果在等待发送ACK时对方的第二个数据包到达,则应立即发送ACK。但是如果对方的三个数据包一个接一个到达,第三个数据段到达时是否立即发送ACK就取决于以上两者。5.4Nagle和DelayedACK会发生什么化学反应?Nagle和DelayedACK都可以提高网络传输的效率,但是放在一起就会好心办坏事。例如下面的场景:A和B进行数据传输:A运行Nagle算法,B运行DelayedACK算法。如果A向B发送数据包,由于DelayedACK,B不会立即响应。而A使用的是Nagle算法,A会等待B的ACK,如果ACK没有来就不发送第二个数据包。如果两个数据包响应同一个请求,那么这个请求会延迟40ms。5.5抓包玩。让我们抓取一个数据包进行验证。在后端HTTP服务上执行以下脚本即可轻松完成抓包过程。sudotcpdump-ieth0tcpandhost10.48.159.165-s0-wtraffic.pcap如下图所示。这是Wireshark分析包内容的展示。红框是一个完整的POST请求处理流程。130序列号和149序列号相差40ms(0.1859-0.1448=0.0411s=41ms),这是Nagle发送DelayedACK的化学反应,其中10.48.159.165运行DelayedACK,10.22.29.180运行纳格尔算法。10.22.29.180在等待ACK,10.48.159.165触发了DelayedACK,所以傻等了40ms。这也解释了为什么测试环境需要39.2ms,因为大部分都被DelayedACK的40ms延迟了。但是在本地重现时,为什么本地测试的平均延迟是55ms,而不是ping26ms?让我们也拿一个数据包。如下图,红框内是一个完整的POST请求处理流程。序号8和序号9相差25ms左右,网络延时大约是ping延时的一半,也就是13ms,所以DelayedAck大约12ms(由于MAC系统和系统有一些差异)Linux本地)。1、Linux使用系统配置/proc/sys/net/ipv4/tcp_delack_min来控制DelayedACK的时间,Linux默认值为40ms;2.MAC通过net.inet.tcp.delayed_ack的系统配置来控制DelayedACK。delayed_ack=0在每个数据包后响应(关闭)delayed_ack=1始终采用延迟ack,6个数据包可以获得1个ackdelayed_ack=2在第二个数据包之后立即ack,每个ack2个数据包(兼容模式)delayed_ack=3应该自动检测何时使用延迟确认,每个确认4个数据包。(DEFAULT)设置为0禁用延迟ACK,设置为1始终延迟ACK,设置为2每两个数据包回复一个ACK,设置为3表示系统自动检测并回复ACK机会。5.6为什么TCP_NODELAY可以解决问题?TCPNODELAY关闭Nagle算法。即使前一个数据包的ACK没有到达,也会发送下一个数据包,从而打破了DelayedACK的影响。一般在网络编程中,强烈建议开启TCPNODELAY,以提高响应速度。当然也可以通过配置DelayedACK相关的系统来解决问题,但是修改机器配置不方便,所以不推荐这种方式。6.小结本文是一次简单的HTTP调用,延迟比较大导致的排查过程。过程中,先由外而内分析相关问题,定位问题,验证解决方案。***追根究底,对TCP传输中的Nagle和DelayedACK进行了全面的讲解,更深入的分析了这个问题的案例。作者:狄胜,本名尹奇,目前就职于一家大型互联网公司的基础设施部,主要负责微服务框架、服务治理、Serverless相关工作。《高可用可伸缩微服务架构:基于Dubbo、Spring Cloud和Service Mesh》的作者之一。本文转载自微信公众号“地生的博客”,可通过以下二维码关注。转载本文请联系迪生博客公众号。
