最近在跟业务做压测,陆续发现了一些问题。业务中的一些常见性能问题。业务日志行号经常使用日志框架,习惯上从网上复制一份日志配置,保证正常输出。举个栗子:......复制代码,其中%L是行号的输出。日常定位问题启用该配置项很方便,但实际计算成本很高。从结果来看,同样的压测并发和数据集,关闭行号后CPU占用率降低了9%左右。关闭无效日志的行号后,发现日志的打印仍然会占用5%左右的CPU。查看日志和火焰图,发现两个问题:核心高流量接口,过多无效日志输出预期的业务异常,大量打印调用栈。对于堆栈的输出,如果在logPatternFormat中没有指定,log4j2默认会在末尾追加%xEx来格式化异常堆栈。Log4j-Log4j2Layouts%xEx最后通过ExtendedThrowablePatternConverter打印堆栈。该过程涉及类加载、依赖JAR读取等操作,也会占用部分CPU资源(火焰图中看到大概4%)。如果想关闭所有异常堆栈打印,可以在末尾添加%ex{0},但不推荐这样做。当真正出现异常,需要定位问题的时候,栈还是很有帮助的。所以这里还是建议,对于业务异常,输出日志的时候不打印栈;对于意外异常,必须打印堆栈。为了稳定起见,我们决定在旺季到来时彻底关闭INFO日志。在压力测试中,我们实际验证了关机后,这部分资源几乎没有被占用。(图中原始log4j的CPU占用率没有了)最常见的循环调用就是循环调用。如果调用涉及HTTP、RPC等,整体时间会比较长。图中是一个循环RPC调用的例子,解决方法比较简单,循环换batch即可。自建轮坑填压测试时,发现老项目使用了内部的MySQL连接池,发现在获取连接时存在性能瓶颈。当连接数足够时,getConnection会耗费很长时间。更换连接池后恢复正常。ResourceID设计优化本次压测,一张基础服务表数据量较大,从MySQL迁移到TiDB。在TiDB的查询过程中,使用涉及多个资源ID的IN查询来查找资源ID的供应商和分区信息。由于TiDB在写入Hash时使用了默认的自增ID,因此数据在TiKV上的分布非常随机,IN查询可能会涉及多个节点查询,导致性能较差。其中,SQL的平均响应时间最高达到了17ms,第99行也达到了40ms以上。对于涉及多个节点的查询,没有更好的优化方法。考虑到缓存和上层业务优化后,我们认为完全可以从ID设计入手来避免这个问题,即在资源ID前预留1个字节(其实4~6位就够了),写供应商和分区信息输入这个字节,这样就可以直接根据ID计算出结果,完全避免数据库查询。(最终作为长期优化方向,改造周期长,旺季前无法完成)第三方阿里云OSSSDK的高并发性能问题压测时,慢接口的链接被跟踪发现耗时的sdk中有计算签名的方法。实际发现函数中的方法调用耗时很短,但是整个函数的执行时间比较长,所以怀疑是函数中的同步锁操作。这里仔细看会发现初始化了一个单例对象,但是问题是赋值的是函数入参,而Java中的函数参数是传值的,所以这里的赋值并没有赋值调用上层的实际对象,导致每次输入函数时macInstance都为null。一般情况下这里是没有问题的,但是在高并发场景下,多线程竞争锁会导致性能问题。我正在使用3.15.0。对比之前版本的SDK,发现老版本2.8.x没有这个问题。它应该是正式的起点,我想重构代码并将sign方法提取到一个新的父类中,导致这个问题,目前已经提了一个Issue,还是等官方新版本解决吧。(github.com/aliyun/aliy...压测环境使用的JMeterHTTPClient配置了JMeter,压测过程中发现很多CLOSE_WAIT连接,进一步抓包发现大量连接和发起的连接肉鸡在压测过程中被断开,最后发现JMeter脚本没有配置发起HTTP请求的Client类型,此时默认使用httpclient,每次HTTP都要重新建立连接请求,请求结束后连接断开,导致大量的Connection建立和断开,这会带来额外的开销,导致肉鸡能够支持的并发数下降,同时也会带来另一个问题,tcpconnect端口消耗大导致的性能下降,最后我们将ClientImplementation设置为Java,打开keep-al同时,类似的现象将不会再出现。tcpconnect端口消耗大导致的性能下降由大佬@打杯的张师处提供。连接时,linux默认默认遍历选择偶数端口。耗尽后遍历奇数端口。一个容易引起的问题是,当端口被大量消耗时,偶数端口被耗尽。每次connect都会遍历所有的偶数端口,然后遍历奇数端口寻找可用端口,带来性能下降。业务实例压力评估刚开始做单场景/接口压测的时候,web服务是IO密集型的,一个请求大部分时间都在等待DB,第三方调用等,所以实际的数量单实例所能支持的并发数应该远大于核心数。最初,在评估业务实例的压力时,尝试观察CPU使用率是否达到瓶颈。但是随着压测压力的增加,部分实例的CPU使用率虽然没有满,但是吞吐量却提不上去,而随着并发数的增加,CPU依然没有满,吞吐量反而下降了。这里抓到的错误是不能只通过CPU使用率来评估,还需要观察LoadAverage。当容器配置8核时,CPU只跑到600%左右,但最近一分钟的负载已经达到14+,说明有6+个线程处于RUNNABLE状态,等待CPU执行,并且当前负载超过可承载的容量。同时也可以发现cs已经达到了5w+,频繁的上下文切换也导致CPU空闲时间增加,最终导致CPU无法完全运行。有一个关于cs的资料可以参考。我测试了一个8核的apisix。当upstream为空时,qpsmax达到5w左右。这个时候cpu基本满了,load在10左右(多出的2个是一些监控指标采集线程),cpu基本在跑工作进程,cs只有8000左右。容器里的load是不准确的是top命令看到的数据是通过读取/prof/stat得到的,而容器中的目录其实就是挂在宿主机的目录,所以top看到的LoadAverage其实就是宿主机的负载。可以看出我们容器中的进程是没有占用CPU的,但是看到的LoadAverage确实比较高。如果想看比较准确的负载信息,可以通过cAdvisor中采集的Load进行查看,或者通过大佬@打杯的张师士修改的ctop查看。ctop-GitHubgithub.com/arthur-zhan...Load的计算逻辑可以参考LinuxLoadAverages:SolvingtheMystery总结一下,本次压测的几个服务都是比较老的基础服务。他们之前经历过很多压力测试。优化也做得比较好,所以发现的业务问题不多。同时,这个过程也让我们发现,在压测的过程中,在基础压测环境和实例压力的评估,压测流程的规范等方面,还有很大的提升空间。