qileilove

blog已经转移至github,大家请访问 http://qaseven.github.io/

一种搭建分布式测试环境和批量性能测试的思路

 背景

  在搜索引擎的测试过程中,经常会遇到以下两个问题:

  ● 需要搭建和更新分布式测试环境

  ● 在性能测试时,我们需要测试不同集群规模和配置下的环境时,如何自动更新测试环境和批量进行性能测试

  因此,我们需要设计一个脚本,这个脚本可以帮我来完成这些事。

  在这里,我推荐使用Python,理由有:

  ● 写起来比较快(测试时间本来就比较紧张),不可能用C或者Java了

  ● 语法比较清晰,Shell、Perl这些维护起来太乱

  ● 自带的库、第三方的库比较丰富

  ● 另外,我个人比较喜欢Python的mako模版引擎和paramikossh2库。

  其实不用paramiko也可以,只要把机器ssh打通就可以。但我个人不太喜欢这种方式,觉得耦合性太强(只能在Linux下运行了)。

  设计

  批量性能测试的设计

  我很喜欢采用YAML格式,YAML格式的一大好处就是可以很方便的定义List、Map等类型

  1. tasks: 
  2.  # 第一个测试用例,我可能需要测试单线程的情况 
  3.  - 
  4.     id:1# ID的作用是你在脚本中可以拿id作为结果存放的目录 
  5.     parallelNum:1# 并发数 
  6.     seconds:1800# 压半个小时 
  7.     targetHost:10.20.137.22 # 目标主机 
  8.     targetPort:9999 
  9.     queryFilePath:/home/admin/access-log/add-600w.query  # 请求放在这儿 
  10.  # 第2个测试用例,我可能需要测试2线程的情况,这时我就只要再写一个,然后parallelNum: 2就可以了 
  11.  - 
  12.     id:1 
  13.     parallelNum:2 
  14.     seconds:1800 
  15.     targetHost:10.20.137.22 
  16.     targetPort:9999 
  17.     queryFilePath:/home/admin/access-log/add-600w.query

  在阿里的搜索平台这边,我们大多使用abench作为性能测试工具,它是一个命令行工具,只要命令+参数就可以了,比起JMeter要写JMeter脚本简单。因此,我再在配置文件中设计一下abench的命令格式。

  因为在运行命令中,有很多参数需要替换成上述测试用例设定的参数,因此需要采用模版引擎的方式。Python的模版引擎很多,我个人比较推荐mako。

  1. abenchPath:/opt/usr/bin/abench  # abench在哪儿? 
  2. abenchCommand:"${abenchPath} -p ${parallelNum} -s ${seconds} -k --http -o /dev/null ${targetHost} ${targetPort} ${queryFilePath}"
 配置文件设计好了,下面我们来写我们的Python脚本了(这里仅仅给出一些主要代码,大致明白意思就可以了)

  1. import subprocess 
  2. from mako.template import Template 
  3. import yaml 
  4.  
  5. # 运行一个测试任务 
  6. def runTask(config, task): 
  7.     runAbench(config, task) 
  8.  
  9. def runAbench(config, task): 
  10.      # 得到完成的abench运行命令 
  11.      command = Template(config["abenchCommand"]).render( 
  12.          abenchPath=config["abenchPath"], 
  13.          parallelNum=task["parallelNum"], 
  14.          seconds=task["seconds"], 
  15.          targetHost=task["targetHost"], 
  16.          targetPort=task["targetPort"], 
  17.          queryFilePath=task["queryFilePath"], 
  18.          ) 
  19.      pipe = subprocess.Popen(command, 
  20.          stdin=subprocess.PIPE, 
  21.          stdout=subprocess.PIPE, 
  22.          stderr=subprocess.PIPE, 
  23.          shell=True 
  24.          ) 
  25.      # 读取abench的运行结果,因为可能得保存下来吧 
  26.      result = pipe.stdout.read() 
  27.      # 下面可能是保存结果什么的 
  28.  
  29. if __name__ == "__main__": 
  30.     config = yaml.load(file(configFile)) 
  31.     for task in config["tasks"]: 
  32.        runTask(config, task)

  自动更新测试环境

  在我实际测试过程中,因为要更新的环境其实相当复杂,最多的时侯需要去10几台机器上做更新环境、停止/启动进程的操作。但我这里主要介绍思路,多一些机器和进程其实都一样。

  接着刚才的配置文件,我们只是在每一个task中设计了加压任务,但在加压前需要更新哪些环境没有涉及,按照阿里巴巴的ISearch架构,我 就启动一个一行两列的Searcher环境,2列Searcher上有一个Merger,然后再有一个clustermap来监控。

  1. abenchPath: /opt/usr/bin/abench  # abench在哪儿? 
  2. abenchCommand: "${abenchPath} -p ${parallelNum} -s ${seconds} -k --http -o /dev/null ${targetHost} ${targetPort} ${queryFilePath}" 
  3. # 关于Searcher的一些通用配置 
  4. searcher: 
  5.     templateConfigFile: /home/admin/access-log/searcher_server.cfg  # 因为启动时的监听端口等信息需要从下面的运行任务中读取,因此这个也设计成一个模版文件 
  6.     templateLogConfigFile: /home/admin/access-log/searcher_log.cfg 
  7.     # 在Search机器上操作的命令 
  8.     commands: 
  9.         - "${searchRoot}/bin/is_searcher_server -c ${configFile} -l ${logConfigFile} -k stop > /dev/null 2>&1" 
  10.         - "${searchRoot}/bin/is_searcher_server -c ${configFile} -l ${logConfigFile} -k start -d > /dev/null 2>&1" 
  11. # 关于Merger的一些通用配置,和Searcher差不多,就不写了 
  12.  
  13. tasks: 
  14.  # 第一个测试用例,我可能需要测试单线程的情况 
  15.  - 
  16.     id: 1 # ID的作用是你在脚本中可以拿id作为结果存放的目录 
  17.     parallelNum: 1 # 并发数 
  18.     seconds: 1800 # 压半个小时 
  19.     targetHost: 10.20.137.22 # 目标主机 
  20.     targetPort: 9999 
  21.     queryFilePath: /home/admin/access-log/add-600w.query  # 请求放在这儿 
  22.  
  23.     # 两台Search机器,定义一个List 
  24.     searchers: 
  25.        - 
  26.          host: 10.20.150.61 
  27.          port: 6322 # 监听的端口 
  28.          username: test # 因为需要通过ssh协议登录上去操作,因此需要用户名密码。如果你已经把机器ssh都打通了,那就不需要了 
  29.          password: 12345 
  30.          configFile: "${searchRoot}/scripts/conf/searcher_server.cfg" # 启动时运行的配置文件 
  31.          logConfigFile: "${searchRoot}/scripts/conf/searcher_log.cfg" # 启动时运行的日志文件 
  32.        - 
  33.          host: 10.20.150.60 
  34.          port: 6322 
  35.          username: test 
  36.          password: 12345 
  37.          configFile: "${searchRoot}/scripts/conf/searcher_server.cfg" 
  38.          logConfigFile: "${searchRoot}/scripts/conf/searcher_log.cfg" 
  39.  
  40.     # 我这边只有一台merger,如果merger也是有多台的话,也可以把这个设计成一个List 
  41.     merger: 
  42.        host: 10.20.137.22 
  43.        port: 6088 
  44.        username: test 
  45.        password: 12345 
  46.        configFile: "${searchRoot}/scripts/conf/merger_server.cfg"
