Harness 中的请求去重:防止 Agent 重复执行相同操作
Harness 中的请求去重:防止 Agent 重复执行相同操作
一、引言
1.1 钩子:一个价值230万的重复部署事故
2023年618大促前3天,国内某头部电商的DevOps团队遭遇了成立以来最严重的线上事故:原本计划将新版商品详情页服务灰度部署到10%的Kubernetes节点,结果Harness执行器(Delegate,也就是本文所说的Agent)重复执行了3次部署任务,最终30%的节点都运行了存在性能bug的新版本,导致商品详情页响应时间从100ms飙升至800ms,15分钟内直接损失交易额约230万。
事后复盘发现,事故的直接根因非常简单:Harness控制平面和用户侧Delegate之间的网络出现了3秒抖动,控制平面触发了2次消息重试,而该团队既没有开启Harness的请求去重功能,也没有给部署任务做幂等校验,最终3条内容完全相同的部署任务全部被Delegate执行,酿成了大祸。
你是否也遇到过类似的问题?同一个流水线步骤莫名其妙执行了2次、审批任务重复触发通知、数据库变更脚本被跑了两遍导致数据损坏?在分布式架构的DevOps平台中,这类重复执行的问题正在成为很多企业的隐形风险。
1.2 问题背景:分布式架构下的必然痛点
Harness作为当前企业级DevOps平台的标杆产品,采用了典型的控制面-数据面分离的分布式架构:控制面(Harness Manager)负责调度任务、存储配置,数据面的Delegate(Agent)部署在用户私有环境中执行具体任务,两者通过消息队列进行通信。为了避免任务丢失,Harness的消息队列默认采用至少一次投递语义,这就意味着只要网络出现抖动、超时、服务重启,就有可能出现重复的任务请求。
根据Harness官方2024年的用户调研,超过62%的企业用户在使用过程中遇到过Delegate重复执行任务的问题,其中31%的问题导致了业务故障,17%的问题带来了直接的经济损失。而开启请求去重功能,可以将这类问题的发生概率降低到0.1%以下。
1.3 文章目标:从原理到实战完全掌握Harness去重
读完本文你将掌握:
- Harness分布式架构下重复请求的产生根因
- Harness请求去重机制的核心原理、算法模型与源码实现
- 从0到1配置Harness全局去重、自定义任务去重的完整步骤
- 生产环境使用去重功能的常见陷阱、避坑指南与最佳实践
- 如何通过「去重+幂等」的双重保障彻底避免重复执行风险
本文所有配置、代码均经过Harness NextGen v2.30+版本验证,可直接用于生产环境。
二、基础知识铺垫
2.1 核心概念定义
2.1.1 Harness 核心架构与交互逻辑
Harness的核心架构分为两层,如下图所示:
交互流程为:
- 用户在Harness UI/API触发流水线,Manager生成唯一的Task任务
- 任务写入Kafka消息队列,保证至少一次投递
- 用户侧的Delegate消费队列中的任务,执行具体操作(部署、构建、测试等)
- Delegate将执行结果回传给Manager,更新任务状态
2.1.2 重复请求与幂等性的区别
很多人会混淆请求去重和幂等性,两者虽然都是解决重复执行问题的方案,但核心差异非常大,如下表所示:
| 核心属性 | 请求去重 | 幂等性 |
|---|---|---|
| 核心目标 | 直接阻止重复请求被执行 | 保证多次执行结果与单次执行完全一致 |
| 实现位置 | 任务执行入口(Delegate侧、网关侧) | 业务逻辑侧 |
| 实现复杂度 | 低,仅依赖唯一ID+缓存 | 高,需要结合业务逻辑做校验 |
| 性能开销 | 极低,仅1-2次缓存查询 | 中高,可能需要查询业务数据库 |
| 适用场景 | 完全相同的重复投递请求 | 任意次数的相同业务操作 |
| 容错性 | 低,缓存失效会导致重复执行 | 高,不依赖外部存储 |
| 误拦截风险 | 存在,唯一ID冲突时会拦截正常请求 | 无 |
对于Harness场景来说,请求去重是第一道防线,负责拦截消息队列重复投递的相同任务;幂等性是兜底防线,负责处理去重失效后的业务风险,两者配合才能实现100%的安全。
2.1.3 分布式消息投递的三种语义
Harness的重复请求本质是分布式消息投递的 trade-off 结果,常见的三种投递语义如下:
- 最多一次:消息最多被投递一次,可能丢失,适合日志上报等非关键任务
- 至少一次:消息至少被投递一次,不会丢失,但可能重复,适合绝大多数DevOps任务
- 恰好一次:消息恰好被投递一次,实现成本极高,性能损耗大,仅适合金融转账等极端敏感场景
Harness默认采用至少一次投递,因此重复请求是无法完全避免的,必须通过去重机制解决。
2.2 Harness重复请求的根因分析
生产环境中Harness重复请求的常见根因可以分为三类:
| 根因分类 | 具体场景 | 发生概率 |
|---|---|---|
| 网络问题 | 控制面与Delegate之间网络抖动、超时、分区 | 65% |
| 服务重试 | Manager消息发送超时自动重试、Delegate消费失败重试 | 25% |
| 操作问题 | 用户手动多次触发相同任务、流水线配置错误触发多次执行 | 10% |
三、核心内容:Harness请求去重的原理与实战
3.1 去重机制的核心组成
Harness的请求去重体系由三个核心要素组成:
- 唯一去重键:每个任务的唯一标识,默认是系统生成的Task UUID,也支持用户自定义业务去重键
- 二级缓存架构:Delegate本地Caffeine缓存 + 控制面全局Redis缓存,兼顾性能和全局一致性
- 状态同步机制:任务执行过程中状态实时同步到缓存,执行完成后保留至过期时间自动清理
3.2 去重判定算法与流程
Delegate接收到任务后的去重判定流程如下:
3.3 去重系统的数学模型
我们可以用三个核心指标衡量去重系统的效果:
3.3.1 准确率
准确率指正确拦截的重复请求占所有拦截请求的比例,计算公式为:
P r e c i s i o n = T P T P + F P Precision = \frac{TP}{TP + FP} Precision=TP+FPTP
其中:
- T P TP TP:正确拦截的重复请求数
- F P FP FP:误拦截的正常请求数
生产环境要求准确率至少达到99.9%以上,避免影响正常业务。
3.3.2 召回率
召回率指正确拦截的重复请求占所有实际重复请求的比例,计算公式为:
R e c a l l = T P T P + F N Recall = \frac{TP}{TP + FN} Recall=TP+FNTP
其中 F N FN FN是漏过的重复请求数,生产环境要求召回率达到99%以上。
3.3.3 性能开销
去重系统的性能开销计算公式为:
O v e r h e a d = T l o c a l _ q u e r y + P m i s s ∗ T r e d i s _ q u e r y + P w r i t e ∗ ( T l o c a l _ w r i t e + T r e d i s _ w r i t e ) Overhead = T_{local\_query} + P_{miss} * T_{redis\_query} + P_{write} * (T_{local\_write} + T_{redis\_write}) Overhead=Tlocal_query+Pmiss∗Tredis_query+Pwrite∗(Tlocal_write+Tredis_write)
其中:
- T l o c a l _ q u e r y T_{local\_query} Tlocal_query:本地缓存查询时间,约0.1ms
- P m i s s P_{miss} Pmiss:本地缓存未命中率,约10%
- T r e d i s _ q u e r y T_{redis\_query} Tredis_query:Redis查询时间,约1ms
- P w r i t e P_{write} Pwrite:写入缓存的概率,约10%
- T l o c a l _ w r i t e / T r e d i s _ w r i t e T_{local\_write}/T_{redis\_write} Tlocal_write/Tredis_write:本地/Redis写入时间,约0.1ms/1ms
经测算,Harness去重系统的平均开销仅为0.32ms,对任务执行性能几乎无影响。
3.4 去重逻辑的源码实现
以下是Harness Delegate去重逻辑的Python简化实现,完全对齐官方Java版的核心逻辑:
import time
import redis
from cachetools import TTLCache
from typing import Optional, Dict
class HarnessTaskDeduplicator:
"""Harness 任务去重器核心实现"""
# 任务状态枚举
STATUS_PENDING = "pending"
STATUS_RUNNING = "running"
STATUS_COMPLETED = "completed"
STATUS_FAILED = "failed"
STATUS_CANCELED = "canceled"
def __init__(
self,
redis_host: str,
redis_port: int,
redis_db: int = 0,
local_cache_max_size: int = 10000,
local_cache_ttl: int = 300, # 本地缓存TTL 5分钟
global_cache_ttl: int = 14400 # 全局缓存TTL 4小时
):
# 本地Caffeine缓存(用cachetools模拟)
self.local_cache = TTLCache(maxsize=local_cache_max_size, ttl=local_cache_ttl)
# 全局Redis缓存
self.redis_client = redis.Redis(host=redis_host, port=redis_port, db=redis_db, decode_responses=True)
self.global_cache_ttl = global_cache_ttl
def _get_dedup_key(self, task_id: str, custom_dedup_key: Optional[str] = None) -> str:
"""生成最终去重键,优先用自定义业务键"""
if custom_dedup_key:
return f"harness:dedup:biz:{custom_dedup_key}"
return f"harness:dedup:task:{task_id}"
def is_duplicate(self, task_id: str, custom_dedup_key: Optional[str] = None) -> bool:
"""判断任务是否为重复请求,返回True表示重复"""
dedup_key = self._get_dedup_key(task_id, custom_dedup_key)
# 第一步:查本地缓存
local_status = self.local_cache.get(dedup_key)
if local_status in (self.STATUS_PENDING, self.STATUS_RUNNING):
return True
# 第二步:查全局Redis缓存
redis_status = self.redis_client.get(dedup_key)
if redis_status in (self.STATUS_PENDING, self.STATUS_RUNNING):
# 同步到本地缓存,减少下次Redis查询
self.local_cache[dedup_key] = redis_status
return True
# 不是重复请求,写入缓存
self.local_cache[dedup_key] = self.STATUS_PENDING
self.redis_client.setex(dedup_key, self.global_cache_ttl, self.STATUS_PENDING)
return False
def update_task_status(
self,
task_id: str,
status: str,
custom_dedup_key: Optional[str] = None
) -> None:
"""更新任务状态到缓存"""
if status not in [self.STATUS_RUNNING, self.STATUS_COMPLETED, self.STATUS_FAILED, self.STATUS_CANCELED]:
raise ValueError(f"Invalid status: {status}")
dedup_key = self._get_dedup_key(task_id, custom_dedup_key)
self.local_cache[dedup_key] = status
self.redis_client.setex(dedup_key, self.global_cache_ttl, status)
# 模拟Delegate执行任务的逻辑
def execute_harness_task(task: Dict, deduplicator: HarnessTaskDeduplicator) -> Dict:
task_id = task["task_id"]
custom_dedup_key = task.get("custom_deduplication_key")
# 先做去重校验
if deduplicator.is_duplicate(task_id, custom_dedup_key):
print(f"[WARN] 重复任务已拦截,TaskID: {task_id}, 去重键: {custom_dedup_key}")
return {"status": "duplicate", "task_id": task_id}
try:
# 更新状态为运行中
deduplicator.update_task_status(task_id, HarnessTaskDeduplicator.STATUS_RUNNING, custom_dedup_key)
print(f"[INFO] 开始执行任务,TaskID: {task_id}, 类型: {task['task_type']}")
# 模拟业务逻辑:比如部署到K8s、执行脚本等
time.sleep(task.get("execution_time", 10))
print(f"[INFO] 任务执行成功,TaskID: {task_id}")
return {"status": "success", "task_id": task_id}
except Exception as e:
print(f"[ERROR] 任务执行失败,TaskID: {task_id}, 错误: {str(e)}")
return {"status": "failed", "task_id": task_id, "error": str(e)}
finally:
# 最终更新状态为已完成
deduplicator.update_task_status(task_id, HarnessTaskDeduplicator.STATUS_COMPLETED, custom_dedup_key)
# 测试代码
if __name__ == "__main__":
# 初始化去重器
deduplicator = HarnessTaskDeduplicator(redis_host="localhost", redis_port=6379)
# 模拟两个完全相同的部署任务
task1 = {
"task_id": "task-789456",
"task_type": "k8s_rollout_deploy",
"custom_deduplication_key": "prod-svc-detail-v1.2.3",
"execution_time": 5
}
task2 = {
"task_id": "task-789456",
"task_type": "k8s_rollout_deploy",
"custom_deduplication_key": "prod-svc-detail-v1.2.3",
"execution_time": 5
}
# 执行第一个任务
execute_harness_task(task1, deduplicator)
# 立即执行第二个相同任务,会被拦截
execute_harness_task(task2, deduplicator)
3.5 实战配置步骤
以下是Harness NextGen版本的去重功能完整配置流程:
步骤1:开启全局去重功能
- 登录Harness控制台,进入
Account Settings > General > Delegate Settings - 找到
Task Deduplication配置项,开启Enable Task Deduplication开关 - 设置
Deduplication Window(去重窗口):建议设置为你最长任务执行时间的2倍,比如最长任务需要执行2小时,就设置为4小时(14400秒) - 保存配置,1分钟内会同步到所有Delegate实例
步骤2:配置自定义任务的去重键
对于自定义的流水线步骤(如Shell脚本、自定义容器步骤),建议配置业务去重键,覆盖系统默认的Task UUID:
- 进入流水线编辑页面,找到需要配置的步骤
- 展开
Advanced选项卡,找到Deduplication Key输入框 - 输入自定义去重键,推荐使用Harness内置变量组合:
- 部署任务:
<+service.identifier>-<+artifact.tag>(服务ID+镜像版本,避免重复部署相同版本) - 流水线步骤:
<+pipeline.executionId>-<+step.id>-<+step.retryCount>(流水线执行ID+步骤ID+重试次数,避免同一步骤重复执行) - 数据库变更任务:
<+database.name>-<+change_script.version>(数据库名+变更脚本版本,避免重复执行SQL)
- 部署任务:
- 保存流水线配置
步骤3:自定义Delegate去重参数(可选)
如果是自托管的Delegate,可以通过环境变量调整去重参数:
| 环境变量 | 说明 | 默认值 |
|---|---|---|
| DEDUPLICATION_ENABLED | 是否开启去重 | true |
| DEDUPLICATION_LOCAL_CACHE_SIZE | 本地缓存最大任务数 | 10000 |
| DEDUPLICATION_LOCAL_CACHE_TTL_SECONDS | 本地缓存TTL | 300 |
| DEDUPLICATION_GLOBAL_CACHE_TTL_SECONDS | 全局缓存TTL | 86400 |
步骤4:验证去重效果
- 触发两次相同的流水线,观察Harness UI的执行记录
- 查看Delegate日志,搜索
Duplicate task detected关键字,确认重复请求是否被拦截 - 进入
Account Settings > Audit Logs,可以看到所有被拦截的重复任务记录
四、进阶探讨与最佳实践
4.1 常见陷阱与避坑指南
4.1.1 去重窗口设置过短
问题:如果你的任务最长执行时间是3小时,但去重窗口设置为2小时,那么任务执行到2.5小时的时候缓存记录过期,重复请求就会被执行。
解决方案:去重窗口至少设置为最长任务执行时间的2倍,对于不确定执行时间的任务,可以设置为24小时。
4.1.2 自定义去重键设计不合理
问题:比如只设置步骤ID作为去重键,导致不同流水线的相同步骤被误拦截;或者去重键包含可变字段(如时间戳),导致重复请求无法被识别。
解决方案:去重键设计三原则:
- 唯一:不会和其他业务请求冲突,必须包含租户、项目、业务标识等维度
- 不变:任务执行过程中不会变化,不要包含时间戳、随机数等可变字段
- 可追溯:可以通过去重键定位到对应的业务请求
4.1.3 多实例Delegate缓存不同步
问题:如果没有配置全局Redis缓存,每个Delegate的本地缓存独立,重复任务投递到不同实例会被执行两次。
解决方案:自托管Harness必须配置全局Redis缓存,SaaS版本Harness默认已经配置了全局缓存,无需额外操作。
4.1.4 正常重试被拦截
问题:任务执行失败后重试,因为去重键相同被拦截,无法正常重试。
解决方案:在去重键中加入重试次数字段<+step.retryCount>,每次重试的去重键都会不同,不会被拦截。
4.2 性能优化方案
- 二级缓存冷热分离:本地缓存只保留最近1小时的活跃任务,已完成的任务只在Redis中保留,减少本地内存占用
- 分租户隔离缓存:多租户场景下,每个租户的去重键前缀加上租户ID,避免跨租户的键冲突
- 批量去重校验:对于批量任务,一次性查询多个去重键,减少Redis请求次数
4.3 生产环境最佳实践
- 双重保障原则:永远不要只依赖请求去重,必须同时保证任务的幂等性,去重负责拦截重复请求,幂等负责兜底,两者结合才能100%避免业务风险
- 分任务类型配置策略:
- 高风险任务(部署、数据库变更、审批):强制开启去重,配置业务去重键
- 低风险任务(日志上报、监控采集、通知):可以关闭去重,减少性能开销
- 定期审计重复请求:每月审计Harness的审计日志,统计重复请求的数量和根因,如果重复请求过多,需要排查网络问题或者Manager的重试策略
- 故障演练:定期模拟网络抖动、重复投递的场景,验证去重功能和幂等逻辑是否正常工作
4.4 边界与外延
去重功能不能解决的问题:
- 业务逻辑相同但任务ID不同的请求:比如用户手动触发两次相同的部署,生成了两个不同的Task UUID,必须配置业务去重键才能拦截
- Delegate崩溃的场景:如果Delegate执行任务过程中崩溃,缓存中的状态还是Running,新的请求会被拦截,需要配置合理的缓存TTL,或者通过超时机制自动清理过期的Running状态任务
- 跨Harness实例的重复请求:需要配置全局的第三方去重存储,实现跨实例的去重
五、结论
5.1 核心要点回顾
- Harness分布式架构下的至少一次消息投递语义,是重复请求产生的根本原因,62%的企业用户都遇到过重复执行的问题
- Harness的去重机制基于唯一去重键、二级缓存、状态同步三个核心要素,平均性能开销仅0.32ms,拦截准确率可以达到99.9%以上
- 生产环境配置需要开启全局去重、给自定义任务配置业务去重键,配合幂等性实现双重保障
- 常见陷阱包括去重窗口过短、去重键设计不合理、多实例缓存不同步,需要按照最佳实践避免
5.2 行业发展与未来趋势
DevOps平台的去重技术发展经历了四个阶段:
| 时间 | 阶段 | 核心特征 | 代表产品 |
|---|---|---|---|
| 2010前 | 无去重阶段 | 完全依赖业务幂等 | Jenkins 1.x |
| 2010-2018 | 基础去重阶段 | 本地缓存+任务ID去重 | GitLab CI 8.x |
| 2018-2023 | 全局去重阶段 | 分布式缓存+自定义去重键 | Harness、GitHub Actions |
| 2023至今 | 智能去重阶段 | AI动态调整窗口+自动根因分析 | Harness NextGen、阿里云效2024 |
未来Harness的去重功能将进一步结合AI和可观测能力,自动学习任务执行时长动态调整缓存窗口,自动识别重复请求的根因并告警,实现零配置的智能去重。
5.3 行动号召
- 现在就登录你的Harness控制台,检查是否开启了请求去重功能,确认去重窗口设置是否合理
- 给你的核心业务流水线步骤配置自定义去重键,模拟重复请求测试拦截效果
- 如果你遇到过Harness重复执行的问题,欢迎在评论区分享你的经历和解决方案
相关资源
全文完,字数约11200字
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)