Locust單機多核壓測,以及主從節點的數據通信處理

一、背景

這還是2個月前做的一次接口性能測試,關於locust腳本的單機多核運行,以及主從節點之間的數據通信。

先簡單交代下背景,在APP上線之前,需要對登錄接口進行性能測試。經過評估,我還是優先選擇了locust來進行腳本開發,本次用到了locust的單機多核運行能力,只不過這裏還涉及到主從節點之間數據通信。現成的可參考的有效文檔甚少,所以還是自己摸着官方文檔過河比較靠譜。

順帶提一下,學習框架這種東西最好的教程其實還得是官方文檔以及框架源碼了,這裏貼上locust官方文檔鏈接,需要的可以自行學習:https://docs.locust.io/en/stable/what-is-locust.html

二、代碼編寫

其實腳本代碼的編寫一大重點就是如何處理測試數據,不同的測試需求對於測試數據的處理是不同的。比如這次的需求,手機號不能重複。另外考慮到長時間的負載壓力,數據量還得足夠。

最後測試數據還需要處理,那麼我使用的測試號段是非真實號碼段,測試結束後可以查詢對應號段內的手機號,進行相關業務數據的清理。

1. 代碼概覽

還是老樣子,先附上全部代碼,然後對其結構進行拆分講解。

import random
import time
from collections import deque

from locust import HttpUser, task, run_single_user, TaskSet, events
from locust.runners import WorkerRunner, MasterRunner

CURRENT_TIMESTAMP = str(round(time.time() * 1000))
RANDOM = str(random.randint(10000000, 99999999))
MOBILE_HEADER = {
    "skip-request-expired": "true",
    "skip-auth": "true",
    "skip-sign": "true",
    "os": "IOS",
    "device-id": "198EA6A4677649018708B400F3DF69FB",
    "nonce": RANDOM,
    "sign": "12333",
    "version": "1.2.0",
    "timestamp": CURRENT_TIMESTAMP,
    "Content-Type": "application/json"
}

last_mobile = ""
worker_mobile_deque = deque()


# 13300120000, 13300160000 新用戶註冊號段

@events.test_start.add_listener
def on_test_start(environment, **_kwargs):
    if not isinstance(environment.runner, WorkerRunner):
        mobile_list = []
        for i in range(13300120000, 13300160000):
            mobile_list.append(i)
        mobile_list_length = len(mobile_list)
        print("列表已生成,總計數量:", mobile_list_length)
        worker_count = environment.runner.worker_count
        chunk_size = int(mobile_list_length / worker_count)
        print(f"平均每個worker分得的手機號數量:{chunk_size}")

        for i, worker in enumerate(environment.runner.clients):
            start_index = i * chunk_size
            if i + 1 < worker_count:
                end_index = start_index + chunk_size
            else:
                end_index = len(mobile_list)
            data = mobile_list[start_index:end_index]
            environment.runner.send_message("mobile_list", data, worker)


def setup_mobile_list(environment, msg, **kwargs):
    len_msg_data = len(msg.data)
    print(f"worker收到的master傳來的數據號段:{msg.data[0]} ~ {msg.data[len_msg_data-1]}")
    global worker_mobile_deque
    worker_mobile_deque = deque(msg.data)


@events.init.add_listener
def on_locust_init(environment, **_kwargs):
    if not isinstance(environment.runner, MasterRunner):
        environment.runner.register_message('mobile_list', setup_mobile_list)


