第一章:Go实现千万级App分发系统概览
现代移动应用分发平台需在高并发、低延迟、强一致性与弹性伸缩之间取得平衡。Go语言凭借其轻量级协程(goroutine)、高效的网络栈、静态编译及原生支持并发模型等特性,成为构建千万级App分发系统的核心选型。该系统日均处理超2000万次安装请求,峰值QPS达18,000+,平均端到端响应时间低于120ms,同时支持灰度发布、多区域CDN协同、版本签名验签与断点续传等关键能力。
核心架构设计原则
- 无状态服务层:所有API网关与业务逻辑服务均无本地状态,依赖Redis集群缓存元数据(如App包信息、下载计数、权限策略),并通过一致性哈希实现缓存分片;
- 分层存储策略:热数据(
- 流量分级调度:基于用户UA、地域IP、运营商标识进行实时路由,优先将请求导向最近CDN边缘节点,并通过gRPC流式接口与中心调度服务同步带宽配额。
关键组件技术选型
| 组件 | 选型 | 说明 |
|---|---|---|
| API网关 | Kong + 自研Go插件 | 基于Go Plugin机制注入JWT鉴权、限流熔断逻辑 |
| 下载服务 | 自研Go HTTP Server | 启用http.Server{ReadTimeout: 5s, WriteTimeout: 300s},启用net/http/pprof性能分析端点 |
| 签名验签 | Go标准库crypto/ecdsa | 使用P-256椭圆曲线,私钥离线存储,公钥预加载至内存Map |
快速验证服务可用性
执行以下命令可启动最小化下载服务并触发健康检查:
# 编译并运行(假设main.go已包含/healthz端点)
go build -o appdist-server main.go
./appdist-server --addr ":8080" --storage "minio://localhost:9000/app-bucket"
# 验证服务响应(返回HTTP 200 + JSON {"status":"ok"})
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/healthz
该命令链验证了服务进程启动、监听就绪及基础健康探针通路,是部署流水线中CI阶段的必检项。
第二章:零信任校验体系设计与落地
2.1 基于SPIFFE/SPIRE的设备身份联邦建模与Go SDK集成
在异构边缘设备场景中,SPIFFE ID(spiffe://domain/ns/workload)作为统一身份标识,支撑跨域信任联邦。SPIRE Server 通过 Workload API 向客户端分发 SVID(X.509证书+密钥),实现零信任身份供给。
设备身份联邦建模要点
- 每台设备注册为独立
Agent,绑定唯一node_id与attestation策略(如 TPM/TPM2.0、AWS IAM Role) - 工作负载身份继承节点策略,并支持细粒度
selector标签(如os:linux,env:prod)
Go SDK 集成示例
// 初始化 SPIRE 客户端(需提前配置 trust domain 和 socket path)
client, err := spireclient.NewClient(
context.Background(),
"/run/spire/sockets/agent.sock", // Unix domain socket
spireclient.WithTrustDomain("example.org"),
)
if err != nil {
log.Fatal(err) // 连接失败通常因 agent 未就绪或权限不足
}
该调用建立与本地 SPIRE Agent 的 gRPC 连接;WithTrustDomain 确保 SVID 签发者可信,agent.sock 路径需与 SPIRE Agent 配置一致。
SVID 获取与验证流程
graph TD
A[设备启动] --> B[向本地 SPIRE Agent 请求 SVID]
B --> C{Agent 执行 attestation}
C -->|成功| D[签发 X.509 SVID + JWT-SVID]
C -->|失败| E[拒绝注册并上报 audit log]
D --> F[应用加载证书链用于 mTLS]
| 组件 | 作用 | 是否可插拔 |
|---|---|---|
| Attestation Plugin | 验证设备硬件/云平台身份 | 是 |
| Upstream Authority | 对接外部 CA(如 HashiCorp Vault) | 是 |
| Federated Trust Domain | 跨组织 SPIFFE ID 互信 | 是 |
2.2 双向mTLS+JWT Token链式签名校验的Go实现与性能压测
核心验证流程
双向mTLS建立可信信道后,服务端校验客户端证书链;随后解析JWT,验证其签名是否由上游可信Issuer(如API网关)用私钥签署,并检查jti防重放。
// 链式校验:先mTLS,再JWT
func ChainVerify(r *http.Request) error {
if !isClientCertValid(r.TLS.PeerCertificates) {
return errors.New("mTLS cert chain invalid")
}
token, err := jwt.Parse(r.Header.Get("Authorization")[7:], keyFunc)
if err != nil || !token.Valid {
return errors.New("JWT signature or claims invalid")
}
return nil
}
keyFunc动态加载Issuer公钥;[7:]跳过”Bearer “前缀;证书链需包含根CA→中间CA→终端证书三级完整路径。
性能对比(10K并发,P99延迟)
| 方案 | P99延迟 | QPS |
|---|---|---|
| 仅JWT | 42ms | 8300 |
| mTLS + JWT | 68ms | 5100 |
| mTLS + JWT + JTI缓存 | 53ms | 6700 |
graph TD
A[Client] -->|双向证书握手| B[Server TLS Layer]
B --> C{mTLS OK?}
C -->|Yes| D[Extract JWT]
D --> E[Verify Signature with Issuer's Public Key]
E --> F[Check jti in Redis Bloom Filter]
2.3 应用包完整性验证:TUF规范兼容的Go签名/验证双通道引擎
TUF(The Update Framework)通过多角色密钥分层与元数据快照机制,为软件分发构建强一致性保障。本引擎在Go中实现双通道并行处理:signer通道生成符合TUF targets.json 和 snapshot.json 结构的签名元数据;verifier通道则按TUF委托链逐级校验——从根root.json → targets.json → snapshot.json → 具体包哈希。
核心验证流程
// VerifyTargetPackage 验证单个应用包的TUF合规性
func VerifyTargetPackage(pkgPath string, repoRoot *tuf.Repository) error {
target, ok := repoRoot.Targets().GetTarget(pkgPath) // 获取目标元数据
if !ok {
return errors.New("target not found in targets.json")
}
return repoRoot.VerifyTarget(pkgPath, target) // 调用TUF标准验证逻辑
}
逻辑分析:
repoRoot.VerifyTarget()内部执行三重检查——① 目标文件哈希是否匹配targets.json中声明值;②targets.json是否由有效委托密钥签名;③ 当前snapshot.json版本是否未被回滚(防rollback攻击)。参数repoRoot需已加载完整TUF元数据树。
双通道协同机制
| 通道 | 输入 | 输出 | 安全职责 |
|---|---|---|---|
| Signer | 二进制包 + 私钥 | 签名后targets.json等 |
防篡改、防冒名 |
| Verifier | 包路径 + 元数据树 | true/error |
防降级、防中间人劫持 |
graph TD
A[客户端请求 pkg-v1.2.0] --> B{Verifier通道}
B --> C[加载 root.json]
C --> D[验证 targets.json 签名]
D --> E[比对 pkg-v1.2.0 哈希]
E --> F[确认 snapshot 版本新鲜性]
2.4 运行时可信度动态评估:eBPF辅助的进程行为审计模块
传统静态签名校验无法捕获运行时异常行为。本模块通过加载eBPF程序,在sys_enter/sys_exit、task_newtask等内核事件点实时采集进程系统调用序列、文件访问路径、网络目标地址及父进程关系图谱。
核心数据结构设计
// audit_event.h:轻量级审计事件结构(BPF侧)
struct audit_event {
__u64 timestamp; // 纳秒级时间戳,用于行为时序建模
__u32 pid; // 当前进程PID
__u32 ppid; // 父进程PID,支持溯源链构建
__u16 syscall_id; // 系统调用号(如__NR_openat=257)
__u8 is_sensitive; // 是否触发敏感策略(0/1)
char comm[16]; // 进程名截断(避免BPF栈溢出)
};
该结构经bpf_perf_event_output()推送至用户态ring buffer;comm字段长度限制为16字节,符合eBPF verifier对栈变量大小的硬性约束;is_sensitive由BPF程序依据预置规则(如execve后立即mmap(PROT_EXEC))在线标记。
评估维度与权重示意
| 维度 | 权重 | 触发条件示例 |
|---|---|---|
| 异常调用序列 | 0.35 | openat → write → mmap(PROT_EXEC) |
| 非法文件路径访问 | 0.25 | /etc/shadow 或 /proc/kcore |
| 子进程行为偏离 | 0.40 | shell进程spawn出python -c "import ctypes" |
行为评估流程
graph TD
A[syscall trace] --> B{BPF程序过滤}
B -->|匹配敏感模式| C[打标 is_sensitive=1]
B -->|常规行为| D[仅记录基础字段]
C --> E[perf ringbuf输出]
D --> E
E --> F[用户态评估引擎聚合时序特征]
2.5 零信任策略引擎:OPA WASM插件化策略执行与Go Runtime热加载
OPA(Open Policy Agent)通过 WebAssembly(WASM)实现策略沙箱化执行,解耦策略逻辑与宿主运行时。WASM模块由 Rego 编译生成,具备内存隔离、确定性执行和跨平台兼容特性。
策略加载与热更新机制
Go Runtime 通过 wasmer 或 wazero 运行时动态加载 .wasm 模块,并利用 fsnotify 监听策略文件变更,触发无中断热替换:
// 使用 wazero 加载并热更新 WASM 策略
rt := wazero.NewRuntime(ctx)
mod, _ := rt.InstantiateModuleFromBinary(ctx, wasmBytes)
policy := opa.NewWasmPolicy(mod, "data.authz.allow")
// 注册 fsnotify 回调:检测 policy.rego 变更后重建 wasmBytes 并 reload
逻辑分析:
wazero.NewRuntime(ctx)创建轻量级、无 CGO 依赖的 WASM 运行时;InstantiateModuleFromBinary安全加载二进制策略;opa.NewWasmPolicy封装策略入口函数名(如"data.authz.allow"),供 Go 层统一调用。热加载不重启进程,毫秒级生效。
执行模型对比
| 特性 | 原生 Rego(Rego VM) | WASM 插件模式 |
|---|---|---|
| 启动延迟 | 低 | 中(首次编译开销) |
| 内存隔离性 | 弱(共享 Go 堆) | 强(WASM 线性内存) |
| 热加载支持 | 需重启 OPA Server | 支持模块级原子替换 |
graph TD
A[HTTP 请求] --> B{OPA Gateway}
B --> C[WASM Runtime]
C --> D[策略模块1.wasm]
C --> E[策略模块2.wasm]
D --> F[Go Runtime 热加载器]
E --> F
F --> G[fsnotify 监听 policy.rego]
第三章:动态路由分发核心架构
3.1 基于一致性哈希+权重感知的Go分发路由调度器实现
传统一致性哈希在节点扩容/缩容时存在负载倾斜问题。本实现引入动态权重因子,使高配节点承接更多请求,同时保障哈希环平滑迁移。
核心设计要点
- 权重映射:节点权重参与虚拟节点数量计算(
vNodes = base * weight / avgWeight) - 环更新原子性:使用
sync.RWMutex保护哈希环结构体 - 请求路由:先定位哈希环位置,再按权重比例二次筛选真实节点
路由核心逻辑(带注释)
func (r *Router) GetNode(key string) *Node {
hash := r.hasher.Sum64(key) // 使用murmur3_64保证分布均匀
idx := sort.Search(len(r.sortedHashes), func(i int) bool {
return r.sortedHashes[i] >= hash
}) % len(r.sortedHashes)
return r.hashToNode[r.sortedHashes[idx]] // O(log N)查找
}
hasher.Sum64 输出64位哈希值,sort.Search 实现二分定位,时间复杂度 O(log N);% len(...) 防止越界,确保环形语义。
虚拟节点权重分配示意
| 节点 | 权重 | 基础虚拟节点数 | 实际分配数 |
|---|---|---|---|
| node-a | 2 | 100 | 200 |
| node-b | 1 | 100 | 100 |
graph TD
A[请求Key] --> B{Hash计算}
B --> C[64位哈希值]
C --> D[二分查找环上最近顺时针节点]
D --> E[返回对应物理节点]
3.2 多维度路由决策:地域/运营商/网络质量/设备特征的实时聚合计算
现代边缘路由系统需在毫秒级完成多源异构信号的联合判据生成。核心在于构建统一特征向量空间,将离散标签(如CN-BJ-UNICOM)与连续指标(如RTT=42ms, 丢包率=0.3%)归一化映射。
特征融合逻辑示例
def compute_route_score(features: dict) -> float:
# features = {"region": "SH", "isp": "CMCC", "rtt_ms": 38.2, "loss_pct": 0.15, "device_type": "mobile"}
region_weight = {"BJ": 0.9, "SH": 0.85, "SZ": 0.8} # 地域亲和度基线
isp_penalty = {"CMCC": 0.0, "CUCC": -0.1, "CTCC": -0.05} # 运营商QoS历史偏差
qoe_score = 1.0 - (features["rtt_ms"] * 0.01 + features["loss_pct"] * 0.5)
return (
region_weight.get(features["region"], 0.7) +
isp_penalty.get(features["isp"], 0.0) +
min(max(qoe_score, 0.3), 1.0) +
(0.1 if features["device_type"] == "mobile" else 0.0) # 移动端降权补偿
)
该函数将四维输入压缩为单一标量得分,各参数经A/B测试校准:rtt_ms系数0.01对应每100ms衰减1分,loss_pct权重0.5体现其对视频流影响更敏感。
决策维度权重分布
| 维度 | 权重 | 实时更新频率 | 数据来源 |
|---|---|---|---|
| 网络质量 | 45% | 每5秒 | 主动探测+被动TCP指标 |
| 地域/运营商 | 30% | 每小时 | GeoIP+ASN数据库 |
| 设备特征 | 15% | 首次会话 | UA解析+客户端上报 |
| 历史会话反馈 | 10% | 实时滑动窗口 | CDN边缘日志聚合 |
动态路由选择流程
graph TD
A[原始请求] --> B{提取四维特征}
B --> C[地域/ISP查表]
B --> D[实时RTT/丢包采集]
B --> E[UA解析设备类型]
C & D & E --> F[归一化加权聚合]
F --> G[Top-3候选节点]
G --> H[执行灰度流量染色验证]
3.3 分布式会话亲和性保持:Redis Cluster + Go原子操作的Session粘滞方案
在无状态微服务集群中,用户会话需跨节点一致路由。传统反向代理的IP哈希易受扩容/故障影响,而应用层粘滞需高可用、低延迟的会话锚点。
核心设计原则
- 以
session_id为键,绑定唯一node_id(如svc-worker-03) - 利用 Redis Cluster 的哈希槽分片 + Go
atomic.CompareAndSwapUint64保障写入竞态安全
数据同步机制
// 初始化粘滞映射(仅首次写入生效)
func SetStickyNode(ctx context.Context, rdb *redis.ClusterClient, sid, nodeID string) error {
return rdb.SetNX(ctx, "sess:sticky:"+sid, nodeID, 24*time.Hour).Err()
}
SetNX 原子性确保首次请求唯一绑定;TTL 防止僵尸会话长期占用槽位。
| 组件 | 作用 | 保障点 |
|---|---|---|
| Redis Cluster | 分布式键存储与自动分片 | 槽一致性、高可用 |
| Go atomic | 本地节点内并发控制 | 本地会话状态一致性 |
graph TD
A[HTTP请求] --> B{是否含session_id?}
B -->|否| C[生成sid + 随机选node]
B -->|是| D[查Redis sess:sticky:sid]
D --> E[路由至对应node]
C --> F[SetNX写入粘滞映射]
第四章:热更新全链路支撑机制
4.1 模块级热替换:Go Plugin + ELF符号重绑定的无停机更新实践
传统热更新依赖进程重启或 goroutine 级别 reload,粒度粗、状态易丢失。模块级热替换通过 plugin.Open() 加载编译为 .so 的 Go 插件,并结合 dlsym/dladdr 在运行时动态解析与重绑定 ELF 符号,实现函数指针的原子切换。
核心流程
- 编译插件:
go build -buildmode=plugin -o handler_v2.so handler.go - 加载与符号获取:
p, _ := plugin.Open("handler_v2.so") sym, _ := p.Lookup("ProcessRequest") processV2 := sym.(func([]byte) []byte) // 原子替换全局处理函数指针 atomic.StorePointer(¤tHandler, unsafe.Pointer(&processV2))currentHandler为unsafe.Pointer类型变量;StorePointer保证写入可见性;ProcessRequest必须导出且签名严格匹配。
关键约束对比
| 维度 | Go Plugin 限制 | ELF重绑定补充能力 |
|---|---|---|
| 类型安全 | 编译期校验接口一致性 | 运行时符号地址硬跳转 |
| 内存布局 | 插件与主程序独立堆 | 可共享全局变量(需同编译器版本) |
graph TD
A[主程序启动] --> B[加载v1插件]
B --> C[调用currentHandler]
D[部署v2.so] --> E[Open+Lookup]
E --> F[原子替换函数指针]
F --> C
4.2 配置热生效:etcd Watch + Go泛型配置解析器与版本快照回滚
数据同步机制
etcd Watch 以长连接监听指定 key 前缀变更,事件流触发泛型解析器 ConfigWatcher[T any] 自动反序列化并校验结构完整性。
泛型解析器核心逻辑
type ConfigWatcher[T any] struct {
client *clientv3.Client
path string
cache sync.Map // key: version, value: *T
}
func (w *ConfigWatcher[T]) Watch(ctx context.Context) error {
rch := w.client.Watch(ctx, w.path, clientv3.WithPrefix())
for resp := range rch {
for _, ev := range resp.Events {
var cfg T
if err := json.Unmarshal(ev.Kv.Value, &cfg); err != nil { continue }
version := ev.Kv.Version // etcd 内置版本号,天然唯一
w.cache.Store(version, &cfg)
}
}
return nil
}
ev.Kv.Version 提供原子递增版本标识,作为快照索引;sync.Map 支持高并发读取,避免锁竞争。
快照回滚能力
| 版本 | 配置哈希 | 生效时间 | 回滚状态 |
|---|---|---|---|
| 127 | a3f9… | 2024-06-01T10:23 | ✅ |
| 128 | b7c1… | 2024-06-01T10:25 | ⚠️(当前) |
回滚流程
graph TD
A[触发回滚请求] --> B{查缓存是否存在目标版本}
B -- 是 --> C[加载并校验签名]
B -- 否 --> D[返回404]
C --> E[原子替换运行时配置指针]
E --> F[广播ReloadEvent]
4.3 业务逻辑热加载:WASM Runtime嵌入与Go FFI调用桥接设计
为实现无重启更新核心策略,系统将风控规则引擎编译为 WASM 模块,由 wasmedge-go Runtime 嵌入 Go 主进程。
桥接层设计原则
- 零拷贝传递上下文(如
*C.struct_context) - 所有回调函数通过
//export标记并注册至 WASM 导入表 - Go 侧统一管理模块生命周期(实例化/销毁/重载)
FFI 函数导出示例
//export go_call_validate
func go_call_validate(
ctx unsafe.Pointer, // 指向Go管理的Session结构体
user_id uint64, // 用户ID(WASM仅支持i32/i64)
amount int64, // 交易金额(纳秒级精度保留)
) int32 { // 返回0=通过,1=拒绝,-1=错误
s := (*session.Session)(ctx)
return int32(s.Validate(user_id, amount))
}
该函数被 WASM 模块通过 env.go_call_validate 调用;ctx 实现跨语言对象引用,避免序列化开销;返回值遵循 WebAssembly ABI 规范。
WASM 导入函数表对照
| WASM 导入名 | Go 导出函数 | 用途 |
|---|---|---|
env.now_ns |
go_now_ns |
获取纳秒级时间戳 |
env.log_debug |
go_log_debug |
结构化调试日志输出 |
env.call_validate |
go_call_validate |
同步业务校验入口 |
graph TD
A[WASM Module] -->|call env.call_validate| B[Go FFI Export]
B --> C[Session.Validate]
C -->|return int32| A
A -->|import env.now_ns| D[Go: time.Now().UnixNano()]
4.4 热更新安全沙箱:基于gVisor隔离的WASM模块权限约束与资源配额控制
WASM模块在热更新场景下需严格隔离,避免越权访问宿主系统。gVisor作为用户态内核,为WASM运行时提供细粒度系统调用拦截能力。
权限约束机制
通过runsc配置限制WASM可调用的syscalls:
{
"syscalls": {
"allowed": ["read", "write", "clock_gettime"],
"blocked": ["openat", "mmap", "clone"]
}
}
该配置使WASM无法打开文件或创建进程,仅允许基础I/O与时间查询,确保零宿主文件系统穿透。
资源配额控制
| 资源类型 | 配额上限 | 控制方式 |
|---|---|---|
| CPU | 500m | CFS quota + gVisor vCPU throttling |
| 内存 | 128MB | Memory cgroup + WASM linear memory limit |
| 文件描述符 | 32 | rlimit.nofile + seccomp-bpf filter |
执行流程
graph TD
A[WASM模块加载] --> B[gVisor intercept syscall]
B --> C{是否在白名单?}
C -->|是| D[执行并计量资源]
C -->|否| E[返回ENOSYS并计数告警]
D --> F[超配额?]
F -->|是| G[OOM kill or SIGXCPU]
第五章:企业级分发源码复刻总结
在某头部金融云平台的私有化交付项目中,团队基于 Harbor 2.8 和 ChartMuseum 0.15 构建了符合等保三级要求的企业级分发源码复刻体系。整个复刻流程并非简单镜像同步,而是融合策略校验、元数据注入与灰度验证三重机制的闭环工程。
分发源结构设计原则
源码仓库采用三级命名空间:{租户}/{产品线}/{组件}(如 bank-core/transaction-service/go-sdk),所有组件均携带 SECURITY_POLICY=strict 标签,并强制启用 immutable tag 策略。以下为关键目录结构示例:
├── manifests/
│ ├── k8s-deployment.yaml # 带 podSecurityContext 的声明式配置
│ └── helm-values-prod.yaml # 启用 TLS 双向认证的生产值文件
├── build/
│ └── Dockerfile.secure # 多阶段构建 + trivy 扫描阶段
└── .harbor-policy.json # 定义 CVE-2023-XXXX 漏洞阻断阈值
自动化复刻流水线
通过 GitLab CI 触发复刻任务,核心步骤包含:
- 使用
skopeo copy --src-tls-verify=false --dest-tls-verify=true实现跨域安全传输 - 调用
cosign verify-blob --certificate-oidc-issuer https://auth.enterprise.com验证签名完整性 - 执行
helm package --version 1.2.3-20240521-enterprise生成带企业水印的 chart 包
安全合规性验证矩阵
| 检查项 | 工具链 | 通过阈值 | 实际结果 |
|---|---|---|---|
| OSV 漏洞扫描 | Trivy + OSV DB | CVE 严重等级≤7.5 | 6.8 |
| SBOM 一致性 | Syft + CycloneDX | SHA256 匹配率100% | 100% |
| 签名链完整性 | Cosign + Fulcio PKI | 证书链深度≥3 | 4 |
生产环境故障回滚实录
2024年3月某次 Kubernetes Operator 升级引发集群事件:复刻后的 cert-manager-v1.12.3-enterprise 镜像因误删 /etc/ssl/certs/ca-certificates.crt 导致 webhook 超时。团队立即执行以下操作:
- 从离线备份库
s3://enterprise-backup/harbor/20240315/提取原始镜像层 - 使用
umoci unpack --image sha256:... /tmp/recover-rootfs解包验证文件树 - 通过
runc run --no-pivot --root /var/run/runc启动隔离容器进行热修复测试
元数据增强实践
所有复刻产物自动注入企业专属字段:
io.enterprise.release-type: "air-gapped"io.enterprise.audit-id: "AUD-2024-0521-8873"io.enterprise.signed-by: "CN=Enterprise CA,O=SecOps,OU=PKI"
该字段被下游 CI/CD 流水线强制读取,未携带 audit-id 的制品禁止进入 UAT 环境。某次审计中发现 17 个历史镜像缺失该字段,系统自动触发 curl -X PATCH https://api.harbor.enterprise/v2.8/projects/core/repositories/.../artifacts/sha256:.../labels -d '{"id": "audit-missing"}' 进行标记隔离。
性能压测基准数据
在 10Gbps 专线环境下,单节点复刻吞吐量达:
- Helm Chart:平均 28.4 MiB/s(含签名+校验)
- OCI 镜像(5GB):峰值 92 秒完成(对比原生 docker pull 耗时 143 秒)
- 并发复刻 12 个组件时 CPU 利用率稳定在 68%±3%,内存占用 3.2GB
依赖图谱可视化
graph LR
A[上游开源源码] -->|git clone --depth=1| B(GitLab EE)
B -->|webhook trigger| C[CI Runner]
C --> D{Policy Engine}
D -->|allow| E[Harbor Registry]
D -->|block| F[Quarantine Bucket]
E --> G[Prometheus Metrics]
G --> H[AlertManager Rule: rate(harbor_replication_failed_total[1h]) > 0.5] 