一種搭建分佈式測試環境和批量性能測試的思路 背景 設計 總結

在搜索引擎的測試過程中,經常會遇到以下兩個問題:

  • 需要搭建和更新分佈式測試環境
  • 在性能測試時,我們需要測試不同集羣規模和配置下的環境時,如何自動更新測試環境和批量進行性能測試

因此,我們需要設計一個腳本,這個腳本可以幫我來完成這些事。
在這裏,我推薦使用Python,理由有:

  • 寫起來比較快(測試時間本來就比較緊張),不可能用C或者Java了
  • 語法比較清晰,Shell、Perl這些維護起來太亂
  • 自帶的庫、第三方的庫比較豐富
  • 另外,我個人比較喜歡Python的mako模版引擎和paramikossh2庫。

其實不用paramiko也可以,只要把機器ssh打通就可以。但我個人不太喜歡這種方式,覺得耦合性太強(只能在Linux下運行了)。

批量性能測試的設計

我很喜歡採用YAML格式,YAML格式的一大好處就是可以很方便的定義List、Map等類型

tasks:
  # 第一個測試用例,我可能需要測試單線程的情況
  -
    id: 1 # ID的作用是你在腳本中可以拿id作爲結果存放的目錄
    parallelNum: 1 # 併發數
    seconds: 1800 # 壓半個小時
    targetHost: 10.20.137.22  # 目標主機
    targetPort: 9999
    queryFilePath: /home/admin/access-log/add-600w.query  # 請求放在這兒
  # 第2個測試用例,我可能需要測試2線程的情況,這時我就只要再寫一個,然後parallelNum: 2就可以了
  -
    id: 1
    parallelNum: 2
    seconds: 1800
    targetHost: 10.20.137.22
    targetPort: 9999
    queryFilePath: /home/admin/access-log/add-600w.query

在阿里的搜索平臺這邊,我們大多使用abench作爲性能測試工具,它是一個命令行工具,只要命令+參數就可以了,比起JMeter要寫JMeter腳本簡單。因此,我再在配置文件中設計一下abench的命令格式。
因爲在運行命令中,有很多參數需要替換成上述測試用例設定的參數,因此需要採用模版引擎的方式。Python的模版引擎很多,我個人比較推薦mako

abenchPath: /opt/usr/bin/abench  # abench在哪兒?
abenchCommand: "${abenchPath} -p ${parallelNum} -s ${seconds} -k --http -o /dev/null ${targetHost} ${targetPort} ${queryFilePath}"

配置文件設計好了,下面我們來寫我們的Python腳本了(這裏僅僅給出一些主要代碼,大致明白意思就可以了)

import subprocess
from mako.template import Template
import yaml

# 運行一個測試任務
def runTask(config, task):
    runAbench(config, task)

def runAbench(config, task):
     # 得到完成的abench運行命令
     command = Template(config["abenchCommand"]).render(
         abenchPath=config["abenchPath"],
         parallelNum=task["parallelNum"],
         seconds=task["seconds"],
         targetHost=task["targetHost"],
         targetPort=task["targetPort"],
         queryFilePath=task["queryFilePath"],
         )
     pipe = subprocess.Popen(command,
         stdin=subprocess.PIPE,
         stdout=subprocess.PIPE,
         stderr=subprocess.PIPE,
         shell=True
         )
     # 讀取abench的運行結果,因爲可能得保存下來吧
     result = pipe.stdout.read()
     # 下面可能是保存結果什麼的

if __name__ == "__main__":
    config = yaml.load(file(configFile))
    for task in config["tasks"]:
       runTask(config, task)

自動更新測試環境

在我實際測試過程中,因爲要更新的環境其實相當複雜,最多的時侯需要去10幾臺機器上做更新環境、停止/啓動進程的操作。但我這裏主要介紹思路,多一些機器和進程其實都一樣。
接着剛纔的配置文件,我們只是在每一個task中設計了加壓任務,但在加壓前需要更新哪些環境沒有涉及,按照阿里巴巴的ISearch架構,我就啓動一個一行兩列的Searcher環境,2列Searcher上有一個Merger,然後再有一個clustermap來監控。

