1230万个并发WebSocket

关于WebSockets的一件事是,您需要在客户端上提供大量资源来生成足够高的负载,以使服务器实际上耗尽所有CPU资源。


您必须克服几个挑战,因为WebSockets协议在客户端比在服务器端对CPU的要求更高。 同时,如果您有数百万个打开的连接,则需要大量RAM来存储有关打开的连接的信息。


我很幸运能够在有限的时间内购买到几台新服务器,以进行硬件“老化”测试。 因此,我决定使用我的Lua应用服务器-LAppS来完成这两项工作:测试硬件并执行LAppS高负载测试。


让我们看看细节


挑战性


首先,客户端上的WebSocket需要非常快速的RNG进行流量屏蔽,除非您想伪造它。 我希望测试是现实的,所以我放弃了用常量替换RNG的想法。 剩下的唯一选择-大量的CPU功能。 就像我说的,我有两台服务器:一台带有双Intel Gold 6148 CPU(总共40个内核)256GB RAM(DDR4-2666),一台带有四核Intel Platinum 8180 CPU(总共112个内核)384GB RAM(DDR4-2666) 。 此后,我将它们称为服务器A和服务器B。


第二个挑战-没有足够快的WebSocket库或测试套件。 这就是为什么我被迫为LAppS( cws )开发WebSockets客户端模块。


测试设置


两台服务器均具有双端口10GbE卡。 每个卡的端口1聚合到绑定接口,并且每个卡上的这些端口都以RR模式彼此连接。 每个卡的端口2以活动备份模式聚合到绑定接口,并通过以太网交换机连接。 每个硬件服务器都在运行RHEL 7.6。 我必须从源代码安装gcc-8.3才能拥有现代的编译器,而不是默认的gcc-4.8.5。 这不是性能测试的最佳设置,但是乞g不能成为选择者。


服务器A(2xGold 6148)上的CPU的频率高于服务器B(4xPlatinum 8180)上的CPU。 同时,服务器B具有更多核心,因此我决定在服务器A上运行回显服务器,在服务器B上运行回显客户端。


我想了解LAppS在具有数百万个连接的高负载下的行为,以及每秒可以服务的最大回声请求数量。 实际上,这是两组不同的测试,因为随着连接数量的增长,服务器和客户端的工作越来越复杂。


在此之前,我已经在用于开发的家用PC上进行了所有测试。 这些测试的结果将用于比较。 我的家用PC具有Intel Core i7-7700 CPU 3.6GHz(4.0GHz Turbo),4个内核和32GB DDR4-2400 RAM。 这台PC运行带有4.14.72内核的Gentoo。


幽灵和崩溃补丁级别
  • 家用电脑
    /sys/devices/system/cpu/vulnerabilities/l1tf:Mitigation: PTE Inversion; VMX: conditional cache flushes, SMT vulnerable /sys/devices/system/cpu/vulnerabilities/meltdown:Mitigation: PTI /sys/devices/system/cpu/vulnerabilities/spec_store_bypass:Mitigation: Speculative Store Bypass disabled via prctl and seccomp /sys/devices/system/cpu/vulnerabilities/spectre_v1:Mitigation: __user pointer sanitization /sys/devices/system/cpu/vulnerabilities/spectre_v2:Vulnerable: Minimal generic ASM retpoline, IBPB, IBRS_FW 
  • 服务器A和B
     /sys/kernel/debug/x86/ibpb_enabled : 1 /sys/kernel/debug/x86/pti_enabled : 1 /sys/kernel/debug/x86/ssbd_enabled : 1 /sys/kernel/debug/x86/ibrs_enabled : 1 /sys/kernel/debug/x86/retp_enabled : 3 

无论是否启用了这些修补程序,我都会对服务器A和B的测试结果做一个额外说明。


在我的家用PC上,补丁级别从未更改。


安装LAppS 0.8.1


安装必备组件和LAppS

LAppS依赖于luajit-2.0.5或更高版本,libcrypto ++ 8.2和wolfSSL-3.15.7库,它们必须从RHEL 7.6以及可能的任何其他Linux发行版中的源安装。


