百萬級別長連接,併發測試指南

前言

都說haproxy很牛x, 可是測試的結果實在是不算滿意, 越測試越失望,無論是長連接還是併發, 但是測試的流程以及工具倒是可以分享分享。也望指出不足之處。

100w的長連接實在算不上太難的事情,不過對於網上關於測試方法以及測試工具的相關文章實在不甚滿意,纔有本文。

本文有兩個難點,我算不上完全解決。

  • 後端代碼的性能.
  • linux內核參數的優化.

環境說明

下面所有的測試機器都是基於openstack雲平臺,kvm虛擬化技術創建的雲主機。

由於一個socket連接一般佔用8kb內存,所以百萬連接至少需要差不多8GB內存.

建立長連接主要是需要內存hold住內存,理論上只需要內存就足夠了,不會消耗太多cpu資源, 相對內存而言.

而併發則對cpu很敏感,因爲需要機器儘可能快的處理客戶端發起的連接。

本文的併發主要指每秒處理的請求.

硬件配置

類型 配置 數量
後端 16核32GB 1
客戶端 2核4GB 21

軟件配置

類型 長連接 併發
後端 python && gevent golang
客戶端 locust && pdsh locust & pdsh

IP地址

haproxy 192.168.111.111
client-master 192.168.111.31
client-slave 192.168.111.1[13-32]

測試步驟

系統調優

  • 最大文件打開數
  • 進程數
  • socket設置

客戶端

在/etc/sysctl.conf加入以下內容

# 系統級別最大打開文件
fs.file-max = 100000

# 單用戶進程最大文件打開數
fs.nr_open = 100000

# 是否重用, 快速回收time-wait狀態的tcp連接
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1

# 單個tcp連接最大緩存byte單位
net.core.optmem_max = 8192

# 可處理最多孤兒socket數量,超過則警告,每個孤兒socket佔用64KB空間
net.ipv4.tcp_max_orphans = 10240

# 最多允許time-wait數量
net.ipv4.tcp_max_tw_buckets = 10240

# 從客戶端發起的端口範圍,默認是32768 61000,則只能發起2w多連接,改爲一下值,可一個IP可發起差不多6.4w連接。
net.ipv4.ip_local_port_range = 1024 65535

在/etc/security/limits.conf加入以下內容

# 最大不能超過fs.nr_open值, 分別爲單用戶進程最大文件打開數,soft指軟性限制,hard指硬性限制
* soft nofile 100000
* hard nofile 100000
root soft nofile 100000
root hard nofile 100000

服務端

在/etc/sysctl.conf加入以下內容

# 系統最大文件打開數
fs.file-max = 20000000

# 單個用戶進程最大文件打開數
fs.nr_open = 20000000

# 全連接隊列長度,默認128
net.core.somaxconn = 10240
# 半連接隊列長度,當使用sysncookies無效,默認128
net.ipv4.tcp_max_syn_backlog = 16384
net.ipv4.tcp_syncookies = 0

# 網卡數據包隊列長度  
net.core.netdev_max_backlog = 41960

# time-wait 最大隊列長度
net.ipv4.tcp_max_tw_buckets = 300000

# time-wait 是否重新用於新鏈接以及快速回收
net.ipv4.tcp_tw_reuse = 1  
net.ipv4.tcp_tw_recycle = 1

# tcp報文探測時間間隔, 單位s
net.ipv4.tcp_keepalive_intvl = 30
# tcp連接多少秒後沒有數據報文時啓動探測報文
net.ipv4.tcp_keepalive_time = 900
# 探測次數
net.ipv4.tcp_keepalive_probes = 3

# 保持fin-wait-2 狀態多少秒
net.ipv4.tcp_fin_timeout = 15  

# 最大孤兒socket數量,一個孤兒socket佔用64KB,當socket主動close掉,處於fin-wait1, last-ack
net.ipv4.tcp_max_orphans = 131072  

# 每個套接字所允許得最大緩存區大小
net.core.optmem_max = 819200

# 默認tcp數據接受窗口大小
net.core.rmem_default = 262144  
net.core.wmem_default = 262144  
net.core.rmem_max = 16777216  
net.core.wmem_max = 16777216

# tcp棧內存使用第一個值內存下限, 第二個值緩存區應用壓力上限, 第三個值內存上限, 單位爲page,通常爲4kb
net.ipv4.tcp_mem = 786432 4194304 8388608
# 讀, 第一個值爲socket緩存區分配最小字節, 第二個,第三個分別被rmem_default, rmem_max覆蓋
net.ipv4.tcp_rmem = 4096 4096 4206592
# 寫, 第一個值爲socket緩存區分配最小字節, 第二個,第三個分別被wmem_default, wmem_max覆蓋
net.ipv4.tcp_wmem = 4096 4096 4206592

在/etc/security/limits.conf加入一下內容

# End of file
root      soft    nofile          2100000
root      hard    nofile          2100000
*         soft    nofile          2100000
*         hard    nofile          2100000

