背景

在使用 jcci 项目后,发现了一些功能其实可以更加完美,通过传入项目 git 地址 分支 两次的 commit id,即可分析出两次 commit id 之间代码改动所带来的影响,并生成树图数据方便展示影响链路(README 原话)通过解析 cci 文件,得知具体受影响的 API,但无法得知具体是哪个微服务下的 API 受影响了,也不知道具体 API 名称,为此需要对这个进行完善,做到精准回归测试

方案设计

流程图

先看一下流程图,我们是以部署节点作为最新的 commit id,与本地的 commit id 作为比较分析,譬如刚开始初始化克隆项目后,去到项目根目录执行git rev-parse HEAD即可获取当前的 commit id,当开发在流水线平台操作部署时,通过 JenkinsFile 脚本,即可获取最新的 commit id,再调用执行分析 api,即可分析出此次需要回归的接口(图中红色框则需要完善的功能,jcci 分析直接调包即可调包侠

维护关系

上图,我们可以得知有以下关系,一个 Git 项目维护着好几个微服务应用,mh-oms-parent 项目下面有 3 个微服务应用(mh-oms-adminmh-oms-bizmh-oms-paymh-oms-user)需要把各个微服务的接口文档数据同步下来,这里需要把解析 swagger 文档数据,再将解析后的数据进行入库,支持手动页面同步 + 定时器补偿(这里就不分享源码,都是些 curd 没意义,倒是可以看看库表设计,有兴趣可以问问我相关的细节)

# api项目表(微服务维度)
CREATE TABLE `api_project` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `business` varchar(64) NOT NULL COMMENT '业务线: MH_BUSINESS_TYPE',
  `project_name` varchar(64) NOT NULL COMMENT '项目名',
  `git_project_name` varchar(64) NOT NULL COMMENT 'git项目名',
  `description` varchar(64) DEFAULT NULL COMMENT '项目描述',
  `owner_name` varchar(32) NOT NULL COMMENT '负责人',
  `owner_code` varchar(32) NOT NULL COMMENT '负责人编码',
  `app_name` varchar(32) NOT NULL COMMENT '服务名',
  `create_code` varchar(20) NOT NULL COMMENT '创建人编码',
  `create_name` varchar(20) NOT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_code` varchar(20) DEFAULT NULL COMMENT '更新人编码',
  `update_name` varchar(20) DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` smallint(6) NOT NULL COMMENT '0: 未删除 1: 已删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;
# 数据源表
CREATE TABLE `api_project_source` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `project_id` int(11) NOT NULL COMMENT '关联的项目id',
  `source_name` varchar(64) NOT NULL COMMENT '数据源名称',
  `source_format` smallint(6) NOT NULL COMMENT '数据源格式: TOOLS_DOC_TYPE',
  `source_url` varchar(255) NOT NULL COMMENT '数据源URL',
  `path_prefix` varchar(32) DEFAULT NULL COMMENT '路径前缀',
  `enable` tinyint(1) NOT NULL COMMENT '是否启用',
  `import_rate` smallint(6) NOT NULL COMMENT '导入频率,业务字典:TOOLS_DOC_IMPORT_RATE',
  `state` smallint(6) DEFAULT NULL COMMENT '运行状态,业务字典:TOOLS_DOC_RUN_STATE',
  `last_import_time` datetime DEFAULT NULL COMMENT '上次导入时间',
  `create_code` varchar(20) NOT NULL COMMENT '创建人编码',
  `create_name` varchar(20) NOT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_code` varchar(20) DEFAULT NULL COMMENT '更新人编码',
  `update_name` varchar(20) DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` smallint(6) NOT NULL COMMENT '0: 未删除 1: 已删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
# 目录表
CREATE TABLE `api_directory` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `project_id` int(11) NOT NULL COMMENT '项目id',
  `parent_id` int(11) DEFAULT NULL COMMENT '父id,为空就是根目录',
  `name` varchar(128) DEFAULT NULL COMMENT '目录名',
  `type` smallint(6) NOT NULL COMMENT '目录类型: 1:api_object_data, 2:sql_object, 3:dubbo_object, 4:case_object, 5:suite_object',
  `index` int(11) NOT NULL COMMENT '目录排序',
  `create_code` varchar(20) NOT NULL COMMENT '创建人编码',
  `create_name` varchar(20) NOT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_code` varchar(20) DEFAULT NULL COMMENT '更新人编码',
  `update_name` varchar(20) DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` smallint(6) NOT NULL COMMENT '0: 未删除 1: 已删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4;
# api对象数据表
CREATE TABLE `api_object_data` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `project_id` int(11) NOT NULL COMMENT '项目id',
  `directory_id` int(11) DEFAULT NULL COMMENT '目录id',
  `name` varchar(128) NOT NULL COMMENT '接口名称',
  `description` varchar(256) DEFAULT NULL COMMENT '接口描述',
  `tag` varchar(256) DEFAULT NULL COMMENT '接口标签',
  `base_url` varchar(256) DEFAULT NULL COMMENT '接口域名',
  `base_path` varchar(128) NOT NULL COMMENT '接口路径',
  `method` varchar(32) NOT NULL COMMENT '接口请求方法',
  `header` text COMMENT '接口请求头',
  `query` text COMMENT '接口查询参数',
  `body` text COMMENT '接口请求参数',
  `body_type` smallint(6) NOT NULL COMMENT 'body类型: 0: none 1: json 2: form 3: x-form 4: binary 5: GraphQL',
  `response` text COMMENT '接口返回参数',
  `create_code` varchar(20) NOT NULL COMMENT '创建人编码',
  `create_name` varchar(20) NOT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_code` varchar(20) DEFAULT NULL COMMENT '更新人编码',
  `update_name` varchar(20) DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` smallint(6) NOT NULL COMMENT '0: 未删除 1: 已删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=171 DEFAULT CHARSET=utf8mb4;

维护 Jcci 项目(Git 项目)

这里需要维护 Git 项目的相关信息,主要为 git url、branch、git token 等

# Jcci项目表
CREATE TABLE `jcci_git_project` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `project_name` varchar(64) NOT NULL COMMENT '项目名称',
  `description` varchar(64) DEFAULT NULL COMMENT '项目描述',
  `git_project` varchar(64) NOT NULL COMMENT 'git项目名',
  `git_url` varchar(256) NOT NULL COMMENT 'git地址',
  `git_branch` varchar(32) NOT NULL COMMENT 'git分支名',
  `git_token` varchar(256) NOT NULL COMMENT 'git token',
  `local_commit_id` varchar(256) DEFAULT NULL COMMENT '本地提交id',
  `status` smallint(6) NOT NULL COMMENT 'jcci项目状态: jcci_project_status',
  `create_code` varchar(20) NOT NULL COMMENT '创建人编码',
  `create_name` varchar(20) NOT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_code` varchar(20) DEFAULT NULL COMMENT '更新人编码',
  `update_name` varchar(20) DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` smallint(6) NOT NULL COMMENT '0: 未删除 1: 已删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

执行步骤

  1. 执行更新 swagger 文档数据
  2. 触发 jcci 分析 获取影响 urls
  3. 通过 urls 拼接机器人报告

重点说一下第二步,如何改造 jcci 的代码

主要用的是 jcci 项目中的analyze_two_commit方法,里面_can_analyze方法存在sys.exit(0)需要将其抛出异常或者直接 return,具体微改造如下:

analyze_two_commit方法改造如下:

嗯,改动点不是很多,调包完事,分析完,只要解析分析结果数据就好了

踩坑点

踩坑一:apscheduler 多进程环境重复运行

使用 Gunicorn 部署时,Gunicorn 可以指定 worker 参数,指的是开启的进程数,项目启动时,每次开一个 worker,都会启动一个 scheduler,这就导致了这些定时任务是由不同的进程创建的。
解决方法:
使用 Redlock(redis 分布式锁)进行定时任务上锁

def lock(key: Union[str, Callable], ttl: int = 3000):
    """
    redis分布式锁,基于redlock
    :param key: Redis key
    :param ttl: 锁释放时间
    :return:
    """

    def decorator(func):
        def wrapper(*args, **kwargs):
            try:
                redis_key = key(*args, **kwargs) if callable(key) else key
                if not redis_key:
                    raise BusinessException('缺少redis key无法上锁操作!')
                # 锁释放时间为30s
                with RedLock(redis_key, connection_details=Config.REDIS_NODES, ttl=ttl):
                    return func(*args, **kwargs)
            except RedLockError:
                appContextRepo.logger.error(f"redis key 为 {redis_key}")
                appContextRepo.logger.error(f"进程: {os.getpid()}, 执行函数{func.__name__}失败, 不用担心, 还有其他哥们给你执行了")
                raise RedLockException("操作太频繁了, 请稍后再试!!!")
        return wrapper
    return decorator

踩坑二:TimeZone offset does not match system offset

这里意思是运行的时区和系统时区不匹配
解决方法:

  1. 初始化调度器,指定时区

  1. 修改 Dockerfile 文件,指定时区

线上环境是 docker 容器,进行 cat /etc/timezone,显示的是 Etc/UTC,解决的思路是修改 Dockerfile,配置正确的时区,在 Dockerfile 中加入此行配置即可

踩坑三:json 格式化问题

将发送版本变更接口影响报告集成到流水线上,发现获取的提交信息和提交作者出现了换行,导致解析 json 失败

解决办法:

通过replace('\n','')将换行符替换掉

踩坑四:发送企微报告脚本异常

原本脚本上设定的超时时间为 2s,如果影响接口比较多的情况下,接口处理会比较慢,导致超时

解决方法:

将接口设置成异步接口 (直接用BackgroundTasks)

JenkinsFile 脚本

新增一个节点(作为分支节点,不影响原来部署流程),编写 curl 脚本,触发影响分析报告(异步生成报告,不影响原有流水线的构建速度)

# 获取提交人和提交信息 
commit_author = sh( script:"""echo `git log -1 --pretty=tformat:"%cn" `""", returnStdout: true)
println commit_author
commit_text = sh( script:"""echo `git log  -1 --pretty=tformat:"%s" `""", returnStdout: true)
println commit_text
try{
      timeout(2){
      def wxstdout = sh script:"""
      curl --location --request POST    'http://192.168.240.110:30080/api/tool/application/send' \
      --header 'Content-Type: application/json' \
      --data-raw '{
      "project": "mh-oms-parent",
      "commit_id": "${env.GIT_COMMIT}",
      "commit_text": "${commit_text.replace('\n','')}",
      "commit_author": "${commit_author.replace('\n','')}",
      "branch": "${env.GIT_BRANCH}",
      "key": "xxxxxxxxxxxxx"
      }'
      """, returnStdout: true
      println wxstdout
      }

      } catch (Exception e){
      println e
      echo '触发异常2!'
      }
}