安装的前缀是/ usr / local。 这是WolfSSL安装的几乎不言自明的Dockerfile部分


 ADD https://github.com/wolfSSL/wolfssl/archive/v3.15.7-stable.tar.gz ${WORKSPACE} RUN tar xzvf v3.15.7-stable.tar.gz WORKDIR ${WORKSPACE}/wolfssl-3.15.7-stable RUN ./autogen.sh RUN ./configure CFLAGS="-pipe -O2 -march=native -mtune=native -fomit-frame-pointer -fstack-check -fstack-protector-strong -mfpmath=sse -msse2avx -mavx2 -ftree-vectorize -funroll-loops -DTFM_TIMING_RESISTANT -DECC_TIMING_RESISTANT -DWC_RSA_BLINDING" --prefix=/usr/local --enable-tls13 --enable-openssh --enable-aesni --enable-intelasm --enable-keygen --enable-certgen --enable-certreq --enable-curve25519 --enable-ed25519 --enable-intelasm --enable-harden RUN make -j40 all RUN make install 

这是libcrypto ++安装的一部分


 ADD https://github.com/weidai11/cryptopp/archive/CRYPTOPP_8_2_0.tar.gz ${WORKSPACE} RUN rm -rf ${WORKSPACE}/cryptopp-CRYPTOPP_8_2_0 RUN tar xzvf ${WORKSPACE}/CRYPTOPP_8_2_0.tar.gz WORKDIR ${WORKSPACE}/cryptopp-CRYPTOPP_8_2_0 RUN make CFLAGS="-pipe -O2 -march=native -mtune=native -fPIC -fomit-frame-pointer -fstack-check -fstack-protector-strong -mfpmath=sse -msse2avx -mavx2 -ftree-vectorize -funroll-loops" CXXFLAGS="-pipe -O2 -march=native -mtune=native -fPIC -fomit-frame-pointer -fstack-check -fstack-protector-strong -mfpmath=sse -msse2avx -mavx2 -ftree-vectorize -funroll-loops" -j40 libcryptopp.a libcryptopp.so RUN make install 

和luajit


 ADD http://luajit.org/download/LuaJIT-2.0.5.tar.gz ${WORKSPACE} WORKDIR ${WORKSPACE} RUN tar xzvf LuaJIT-2.0.5.tar.gz WORKDIR ${WORKSPACE}/LuaJIT-2.0.5 RUN env CFLAGS="-pipe -Wall -pthread -O2 -fPIC -march=native -mtune=native -mfpmath=sse -msse2avx -mavx2 -ftree-vectorize -funroll-loops -fstack-check -fstack-protector-strong -fno-omit-frame-pointer" make -j40 all RUN make install 

LAppS的一个可选依赖项(可以忽略)是Microsoft mimalloc的新库。 该库可显着提高性能(约1%),但要求cmake-3.4或更高。 鉴于测试时间有限,我决定牺牲提到的性能改进。


在家用PC上,我不会在测试期间禁用mimalloc


让我们从存储库中签出LAppS:


 WORKDIR ${WORKSPACE} RUN rm -rf ITCLib ITCFramework lar utils LAppS RUN git clone https://github.com/ITpC/ITCLib.git RUN git clone https://github.com/ITpC/utils.git RUN git clone https://github.com/ITpC/ITCFramework.git RUN git clone https://github.com/ITpC/LAppS.git RUN git clone https://github.com/ITpC/lar.git WORKDIR ${WORKSPACE}/LAppS 

现在,我们需要从nbproject子目录中的所有Makefile中删除“ -lmimalloc”,以便我们可以构建LAppS(假设当前目录为$ {WORKSPACE} / LAppS)


 # find ./nbproject -type f -name "*.mk" -exec sed -i.bak -e 's/-lmimalloc//g' {} \; # find ./nbproject -type f -name "*.bak" -exec rm {} \; 

现在我们可以构建LAppS。 LAppS提供了几种构建配置,这些配置可能会也可能不会排除服务器端的某些功能:


  • 具有SSL支持和统计信息收集支持
  • 使用SSL且不收集统计信息(尽管在运行时用于动态LAppS调整的情况下,将继续进行最少的统计收集)
  • 没有SSL,也没有统计信息收集。