abenchPath: /opt/usr/bin/abench  # abench在哪兒?
abenchCommand: "${abenchPath} -p ${parallelNum} -s ${seconds} -k --http -o /dev/null ${targetHost} ${targetPort} ${queryFilePath}"
# 關於Searcher的一些通用配置
searcher:
    templateConfigFile: /home/admin/access-log/searcher_server.cfg  # 因爲啓動時的監聽端口等信息需要從下面的運行任務中讀取,因此這個也設計成一個模版文件
    templateLogConfigFile: /home/admin/access-log/searcher_log.cfg
    # 在Search機器上操作的命令
    commands:
        - "${searchRoot}/bin/is_searcher_server -c ${configFile} -l ${logConfigFile} -k stop > /dev/null 2>&1"
        - "${searchRoot}/bin/is_searcher_server -c ${configFile} -l ${logConfigFile} -k start -d > /dev/null 2>&1"
# 關於Merger的一些通用配置,和Searcher差不多,就不寫了

tasks:
  # 第一個測試用例,我可能需要測試單線程的情況
  -
    id: 1 # ID的作用是你在腳本中可以拿id作爲結果存放的目錄
    parallelNum: 1 # 併發數
    seconds: 1800 # 壓半個小時
    targetHost: 10.20.137.22  # 目標主機
    targetPort: 9999
    queryFilePath: /home/admin/access-log/add-600w.query  # 請求放在這兒

    # 兩臺Search機器,定義一個List
    searchers:
       -
         host: 10.20.150.61
         port: 6322 # 監聽的端口
         username: test # 因爲需要通過ssh協議登錄上去操作,因此需要用戶名密碼。如果你已經把機器ssh都打通了,那就不需要了
         password: 12345
         configFile: "${searchRoot}/scripts/conf/searcher_server.cfg" # 啓動時運行的配置文件
         logConfigFile: "${searchRoot}/scripts/conf/searcher_log.cfg" # 啓動時運行的日誌文件
       -
         host: 10.20.150.60
         port: 6322
         username: test
         password: 12345
         configFile: "${searchRoot}/scripts/conf/searcher_server.cfg"
         logConfigFile: "${searchRoot}/scripts/conf/searcher_log.cfg"

    # 我這邊只有一臺merger,如果merger也是有多臺的話,也可以把這個設計成一個List
    merger:
       host: 10.20.137.22
       port: 6088
       username: test
       password: 12345
       configFile: "${searchRoot}/scripts/conf/merger_server.cfg"

然後比如關於Searcher的配置文件,在上面也是一個模版文件阿,我們可以把這個文件設計成:

se_conf_file=${searchRoot}/scripts/conf/se.conf
simon_conf_path=${searchRoot}/scripts/conf/simon_searcher.xml
sort_config=${searchRoot}/scripts/conf/searcher_sort.xml
cache_size=0
cache_min_doc=0
conn_queue_limit=500
[services]
tcp ${port} # 主要就是爲了替換監聽的端口,其實要做得通用一點的話,很多配置都可以搞成變量,但就是可能你自己的配置文件變得很複雜。因此我們能不改的就儘量不改。

[clustermap]
local_config_path=${searchRoot}/scripts/conf/clustermap.xml

上述就是關於searcher和merger多行多列的配置,下面我們完善一下我們剛纔的Python腳本

