都說haproxy很牛x, 但是測試的結果實在是不算滿意, 越測試越失望,不管是長鏈接仍是併發, 可是測試的流程以及工具卻是能夠分享分享。也望指出不足之處。html
100w的長鏈接實在算不上太難的事情,不過對於網上關於測試方法以及測試工具的相關文章實在不甚滿意,纔有本文。python
本文有兩個難點,我算不上徹底解決。linux
下面全部的測試機器都是基於openstack雲平臺,kvm虛擬化技術建立的雲主機。git
因爲一個socket鏈接通常佔用8kb內存,因此百萬鏈接至少須要差很少8GB內存.github
創建長鏈接主要是須要內存hold住內存,理論上只須要內存就足夠了,不會消耗太多cpu資源, 相對內存而言.golang
而併發則對cpu很敏感,由於須要機器儘量快的處理客戶端發起的鏈接。web
本文的併發主要指每秒處理的請求.後端
類型 | 配置 | 數量 |
---|---|---|
後端 | 16核32GB | 1 |
客戶端 | 2核4GB | 21 |
類型 | 長鏈接 | 併發 |
---|---|---|
後端 | python && gevent | golang |
客戶端 | locust && pdsh | locust & pdsh |
haproxy 192.168.111.111
client-master 192.168.111.31
client-slave 192.168.111.1[13-32]緩存
在/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
一個用python編寫的很是出色的測試框架,知足大多數測試場景.內置http client, 可自定義client, 支持水平擴展.
下載安裝參考: https://docs.locust.io/en/latest/index.html
用於調試啓動多個locust客戶端以及一些批量操做.
下載安裝使用參考:
https://github.com/chaos/pdsh
http://kumu-linux.github.io/blog/2013/06/19/pdsh/
長鏈接經過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()) }
長鏈接腳本
#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 -f /root/loadtest/socket_load_backend.py --master
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 -f /root/loadtest/http_load_backend.py --master
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即是那件不錯的工具,可是有了工具還要設定正確的目標,以及步驟,否則很難成功.這裏算是拋磚引玉了吧.
沒有對吞吐量作測試,即服務端發送不一樣的文本大小,這裏只是測試2字節的相應內容.
之因此想寫一篇大數量級的測試方式,是由於,網上大多數文章要麼是給測試代碼或者工具,要麼是給一堆解釋的不是很清楚的參數,再者就是隻貼鏈接數的數量,若是隻是達到這麼多的鏈接,卻不給出成功失敗率,實在是有點耍流氓。
有意思的是這麼強勢的測試框架竟然相關內容這麼少,有空讀讀源碼.
本文全部的代碼能夠在如下連接找到
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