在搜索引擎的測試過程中,經常會遇到以下兩個問題:
- 需要搭建和更新分佈式測試環境
- 在性能測試時,我們需要測試不同集羣規模和配置下的環境時,如何自動更新測試環境和批量進行性能測試
因此,我們需要設計一個腳本,這個腳本可以幫我來完成這些事。
在這裏,我推薦使用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>
上述就是我在測試中,對分佈式環境的自動更新和批量性能測試,這樣大大減少了我們來回搗固機器、修改配置的時間。而且對測試結果的自動收集和解析也可以幫助我們來分析測試結果。我覺得這是一個不錯的嘗試,大家可以都可以試試看。