一定要看这个notion笔记

  1. https://app.notion.com/p/389be42d0e8580a0be1ee829f3d4bfa6
  2. 看看里面的答辩板块,会讲这个项目的价值

要重点看一下:

  1. 设计任务级租约锁(防并发重复提交)、实例级永久锁减小锁的粒度。并使用了老系统的POD 锁保证跨服务互斥,同时实现了事务失败时老系统POD锁的跨服务补偿。
  2. 升级异步执行机制,使用defer实现异常情况的自动处理,并增加定时兜底扫描保证最终一致性。【保证最终一致性:进程崩溃后租约锁过期,定时任务扫描并标记孤儿批次(批次状态为“执行中”但是对应任务的租约锁已过期的批次)为失败,保证批次执行状态的最终一致性】【要了解Python GIL的缺点和可以用的解决方法】【现在换成了Go有啥好处对应讲讲】
  3. 基于租约锁实现定时任务分布式选主,并与服务发现系统联动确保下线的实例不再执行定时任务。
  4. 用批量查询替代逐个查询,数量超过单次限制时自动分批;批量写入减少DB操作。对于多LDC且大批量Pod的流量变更任务,实现自动拆单,避免变更流量时由于服务状态发现限频导致的卡单。
  5. 结合策略注册器和策略模式实现风控规则的可配置、可热更新;能根据变更的各个阶段与变更场景(上线、下线、流量调权)自动确定所需的风险防控检查项和对应的级别(阻塞/提示)。
  6. 减少老系统的黑盒感,修改交互逻辑提升用户对系统的掌控能力,提升系统易用性。

更多:

两层锁解决的是不同层面的并发问题

1. LeaseMutex(任务级租约锁)— 防”同任务并发”

1
2
3
用户连续点击"执行" → 两次请求都进入 offline.go
→ 第一次 acquireTaskLease(taskID) 成功
→ 第二次 acquireTaskLease(taskID) 失败 → 拒绝
  • 锁 keytask_exec:{taskID}(任务维度)
  • 场景:防止用户重复点击、并发请求重复创建批次
  • 为什么用租约锁:进程崩溃 → 续约停止 → 锁自动过期释放,任务不会被永久卡死

2. Mutex(实例级锁)— 防”跨任务实例冲突”

1
2
任务A 下线机器X  → handleLockCheck 锁住 machine:X
任务B 也要下线机器X → check_task_conflict 检测到冲突 → 拒绝建单
  • 锁 key{ServiceType}:{ServiceName}:{Namespace}:{ResourceID}:{Port}(实例维度)
  • 场景:防止不同任务操作同一服务发现实例
  • 注意:灰度上线时的分批策略每个批次内会有重复的实例,从每个批次整合收集实例后要注意去重再加锁,否则DB会报错 msg:Duplicate entry ;每个任务建单时先一次性锁定所有批次的实例,每批次执行时只补偿锁定当前批次,避免每批次执行时全量加锁导致冲突提示指向非本次实例
  • 为什么用永久锁:实例锁需要跨任务存在(任务A加锁后,任务B的风控检查要能看到),不能因某个进程崩溃就自动释放(否则任务A还在执行,实例锁却释放了,任务B就能操作同一实例)

每个任务建单时先一次性锁定所有批次的实例,每批次执行时只补偿锁定当前批次,避免每批次执行时全量加锁导致冲突提示指向非本次实例

一个上线任务有 3 个正向批次,实例分布如下:

批次 实例 状态
批次1(当前执行) pod-A 待加锁
批次2(待执行) pod-B 待加锁
批次3(待执行) pod-C 已被另一个任务 T2 锁住
改动前:执行时全量加锁

ExecuteTask 执行批次1时,listFrontendInstancesByBatchType(Forward) 收集所有正向批次的实例(pod-A、pod-B、pod-C),一起调 handleLockCheck 加锁。

  • pod-C 被任务 T2 占用 → 触发锁冲突 → 报错:

    存在任务冲突,冲突任务: ops/T2

  • 问题:用户执行的是批次1(pod-A),报错却指向 pod-C / 任务 T2。pod-C 不属于本次批次,冲突提示指向了非本次实例,造成误导。

