从语雀迁移到个人博客,知乎也转载一份。本文记录了一次nodejs压测的问题解决过程。https://github.com/metroluffy/blog/issues/31github.com
更新:补一条爝神的评论,依赖io资源上timeout,然后做熔断qps就可以上去。
背景是基于Egg开发的一个node Web应用,在压测中qps没到理想情况,在Daruk交流群请教以后,在此做些记录。
8c16g机器,8个worker进程,请求通过spring cloud getway直接打到node qps不到300,cpu使用率20%(max 26%),实压1000qps 。
压测工具:jmeter(阿里云)
小爝之前有发过一次node应用压测的想法,记录如下:
1,压力测试的时候,需要系统参数调优,本机不是linux,一定要上开发机,把内核参数搞好。
2,压力测试不能压测脚本和被压的服务在一个机器,会互相影响。
3,开gzip压和不开gzip压差别非常大。
4,不同的压测工具,比如ab,webbench,wrk,http_load实现的原理不一样,压出来的结果也不一样。
5,压测机的系统参数也要调优,否则发不出去那么大的请求和起步起来那么多的连接数。
6,压测机越好,结果越好,对被压的服务压力越大,因为客户端压测机处理tcp结果, 处理多线程的能力,测试时客户机的CPU状态、内存状态都会对测试结果造成非常大的影响。
7,使用缓存服务时,序列化是nodejs瓶颈,使用msgpack可以提高并发,存取都直接是object,缓存的内容是buffer,使用zlib可以大大降低保存的buffer内容体积,虽然占用了计算,但是缓存服务一般都是外部网络服务,缓存的体积大,IO的损耗会更大(体积影响下载时间)。
8,本地cache需要压缩保存,使用时再解压缩,更合理利用前端机的内存。
9,mc不要存超过1MB的东西。
10,ejs的模板预编译缓直接缓存render 函数即可,minHTML操作如果不加cache,计算量会特别大,所以如果不cache整个html内容,那不如不压html。
11,nodejs性能非常好,qps底是因为你压的不对。。
问题点一:未做Gzip
加了Gzip后发现qps还是没有上去QAQ~
问题点二:针对node的系统调参(做了)
最大连接数限制 最大连接数限制就是系统所能打开的最大文件数(文件描述符)的限制,分全局和进程两种,相应的命令如下:
$ sysctl kern.maxfiles 输出:kern.maxfiles: 12288 说明:全局限制,也就是系统默认的最大连接数限制是12288
$ sysctl kern.maxfilesperproc 输出:kern.maxfilesperproc: 10240 说明:单个进程默认最大连接数限制是10240
$ sudo sysctl -w kern.maxfiles=1048600 输出:kern.maxfiles: 12288 -> 1048600 说明:设置系统最大连接数从12288到1048600
$ sudo sysctl -w kern.maxfilesperproc=1048576 输出:kern.maxfilesperproc: 10240 -> 1048576 说明:设置进程连接数限制,进程的最大连接数要小于等于全局连接数
ulimit命令 $ ulimit -n 输出:2560 说明:“ulimit -n”命令显示当前shell能打开的最大文件数,默认值:2560,该值总是小于kern.maxfilesperproc的值,因为一个shell就是一个进程。
$ ulimit -n 1048576 说明:设置当前shell能打开的最大文件数为1048576,该值不能大于kern.maxfilesperproc,否则会提示设置失败。
动态端口范围 $ sysctl net.inet.ip.portrange 输出: net.inet.ip.portrange.first: 49152 net.inet.ip.portrange.last: 65535
Linux动态端口号默认范围是32768-65535,也就是说,作为客户端连接同一个IP和同一个端口号,最多只能建立30000多个连接,而Mac默认只能建立16000个左右的连接。
$ sysctl -w net.inet.ip.portrange.first=32768 说明:设置动态分配起点端口号为32768,这样可以增大客户端可以建立的连接数。
参考: The IANA list of port numbers includes the well-known and registered port numbers and specifies the dynamic port number range.
问题 按以上的方式设置参数有个问题,当系统重启后,这些参数又恢复成了默认值,解决办法就是把参数写到/etc/sysctl.conf文件中,但是,默认这个文件是不存在的,所以首先就要创建它:
sudo touch /etc/sysctl.conf 然后把参数写到文件里
kern.maxfiles=1048600 kern.maxfilesperproc=1048576 net.inet.ip.portrange.first=49152 net.inet.ip.portrange.last=65535 重启系统,查看结果,显示成功。
至于ulimit-n的值,可以把ulimit-n 1048576 写到.bashrc中实现自动修改。
此外可以看下nodejs的pid设置,cat /proc/{pid}。
有关connection reset error
链接被重置,有可能是本地reset的也可能是服务端reset的,这个情况很常见,需要自己处理,比如加重试逻辑,http request的话建议加3次retry(retry-request这个包可以用),错误能少很多。然后还有几个情况会导致,比如你请求的时候没有做http keep alive的agent导致并发高了,本地连接池不够出错。还有就是nodejs的dns很辣鸡,不要开ipv6,建议把lookup方法重写了,强制ipv4解析,加nodejs自己的dns cache(dnscache 这个包可以用)。 重写lookup方法。
function reWriteDNSLookup() {
const overwriteLookup = dns.lookup;
dns.lookup = function lookup(
domain: string,
options: any,
callback: (err: any, address: string | any[], family: number) => void
): void {
if (options.family !== 4) options.family = 4;
return overwriteLookup(domain, options, callback);
};
}
DNS 应该不太会影响 Connection Rest by peer ,应该是整体耗时增长,但是不这么设置,并发高的时候会有另外一个DNS XXX的错。
最后
排除以上问题后,还是上不去,后面经排查日志发现是慢请求导致的,应用本身是作为一个BFF层存在,大量业务需要查询下游其他业务系统,当下游存在瓶颈时,自然上游的qps就上不去了,惊不惊喜。。。此时有两种方法还可以优化,针对get请求,可以做一个1s cache,Redis、mc或者node的lru-cache(内存缓存),第二一个就是优化下游的慢请求咯,不然BFF层的意义其实不大。值得一提的是,必要的时候可以做熔断机制,避免因为下游的某个服务导致全线挂掉。