然后比如关于Searcher的配置文件,在上面也是一个模版文件阿,我们可以把这个文件设计成:

  1. se_conf_file=${searchRoot}/scripts/conf/se.conf 
  2. simon_conf_path=${searchRoot}/scripts/conf/simon_searcher.xml 
  3. sort_config=${searchRoot}/scripts/conf/searcher_sort.xml 
  4. cache_size=0 
  5. cache_min_doc=0 
  6. conn_queue_limit=500 
  7. [services] 
  8. tcp ${port} # 主要就是为了替换监听的端口,其实要做得通用一点的话,很多配置都可以搞成变量,但就是可能你自己的配置文件变得很复杂。因此我们能不改的就尽量不改。 
  9.  
  10. [clustermap] 
  11. local_config_path=${searchRoot}/scripts/conf/clustermap.xml

  上述就是关于searcher和merger多行多列的配置,下面我们完善一下我们刚才的Python脚本

  1. # 得的一个ssh登录后的client对象,用于调用远程机器上的命令 
  2. def getClient(host, port, username, password): 
  3.     client = paramiko.SSHClient() 
  4.     client.load_system_host_keys() 
  5.     client.set_missing_host_key_policy(paramiko.WarningPolicy() 
  6.     client.connect(hostname, port, username, password) 
  7.     return client 
  8.  
  9. # 得到一个sftp对象,因为需要scp渲染好的配置文件什么的,因此需要sftp对象,它的put方法其实就类似scp 
  10. def getSftp(host, port, username, password): 
  11.     transport = paramiko.Transport((hostname, port)) 
  12.     transport.connect(username=username, password=password) 
  13.     sftp = paramiko.SFTPClient.from_transport(transport) 
  14.     return sftp 
  15.  
  16. # 更新和部署Searchers 
  17. def cleanSearchers(config, searchers): 
  18.     for searcher in searchers: 
  19.         # 得到渲染好的配置文件的内容 
  20.         templateLine = Template(file(config["searcher"]["templateConfigFile"]).read()).render( 
  21.             port=searcher["port"], 
  22.             searchRoot=config["searchRoot"] 
  23.             ) 
  24.         # 将渲染好的配置文件写入一个临时文件 
  25.         tmpConfigFile = tempfile.NamedTemporaryFile(delete=False) 
  26.         tmpConfigFile.file.write(templateLine) 
  27.         tmpConfigFile.file.close() 
  28.         # 将这个临时文件scp拷远程机器上的哪儿 
  29.         targetConfigFile = Template(searcher["configFile"]).render(searchRoot=config["searchRoot"]) 
  30.         sftp = getSftp(searcher["host"], 22, searcher["username"], searcher["password"]) 
  31.         sftp.put(tmpConfigFile.name, targetConfigFile) 
  32.         sftp.close() 
  33.         # 删除掉之前的临时文件 
  34.         os.remove(tmpConfigFile.name) 
  35.         # 运行启动searcher的命令 
  36.         client = getClient(searcher["host"], 22, searcher["username"], searcher["password"]) 
  37.         for command in config["searcher"]["commands"]: 
  38.             command = Template(command).render( 
  39.                 searchRoot=config["searchRoot"], 
  40.                 configFile=targetConfigFile, 
  41.                 logConfigFile=targetLogConfigFile 
  42.                 ) 
  43.             client.exec_command(cmd) 
  44.         client.close()
 关于clustermap的配置

  在阿里巴巴的ISearch架构中,searchers几行几列是由clustermap来配置的,我们这边也稍微简单话一点,不考虑 merger有多台的情况,就设计searchers几行几列的情况。更新一下刚才在task中的配置,加上关于clustermap的配置

  1.      clustermap: 
  2.        host: 10.20.137.22 
  3.        username: test 
  4.        password: 12345 
  5.        configFile: "${searchRoot}/scripts/conf/clustermap.xml" 
  6.        # 一台merge 
  7.        merger: 
  8.          host: 10.20.137.22 
  9.          port: 6088 
  10.        # 关于searcher的配置,其实是一个二维数组。第一个纬度是列,第2个纬度是行。以下这个例子是1列2行 
  11.        searchers: 
  12.          - 
  13.            servers: # 同一列下的机器 
  14.              - 
  15.                host: 10.20.150.61 
  16.                port: 6322 
  17.          - 
  18.            servers: 
  19.              - 
  20.                host: 10.20.150.60 
  21.                port: 6322

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

  1.        searchers: 
  2.          - 
  3.            servers: # 同一列下的机器 
  4.              - 
  5.                host: 10.20.150.61 
  6.                port: 6322 
  7.              - 
  8.                host: 10.20.150.59 
  9.                port: 6322 
  10.          - 
  11.            servers: 
  12.              - 
  13.                host: 10.20.150.60 
  14.                port: 6322 
  15.              - 
  16.                host: 10.20.150.62 
  17.                port: 6322
 然后为了迎合clustermap配置的这种设计,在clustermap的模版配置文件也需要改一下:

  1. <?xml version="1.0"?> 
  2. <clustermap> 
  3.     <!-- 关于Merger的配置,这里我暂时不考虑merger多台的情况 --> 
  4.     <merger_list> 
  5.             <merger_cluster name=m1 level=1> 
  6.                 <merger ip=${merger["host"]} port=${merger["port"]} protocol=http/> 
  7.             </merger_cluster> 
  8.     </merger_list> 
  9.  
  10. <!-- 下面是searcher的多行行列的配置,是一个二维数组 --> 
  11. <search_list> 
  12. <% 
  13. id = 1 # 这个值是纪录searcher列的名字 
  14. %> 
  15. <!-- 第一个纬度,同一列的 --> 
  16. % for searcher in searchers: 
  17. <search_cluster name=c${id} docsep=false level=1 partition=0> 
  18.     <!-- 第二个纬度,同一行的 --> 
  19.     % for server in searcher["servers"]: 
  20.     <search ip=${server["host"]} port=${server["port"]} protocol=tcp type=mix /> 
  21.     % endfor 
  22. </search_cluster> 
  23.     <% 
  24.     id += 1 
  25.     %> 
  26. % endfor 
  27. </search_list> 
  28.  
  29.     <merger_cluster_list> 
  30.             <merger_cluster name=m1> 
  31.                 % for i in range(1, id): 
  32.                 <search_cluster name=c${i} /> 
  33.                 % endfor 
  34.             </merger_cluster> 
  35.     </merger_cluster_list> 
  36. </clustermap>

  这样比如1行2列渲染出来成了:

  1. <?xml version="1.0"?> 
  2. <clustermap> 
  3.     <merger_list> 
  4.             <merger_cluster name=m1 level=1> 
  5.                 <merger ip=10.20.137.22 port=6088 protocol=http/> 
  6.             </merger_cluster> 
  7.     </merger_list> 
  8.  
  9. <search_list> 
  10. <search_cluster name=c1 docsep=false level=1 partition=0> 
  11.     <search ip=10.20.150.60 port=6322 protocol=tcp type=mix /> 
  12. </search_cluster> 
  13. <search_cluster name=c1 docsep=false level=1 partition=0> 
  14.     <search ip=10.20.150.61 port=6322 protocol=tcp type=mix /> 
  15. </search_cluster> 
  16. </search_list> 
  17.  
  18.     <merger_cluster_list> 
  19.             <merger_cluster name=m1> 
  20.                 <search_cluster name=1 /> 
  21.             </merger_cluster> 
  22.     </merger_cluster_list> 
  23. </clustermap>

  总结

  上述就是我在测试中,对分布式环境的自动更新和批量性能测试,这样大大减少了我们来回捣固机器、修改配置的时间。而且对测试结果的自动收集和解析也可以帮助我们来分析测试结果。我觉得这是一个不错的尝试,大家可以都可以试试看。

posted on 2013-01-04 14:27 顺其自然EVO 阅读(915) 评论(0)  编辑  收藏 所属分类: 性能测试


只有注册用户登录后才能发表评论。


网站导航:
 
<2013年1月>
303112345
6789101112
13141516171819
20212223242526
272829303112
3456789

导航

统计

常用链接

留言簿(55)

随笔分类

随笔档案

文章分类

文章档案

搜索

最新评论

阅读排行榜

评论排行榜