改动后:建单全量锁 + 执行只锁当前批次
1. 建单时一次性锁定全部实例

createTaskRecordAndBatches 事务内,持久化批次后调用 handleLockCheck 锁住 pod-A、pod-B、pod-C。

  • 若 pod-C 当时就被 T2 占用 → 建单直接失败,提示 pod-C 冲突。
  • 此时报冲突合理:建单本就要占用全部实例,冲突就该在建单时机暴露。
2. 执行时仅补偿当前批次

checkInstancesAndLock 只对当前批次实例(批次1 → pod-A)做补偿加锁。

  • 建单时已锁,此处基本是 HeldByMe 直接通过。
  • pod-B、pod-C 完全不参与,不会冒出”非本次实例”的冲突提示。
  • 补偿锁可防御锁过期或异常释放。
结论

冲突提示应在”真正要占用该实例的时机”出现。

时机 占用范围 报冲突是否合理
建单 全部实例 合理(建单要占用全部)
执行批次N 仅当前批次实例 只该报当前批次的冲突

改动把”全量加锁”从执行阶段前移到建单阶段,执行阶段只保留当前批次的补偿锁,避免冲突提示指向非本次批次实例。

两层配合关系

1
2
3
4
5
1. acquireTaskLease(taskID)           → 任务级锁,防并发
2. handleLockCheck(instances, taskID) → 实例级锁,防冲突
3. 如果实例锁失败 → releaseTaskLease() 释放任务级锁
4. 异步执行完成 → releaseTaskLease() 释放任务级锁
5. 任务完成/回滚 → 释放实例级锁

为什么不能用一层

场景 只用任务级锁 只用实例级锁 两层
同任务重复点击 ✅ 拦截 ❌ 拦不住(同 taskID 能拿到锁)
跨任务操作同实例 ❌ 拦不住(不同 taskID) ✅ 拦截
进程崩溃后恢复 ❌ 任务卡死(永久锁) ✅ 租约过期
实例锁跨任务可见 ❌ 不行

LeaseMutex 租约锁 和 Mutex 永久锁 共用同一张本地数据库表 t_global_lock,会设置 expire_time ,永久锁 100 年不会过期,租约锁TTL过期时间默认 30 秒,续约间隔是 TTL 的三分之一,每 10 秒续约一次租约。 NTMS POD 锁则完全不同,它通过远程 HTTP API 调用老系统(NTMS)的 /api/v2/lock/check_and_lock 接口实现,是跨服务锁,不写本地表。

分布式选主的逻辑和实现【与老系统的关键区别在于锁有没有续约】:为什么要选主,选的是什么主;选主实现;选主机制的设计要点。 老系统的 Redis set nx ex 180 是一次性锁,设了 180 秒就不管了,锁过期了,另一个实例发现 key 不存在就能拿到锁,开始重复执行同一个任务。它本质上只能去重短任务,长任务根本兜不住。

看 HVV 性能问题 的具体解决方法:北极星 QPS 限流 指数退避重试替代固定延迟;DB 跨城耗时。

注册器模式(Registry Pattern):策略和场景通过注册表管理,支持动态查找 责任链变体(Chain of Responsibility Variant):策略链按组串行执行,任一组失败则中断 策略链:按场景×阶段组合,组间串行、组内并行 并行执行优化:单策略直接执行避免 goroutine 开销,多策略用 trpc.GoAndWait 并行 策略注册:通过 uber/fx 依赖注入注册所有策略 参数化配置模式(Parameterized Configuration Pattern):根据服务类型动态确定检查维度,而非硬编码固定维度 配置覆盖与热更新【配置从公司的配置平台加载,使用SDK接入即可】

看具体的风险检测项的实现和灰度策略的实现

  1. https://app.notion.com/p/389be42d0e858066ba5cd8722e4ead68
  2. https://app.notion.com/p/389be42d0e85809b8985f3cb3d2689de