重啓使上述內容生效
不願意重啓就使用以下命令

sysctl -p

宿主機

一般宿主機都會啓用防火牆,所以防火牆會記錄每一條tcp連接記錄,所以如果當虛擬機建立的tcp數量超過宿主機的防火最大記錄數,則會drop掉後來的tcp.主要通過/etc/sysctl.conf下的這個配置項。

# 將連接改爲200w+以滿足單機100w長連接.
net.nf_conntrack_max=2048576

測試工具選取

locust

一個用python編寫的非常出色的測試框架,滿足大多數測試場景.內置http client, 可自定義client, 支持水平擴展.

下載安裝參考: https://docs.locust.io/en/latest/index.html

pdsh

用於調試啓動多個locust客戶端以及一些批量操作.

下載安裝使用參考:
https://github.com/chaos/pdsh

http://kumu-linux.github.io/blog/2013/06/19/pdsh/

server腳本編寫

長連接通過tcp協議測試, 藉助gevent框架.

腳本如下

#coding: utf-8
from __future__ import print_function
from gevent.server import StreamServer
import gevent

# sleeptime = 60

def handle(socket, address):
    # print(address)
    # data = socket.recv(1024)
    # print(data)
    while True:
        gevent.sleep(sleeptime)
        try:
            socket.send("ok")
        except Exception as e:
            print(e)

if __name__ == "__main__":
    import sys
    port = 80
    if len(sys.argv) > 2:
        port = int(sys.argv[1])
        sleeptime = int(sys.argv[2])
    else:
        print("需要兩個參數!!")
        sys.exit(1)
    # default backlog is 256
    server = StreamServer(('0.0.0.0', port), handle, backlog=4096)
    server.serve_forever()

併發通過http協議測試,藉助golang, 因爲golang可以充分利用多核且效率高.
腳本如下

package main

import (
    // "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "time"
)

type myHandler struct{}

func (*myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // time.Sleep(time.Second * 1)
    io.WriteString(w, "ok")
}

func main() {
    var port string
    port = ":" + os.Args[1]

    srv := &http.Server{
        Addr:         port,
        Handler:      &myHandler{},
        ReadTimeout:  30 * time.Second,
        WriteTimeout: 30 * time.Second,
    }

    log.Fatal(srv.ListenAndServe())
}

client腳本編寫

長連接腳本

#coding: utf-8
import time
from gevent import socket
from locust import Locust, TaskSet, events, task

class SocketClient(object):
    """
    Simple, sample socket client implementation that wraps xmlrpclib.ServerProxy and
    fires locust events on request_success and request_failure, so that all requests
    gets tracked in locust's statistics.
    """

    def __init__(self):
        # 僅在新建實例的時候創建socket.
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.__connected = False

    def __getattr__(self, name):
        skt = self._socket

        def wrapper(*args, **kwargs):
            start_time = time.time()
            # 判斷是否之前建立過連接,如果是則建立連接,否則直接使用之前的連接
            if not self.__connected:
                try:
                    skt.connect(args[0])
                    self.__connected = True
                except Exception as e:
                    total_time = int((time.time() - start_time) * 1000)
                    events.request_failure.fire(request_type="connect", name=name, response_time=total_time, exception=e)
            else:
                try:
                    data = skt.recv(1024)
                    # print(data)
                except Exception as e:
                    total_time = int((time.time() - start_time) * 1000)
                    events.request_failure.fire(request_type="recv", name=name, response_time=total_time, exception=e)
                else:
                    total_time = int((time.time() - start_time) * 1000)
                    if data == "ok":
                        events.request_success.fire(request_type="recv", name=name, response_time=total_time, response_length=len(data))
                    elif len(data) == 0:
                        events.request_failure.fire(request_type="recv", name=name, response_time=total_time, exception="server closed")
                    else:
                        events.request_failure.fire(request_type="recv", name=name, response_time=total_time, exception="wrong data: {}".format(data))

        return wrapper

class SocketLocust(Locust):
    """
    This is the abstract Locust class which should be subclassed. It provides an XML-RPC client
    that can be used to make XML-RPC requests that will be tracked in Locust's statistics.
    """

    def __init__(self, *args, **kwargs):
        super(SocketLocust, self).__init__(*args, **kwargs)
        self.client = SocketClient()

class SocketUser(SocketLocust):
    # 目標地址
    host = "192.168.111.30"
    # 目標端口
    port = 80
    min_wait = 100
    max_wait = 1000

    class task_set(TaskSet):
        @task(1)
        def connect(self):
            self.client.connect((self.locust.host, self.locust.port))

併發腳本

#coding: utf-8
from __future__ import print_function
from locust import HttpLocust, TaskSet, task

class WebsiteUser(HttpLocust):
    host = "http://192.168.111.30"
    # 目標端口
    port = 80
    min_wait = 100
    max_wait = 1000

    class task_set(TaskSet):
        @task(1)
        def index(self):
            self.client.get("/")

監控工具選擇

