前言
在前几章中,我们完成了手动发布的全流程:选择业务、选择目标机器、点击"一键部署"按钮,系统自动执行 git clone、maven 打包、rsync 同步、SSH 重启。这个流程已经比纯手工 ssh 上去敲命令高效很多,但每次 push 代码都要打开 Web 页面点一下,仍然低效------尤其是验证环境频繁提交、多团队共用发布系统时。
本章介绍发布系统的 Webhook 自动发布机制:开发者在 GitLab 上 push 代码后,系统自动触发构建和部署,真正实现"git push 即发布"。核心涉及 TriggerController、TriggerService、PublishRelation 分支映射和 LinkedBlockingQueue 削峰队列。
9.1 整体架构
Webhook 自动发布的链路如下:
GitLab Push → POST /webhook/trigger
→ TriggerController.trigger()
→ TriggerService.trigger()
→ 事件过滤(只处理 Push/Tag Push)
→ pushProcess() 匹配 PublishRelation
→ 创建 PublishTask(type=onekey)
→ 加入 LinkedBlockingQueue
→ @Scheduled(fixedDelay=6000) 定时消费
→ AsyncDeploy.deploy()(同手动发布流程)
整个流程由三个角色协作完成:TriggerController 负责接收 HTTP 请求,TriggerService 负责业务逻辑和队列管理,AsyncDeploy 负责实际的部署执行。
9.2 TriggerController --- Webhook 入口
GitLab Webhook 的核心是一个 HTTP POST 请求。发布系统暴露了两个入口:
java
// TriggerController.java
@RestController
@RequestMapping("/webhook")
@Slf4j
public class TriggerController {
@Autowired
private TriggerService triggerService;
@PostMapping({"/trigger", "/push"})
public String trigger(@RequestBody PushBean bean) {
log.info("webhook trigger received: projectId={}, ref={}",
bean.getProjectId(), bean.getRef());
triggerService.trigger(bean);
return "ok";
}
@PostMapping("/system")
public String system(@RequestBody SystemBean bean) {
log.info("webhook system event received: eventName={}",
bean.getEventName());
triggerService.systemEvent(bean);
return "ok";
}
}
设计上非常简洁:接收 JSON 请求体,映射为 PushBean 或 SystemBean,交给 TriggerService 处理,然后立即返回 "ok"。这里的关键设计决策是不做同步部署------Controller 只负责接收事件并入队,真正耗时的构建和部署由后台队列异步消费。这样可以确保 Webhook 回调不会超时(GitLab 默认 10 秒超时),也避免了大量 push 事件瞬间涌入时的资源竞争。
9.3 PushBean 数据模型
PushBean 是 GitLab Push Event 的核心数据载体,字段精简到只保留发布系统需要的信息:
java
// PushBean.java
@Data
public class PushBean {
private String objectKind; // 事件类型:push / tag_push / merge_request
private String ref; // 完整引用:refs/heads/master 或 refs/tags/v1.0
private String userEmail; // 提交者邮箱
private List<CommitBean> commits; // 提交列表
private long projectId; // GitLab 项目 ID
}
GitLab 原始 Webhook 负载有几十个字段,但发布系统只关注这几个。objectKind 决定事件路由,ref 决定分支匹配,projectId 关联到 publish_biz 表找到对应的业务配置。
9.4 TriggerService --- 事件路由与任务创建
TriggerService 是 Webhook 处理的核心。它完成四件事:事件过滤、分支->项目匹配、任务创建、入队。
9.4.1 事件过滤
GitLab Webhook 会推送多种事件类型。发布系统只处理 Push 和 Tag Push,忽略 Merge Request 和 System 事件:
java
// TriggerService.java --- 事件常量定义
public static class EventName {
public static final String PUSH = "push";
public static final String TAG_PUSH = "tag_push";
public static final String MERGE_REQUEST = "merge_request";
public static final String SYSTEM = "system";
}
// trigger() --- 事件分发
public void trigger(PushBean bean) {
if (bean == null || bean.getObjectKind() == null) return;
switch (bean.getObjectKind()) {
case EventName.PUSH:
case EventName.TAG_PUSH:
pushProcess(bean);
break;
default:
log.debug("ignored webhook event: {}", bean.getObjectKind());
}
}
Merge Request 被忽略是因为 MR 只是代码审查流程,不应触发部署。systemEvent() 方法单独处理 GitLab 系统钩子(如项目创建、用户变更),当前仅做日志记录,预留扩展空间。
9.4.2 pushProcess --- 核心匹配与任务创建
这是整个自动发布最核心的方法,执行五步匹配链:
java
private void pushProcess(PushBean bean) {
long projectId = bean.getProjectId();
String ref = bean.getRef();
String branch = extractBranchName(ref);
// Step 1: 根据 projectId 查找 PublishBiz
List<PublishBiz> bizList = bizService.findByProjectId(projectId);
if (bizList.isEmpty()) {
log.warn("no biz found for projectId {}", projectId);
return;
}
for (PublishBiz biz : bizList) {
// Step 2: 根据 branch 查找匹配的 PublishRelation
List<PublishRelation> relations = relationService.lambdaQuery()
.eq(PublishRelation::getBizId, biz.getId())
.eq(PublishRelation::getBranch, branch)
.list();
if (relations.isEmpty()) {
log.debug("no relation found for biz {} branch {}",
biz.getName(), branch);
continue;
}
for (PublishRelation relation : relations) {
// Step 3: 找到对应 PublishApp
PublishApp app = appService.getById(relation.getAppId());
if (app == null) {
log.warn("app not found for relation {}", relation.getId());
continue;
}
// Step 4: 创建 PublishTask
PublishTask task = new PublishTask();
task.setBizId(biz.getId());
task.setName(biz.getName() + "-" + branch);
task.setProfile(app.getProfile());
task.setCode(branch);
task.setType(TaskType.onekey.getName());
task.setStatus(TaskStatus.create.getName());
task.setDescription("auto trigger from webhook: " + objectKind);
taskService.insertAndSetObjectId(task);
// Step 5: 加入队列
taskQueue.offer(task);
}
}
}
每一步都有明确的失败处理:
- 找不到 Biz:直接 return,日志 warn。这种情况通常是因为 GitLab 项目还没有在发布系统中注册。
- 找不到 Relation:continue 到下一个 Biz,日志 debug。说明这个分支没有配置自动部署,属于正常情况(比如临时分支不需要部署)。
- 找不到 App:continue,日志 warn。Relation 存在但 App 被删除了,数据不一致。
extractBranchName() 辅助方法负责从 Git 引用中提取分支名:
java
private String extractBranchName(String ref) {
if (ref == null) return null;
if (ref.startsWith("refs/heads/")) {
return ref.substring("refs/heads/".length()); // → master
}
if (ref.startsWith("refs/tags/")) {
return ref.substring("refs/tags/".length()); // → v1.0
}
return ref;
}
9.4.3 队列消费与削峰
任务创建后不是直接执行 deploy,而是放入 LinkedBlockingQueue,由定时任务消费:
java
private final LinkedBlockingQueue<PublishTask> taskQueue =
new LinkedBlockingQueue<>();
@Scheduled(fixedDelay = 6000)
public void consumeQueue() {
PublishTask task = taskQueue.poll();
if (task == null) return;
try {
log.info("consuming auto task {}", task.getId());
deploy(task);
} catch (Exception e) {
log.error("deploy task {} failed", task.getId(), e);
task.setStatus(TaskStatus.fail.getName());
taskService.updateById(task);
}
}
这里有两个设计要点:
为什么用队列而不是直接 deploy? Webhook 回调在 HTTP 线程中执行,如果直接调用 AsyncDeploy.deploy()(包含 git clone、maven 编译、rsync 同步等耗时操作),HTTP 线程会被长时间阻塞,GitLab 端会因为超时认为 Webhook 失败而重试。用队列可以立即返回 "ok",让 GitLab 确认收到事件。
为什么是 6 秒消费一个? fixedDelay=6000 意味着每次任务完成后等待 6 秒再取下一个。这不是简单的限流------它保证了两件事:一是在多台机器部署的场景下,给上一台机器的服务启动留出缓冲;二是防止大量 push 事件同时涌入导致编译服务器 CPU 打满。队列的 LinkedBlockingQueue 天然是线程安全的,一个 Producer(HTTP 线程)多个 Consumer(虽然当前只有一个定时任务)的经典模式。
9.5 PublishRelation --- 分支到机器的映射
publish_relation 表是整个自动发布的配置核心,它是一个三元组:biz_id + app_id + branch。
java
// PublishRelation.java
@TableName("publish_relation")
public class PublishRelation extends BaseEntity {
private long bizId; // 关联业务 ID
private long appId; // 关联目标机器 ID
private String branch; // 触发分支名
private Date createTime;
}
这个设计支持灵活的分支部署策略:
- 同一分支绑定多台机器:一条 branch = "master" 的记录对应多个 appId,push master 后同时部署到多台机器。
- 不同分支绑定不同机器:dev 分支绑定测试机,master 分支绑定生产机。
- 不绑定的分支不触发:feature 分支不创建 Relation,push 后系统静默忽略。
9.6 在管理后台配置自动部署
自动部署的配置入口在 BizController.addDeployment():
ANY /biz/app/autoDeploy/create
配置流程简述:
- 进入业务管理页面,选择一个 App(目标机器)
- 点击"配置自动部署",系统通过 GitLab API 拉取该项目的分支列表
- 选择要自动部署的分支
- 系统检查是否已存在 GitLab Webhook 配置(
publish_hook表),没有则自动创建 - 在
publish_relation表中插入一条(bizId, appId, branch)记录
其中自动创建 GitLab Webhook 是一个关键的自动化步骤------管理员不需要手动去 GitLab 项目设置里填写 URL,系统内部调用 GitLab API 完成注册:
java
// BizController.addDeployment() 中的 Webhook 自动创建逻辑
List<PublishHook> hooks = hookService.lambdaQuery()
.eq(PublishHook::getBizId, deploy.getBizId()).list();
if (hooks.isEmpty()) {
String hookId = addProjectHook(token, biz.getProjectId());
// 保存 hook 记录到 publish_hook 表
PublishHook hook = new PublishHook();
hook.setBizId(deploy.getBizId());
hook.setHookId(hookId);
hookService.save(hook);
}
9.7 完整端到端时序
以一个开发者在 GitLab 上 push master 分支为例,完整的发布流程:
1. git push origin master
│
2. GitLab 发送 POST /webhook/trigger
Body: { "objectKind":"push", "ref":"refs/heads/master", "projectId":42 }
│
3. TriggerController.trigger() 接收,立即返回 "ok"
│
4. TriggerService.trigger() → pushProcess()
├─ extractBranchName("refs/heads/master") → "master"
├─ findByProjectId(42) → PublishBiz(id=1, name="hotel-api")
├─ 查询 publish_relation WHERE biz_id=1 AND branch="master"
│ → [relation1(appId=101), relation2(appId=102)]
├─ 创建 PublishTask(type=onekey, code="master", status=create)
└─ taskQueue.offer(task)
│
5. @Scheduled(fixedDelay=6000) 在下一个 6 秒窗口消费
└─ consumeQueue() → taskQueue.poll() → deploy(task)
│
6. AsyncDeploy.deploy()(同手动发布流程)
├─ git clone / pull
├─ maven package
├─ rsync to 192.168.1.101
├─ SSH restart
├─ rsync to 192.168.1.102
└─ SSH restart
整个过程,开发者只做了一件事:git push。其余全部由发布系统自动完成。
9.8 Webhook 安全与建议
当前实现中,Webhook 端点没有 Secret Token 验证。对于内网部署的发布系统,这通常是可接受的------发布系统不暴露在公网,GitLab 也是内网服务。但如果发布系统需要开放到公网或更严格的安全环境,建议增加验证:
- 在 GitLab Webhook 设置中填写 Secret Token
- TriggerController 中校验请求头
X-Gitlab-Token - 对请求体做 HMAC 签名校验
总结
本章从 TriggerController 入口开始,逐层分析了 Webhook 自动发布的完整链路:事件接收 → 类型过滤 → 项目匹配 → 分支映射 → 任务创建 → 队列削峰 → 定时消费 → 异步部署。核心设计思想是快速响应 + 异步处理------HTTP 层只做转发,业务逻辑由队列解耦,部署执行复用成熟的 AsyncDeploy 流程。
PublishRelation 的三元组设计使得分支到机器的映射灵活且可扩展,支持同一分支多机部署、不同分支不同环境等常见场景。
下一章将介绍部署流程中的路由管理------如何在部署前摘除流量、部署后恢复流量,以及灰度发布的雏形实现。