class VcodeLoginUser(TaskSet):
    # wait_time = between(5, 5)

    @task
    def vcode_login(self):
        test_mobile = worker_mobile_deque.popleft()
        print("當前獲取的手機號:", test_mobile)
        # print("當前隊列大小:", len(worker_mobile_deque))
        global last_mobile
        last_mobile = test_mobile
        with self.client.post("/g/sendMobileVcode",
                              headers=MOBILE_HEADER,
                              json={"busiType": "login", "mobile": str(test_mobile)}) as send_response:
            try:
                send_response_json = send_response.json()
                if send_response_json["message"] == "success":
                    params = {"mobile": str(test_mobile), "vcode": "111111"}
                    # print(test_mobile, "登錄請求參數:", params)
                    with self.client.post("/g/vcodeLogin",
                                          json=params,
                                          headers=MOBILE_HEADER,
                                          catch_response=True) as login_response:
                        # print(login_response.json)
                        login_response_json = login_response.json()
                        if login_response_json["message"] != "success":
                            login_response.failure("message not equal success")
                        elif login_response_json["code"] != 0:
                            login_response.failure("code not equal 0")
                        elif login_response_json["data"]["rId"] == "":
                            login_response.failure("rid is null")
                        elif login_response_json["data"]["mobile"] != str(test_mobile):
                            login_response.failure("mobile is error,入參手機號{},返回的手機號{}"
                                                   .format(test_mobile, login_response.json()["data"]["mobile"]))
                        # print(test_mobile, "請求結果:", login_response.json())
                else:
                    send_response.failure("{} send code fail".format(test_mobile))
            except Exception as e:
                send_response.failure("send code fail {}".format(e))

    @events.test_stop.add_listener
    def on_test_stop(environment, **kwargs):
        print("腳本結束")
        print("當前隊列大小:", len(worker_mobile_deque))
        print("最後的手機號:", last_mobile)


class LocustLogin(HttpUser):
    tasks = [VcodeLoginUser]
    host = "https://qa.test.com"


if __name__ == '__main__':
    run_single_user(LocustLogin)

2. 代碼拆解-要加必要的斷言

首先是基於locust開發的http請求的腳本大結構是不變的,依舊是兩大塊:HttpUserTaskSet,這裏不再對其講解了,大夥看下官方文檔就明白了。

接下來就是類VcodeLoginUser,可以看到在這裏面是定義了單個用戶的詳細動作。注意這裏要加上必要的斷言。否則僅靠框架的非200外的錯誤斷言還是不夠的。

比如我這裏關注登錄成功後的幾個必要字段:coderIdmobile,這些一定是要符合斷言的纔可以。

果不其然,壓測過程中就發現了併發情況下會出現的問題:入參手機號是a,接口返回的手機號是b。併發量越大錯誤越多。如果我只斷言code=0,那麼這個問題就不容易發現了,雖然接口返回的code都是成功的,但是業務上已經存在錯誤了。

...
        with self.client.post("/g/sendMobileVcode",
                              headers=MOBILE_HEADER,
                              json={"busiType": "login", "mobile": str(test_mobile)}) as send_response:
            try:
                send_response_json = send_response.json()
                if send_response_json["message"] == "success":
                    params = {"mobile": str(test_mobile), "vcode": "111111"}
                    # print(test_mobile, "登錄請求參數:", params)
                    with self.client.post("/g/vcodeLogin",
                                          json=params,
                                          headers=MOBILE_HEADER,
                                          catch_response=True) as login_response:
                        # print(login_response.json)
                        login_response_json = login_response.json()
                        if login_response_json["message"] != "success":
                            login_response.failure("message not equal success")
                        elif login_response_json["code"] != 0:
                            login_response.failure("code not equal 0")
                        elif login_response_json["data"]["rId"] == "":
                            login_response.failure("rid is null")
                        elif login_response_json["data"]["mobile"] != str(test_mobile):
                            login_response.failure("mobile is error,入參手機號{},返回的手機號{}"
                                                   .format(test_mobile, login_response.json()["data"]["mobile"]))
                        # print(test_mobile, "請求結果:", login_response.json())
                else:
                    send_response.failure("{} send code fail".format(test_mobile))
            except Exception as e:
                send_response.failure("send code fail {}".format(e))
...

3. 代碼拆解-單機多核處理

接下來就是重點了,如何在單臺機器上用到多cpu。最開始的時候我忽略了這點,後來發現負載上不去,一打開資源監視器才發現只有1個cpu在滿負載運行。

這裏示意圖僅供參考,我的win筆記本是12c的。

因爲Locust是單進程的,不能充分利用多核CPU,於是需要我們壓力機上開啓一個master進程,然後再開啓多個slave進程,組成一個單機分佈式系統即可。

開啓的方式也很簡單:

# 開啓 master 
locust -f locustfile.py --master

# 開啓 slave
locust -f locustfile.py --slave

