6.2 团队的实践
我们之前的测试环境是虚拟机,在实际使用过程中遇到了不少问题,随着团队的增长,这些测试机资源严重不足,并且目录设置混乱,导致了数据污染,甚至因存储空间不足而使服务挂掉。2017年,我们的测试环境逐渐迁移到了容器环境JDOS。JDOS提供了两种访问方式来部署测试环境,第一种是页面访问,这种访问方式很直观,但缺点就是用户需要手动进行每一个步骤:编译、构建镜像、创建分组、配置集群、集群上线等等。这样的方式操作比较繁琐,效率也低。另一种方式就是JDOS提供了一组比较完备的API,通过API可以完成以上所有操作。基于这套API,我们开发了适用于本部门的持续集成系统。
6.2.1 实现思路
这套系统我们是通过两步实现的,第一步是先实现了一套自动部署系统,使大家能够方便的部署和管理测试环境,同时为实现持续集成系统提供基础环境。之后我们开发了运行单元测试和接口测试的服务和任务管理服务,和自动部署一起组成了持续集成系统。
整个系统的架构如图6-5所示。
图6-5 部门CI系统架构图
下面逐个对这些服务进行介绍。
6.2.2 部署服务
部署服务的设计非常简单,其实就是通过调用JDOS的API将部署的过程串联起来。需要注意的是,JDOS提供的RESTful接口都是异步的,因此在调用接口触发了一项耗时的任务后,需要以一定的频率去调用相应的查询接口去判断前一个步骤的结果,如流程图6-6所示:
图6-6 自动部署流程图
为了适应我们一些比较早期的项目,部署系统要支持一些模块间或应用间的依赖问题,这些模块没有上传到maven仓库,而是需要先在本地编译被依赖的模块,添加到本地maven库,然后再编译需要依赖那个模块的应用,最后再将应用部署到容器环境中。
解决本地编译我们的思路是不想依赖比较流行的Jenkins来管理从下载代码到编译构建这部分的任务。Jenkins是很强大的任务调度平台,但是如果用在这里只是解决我们的依赖编译问题,对功能上和性能上都是很大的浪费。
我们选择了jgit来下载代码到指定的工作空间,为了使每次构建的工作空间都不受污染,都会建立新的文件夹作为工作空间。将这段xml粘贴到pom.xml即可引入jgit,如代码清单6-2所示。
代码清单6-2 Pipeline Script
1.<dependency> 2. <groupId>org.eclipse.jgit</groupId> 3. <artifactId>org.eclipse.jgit</artifactId> 4. <version>${jgit.version}</version> 5.</dependency> |
下面这段代码演示了怎样通过jgit来配置用户名、密码、git地址,最后克隆代码到本地工作空间的,如代码清单6-3所示。
代码清单6-3 Clone代码
1.public static Path cloneRepository(Path workspace, String url, String branch, String username, String password) throws IOException { 2. Path workingPath = workspace == null ? createWorkSpace() : workspace; 3. logger.info("Start to clone git repository {}", sb.toString()); 4. CredentialsProvider cp = new UsernamePasswordCredentialsProvider(username, password); 5. String exceptionMessage = String.format("Clone git repository repository %s of branch %s failed", url, branch); 6. try (Git git = Git.cloneRepository().setURI(url).setBranch(branch).setDirectory(new File(workingPath.toUri())).setCredentialsProvider(cp).call()) { 7. logger.info("Clone git repository repository {} of branch {} finished", url, branch); 8.} catch (GitAPIException e) { 9. logger.error(String.format("Clone git repository repository %s of branch %s failed", url, branch)); 10.} 11. return workingPath; 12.} |
引入maven-invoker,如代码清单6-4所示。
代码清单6-4 引入maven-invoker
1.<dependency> 2. <groupId>org.apache.maven.shared</groupId> 3. <artifactId>maven-invoker</artifactId> 4. <version>${maven.invoker.version}</version> 5.</dependency> |
然后进行本地编译,如代码清单6-5所示。
代码清单6-5 编译代码
1.public static File buildMavenProject(Path workingPath, BuildJob job) throws MavenInvocationException, IllegalStateException { 2. logger.debug("Start maven build for git project {}", job.getGitUri()); 3. InvocationRequest request = new DefaultInvocationRequest(); 4. 5. // 设置环境变量 request.addShellEnvironment("JAVA_HOME",getJavaHome(job.getJdk()).getAbsolutePath()); 6. request.addShellEnvironment("M2_HOME",getMavenHome(job.getMvnVersion()).getAbsolutePath()); 7. 8. // 设置maven的任务参数 9. request.setPomFile(new File(workingPath.toString() + "/" + job.getRootPom())); 10. request.setBaseDirectory(workingPath.toFile()); 11. request.setJavaHome(getJavaHome(job.getJdk())); 12. MavenArgs args = parseArguments(job.getMvnCommand(), request); // 需要解析maven命令中的各个参数 13. request.setGoals(args.getGoals()); // 目标可能包含clean、validate、compile、test、package、verify、install、deploy中的一个或多个 14. request.setProfiles(args.getActivateProfiles()); // 即是-P参数 15. request.setProperties(args.getDefines()); // 即是-D参数 16. request.setUpdateSnapshots(args.isUpdateSnapshots()); // 即是-U参数 17. request.setDebug(true); 18. 19. Invoker invoker = new DefaultInvoker(); 20. invoker.setMavenHome(getMavenHome(job.getMvnVersion())); 21. 22. // 执行maven命令并获得结果 23. InvocationResult result = invoker.execute(request); 24. 25. // 如果ExitCode不等于零,说明执行失败 26. if (result.getExitCode() != 0) { 27. logger.debug("Maven build failed for git project {}" + job.getGitUri()); 28. throw new IllegalStateException("Maven build failed for " + job.getGitUri()); 29. } 30. 31. // 返回编译出来的包地址 32. File warFile = job.getWarPath() == null ? null : new File(workingPath.toString() + "/" + job.getWarPath()); 33. logger.debug("Finished maven build for git project {}, package was placed {}", job.getGitUri(), warFile == null ? EMPTY_STRING : warFile.getAbsolutePath()); 34. return warFile; 35. } 36.} |
BuildJob是包含编译相关的参数的对象,如代码清单6-6所示。
代码清单6-6 BuildJob
1.public class BuildJob { 2. private String appName; //应用名 3. private String username; // Git用户名 4. private String password; // Git密码 5. private String gitUri; // Git地址 6. private String branch; // Git分支 7. private int jdk; // JDK版本 8. private int mvnVersion; //maven版本号 9. private String rootPom; //pom.xml文件的位置 10. private String mvnCommand; // maven命令 11. ...... 12.} |
在手动部署的过程中,用户最少需要填20多个参数,但是经过研究我们发现,对于一个确定的系统来说在集群多次上线过程中绝大部分参数都不用更改,因此我们想到自己设计配置脚本来保存编译构建和部署的参数,以达到复用和减少用户输入的目的。如代码清单6-7所示。
代码清单6-7 应用部署脚本
1.{ 2. "jdosDependencies": // 依赖编译,列表中每一个元素都是一个被依赖的模块,这些模块会按顺序依次编译 3. [{ 4. "jobs":[{ 5. "appName":"${appName}", // 模块名 6. "password":"${password}", // Git用户名 7. "password":"${password}", // Git密码 8. "gitUri":"${url}", // Git地址 9. "branch":"${branch}", // Git分支 10. "jdk":"${jdkVersion}", // JDK版本 11. "mvnVersion":"${mvnVersion}", // maven版本,可以是2或者3 12. "rootPom":"${path}", // pom文件路径 13. "mvnCommand":"${command}" // maven编译命令 14. }] 15. }], 16. "systemName": "${systemName}", /*系统名,由用户在JDOS中定义*/ 17. "jdosAppProcesses": [ /*定义系统下所有应用部署过程中的参数*/ 18. { 19. "appName": "${appName}", /*应用名,由用户在JDOS中定义*/ 20. "javaGitCompileParam": { /*应用的编译参数*/ 21. "compileEnvironment": "linux", /*编译环境的系统,目前只支持Linux*/ 22. "projectCode": "utf-8", /*项目编码*/ 23. "packageType": "maven-3", /*Java应用都是通过maven管理依赖的,版本有maven-2或maven-3*/ 24. "gitUrl": "${gitUrl}", /*项目git地址*/ 25. "branchName": "${branch}", /*分支名*/ 26. "rootPom": "pom.xml", /*编译的pom文件位置*/ 27. "compileCommand": "${mvnCommand}", /*maven编译命令*/ 28. "jsfAliasProperties": "${jsfAliasProperties}", /*配置JSF框架中服务的别名*/ 29. "targetPath": "${path}", /*应用的抽包地址*/ 30. "params": "${params}", /*即是mvn后面的-P参数*/ 31. "compileLanguage": "jdk-8", /*Java版本*/ 32. "systemName": "${systemName}" 33. }, 34. "buildJavaImageParam": { /*基础镜像的相关配置*/ 35. "count": "1", /*容器镜像内的tomcat实例数,推荐单实例*/ 36. "repositoryName": "${repositoryName}", /*编译后的镜像名*/ 37. "selectImage": "${baseImage}", /*基础镜像*/ 38. "versionSuffix": "${versionSuffix}" /*版本后缀,可以帮助用户区分不同版本的构建*/ 39. }, 40. "createGroupParam": { /*创建分组的参数*/ 41. "systemName": "${systemName}", 42. "appName": "${appName}", 43. "groupName": "${groupName}", /*分组名,准确的说是分组的真名,所有的RESTful API都是通过这个名字操作已有分组的*/ 44. "nickname": "${nickname}", /*别名,这个名字会显示在分组的标签上*/ 45. "desc": "${desc}", /*对分组的描述*/ 46. "environment": "${env}", /*容器部署的目标环境,有test用来做测试环境,和prod作为生产环境*/ 47. "jsfStatus": "active", /*是否自动上线JSF服务*/ 48. "platformHosted": "true", /*是否自动拉起容器,如果自动拉起,在容器出现故障后或者JDOS从系统故障恢复的过程中,容器会被重置*/ 49. "region": "${region}" /*选择机房*/ 50. }, 51. "createGroupConfigParam": { /*配置分组*/ 52. "systemName": "${systemName}", 53. "appName": "${appName}", 54. "groupName": "${group}", 55. "flavor": "${flavor}", /*容器规格*/ 56. "diskSize": 10, /*容器的磁盘空间*/ 57. "envs": [ /*配置容器内的环境变量*/ 58. { 59. "key": "${key}", 60. "value": "${value}" 61. } 62. ] 63. }, 64. "createGroupConfigFileParams": [ /*创建容器内的配置文件*/ 65. { 66. "systemName": "${systemName}", 67. "appName": "${appName}", 68. "groupName": "${group}", 69. "filePath": "${path}", /*文件路径*/ 70. "fileContent": "${content}", /*文件内容*/ 71. "isImportant": "false" /*文件是否加密*/ 72. } 73. ], 74. "containerNumber": "${number}" /*集群中容器的数量*/ 75. } 76. ] 77.} |
jdosAppProcesses下面的那些应用的部署是多线程执行的,能节省大量的时间。经测试,如果一个系统由五个应用组成,通过自动部署服务只需要不到六分钟即可部署到测试环境上,比手动部署快五倍以上。
相关推荐:
版权声明:51Testing软件测试网获人民邮电出版社和作者授权连载本书部分章节。
任何个人或单位未获得明确的书面许可,不得对本文内容复制、转载或进行镜像,否则将追究法律责任。