还可以多讲:

工作亮点四:统一异步执行引擎

工作亮点六:uber/fx 依赖注入 + DDD 四层架构 + 服务发现统一抽象【统一了多套服务发现系统、还统一了鉴权认证】

工作亮点三:审计事件独立写入

将调用服务发现的操作重构为Batch操作;对于多LDC且大批量Pod的流量变更任务,实现自动拆单,避免变更流量时由于服务发现限频导致的卡单

变更Pod小于5000不会卡;变更Pod超过5000要拆单【有多LDC(地区机房)的情况,灰度策略可能会拆出100步灰度步骤(13个不同地区的机房)】

服务状态发现服务【司南,限频10分钟内100次,Batch操作批量一次可以请求50个Pod的状态】

建立一个流量变更任务单,需要按灰度策略计算出每一批灰度的行为,这个需要把本变更单的所有Pod的状态都查出来【判断是上线、下线、流量调权,这样才能选择对应的灰度策略】,所以太多Pod的时候只能拆单【优先按照LDC(地区机房)拆单,这样逻辑比较顺;还是不行的话按照 Pod 和 物理机 拆;再不行的话手动按照命名空间拆分】

灰度策略见notion笔记:https://app.notion.com/p/389be42d0e85809b8985f3cb3d2689de

正向灰度、回退、强制关单的逻辑,对应的锁是咋加的?

正向灰度、回退、强制关单的逻辑见notion笔记【总设计】:https://app.notion.com/p/38abe42d0e85800b8830ffabc53e1182

加锁策略见notion笔记:https://app.notion.com/p/38abe42d0e8580f1802dcb1b08f85322

自己看代码中的老流控如何加锁见notion笔记:https://app.notion.com/p/38ebe42d0e85806b90ded2e0527e0bf6

自己看代码中的新流控如何加锁见notion笔记:https://app.notion.com/p/38ebe42d0e8580bfb8a2c6934534cbbd

自己看代码中的正向灰度、回退、强制关单的逻辑见notion笔记:https://app.notion.com/p/38ebe42d0e8580609e6bc093029e6710

实现变更各阶段(建单前置、执行前置、执行后异步检查)的风险防控检查项、级别(阻塞/提示)、适用场景(上线、下线、流量调权)以及具体方案

风险防控措施见notion笔记:https://app.notion.com/p/389be42d0e858066ba5cd8722e4ead68

前端建单时建单前置的风险检查,前端每个环节一次请求调用,一个检查项一次请求,这样返回的快,用户可以更好的感知进度

后台变更,脚本自动在后台调用的时候就走设置好的风险检查策略链,一次调用全部检查完后一次返回

后台脚本自动变更和前端手动变更风险检查的检查项是不一样的

根据用户反馈优化系统前端的展示与操作逻辑

建单时要填的所有东西要在一页里面,页面太长就在右边加上可跳转导航。填完确认和风险检查的页面单独一页。

默认的一般不需要改的地方就折叠仅展示仅可读不可改,要改的话点击修改才让改并具体展开详细的值与说明

权重不可以小于0,但可以大于100【历史遗留,且可以方便SRE同学精细流量变更】

批量上线、批量下线、批量设置权重

同一个流量变更单中流量变更的方向需要相同

保留去除填入的IP、不知到服务名时通过 IP+端口号 找出对应POD【等方便的查询功能】

本次实习与之前实习部门关心点的不同?

之前那边主要关注可观测(AppSet监控平台【根因分析、告警治理】、混沌实验、灰度变更【可观测,内部有监控埋点、需要扩容缩容】、变更管控【变更时的各种可观测、变更阻断、变更审计】)

本次这边关注的方面更多(可观测、发布、变更、故障定位、容量管理、风险巡检、运维【容器编排、CMDB、组件工具平台】)

对于展示按灰度策略生成的灰度批次,重新设计接口使用分页优化展示数据的读取