Python_服務端效能高併發測試

大话性能發表於2024-10-15

背景

公司業務處於上升期,但是服務端卻 low 的像個 demo,於是乎重構服務端開始了;

關於測試,測試這個行業在大多數網際網路公司比較失敗,做測試的應該都有這種感覺。但是我感覺我很幸運,因為 CTO 很重視測試,他的口頭禪就是不可測試的程式都是不可靠的,所以我們公司的所有程式都會有配套的測試工具。這篇文章中的工具就是專門測試服務端而模擬的客戶端 TSP。

業務需求

重構服務端,達到接入百萬客戶端級別

實現原理

  1. 服務端監聽 3 個埠,這三個埠全部是裝置發起
  2. tsp 模擬客戶端連線服務端邏輯,從而達到裝置、使用者上線
  3. 採用程序池巢狀執行緒池方式,達到高併發的目的
  4. 程式碼中統計發包時間平均值、收包時間平均值、收包位元組大小平均值、收包錯誤率;
  5. 服務端採集硬體配置、cpu 負載、記憶體負載、磁碟 io 等等

具體實現

1.構建主流程,完成基於 socket 請求的收發包

#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
from socket import socket,AF_INET,SOCK_STREAM

import struct

from log import log
log=log()
class socket_main:

    def __init__(self,who,conn_data):
        self.conn_data = conn_data
        self.who = who
        self.__conn(conn_data)

    def __conn(self,addr):

        try:
            log.debug('{} _conn data:{}'.format(self.who,addr))
            self.tcpCliSock = socket(AF_INET, SOCK_STREAM)
            self.tcpCliSock.connect(addr)
            self.tcpCliSock.setblocking(1)
            self.tcpCliSock.settimeout(1)
            # self.tcpCliSock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,8192)
            log.debug('{} socket data:{}'.format(self.who,'success'))
        except BaseException as msg:
            log.error('{} socket conn_result:{}'.format(self.who,msg))

    def send(self,data):
        try:
            try:
                result = self.tcpCliSock.send(data)
            except BaseException as msg:
                # self.__conn(self.conn_data)
                # result = self.tcpCliSock.send(data)
                log.error(msg)
                raise msg
            log.debug('{} socket send_len:{}'.format(self.who, result))
            return result
        except BaseException as msg:
            raise log.error('{} socket send_error:{}'.format(self.who,msg))

    def recv(self,size,code):
        result =''
        try:
            try:
                result = self.tcpCliSock.recv(int(size))

                while len(result)<int(size):
                    result +=self.tcpCliSock.recv(int(size)-len(result))
            except:
                pass
            if not result:
               log.error('{} socket recv_result ERROR:result is null'.format(self.who))
            log.debug('{} socket recv_result:{}'.format(self.who, len(result)))
            data_struct = struct.unpack(code, result)
            log.debug('{} socket recv_result_unpack:{}'.format(self.who, data_struct))
            return data_struct
        except BaseException as msg:
            log.error('{} socket recv_error:{}'.format(self.who,msg))
            raise msg

    def only_recv(self,size):
        try:
            result = self.tcpCliSock.recv(int(size))
            while len(result) < int(size):
                result += self.tcpCliSock.recv(int(size) - len(result))
            log.debug('{} socket only_recv_result_len:{}'.format(self.who, len(result)))
            log.debug('only_recv:{}'.format(result))
            return result
        except BaseException as msg:
            log.error('{} socket recv_error:{}'.format(self.who,msg))
            raise msg

    def close(self):
        try:
            log.debug('{} socket has been closed'.format(self.who))
            self.tcpCliSock.close()
        except BaseException as msg:
            log.debug('{} socket close_error:{}'.format(self.who,msg))
  1. 建立程序池、執行緒池套餐
def main():
  print '業務程式碼'

class MyThread(threading.Thread):
    def __init__(self, func, args, name=''):
        threading.Thread.__init__(self)
        self.name = name
        self.func = func
        self.args = args

    def run(self):
        apply(self.func, self.args)

def main_thread():
    global sn
    threads = []
    nloops = xrange(thread_num)# thread_num併發執行緒數
    for i in nloops:
        mac, mac_real, sn = getMacSn()
        t = MyThread(main, (mac,mac_real,sn))
        threads.append(t)
    for i in nloops:
        threads[i].start()
    for i in nloops:
        threads[i].join()


if __name__=='__main__':
    result = ''
    pool = multiprocessing.Pool(processes=proc)# processes程序池數量
    log.info("main process(%d) running..." % os.getpid())
    for i in xrange(proc_num):# proc_num 併發程序數量
        result = pool.apply_async(main_thread)
    pool.close()
    pool.join()

    if not result.successful():
        log.error('主程序異常:{}'.format(result.successful()))
    else:
        log.info('goodbye:主程序({})執行完畢'.format(os.getpid()))
  1. 透過在業務程式碼中計算統計引數,然後放在記憶體中累計計算,輸出到指定級別日誌中,用到的主要方法是透過設定全域性變數進行統一計算:
class globalMap:
    # 拼裝成字典構造全域性變數  借鑑map  包含變數的增刪改查
    map = {}

    def set_map(self, key, value):
        if(isinstance(value,dict)):
            value = json.dumps(value)
        self.map[key] = value
        log.debug(key + ":" + str(value))


    def set(self, **keys):
        try:
            for key_, value_ in keys.items():
                self.map[key_] = str(value_)
                log.debug(key_+":"+str(value_))
        except BaseException as msg:
            log.error(msg)
            raise msg

    def del_map(self, key):
        try:
            del self.map[key]
            return self.map
        except KeyError:
            log.error("key:'" + str(key) + "'  不存在")


    def get(self,*args):
        try:
            dic = {}
            for key in args:
                if len(args)==1:
                    dic = self.map[key]
                    log.debug(key+":"+str(dic))
                elif len(args)==1 and args[0]=='all':
                    dic = self.map
                else:
                    dic[key]=self.map[key]
            return dic
        except KeyError:
            log.warning("key:'" + str(key) + "'  不存在")
            return 'Null_'

總結

多程序的方式和多執行緒進行對比,雖然 Python 全域性鎖的限制導致執行緒有點瑕疵,但是跟程序比起併發,還是能甩程序幾條街的。透過這種方式進行高密度業務邏輯操作,可以很輕鬆找到服務端瓶頸。

宣告:看到過的好文章,轉載分享,若有侵權,請及時聯絡,速刪。

相關文章