记一次UDP接入服务的性能测试
前言
最近在项目上做了一个对数据接入服务器的性能测试,使用了UDP协议,而且调试过程还挺有意思的,在此记录一下。
背景简介
服务架构
服务器和数据流大致如下图:
终端设备的主要功能是上传GPS,使用的是UDP协议,由数据接入服务接收后转发至消息队列,最终由应用服务存储至数据库。其中数据接入服务部署了两台,由nginx进行负载均衡,同时nginx使用keepalived做了故障转移,这也就是本次测试的主要涉及的内容,其他与本次测试无关的细节就不赘述了。
测试需求
测试需求为需要支撑至少2000台终端设备同时在线:每台设备每10秒上传一条GPS信息,如果服务器没有响应或响应失败,会在下一次上报信息时进行补传。
这个项目本来不是由我负责测试,但是由于该项目中之前有部分TCP协议的设备终端,并且使用了我之前编写的设备模拟器,项目组希望可以修改原有模拟器,使之支持UDP协议用在该性能测试上,所以将我拉了进来,我也得以参与此次测试。
测试分析
测试工具
工具选型
虽然有之前的模拟器的基础,但是与开发人员沟通后发现两种设备的区别并不仅仅是通信协议的类型,协议的内容也有相当大的区别,并不是直接修改一下传输协议就可以的。不过两者的区别更多的是UDP协议设备比TCP协议设备要简化不少,比如UDP设备不需要进行鉴权就可以直接发送消息,所以其实只要发送一条UDP消息即可,并且发送的数据相同也不会影响测试结果。所以最终没有采用修改原有模拟器的方式,而是选择了jmeter这个更为专业的性能测试工具来进行。
Jmeter 配置
Jmeter默认是不支持UDP协议的,需要安装插件,通过Jmeter的插件管理器即可安装,如图:
安装后可以添加 jp@gc - UDP Request
取样器,如图:
当中大部分字段都很容易理解,只有 Data Encode/Decode class
需要特别注意,官方说明如下:
Full Class Name | Comments |
---|---|
kg.apc.jmeter.samplers.HexStringUDPDecoder | This is most useful implementation, converts data from/to HEX-encoded sequences. For example, 6a6d6574657220706c7567696e73 corresponds to jmeter plugins. |
kg.apc.jmeter.samplers.DNSJavaDecoder | Request data must contain three fields, separated with spaces: name, type, class. Example: www.com. A IN. Response data converted to text using DNSJava. Request flags can be set using +/- integer value on new line, eg 7 sets reqursion desired flag. |
kg.apc.jmeter.samplers.UDPSampler | This implementation used as default when no valid class name specified in GUI. It makes no conversion on data. |
这里选用哪个取决于项目中的通信协议,我这里使用的是 kg.apc.jmeter.samplers.HexStringUDPDecoder
,核心脚本示例如下:
测试场景
分析服务架构和测试需求,确定需要测试并发200设备、并发400设备、2000设备同时在线和4000设备同时在线。另外由于有负载均衡和故障转移机制,需要分别测试发送消息至数据接入、nginx和keepalived虚拟IP,以利于分析瓶颈。
测试及优化过程
数据接入服务
首先对数据接入服务测试200并发,发现异常率较高,使用 netstat -su
查看发现存在UDP丢包,查看系统默认和最大缓冲区配置均为208k,尝试调整至1m后没有作用;后建议开发人员在程序中调用 setsockopt
调整缓冲区至1m,首次尝试仍然不起作用,发现系统缓冲区 rmem_max
被恢复成小于1m,于是将 rmem_max
修改为1m后重启接入程序,成功通过所有测试场景。缓冲区查询及临时修改方法如下:
# 查询
cat /proc/sys/net/core/rmem_default
> 212992
cat /proc/sys/net/core/rmem_max
> 212992
# 临时修改(重启后失效)
echo 1048576 > /proc/sys/net/core/rmem_default
echo 1048576 > /proc/sys/net/core/rmem_max
Nginx + Keepalived
测试下来发现这两个端口没有区别,就放在一起说。在对nginx进行200并发时,同样发生了丢包的情况,根据之前的经验就开始调整系统缓冲区配置和nginx中的缓冲区配置 proxy_buffer_size
,但是无论怎么调整,都没有什么作用。后来发现了 proxy_responses
参数,当前配置为1,尝试注释掉后,异常率有所下降。后来 发现nginx的进程数配置只有1,于是尝试改成2,结果大有好转,异常率降为0。
进行400并发测试时,又出现异常,于是将nginx进程数改为4,但此时异常率又大幅升高,这就很奇怪了,因为虚拟机的核心数是4,配置nginx进程数为4不应该有问题才对。后来经过排查发现keepalived中检查nginx健康状态的脚本写的有问题,原始文件如下:
#!/bin/bash
run=`ps -C nginx --no-header |wc -l`
if [ $run -eq 5 ]; then
/path/of/nginx -s stop
/path/of/nginx
sleep 3
if [ `ps -C nginx --no-header | wc -l` -eq 5 ]; then
systemctl stop keepalived
fi
fi
可以看到当Nginx进程数配置为4时,两处if的判断应该是ne却写成了eq,造成Nginx一直在重启,以致请求失败。修改后所有测试结果均正常。结果也反馈给了该项目的测试负责人进行后续跟踪。
另外,在调试nginx配置的时候发现 proxy_buffer_size
和想象中的效果并不一样,包括系统中 rmem_default
配置,增大或者缩小并不会影响nginx请求转发的效果。还有 proxy_responses
参数,虽然我们的程序是一个请求对应一个响应,但是配置为1的时候总会出现异常,而不配置使用默认的时候却很正常。由于这次测试默认值即可满足要求,暂未进行过多研究,对此有了解的小伙伴欢迎与我探讨。
总结
用到的指令
- UDP丢包查看
netstat -su
- 系统缓冲区大小查看
cat /proc/sys/net/core/rmem_default
cat /proc/sys/net/core/rmem_max
- 系统缓冲区大小临时修改
echo 1048576 > /proc/sys/net/core/rmem_default
echo 1048576 > /proc/sys/net/core/rmem_max
- 系统缓冲区大小永久修改
vim /etc/sysctl.conf # 在打开的文件中加入下面两行并保存
net.core.rmem_default = 1048576
net.core.rmem_max = 1048576
sysctl -p # 配置立即生效,或重启系统
- C程序中设置缓冲区
optlen = VALUE_YOU_WANT;
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvBufSize, optlen) < 0)
{
printf("setsockopt error=%d(%s)!!!\n", errno, strerror(errno));
...
}
- Nginx中相关配置
...
worker_processes 4; # 进程数量,根据CPU核心数量配置
...
upstream udp_upstreams { # 负载均衡配置
server {ip}:{port};
server {ip}:{port};
}
server {
listen {port} udp;
proxy_pass udp_upstreams;
proxy_timeout 15s;
# proxy_responses 1; # 转发请求后预期的响应数量,配置为1时会有少量异常,使用默认没有问题,猜测可能是配置具体数量后,nginx需要进行计数,导致处理速度变慢
# proxy_buffer_size 4k; # 缓冲区配置,默认16k,经测试,该配置与这次测试结果没有什么关联
}
...
注意事项
当程序缓冲区配置大于系统 rmem_max
配置时,是不会生效的,需要先将系统 rmem_max
配置成大于等于程序中的配置,再启动程序
心得体会
排查问题时应该尽量避免思维惯性,检查所有可能性,多进行尝试;如这次测试nginx与接入服务的优化点是不一样的,在这里消耗了较多本不需要的精力