netdata

通過本工具可以直觀的感受到系統的各項指標的變化

效果圖如下

百萬級別長連接,併發測試指南

下載安裝參考:https://github.com/firehol/netdata/wiki/Installation

本機腳本

watch -n 1 "ss -s && uptime &&free -m"

簡單查看本機連接數,負載,內存情況。

效果圖如下

百萬級別長連接,併發測試指南

長連接測試步驟

啓動客戶端

  • locust master
locust -f /root/loadtest/socket_load_backend.py --master
  • locust slave
pdsh -w 192.168.111.1[13-32] "locust -f /root/loadtest/socket_load_backend.py --slave --master-host=192.168.111.31"

注意: 在slave端一樣需要又socket_load_backend.py文件.

啓動後端

nohup python /root/loadtest/tcpserver.py 80 550 &> /var/log/tcpserver1.log &

開始測試

登陸locust的web頁面: http://192.168.111.31:8089

開始參數如下.

百萬級別長連接,併發測試指南

Number of users to simulate
代表最終創建多少的用戶.

Hatch rate (users spawned/second)代表每秒創建多少的用戶

由上圖可知,每秒2000個用戶數增長,增長大盤100w需要500秒,所以在後端每個連接保持550秒,以保證至少550秒內達到100w連接.當建立一百萬用戶以後就會每隔一段時間執行自定義的任務,時間間隔在min_wait與max_wait時間範圍內.

測試結果

百萬級別長連接,併發測試指南

百萬級別長連接,併發測試指南

從面結果可以看出,一共完成了200w左右的請求, 每秒請求數量差不多在1800左右.然後負載在1左右,說明cpu資源差不多達到了100%.因爲這裏的後端是單進程的.再者內存使用量在11GB左右,還算合理.

併發測試步驟

啓動客戶端

  • locust master
locust -f /root/loadtest/http_load_backend.py --master
  • locust slave

pdsh -w 192.168.111.1[13-32] "locust -f /root/loadtest/http_load_backend.py --slave --master-host=192.168.111.31"

# 多新建一個終端再次執行以下命令,因爲它是單線程的,所以啓動的數量一般與cpu個數相等,而上面的長連接消耗的主要是內存,所以不需要多啓動一倍的客戶端
pdsh -w 192.168.111.1[13-32] "locust -f /root/loadtest/http_load_backend.py --slave --master-host=192.168.111.31"

啓動後可以發現有40個slave,效果如下.

百萬級別長連接,併發測試指南

啓動後端

nohup go run go/src/server.go 80 &> /var/log/goServer.log &

開始測試

注意這地方的測試應該是1w 1.5w 2w的數量依次的往上加,即,第一次user用戶數填10000,Hatch rate填10000,然後依次分別增加.

這裏就貼最終的結果了.

百萬級別長連接,併發測試指南

測試結果

百萬級別長連接,併發測試指南

百萬級別長連接,併發測試指南

從面結果可以看出,一共完成了10w左右的請求, 每秒請求數量差不多在16000左右.然後負載在9左右,遠遠沒有想想中的強勢...其中主要受兩方面限制, 一是內核參數, 再者就是宿主機性能的限制.

而性能調優暫時不在這篇文章內容內,主要是積累還不夠.再者本文主要是測試.
而負載均衡器暫時還沒看到滿意的,所以併發到1.6w就算本文的結束了。

總結

工慾善其事必先利其器,動手之前應該選一件稱手的工具,locust便是那件不錯的工具,但是有了工具還要設定正確的目標,以及步驟,不然很難成功.這裏算是拋磚引玉了吧.

不足之處

  1. 沒有對吞吐量做測試,即服務端發送不同的文本大小,這裏只是測試2字節的相應內容.

  2. 沒有測試併發更高的情況下的100w長連接.

後記

之所以想寫一篇大數量級的測試方式,是因爲,網上大多數文章要麼是給測試代碼或者工具,要麼是給一堆解釋的不是很清楚的參數,再者就是隻貼連接數的數量,如果只是達到這麼多的連接,卻不給出成功失敗率,實在是有點耍流氓。

有意思的是這麼強勢的測試框架居然相關內容這麼少,有空讀讀源碼.

本文所有的代碼可以在以下鏈接找到
https://github.com/youerning/blog/tree/master/locust-test

參考文檔:
Linux之TCPIP內核參數優化:
https://www.cnblogs.com/fczjuever/archive/2013/04/17/3026694.html

理解 Linux backlog/somaxconn 內核參數:
https://jaminzhang.github.io/linux/understand-Linux-backlog-and-somaxconn-kernel-arguments/

Linux下Http高併發參數優化之TCP參數:
https://kiswo.com/article/1017

單臺服務器百萬併發長連接支持:
http://blog.csdn.net/mawming/article/details/51941771

結合案例深入解析orphan socket產生與消亡:
https://m.aliyun.com/yunqi/articles/91966

最後的最後

百萬級別長連接,併發測試指南

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章