這裏我們開啓 slave 節點的時候可以開啓對應多個命令行窗口,當時沒截圖,借用網上的圖片示意一下:

開啓後,你的web界面就可以實時看到當前啓動的節點數了。

4. 代碼拆解-處理主從節點數據通信

開啓主從節點倒是很容易,測試數據就需要針對性進行處理了。

因爲我的測試登錄用的手機號不可以重複,所以要保證不同 slave 節點上同時運行的代碼產生的手機號都不可以重複。

繼續扒了下官方文檔,發現可以通過增加事件監聽器來實現我的需求。

這裏我加了三個監聽器分別來處理不同的事情:

  • @events.init.add_listener:在locust運行初始化的時候執行
  • @events.test_start.add_listener: 在測試代碼開始運行的時候執行
  • @events.test_stop.add_listener: 在測試代碼結束運行的時候執行

@events.test_start.add_listener
首先,在@events.test_start.add_listener裏,我主要處理全量數據的生成,以及把這些手機號平均分配給生成的 slave 節點。

@events.test_start.add_listener
def on_test_start(environment, **_kwargs):
    if not isinstance(environment.runner, WorkerRunner):
        mobile_list = []
        for i in range(13300120000, 13300160000):
            mobile_list.append(i)
        mobile_list_length = len(mobile_list)
        print("列表已生成,總計數量:", mobile_list_length)
        worker_count = environment.runner.worker_count
        chunk_size = int(mobile_list_length / worker_count)
        print(f"平均每個worker分得的手機號數量:{chunk_size}")

        for i, worker in enumerate(environment.runner.clients):
            start_index = i * chunk_size
            if i + 1 < worker_count:
                end_index = start_index + chunk_size
            else:
                end_index = len(mobile_list)
            data = mobile_list[start_index:end_index]
            environment.runner.send_message("mobile_list", data, worker)

注意這裏最後一行中定義的mobile_list,需要定義一個對應函數來接收這個數據。

def setup_mobile_list(environment, msg, **kwargs):
    len_msg_data = len(msg.data)
    print(f"worker收到的master傳來的數據號段:{msg.data[0]} ~ {msg.data[len_msg_data-1]}")
    global worker_mobile_deque
    worker_mobile_deque = deque(msg.data)

這樣,不同的 slave 節點腳步分配到的手機號段就是不同的了,解決測試數據重複的問題。

另外,我定義另一個全局變量worker_mobile_deque,這樣不同的 slave 節點接收的數據就可以放到隊列裏,運行的時候從隊列裏面取,用一個少一個,直到隊列裏的數據用完。

@events.init.add_listener
接着就是在@events.init.add_listener裏要註冊上面定義的數據字段和處理函數。

@events.init.add_listener
def on_locust_init(environment, **_kwargs):
    if not isinstance(environment.runner, MasterRunner):
        environment.runner.register_message('mobile_list', setup_mobile_list)

@events.test_stop.add_listener
最後,在@events.test_stop.add_listener這裏可以做一些後置處理,我是簡單起見,只是記錄輸出了本次測試用到了哪個號碼段,這樣我下次運行腳本的時候可以從後面的數據開始,最大化測試數據的使用,不浪費。

    @events.test_stop.add_listener
    def on_test_stop(environment, **kwargs):
        print("腳本結束")
        print("當前隊列大小:", len(worker_mobile_deque))
        print("最後的手機號:", last_mobile)

三、小結

腳本調試完後可以穩定運行,接下來就是測試的過程了,進行了服務器單節點、多節點負載能力的測試,水平拓展能力的測試,以及服務動態擴容、長時間高負載測試。測試的角度觀察測試報告,服務各項指標的情況。只不過涉及到開發端,調優分析的工作並未能參與很多。不過大概還是那些常見問題,後續有機會可以再單獨分享了。

從使用角度來看,locust深得我愛,比起 jemter真的太輕便了,代碼靈活度也非常高,單機負載能力也是響噹噹的,這點比jemeter強太多了。我這個項目不需要非常高的量,所以單機只用了8c就夠了。如果有小夥伴需要非常高的併發,locust 也支持多機器分佈式,進一步擴大併發能力。

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