在进行下一步之前,请确保您是/ opt / lapps目录的所有者(或使用sudo运行make installs)


让我们创建两种具有SSL支持和统计信息收集功能的二进制文件(不包含在$ {WORKSPACE} / LAppS目录中):


 # make clean # make CONF=Release.AVX2 install # make CONF=Release.AVX2.NO_STATS.NO_TLS install 

生成的二进制文件是:


  • dist / Release.AVX2 / GNU-Linux / lapps.avx2
  • dist / Release.AVX2.NO_STATS.NO_TLS / GNU-Linux / lapps.avx2.nostats.notls

它们将被安装到/ opt / lapps / bin中。


请注意,用于Lua的WebSockets客户端模块始终带有SSL支持。 是否启用它取决于您在运行时用于连接的URI(ws://或wss://)。


测试1.家用PC上的最佳性能。 基线配置。


我已经确定,当我配置四个基准测试服务实例(每个实例具有100个连接)时,我将获得最佳性能。 同时,我仅需要三个IOWorker和四个回显服务实例即可达到最佳性能。 还记得吗 我这里只有4个核心。


该测试的目的只是为进一步比较建立基准。 真的没什么令人兴奋的。


下面是为测试配置LAppS所需的步骤。


自签名证书


certgen.sh脚本
 #!/bin/bash openssl genrsa -out example.org.key 2048 openssl req -new -key example.org.key -out example.org.csr openssl genrsa -out ca.key 2048 openssl req -new -x509 -key ca.key -out ca.crt openssl x509 -req -in example.org.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out example.org.crt cat example.org.crt ca.crt > example.org.bundle.crt 

从/ opt / lapps / conf / ssl目录中运行此脚本将创建所有必需的文件。 这是脚本的输出以及我输入的内容:


certgen.sh输出
 # certgen.sh Generating RSA private key, 2048 bit long modulus .................................................................................................................................................................+++++ .....................................+++++ e is 65537 (0x010001) You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]:KZ State or Province Name (full name) [Some-State]:none Locality Name (eg, city) []:Almaty Organization Name (eg, company) [Internet Widgits Pty Ltd]:NOORG.DO.NOT.FORGET.TO.REMOVE.FROM.BROWSER Organizational Unit Name (eg, section) []: Common Name (eg server FQDN or YOUR name) []: Email Address []: Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: An optional company name []: Generating RSA private key, 2048 bit long modulus ...+++++ ............................................................................+++++ e is 65537 (0x010001) You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]:KZ State or Province Name (full name) [Some-State]:none Locality Name (eg, city) []:Almaty Organization Name (eg, company) [Internet Widgits Pty Ltd]:none Organizational Unit Name (eg, section) []: Common Name (eg server FQDN or YOUR name) []:*.example.org Email Address []: Signature ok subject=C = KZ, ST = none, L = Almaty, O = NOORG.DO.NOT.FORGET.TO.REMOVE.FROM.BROWSER Getting CA Private Key 

LAppS配置


这是为TLS 1.3设置的WebSockets配置文件,包括上面生成的证书,并配置了3个IOWorkers(请参阅LAppS Wiki中的变量详细说明)。


/opt/lapps/etc/conf/ws.json
 { "listeners" : 2, "connection_weight": 1.0, "ip" : "0.0.0.0", "port" : 5083, "lapps_config_auto_save" : true , "workers" : { "workers": 3, "max_connections" : 40000, "auto_fragment" : false, "max_poll_events" : 256, "max_poll_wait_ms" : 10, "max_inbounds_skip" : 50, "input_buffer_size" : 2048 }, "acl" : { "policy" : "allow", "exclude" : [] }, "tls":true, "tls_server_version" : 4, "tls_client_version" : 4, "tls_certificates":{ "ca":"/opt/lapps/conf/ssl/ca.crt", "cert": "/opt/lapps/conf/ssl/example.org.bundle.crt", "key": "/opt/lapps/conf/ssl/example.org.key" } } 

配置服务:回显服务器和回显客户端(基准),每个都有四个实例。


/opt/lapps/etc/conf/lapps.json
 { "directories": { "app_conf_dir": "etc", "applications": "apps", "deploy": "deploy", "tmp": "tmp", "workdir": "workdir" }, "services": { "echo": { "auto_start": true, "instances": 4, "internal": false, "max_inbound_message_size": 16777216, "protocol": "raw", "request_target": "/echo", "acl" : { "policy" : "allow", "exclude" : [] } }, "benchmark": { "auto_start": true, "instances" : 4, "internal": true, "preload": [ "nap", "cws", "time" ], "depends" : [ "echo" ] } } } 

echo服务非常简单:


回显服务源代码
 echo = {} echo.__index = echo; echo.onStart=function() print("echo::onStart"); end echo.onDisconnect=function() end echo.onShutdown=function() print("echo::onShutdown()"); end echo.onMessage=function(handler,opcode, message) local result, errmsg=ws:send(handler,opcode,message); if(not result) then print("echo::OnMessage(): "..errmsg); end return result; end return echo 

基准服务会创建多达基准 .target的beta.max_connections,然后一直运行直到停止LAppS。 连接的建立没有暂停,也没有回应请求的轰炸。 cws模块API类似于WebSockets的Web API。 一旦建立了所有Benchmark.max_connections ,基准就会打印已连接的Socket数量。 建立连接后,基准测试将向服务器发送一个基准测试消息。 服务器回复后,将调用cws对象的匿名onmessage方法,该方法只会将相同的消息发送回服务器。


基准服务源代码
 benchmark={} benchmark.__index=benchmark benchmark.init=function() end benchmark.messages_counter=0; benchmark.start_time=time.now(); benchmark.max_connections=100; benchmark.target="wss://127.0.0.1:5083/echo"; benchmark.message="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; benchmark.meter=function() benchmark.messages_counter=benchmark.messages_counter+1; local slice=time.now() - benchmark.start_time; if( slice >= 1000) then print(benchmark.messages_counter.." messages received per "..slice.."ms") benchmark.messages_counter=0; benchmark.start_time=time.now(); end end benchmark.run=function() local n=nap:new(); local counter=1; n:sleep(1); local array={}; local start=time.now(); while(#array < benchmark.max_connections) and (not must_stop()) do local sock, err_msg=cws:new(benchmark.target, { onopen=function(handler) local result, errstr=cws:send(handler,benchmark.message,2); if(not result) then print("Error on websocket send at handler "..handler..": "..errstr); end end, onmessage=function(handler,message,opcode) benchmark.meter(); cws:send(handler,message,opcode); end, onerror=function(handler, message) print("Client WebSocket connection is failed for socketfd "..handler..". Error: "..message); end, onclose=function(handler) print("Connection is closed for socketfd "..handler); end }); if(sock ~= nil) then table.insert(array,sock); else print(err_msg); err_msg=nil; collectgarbage("collect"); end -- poll events once per 10 outgoing connections -- this will improve the connection establishment speed if counter == 10 then cws:eventLoop(); counter=1 else counter = counter + 1; end end print("Sockets connected: "..#array); benchmark.start_time=time.now(); while not must_stop() do cws:eventLoop(); end for i=1,#array do array[i]:close(); cws:eventLoop(); end end return benchmark; 

服务安装


现在我们需要将服务脚本放入/opt/lapps/apps//.lua:

  • /opt/lapps/apps/benchmark/benchmark.lua
  • /opt/lapps/apps/echo/echo.lua

我们现在准备运行基准。 只需运行: rm -f lapps.log; /opt/lapps/bin/lapps.avx2 > log rm -f lapps.log; /opt/lapps/bin/lapps.avx2 > log从LAppS目录中rm -f lapps.log; /opt/lapps/bin/lapps.avx2 > log并等待5分钟,然后按Ctrl-C一次,以停止LAppS(它不会立即停止,它将首先关闭连接),或者两次(这将中断关机顺序)。


好的,我们有一个文本文件,里面是这样的:


基准输出

回声:: onStart
回声:: onStart
回声:: onStart
回声:: onStart
跑步
每3196毫秒收到1条消息
每3299毫秒收到1条消息
每3299毫秒收到1条消息
每3305毫秒收到1条消息
连接的插座:100
连接的插座:100
连接的插座:100
连接的插座:100
每1000ms收到134597条消息
每1000毫秒收到139774条消息
每1000ms收到138521条消息
每1000毫秒收到139404条消息
每1000ms收到140162条消息
每1000毫秒收到139337条消息
每1000ms收到140088条消息
每1000ms收到139946条消息
每1000ms收到141204条消息
每1000毫秒收到137988条消息
每1000ms收到141805条消息
每1000毫秒收到134733条消息
...


让我们这样清理该日志文件:


 echo -e ':%s/ms/^V^M/g\n:%g/^$/d\nGdd1G4/Sockets\n4\ndggZZ' | vi $i 

'^ V ^ M'是以下按键的可见表示:Ctrl-V Ctrl-V Ctrl-V Ctrl-M,因此仅复制粘贴此bash行将毫无用处。 简短说明:


  • 我们必须用行尾替换'ms'符号,因为我们不需要它们,它们将在以后使计算混乱,并且并行工作的4个基准可能会在一行中打印出它们的结果。
  • 之后我们需要删除所有空行
  • 我们也会删除最后一行,因为我们停止了服务器
  • 日志文件中,只有前几行包含连接的套接字:100 (这是因为我们仅运行四个基准服务)。 因此,我们跳过了最后一行之后的4行,然后将所有内容删除到文件顶部。
  • 保存文件。

文件已保存,您现在又回到Shell中,日志文件已准备好进行下一步:


 # awk -v avg=0 '{rps=($1/$5);avg+=rps;}END{print ((avg*1000)/NR)*4}' log 

这个awk单线计算每毫秒的回声响应量,并将结果累积在avg变量中。 处理完日志文件中的所有行后,它将avg乘以1000,以得到每秒的回声响应总数,除以行数,再乘以基准服务数量。 这为我们提供了此测试运行每秒平均回声响应的数量。


在我的电脑上,这个数字(ERps)是: 563854


让我们在没有SSL支持的情况下进行相同的操作,看看有什么不同:


  • 将ws.json中的tls变量的值更改为false
  • Benchmark.lua中的Benchmark.target从wss://更改为ws://
  • 运行rm -f lapps.log; /opt/lapps/bin/lapps.avx2.nostats.notls > log 从LAppS目录中的rm -f lapps.log; /opt/lapps/bin/lapps.avx2.nostats.notls > log ,然后重复上述步骤。

我得到: 每秒721236个响应


使用SSL和不使用SSL的性能差异约为22%。 让我们牢记这些数字,以备将来参考。


使用服务器A上的相同设置,我得到:421905带有SSL的ERpS和不带SSL的443145 ERpS。 幽灵和熔毁的补丁已禁用。
在服务器B上,启用了补丁程序后,我获得了带SSL的270996 ERpS和不带SSL的318522 ERpS。 不带SSL和带SSL的385726372126 。 幽灵和熔毁的补丁也被禁用。


使用相同的设置,结果会更糟,因为这些服务器上的CPU频率较低。


请注意,客户端非常依赖/ dev / urandom中的数据可用性。 如果您已经运行过一次,则可能需要一段时间才能真正开始运行客户端。 因此,只要等他们开始工作,他们就可以很快地完成工作。 如果LAppS实例实际上根本没有做任何工作,则仅监视top 。 如果/ dev / urandom耗尽,那么LAppS不会耗尽您的CPU,直到有一些可用数据为止。


为大型测试做准备


首先,我们需要对内核参数进行一些更改,并且不要忘记打开文件的nr的限制。 我使用了与本文几乎相同的设置。


创建具有以下内容的文件


 : sysctl -w fs.file-max=14000000 sysctl -w fs.nr_open=14000000 ulimit -n 14000000 sysctl -w net.ipv4.tcp_mem="100000000 100000000 100000000" sysctl -w net.core.somaxconn=20000 sysctl -w net.ipv4.tcp_max_syn_backlog=20000 sysctl -w net.ipv4.ip_local_port_range="1025 65535" 

然后在两个服务器上使用source ./filename更改内核参数和ulimit。


这次,我的目的是在一台服务器上创建数百万个客户端,并将它们连接到第二台服务器。


服务器A将充当WebSockets回显服务服务器端。
服务器B将充当WebSockets回显服务客户端。


LuaJIT对LAppS施加了限制。 每个LuaJIT VM只能使用2GB的RAM。 这就是您可以在单个LAppS实例中为所有服务使用RAM的限制,因为这些服务是针对一个libluajit实例链接的线程。 如果在单个LAppS实例下运行的任何服务超出此限制,则所有服务都将内存不足。


我发现,每个LAppS实例最多只能建立2 464 000个客户端WebSocket,消息大小为64个字节。 消息的大小可能会稍微更改此限制,因为LAppS通过在LuaJIT内为该消息分配空间将消息传递给cws服务。


这意味着我必须在服务器B上启动几个具有相同配置的LAppS实例,以建立超过240万个WebSocket。 服务器A(回显服务器)在LuaJIT端使用的内存不多,因此LAppS的一个实例将处理1230万个WebSocket,而不会出现任何问题。


让我们为服务器A和B准备两个不同的配置。


服务器A ws.json
 { "listeners" : 224, "connection_weight": 1.0, "ip" : "0.0.0.0", "port" : 5084, "lapps_config_auto_save" : true , "workers" : { "workers": 40, "max_connections" : 2000000, "auto_fragment" : false, "max_poll_events" : 256, "max_poll_wait_ms" : 10, "max_inbounds_skip" : 50, "input_buffer_size" : 2048 }, "acl" : { "policy" : "allow", "exclude" : [] }, "tls": true, "tls_server_version" : 4, "tls_client_version" : 4, "tls_certificates":{ "ca":"/opt/lapps/conf/ssl/ca.crt", "cert": "/opt/lapps/conf/ssl/example.org.bundle.crt", "key": "/opt/lapps/conf/ssl/example.org.key" } } 

服务器A lapps.json
 { "directories": { "app_conf_dir": "etc", "applications": "apps", "deploy": "deploy", "tmp": "tmp", "workdir": "workdir" }, "services": { "echo": { "auto_start": true, "instances": 40, "internal": false, "max_inbound_message_size": 16777216, "protocol": "raw", "request_target": "/echo", "acl" : { "policy" : "allow", "exclude" : [] } } } } 

服务器B ws.json
 { "listeners" : 0, "connection_weight": 1.0, "ip" : "0.0.0.0", "port" : 5083, "lapps_config_auto_save" : true , "workers" : { "workers": 0, "max_connections" : 0, "auto_fragment" : false, "max_poll_events" : 2048, "max_poll_wait_ms" : 10, "max_inbounds_skip" : 50, "input_buffer_size" : 2048 }, "acl" : { "policy" : "deny", "exclude" : [] }, "tls": true, "tls_server_version" : 4, "tls_client_version" : 4, "tls_certificates":{ "ca":"/opt/lapps/conf/ssl/ca.crt", "cert": "/opt/lapps/conf/ssl/example.org.bundle.crt", "key": "/opt/lapps/conf/ssl/example.org.key" } } 

服务器A具有两个接口:


  • bond0-xx203.37
  • bond1-xx23.10

一个越快,另一个越慢,但这并不重要。 无论如何,服务器将承受沉重的负担。


让我们从/opt/lapps/benchmark/benchmark.lua准备一个模板


基准测试
 benchmark={} benchmark.__index=benchmark benchmark.init=function() end benchmark.messages_counter=0; benchmark.start_time=time.now(); benchmark.max_connections=10000; benchmark.target_port=0; benchmark.target_prefix="wss://IPADDR:"; benchmark.target_postfix="/echo"; benchmark.message="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; benchmark.meter=function() benchmark.messages_counter=benchmark.messages_counter+1; local slice=time.now() - benchmark.start_time; if( slice >= 1000) then print(benchmark.messages_counter.." messages received per "..slice.."ms") benchmark.messages_counter=0; benchmark.start_time=time.now(); end end benchmark.run=function() local n=nap:new(); local counter=1; n:sleep(1); local array={}; local start=time.now(); while(#array < benchmark.max_connections) and (not must_stop()) do benchmark.target_port=math.random(5084,5307); local sock, err_msg=cws:new(benchmark.target_prefix..benchmark.target_port..benchmark.target_postfix, { onopen=function(handler) local result, errstr=cws:send(handler,benchmark.message,2); if(not result) then print("Error on websocket send at handler "..handler..": "..errstr); end end, onmessage=function(handler,message,opcode) benchmark.meter(); cws:send(handler,message,opcode); end, onerror=function(handler, message) print("Client WebSocket connection is failed for socketfd "..handler..". Error: "..message); end, onclose=function(handler) print("Connection is closed for socketfd "..handler); end }); if(sock ~= nil) then table.insert(array,sock); else print(err_msg); err_msg=nil; collectgarbage("collect"); -- force garbage collection on connection failure. end -- poll events once per 10 outgoing connections -- this will improve the connection establishment speed if counter == 10 then cws:eventLoop(); counter=1 else counter = counter + 1; end end print("Sockets connected: "..#array); benchmark.start_time=time.now(); while not must_stop() do cws:eventLoop(); end for i=1,#array do array[i]:close(); cws:eventLoop(); end end return benchmark; 

让我们分别将服务器A的IP地址存储到文件IP1和IP2中,然后:


 for i in 1 2 do mkdir -p /opt/lapps/apps/benchmark${i} sed -e "s/IPADDR/$(cat IP1)/g" /opt/lapps/apps/benchmark/benchmark.lua > /opt/lapps/apps/benchmark${i}/benchmark${i}.lua done 

现在,我们修改服务器B上的/opt/lapps/etc/conf/lapps.json以使用以下两个基准服务:


服务器B lapps.json
 { "directories": { "app_conf_dir": "etc", "applications": "apps", "deploy": "deploy", "tmp": "tmp", "workdir": "workdir" }, "services": { "benchmark1": { "auto_start": true, "instances" : 112, "internal": true, "preload": [ "nap", "cws", "time" ] }, "benchmark2": { "auto_start": true, "instances" : 112, "internal": true, "preload": [ "nap", "cws", "time" ] } } } 

我们准备好了吗 不,我们不是。 因为我们打算仅向两个地址生成2240 000个传出套接字,所以我们需要在服务器端增加端口。 创建到同一ip:端口对的连接数不能超过64k(实际上比64k少一点)。


在服务器A上的文件LAppS / include / wsServer.h中,有一个函数void startListeners()。 在此功能中,我们将替换第126行


 LAppSConfig::getInstance()->getWSConfig()["port"], 

与此:


 static_cast<int>(LAppSConfig::getInstance()->getWSConfig()["port"])+i, 

重建LAppS:


 make CONF=Release.AVX2 install 

对2 240 000客户端WebSocket运行测试。


在服务器B上启动LAppS,然后在服务器B上启动LAppS并将输出重定向到如下文件:


 /opt/lapps/bin/lapps.avx2 > log 

编辑,注意:


 if you want to run several LAppS instances, then create separate directory for each instnace (like run1,run2, etc) and run each instance from within these directories. This is required for lapps.log file and of course for resulting standard output, for not to be overlapped/overwritten by concurrent LAppS instances. 

在建立所有连接之前,可能需要一段时间。 让我们监视进度。


不要使用netstat来监视已建立的连接,这毫无意义,尽管它会在几秒钟内建立150k个连接后无限期地运行。 在启动LAppS时所在的目录中,查看服务器A上的lapps.log。 您可以使用以下单行代码来查看如何建立连接以及如何在IOWorkers之间分配连接:


 date;awk '/ will be added to connection pool of worker/{a[$22]++}END{for(i in a){ print i, a[i]; sum+=a[i]} print sum}' lapps.log | sort -n;date 

这是关于如何快速建立这些连接的想法:


 375874 Sun Jul 21 20:33:07 +06 2019 650874 Sun Jul 21 20:34:42 +06 2019 2894 connections per second 1001874 Sun Jul 21 20:36:45 +06 2019 2974 connections per second 1182874 Sun Jul 21 20:37:50 +06 2019 2784 connections per second 1843874 Sun Jul 21 20:41:44 +06 2019 2824 connections per second 2207874 Sun Jul 21 20:45:43 +06 2019 3058 connections per second 

在服务器B上,我们可以检查完成建立它们的连接的基准数量:


 # grep Sockets log | wc -l 224 

在看到数字为224之后,请让服务器工作一会儿,然后使用top监视CPU和内存使用情况。 您可能会看到类似以下内容(服务器B在左侧,服务器A在右侧):


图片


或这样(服务器B在左侧,服务器A在右侧):



显然,运行基准客户端服务的服务器B负载沉重。 服务器A也承受着沉重的负担,但并非总是如此。 有时,服务器B在处理大量任务时会感到不寒而栗。 TLS上的流量加密非常占用CPU。


让我们停止服务器(几次按Ctrl-C)并像以前一样修改日志,但要更改基准测试服务的数量(224):


 echo -e ':%s/ms/^V^M/g\n:%g/^$/d\nGdd1G224/Sockets\n224\ndggZZ' | vi $i awk -v avg=0 '{rps=($1/$5);avg+=rps;}END{print ((avg*1000)/NR)*224}' log 

您可能还希望删除最后几行,以说明停止基准测试服务后的打印输出。 您已经知道如何执行此操作。 因此,我将针对不同的测试场景发布结果,并处理我遇到的问题。


所有其他测试的结果(490万及更高)



测试#4负载


测试5号负载


测试#5平衡以获得相等的CPU共享


测试#6


测试#8


测试#12


测试#13


问题。


在上表中用红色标记。


在服务器B上运行超过224个基准测试实例被证明是一个坏主意,因为客户端的服务器无法在进程/线程之间平均分配CPU时间。 建立所有连接的前224个基准实例占用了大部分CPU资源,其余基准实例则落后。 即使使用renice -20也无济于事(448个基准实例,2个LAppS实例):


448个基准实例


服务器B(左侧)的负载非常重,服务器A仍具有可用的CPU资源。


因此,我将benchmark.max_connections加倍,而不是启动更多单独的LAppS实例。


仍然要运行1,230万个WebSocket,我已经启动了第五个LAppS实例(测试5和6),而没有停止四个已经运行的实例。 并通过使用kill -STOP / -CONT或/手动挂起和重新启动优先进程并重新确定优先级来发挥CFQ的作用。 您可以为此使用以下模板脚本:


 while [ 1 ]; do kill -STOP <4 fast-processes pids> sleep 10 kill -CONT <4 fast-processes pids> sleep 5 done 

欢迎使用2019 RHEL 7.6! 老实说,自2009年以来我第一次使用renice命令。最糟糕的是,-这次我几乎没有成功使用它。


我对NIC的分散收集引擎有问题。 因此,我将其禁用以进行某些测试,而实际上并未将此事件标记到表中。


在高负载和NIC驱动程序错误的情况下,我有部分链路中断,因此我不得不放弃相关的测试结果。


故事结束


老实说,测试比我预期的要顺利得多。


我坚信我没有设法将服务器A上的LAppS完全加载(没有SSL),因为我没有足够的CPU资源供客户端使用。 尽管启用了TLS 1.3,但服务器A上的LAppS却利用了几乎所有可用的CPU资源。


我仍然坚信LAppS是目前最具可扩展性和最快的WebSockets开源服务器,而cws WebSockets客户端模块是同类产品中唯一的,为高负载测试提供了框架。


请在您自己的硬件上验证结果。


意见建议:切勿将nginx或apache用作负载平衡器或WebSockets的代理通道,否则最终将导致性能下降。 它们不是为WebSocket构建的。

Source: https://habr.com/ru/post/zh-CN460847/


All Articles