# 得的一個ssh登錄後的client對象,用於調用遠程機器上的命令
def getClient(host, port, username, password):
    client = paramiko.SSHClient()
    client.load_system_host_keys()
    client.set_missing_host_key_policy(paramiko.WarningPolicy()
    client.connect(hostname, port, username, password)
    return client

# 得到一個sftp對象,因爲需要scp渲染好的配置文件什麼的,因此需要sftp對象,它的put方法其實就類似scp
def getSftp(host, port, username, password):
    transport = paramiko.Transport((hostname, port))
    transport.connect(username=username, password=password)
    sftp = paramiko.SFTPClient.from_transport(transport)
    return sftp

# 更新和部署Searchers
def cleanSearchers(config, searchers):
    for searcher in searchers:
        # 得到渲染好的配置文件的內容
        templateLine = Template(file(config["searcher"]["templateConfigFile"]).read()).render(
            port=searcher["port"],
            searchRoot=config["searchRoot"]
            )
        # 將渲染好的配置文件寫入一個臨時文件
        tmpConfigFile = tempfile.NamedTemporaryFile(delete=False)
        tmpConfigFile.file.write(templateLine)
        tmpConfigFile.file.close()
        # 將這個臨時文件scp拷遠程機器上的哪兒
        targetConfigFile = Template(searcher["configFile"]).render(searchRoot=config["searchRoot"])
        sftp = getSftp(searcher["host"], 22, searcher["username"], searcher["password"])
        sftp.put(tmpConfigFile.name, targetConfigFile)
        sftp.close()
        # 刪除掉之前的臨時文件
        os.remove(tmpConfigFile.name)
        # 運行啓動searcher的命令
        client = getClient(searcher["host"], 22, searcher["username"], searcher["password"])
        for command in config["searcher"]["commands"]:
            command = Template(command).render(
                searchRoot=config["searchRoot"],
                configFile=targetConfigFile,
                logConfigFile=targetLogConfigFile
                )
            client.exec_command(cmd)
        client.close()

關於clustermap的配置

在阿里巴巴的ISearch架構中,searchers幾行幾列是由clustermap來配置的,我們這邊也稍微簡單話一點,不考慮merger有多臺的情況,就設計searchers幾行幾列的情況。更新一下剛纔在task中的配置,加上關於clustermap的配置

clustermap:
       host: 10.20.137.22
       username: test
       password: 12345
       configFile: "${searchRoot}/scripts/conf/clustermap.xml"
       # 一臺merge
       merger:
         host: 10.20.137.22
         port: 6088
       # 關於searcher的配置,其實是一個二維數組。第一個緯度是列,第2個緯度是行。以下這個例子是1列2行
       searchers:
         -
           servers:  # 同一列下的機器
             -
               host: 10.20.150.61
               port: 6322
         -
           servers:
             -
               host: 10.20.150.60
               port: 6322

上述是1列2行的例子,如果要配成2行2列就只要在searchers部分配成:

searchers:
         -
           servers:  # 同一列下的機器
             -
               host: 10.20.150.61
               port: 6322
             -
               host: 10.20.150.59
               port: 6322
         -
           servers:
             -
               host: 10.20.150.60
               port: 6322
             -
               host: 10.20.150.62
               port: 6322

然後爲了迎合clustermap配置的這種設計,在clustermap的模版配置文件也需要改一下:

<?xml version="1.0"?>
<clustermap>
    <!-- 關於Merger的配置,這裏我暫時不考慮merger多臺的情況 -->
    <merger_list>
            <merger_cluster name=m1 level=1>
                <merger ip=${merger["host"]} port=${merger["port"]} protocol=http/>
            </merger_cluster>
    </merger_list>

<!-- 下面是searcher的多行行列的配置,是一個二維數組 -->
<search_list>
<%
id = 1  # 這個值是紀錄searcher列的名字
%>
<!-- 第一個緯度,同一列的 -->
% for searcher in searchers:
<search_cluster name=c${id} docsep=false level=1 partition=0>
    <!-- 第二個緯度,同一行的 -->
    % for server in searcher["servers"]:
    <search ip=${server["host"]} port=${server["port"]} protocol=tcp type=mix />
    % endfor
</search_cluster>
    <%
    id += 1
    %>
% endfor
</search_list>

    <merger_cluster_list>
            <merger_cluster name=m1>
                % for i in range(1, id):
                <search_cluster name=c${i} />
                % endfor
            </merger_cluster>
    </merger_cluster_list>
</clustermap>

這樣比如1行2列渲染出來成了:

<?xml version="1.0"?>
<clustermap>
    <merger_list>
            <merger_cluster name=m1 level=1>
                <merger ip=10.20.137.22 port=6088 protocol=http/>
            </merger_cluster>
    </merger_list>

<search_list>
<search_cluster name=c1 docsep=false level=1 partition=0>
    <search ip=10.20.150.60 port=6322 protocol=tcp type=mix />
</search_cluster>
<search_cluster name=c1 docsep=false level=1 partition=0>
    <search ip=10.20.150.61 port=6322 protocol=tcp type=mix />
</search_cluster>
</search_list>

    <merger_cluster_list>
            <merger_cluster name=m1>
                <search_cluster name=1 />
            </merger_cluster>
    </merger_cluster_list>
</clustermap>

上述就是我在測試中,對分佈式環境的自動更新和批量性能測試,這樣大大減少了我們來回搗固機器、修改配置的時間。而且對測試結果的自動收集和解析也可以幫助我們來分析測試結果。我覺得這是一個不錯的嘗試,大家可以都可以試試看。

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