飞书卡片报告模板

🏄  **项目:**<font color="grey">{project}</font>
🏁  **Commit_Id:**<font color="grey">{commit_id}</font>
📖  **分支:**<font color="grey">{branch}</font>
🙎🏻  **代码提交人:**<font color="grey">{commit_author}</font>
🚀  **代码提交信息:**<font color="grey">{commit_text}</font>
⏰  **发送时间:**<font color="grey">{now_time}</font>


**影响情况:**

报告效果

感谢

感谢 jcci 作者,写出这么棒的项目!@ 白开水 pp

Todo

  1. jcci 项目分析结果记录,前端页面可查看脑图节点(具体 mapper => impl => service => controller)
  2. 盘点出影响 API,回归执行对应的接口自动化脚本和推荐功能测试用例生成
  3. 盘点 N 个 xx 微服务(消费者)因为 yy 微服务(提供者)改动代码所影响的 API【联合注册中心应该能实现,有依赖关系,PS:开发觉得单个项目分析意义不大,比较鸡肋,还不如 ide 点点点我也同意,开发比较关注的是微服务级别的(说人话就是,我改了一个 rpc 接口 [dubbo、feign 等] 的实现方式,但是不知道这个接口,被哪些微服务所依赖,人工盘点影响范围,容易缺漏)】

总结

此次只是实现方案分享,没有源码,没有源码,没有源码(一大堆 curd 的代码,分享了也没意义);大家且看且珍惜,毕竟现在技术 xxxx,有兴趣可以问问我相关的细节,欢迎大家交流👏🏻👏🏻👏🏻


↙↙↙阅读原文可查看相关链接,并与作者交流