第一章:Go风控服务去重模块架构全景概览
去重模块是风控服务中保障决策一致性与资源效率的核心组件,承担着对高频、多源、异构请求(如设备指纹、用户行为事件、交易报文)进行实时唯一性判定的关键职责。在高并发场景下(QPS ≥ 50k),该模块需在毫秒级延迟内完成特征提取、哈希计算、分布式状态查询与写入,同时严格避免漏判(False Negative)与误判(False Positive)。
核心设计原则
- 无状态化:业务逻辑层完全无状态,所有判重状态下沉至统一存储层,便于水平扩展;
- 最终一致性容忍:采用“先写后查”+本地布隆过滤器(Bloom Filter)预检机制,在强一致性与性能间取得平衡;
- 特征可插拔:支持按风控策略动态组合去重键(如
device_id + event_type + timestamp_window),通过 YAML 配置驱动,无需重启服务。
关键组件协作流
- 请求进入后,由
DedupExtractor统一解析原始 payload,生成标准化去重键(dedupKey); LocalBloomGuard对dedupKey进行本地布隆过滤——若返回 likely exists,则跳过后续流程并标记DUPLICATED;- 否则交由
RedisClusterClient执行原子操作:SET key "1" EX 3600 NX,成功即为首次请求,返回UNIQUE;失败则视为重复。
存储选型对比
| 存储方案 | 延迟(P99) | 容量上限 | 一致性模型 | 适用场景 |
|---|---|---|---|---|
| Redis Cluster | TB级 | 弱(异步复制) | 主流事件去重(TTL可控) | |
| BadgerDB(本地) | 单机GB级 | 强 | 低延迟兜底/离线回溯 | |
| TiKV | ~15ms | PB级 | 强(Raft) | 合规审计级全量持久化 |
以下为去重键生成示例代码(含注释):
// 生成带时间窗口的去重键:确保同一设备5分钟内相同事件仅计一次
func GenerateDedupKey(deviceID, eventType string, ts time.Time) string {
window := ts.Truncate(5 * time.Minute).Unix() // 截断到5分钟边界,实现滑动窗口语义
return fmt.Sprintf("dedup:%s:%s:%d", deviceID, eventType, window)
}
// 示例调用:GenerateDedupKey("dev_abc123", "login", time.Now()) → "dedup:dev_abc123:login:1717027200"
第二章:基于布隆过滤器的轻量级去重算法实现
2.1 布隆过滤器数学原理与误判率控制实践
布隆过滤器是一种空间高效的概率型数据结构,用于判断元素是否可能存在于集合中(允许假阳性,但不允许假阴性)。
核心数学模型
误判率 $ \varepsilon $ 由哈希函数个数 $ k $、位数组长度 $ m $ 和插入元素数 $ n $ 共同决定:
$$
\varepsilon \approx \left(1 – e^{-kn/m}\right)^k \approx e^{-k^2 n / (m \ln 2)}
$$
最优 $ k = \frac{m}{n} \ln 2 $,此时 $ \varepsilon \approx (0.6185)^{m/n} $。
误判率控制实践
- 给定目标误判率 $ \varepsilon = 0.01 $、预计元素数 $ n = 10^6 $,计算得:
$ m \approx -\frac{n \ln \varepsilon}{(\ln 2)^2} \approx 9.58 \times 10^6 $ 位(约 1.17 MB),$ k = \frac{m}{n} \ln 2 \approx 7 $
| 参数 | 值 | 说明 |
|---|---|---|
m(位数组长度) |
9,585,059 | 约 1.17 MB |
k(哈希函数数) |
7 | 使用 Murmur3 + 种子偏移实现 |
| 实际误判率 | ~0.00997 | 符合设计目标 |
import mmh3
import math
def bloom_hash(key: str, seed: int) -> int:
"""使用 MurmurHash3 生成均匀分布哈希值"""
return mmh3.hash(key, seed) & 0x7FFFFFFF # 强制非负
# 逻辑分析:通过不同 seed 模拟 k 个独立哈希函数;
# & 0x7FFFFFFF 确保结果在 [0, 2^31) 范围内,再模 m 得到位索引;
# 避免负数取模异常,提升哈希分布均匀性。
graph TD
A[输入元素] --> B{k个独立哈希函数}
B --> C1[哈希1 → bit index]
B --> C2[哈希2 → bit index]
B --> Ck[...]
C1 --> D[位数组 OR 更新]
C2 --> D
Ck --> D
D --> E[查询时全部命中才返回“可能存在”]
2.2 Go标准库与第三方bitset库的内存布局对比分析
内存对齐与字节填充差异
Go 标准库无原生 bitset,常以 []uint64 手动实现;而 github.com/willf/bitset 采用紧凑结构体封装:
// github.com/willf/bitset.BitSet 定义(简化)
type BitSet struct {
words []uint64 // 实际位数组
lastWord uint // 有效位数(非字节数)
}
words 切片头含 len/cap/ptr 三字段(24 字节),而纯 []uint64 在栈上传递时仅传递这 24 字节,实际数据在堆上——二者共享同一底层内存模型,但 BitSet 封装增加了字段语义与边界检查开销。
关键布局参数对比
| 维度 | []uint64(裸切片) |
bitset.BitSet |
|---|---|---|
| 元数据大小 | 24 字节 | 32 字节(+8 字节 lastWord) |
| 对齐要求 | 8 字节 | 8 字节 |
| 零值内存占用 | 24 字节 | 32 字节 |
位寻址逻辑示意
// 计算第 i 位所属 word 索引及位偏移(两者通用)
wordIdx := i / 64
bitPos := uint(i % 64)
// 注:i 为 int 类型;除法与取模由编译器优化为位运算(i>>6, i&63)
// wordIdx 用于索引 words[];bitPos 用于位掩码:1 << bitPos
2.3 并发安全的布隆过滤器分片设计与原子操作封装
为应对高并发写入场景下的竞争问题,采用分片(Sharding)+ 原子封装双策略:将大布隆过滤器逻辑拆分为 N 个独立子过滤器,每片绑定专属 sync/atomic.Value 封装的位图指针。
分片与路由机制
- 请求 key 经
hash(key) % N映射至唯一分片 - 各分片独立维护自己的
[]uint64位数组与哈希种子
原子更新封装示例
type Shard struct {
bits atomic.Value // 存储 *[]uint64
}
func (s *Shard) Set(index uint) {
bits := s.bits.Load().(*[]uint64)
(*bits)[index/64] |= 1 << (index % 64) // 位或写入,无锁安全
}
atomic.Value保证位图指针读写线程安全;index/64定位字单元,index%64计算偏移——避免sync.Mutex全局阻塞。
| 分片数 | 写吞吐提升 | 内存放大率 | 误判率变化 |
|---|---|---|---|
| 1 | 1× | 1.0× | 基准 |
| 8 | ~5.2× | 1.03× | +0.002% |
graph TD
A[Key] --> B{hash % N}
B --> C[Shard_0]
B --> D[Shard_1]
B --> E[...]
B --> F[Shard_{N-1}]
2.4 动态扩容策略:位图预分配 vs 运行时伸缩的性能实测
位图(Bitmap)在布隆过滤器、内存索引等场景中广泛使用,其扩容方式直接影响吞吐与延迟。
预分配位图(固定容量)
// 初始化 1MB 位图(8,388,608 bits),一次性 mmap 分配
uint8_t *bitmap = mmap(NULL, 1 << 20, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
逻辑分析:mmap 避免堆碎片,页表预映射降低首次访问缺页开销;参数 1 << 20 确保 2^20 字节 = 1MB,支持最多 838 万位标记。但内存占用刚性,稀疏场景浪费显著。
运行时伸缩位图(倍增式 realloc)
| 扩容方式 | P99 延迟 | 内存放大率 | GC 压力 |
|---|---|---|---|
| 预分配(1MB) | 12 μs | 1.0× | 无 |
| 动态 realloc | 87 μs | 1.35× | 中等 |
性能权衡本质
graph TD
A[请求写入] --> B{位图是否满?}
B -->|否| C[直接置位]
B -->|是| D[alloc new bitmap<br>copy bits<br>free old]
D --> E[延迟尖刺+缓存失效]
2.5 生产环境布隆参数调优:m/n比、哈希函数选型与CPU缓存友好性验证
布隆过滤器在高并发场景下,性能瓶颈常源于内存访问模式与哈希计算开销。关键调优维度有三:
- m/n 比选择:理论最优为
m/n ≈ 1.44 × k(k 为哈希函数数),但实际需兼顾 L1/L2 缓存行(64B)。当m > 2^16时,随机访存易引发 TLB miss; - 哈希函数选型:优先采用
Murmur3_x64_128分割式双哈希(避免多次完整计算); - 缓存对齐验证:确保位数组起始地址按 64 字节对齐。
// 哈希拆分实现(k=3 时仅需2个基础哈希)
uint64_t h1 = murmur3_64(key, len, seed);
uint64_t h2 = murmur3_64(key, len, seed ^ 0xdeadbeef);
for (int i = 0; i < k; i++) {
uint64_t hash = h1 + i * h2 + i * i; // 二次探测防相关性
set_bit(bloom->bits, hash % bloom->m);
}
该实现将 k 次独立哈希降为 2 次主哈希 + 线性组合,减少分支与内存依赖;i*i 项增强分布均匀性,实测在 1M key 下 FP 率稳定在 0.57%(理论 0.55%)。
| 参数 | 推荐值 | 影响维度 |
|---|---|---|
| m/n | 12–16 | 平衡 FP 率与缓存足迹 |
| k | 3–5 | 计算/存储权衡 |
| 对齐粒度 | 64B | 避免 cache line split |
graph TD
A[原始key] --> B{Murmur3_x64_128}
B --> C[h1: low64]
B --> D[h2: high64]
C & D --> E[线性组合生成k个索引]
E --> F[64B对齐位数组写入]
第三章:LRU-K与时间窗口协同的实时去重状态管理
3.1 LRU-K理论模型在高吞吐风控场景下的适用性再评估
高并发风控请求常呈现短时突发+长尾分布特征,传统LRU-2在缓存驱逐中易将真实热点(如黑产IP的高频复访)误判为噪声。
缓存命中率衰减实测对比(QPS ≥ 50K)
| K值 | 平均命中率 | 热点保留率 | 内存开销增幅 |
|---|---|---|---|
| 1 | 68.2% | 41.7% | +0% |
| 2 | 79.5% | 63.3% | +12% |
| 3 | 82.1% | 89.6% | +28% |
LRU-3热点识别增强逻辑
class LRUKCache:
def __init__(self, capacity: int, k: int = 3):
self.capacity = capacity
self.k = k
self.access_history = defaultdict(deque) # 记录最近k次访问时间戳
self.cache = {}
def get(self, key: str) -> bool:
if key not in self.cache:
return False
# 更新历史:只保留最近k次访问
self.access_history[key].append(time.time())
if len(self.access_history[key]) > self.k:
self.access_history[key].popleft()
return True
该实现通过时间窗口内访问频次密度替代单纯顺序淘汰,使黑产IP(如每秒5次请求)在K=3时稳定保留在缓存中;
access_history双端队列确保O(1)插入与裁剪,k=3在精度与内存间取得帕累托最优。
驱逐决策流程
graph TD
A[新请求到达] --> B{是否在cache中?}
B -->|是| C[更新access_history]
B -->|否| D[检查capacity]
D -->|满| E[按min(第k次访问时间)排序驱逐]
D -->|未满| F[直接插入]
C & E & F --> G[返回响应]
3.2 基于sync.Pool与ring buffer的无GC状态快照实现
传统状态快照常依赖 make([]byte, n) 频繁分配,引发 GC 压力。本方案融合 sync.Pool 对象复用与 ring buffer 的循环覆盖特性,实现零堆分配快照。
核心设计原则
- 快照生命周期严格受限于单次请求处理周期
- 所有缓冲区由
sync.Pool统一托管,避免逃逸 - ring buffer 采用固定长度、原子索引推进,无锁写入
ring buffer 写入逻辑
type RingBuf struct {
data []byte
head uint64 // 读位置(快照消费端)
tail uint64 // 写位置(生产端原子递增)
mask uint64 // len(data)-1,必须为2的幂
}
func (r *RingBuf) Write(p []byte) int {
n := uint64(len(p))
oldTail := atomic.AddUint64(&r.tail, n)
end := oldTail + n
// 环形拷贝(省略边界分段逻辑)
copy(r.data[(oldTail&r.mask):], p)
return len(p)
}
逻辑分析:
tail原子递增确保写并发安全;mask实现 O(1) 索引取模;data来自sync.Pool.Get(),使用后调用Put()归还。避免切片扩容与内存申请。
性能对比(10K/s 请求压测)
| 方案 | 分配次数/秒 | GC 次数/分钟 | 平均延迟 |
|---|---|---|---|
| 原生 make([]byte) | 12,400 | 87 | 1.8ms |
| Pool + ring | 0 | 0 | 0.3ms |
graph TD
A[请求进入] --> B[从sync.Pool获取预分配RingBuf]
B --> C[原子写入ring buffer]
C --> D[生成快照句柄<br>指向当前head/tail区间]
D --> E[响应返回前归还RingBuf到Pool]
3.3 毫秒级滑动窗口与TTL自动驱逐的混合生命周期控制
传统缓存策略常在“精确时效性”与“低开销”间妥协。本节引入双模生命周期协同机制:滑动窗口保障请求洪峰下的实时统计精度,TTL驱逐确保内存安全边界。
核心协同逻辑
# Redis + Lua 实现毫秒级滑窗 + TTL 双控
local key = KEYS[1]
local now_ms = tonumber(ARGV[1]) -- 当前毫秒时间戳
local window_ms = tonumber(ARGV[2]) -- 滑窗宽度(如500ms)
local ttl_sec = tonumber(ARGV[3]) -- 基础TTL(如60s)
-- 1. 检查是否过期(TTL兜底)
if redis.call("PTTL", key) < 0 then
return 0 -- 已驱逐
end
-- 2. 滑窗校验:仅保留 window_ms 内写入的值
local ts = tonumber(redis.call("HGET", key, "ts") or "0")
if now_ms - ts > window_ms then
redis.call("DEL", key) -- 主动清理过期窗口数据
return 0
end
return 1 -- 有效
逻辑分析:该Lua脚本原子执行双重校验。
PTTL提供服务端TTL兜底(防时钟漂移),ts字段记录写入毫秒时间戳,滑窗判断基于客户端传入的now_ms,实现亚秒级精度。window_ms与ttl_sec解耦设计,前者控业务时效,后者保系统稳定性。
策略对比
| 维度 | 纯TTL模式 | 纯滑窗模式 | 混合模式 |
|---|---|---|---|
| 时效精度 | 秒级 | 毫秒级 | 毫秒级(滑窗)+ 安全兜底(TTL) |
| 内存压力 | 低(定时驱逐) | 高(需维护时间索引) | 中(仅存单条ts元数据) |
| 时钟依赖 | 弱(服务端时间) | 强(客户端同步) | 混合(滑窗需NTP校准) |
graph TD
A[请求到达] --> B{滑窗有效性检查}
B -->|通过| C[返回缓存数据]
B -->|失败| D[TTL兜底检查]
D -->|未过期| E[触发异步刷新]
D -->|已过期| F[执行DEL+重建]
第四章:内存极致优化的去重数据结构选型与定制
4.1 uint64位压缩编码:将128位UUID映射至8字节的哈希空间压缩实践
UUID(128位)在分布式系统中广泛用作唯一标识,但其体积大、索引效率低。为适配内存敏感场景(如高频缓存键、布隆过滤器),需将其无损或可控损失地压缩至 uint64(8字节)。
哈希压缩策略对比
| 方法 | 冲突率(1M UUID) | 是否可逆 | 计算开销 |
|---|---|---|---|
xxHash64(uuid.Bytes()) |
~0.003% | 否 | 极低 |
Murmur3_64(uuid.String()) |
~0.005% | 否 | 中 |
| 高位截断(前8字节) | >15% | 否 | 无 |
推荐实现(xxHash64)
import "github.com/cespare/xxhash/v2"
func uuidToUint64(u [16]byte) uint64 {
// 输入为标准UUID字节数组(16字节,网络序)
return xxhash.Sum64(u[:]).Sum64()
}
该函数将16字节UUID直接喂入xxHash64,输出64位哈希值。xxHash具备高吞吐(>10 GB/s)、强雪崩效应与低碰撞率,适用于大规模ID空间压缩。注意:原始UUID字节顺序必须保持一致(RFC 4122规范),避免因端序或字符串化引入额外熵损。
graph TD A[128-bit UUID] –> B[Raw 16-byte array] B –> C[xxHash64 hash] C –> D[uint64 key]
4.2 内存页对齐与struct字段重排:减少padding提升缓存行利用率
现代CPU缓存以64字节缓存行为单位工作,而结构体中因字段大小/顺序不当产生的内存填充(padding)会割裂数据局部性,降低缓存命中率。
字段重排前后的内存布局对比
// 重排前:16字节结构体,因对齐产生8字节padding
type BadExample struct {
a uint32 // 0-3
b uint64 // 8-15 ← 跳过4-7(padding)
c uint16 // 16-17
} // total: 24 bytes (16+8 padding)
// 重排后:紧凑布局,0 padding
type GoodExample struct {
b uint64 // 0-7
a uint32 // 8-11
c uint16 // 12-13
} // total: 16 bytes
uint64需8字节对齐,BadExample中a(4B)后无法直接存放b,强制插入4B padding;重排后按大小降序排列,自然消除填充。
缓存行利用率提升效果
| 结构体类型 | 单实例大小 | 每64B缓存行容纳实例数 |
|---|---|---|
| BadExample | 24 B | ⌊64/24⌋ = 2 |
| GoodExample | 16 B | ⌊64/16⌋ = 4(+100%) |
自动化检测建议
- 使用
go tool compile -S查看汇编中的字段偏移; - 运行
go vet -v检测潜在填充警告; - 集成
structlayout工具分析内存布局。
4.3 零拷贝序列化:unsafe.Pointer+reflect.SliceHeader绕过runtime分配
传统 []byte 序列化需内存拷贝与 runtime 分配,而零拷贝方案直接复用底层数据视图。
核心原理
利用 unsafe.Pointer 跳过类型安全检查,配合 reflect.SliceHeader 重解释内存布局:
func sliceFromPtr(ptr unsafe.Pointer, len, cap int) []byte {
return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
Data: uintptr(ptr),
Len: len,
Cap: cap,
}))
}
逻辑分析:
reflect.SliceHeader是 Go 运行时内部结构(Data/Len/Cap),通过unsafe构造其内存镜像并强制类型转换,使 runtime 认为该[]byte指向已有内存,跳过make()分配。
关键约束
- 原始内存必须持续有效(不可为栈局部变量);
- 长度/容量不得越界,否则触发 panic 或 UB;
- 禁止在 GC 可回收内存上使用(如未固定
runtime.KeepAlive)。
| 方案 | 分配开销 | 内存复用 | 安全性 |
|---|---|---|---|
bytes.Buffer |
✅ | ❌ | ✅ |
unsafe+Header |
❌ | ✅ | ⚠️ |
graph TD
A[原始字节切片] --> B[获取底层数组指针]
B --> C[构造SliceHeader]
C --> D[强制类型转换为[]byte]
D --> E[零拷贝视图]
4.4 mmap共享内存池在多实例间复用去重状态的可行性验证
核心设计思路
利用mmap(MAP_SHARED)将去重状态(如布隆过滤器位图)映射至同一物理页,使多个进程实例可原子读写同一内存区域。
关键验证步骤
- 启动两个独立Worker进程,均
mmap同一命名文件(/dev/shm/dedup_pool); - 并发插入相同哈希值,观察
__sync_fetch_and_or对位图的竞态行为; - 通过
msync(MS_SYNC)确保跨实例持久化可见性。
原子写入示例
// 共享位图基址 + 哈希偏移计算
size_t idx = hash_val % (pool_size * 8); // pool_size单位:字节
uint8_t *byte_ptr = (uint8_t*)shared_pool + (idx / 8);
uint8_t mask = 1U << (idx % 8);
__sync_fetch_and_or(byte_ptr, mask); // 无锁置位
__sync_fetch_and_or保证单字节内位操作原子性;idx / 8定位字节偏移,idx % 8生成掩码。需确保shared_pool按sysconf(_SC_PAGESIZE)对齐。
性能对比(10M key/s场景)
| 方案 | 内存开销 | 跨实例同步延迟 | 状态一致性 |
|---|---|---|---|
| 独立内存 | ×2 | — | 不一致 |
| mmap共享 | ✓ | 强一致 |
graph TD
A[Worker1] -->|mmap| C[Shared Page]
B[Worker2] -->|mmap| C
C --> D[Atomic Bit Set]
第五章:压测结果与线上稳定性归因分析
压测环境与基准配置
本次压测基于真实生产镜像构建,采用 8 台 16C32G 节点组成的 Kubernetes 集群,服务部署版本为 v2.4.7(SHA: a9f3c1d),数据库为 MySQL 8.0.33(主从分离,读写分离中间件为 ShardingSphere-JDBC 5.3.2)。压测工具选用 JMeter 5.5,通过 200 并发线程组持续施压 30 分钟,模拟用户登录→商品查询→下单→支付全流程链路。所有接口均启用全链路 TraceID(SkyWalking v9.4.0)与日志采样(采样率 100%)。
核心接口性能拐点识别
下表汇总关键路径在不同并发梯度下的 P95 响应时间与错误率变化:
| 并发数 | /api/v2/login (ms) | /api/v2/items (ms) | /api/v2/order (ms) | 错误率 |
|---|---|---|---|---|
| 50 | 128 | 86 | 215 | 0.02% |
| 120 | 294 | 317 | 1120 | 1.8% |
| 180 | 1420 | 2850 | timeout(>30s) | 23.7% |
当并发升至 120 时,/api/v2/order 接口 P95 跃升至 1120ms,且 DB 慢查询日志中出现大量未命中索引的 SELECT * FROM t_order WHERE user_id = ? AND status IN (?, ?) ORDER BY created_at DESC LIMIT 20(执行耗时均值 2.4s)。
数据库连接池雪崩现象复现
通过 Arthas 实时监控发现:HikariCP 连接池活跃连接数在 120 并发下迅速打满(maxPoolSize=20),等待队列堆积达 137 个请求,平均等待超时时间达 4.2s。进一步抓取线程堆栈,确认 68% 的线程阻塞在 HikariPool.getConnection(),而底层 MySQL 连接实际仅占用 17 个(SHOW PROCESSLIST 验证),证实为连接复用不足导致的虚假瓶颈。
线上故障根因交叉验证
将压测期间采集的 JVM heap dump 与线上 7 月 12 日凌晨 GC 异常时段 dump 对比,使用 Eclipse MAT 分析发现:com.example.order.service.OrderCacheService 中静态 ConcurrentHashMap 缓存了 240 万+ OrderDetail 对象(单对象平均 1.2MB),且 key 为未重写 hashCode() 的自定义 DTO,引发严重哈希碰撞——该缓存本意为防缓存穿透,但未设置 size limit 与 LRU 清理策略,最终触发 Full GC 频次由 12h/次激增至 8min/次。
// 问题代码片段(已修复)
private static final Map<OrderKey, OrderDetail> cache = new ConcurrentHashMap<>();
// 修复后替换为 Caffeine.newBuilder()
// .maximumSize(10_000)
// .expireAfterWrite(10, TimeUnit.MINUTES)
// .build();
依赖服务级联超时传播
通过 SkyWalking 的拓扑图(mermaid 渲染)可清晰定位稳定性断点:
graph LR
A[Frontend] --> B[Auth Service]
A --> C[Order Service]
C --> D[Payment Service]
C --> E[Inventory Service]
D --> F[Bank Gateway]
E --> G[Redis Cluster]
style C stroke:#ff6b6b,stroke-width:3px
style D stroke:#4ecdc4,stroke-width:2px
style G stroke:#45b7d1,stroke-width:2px
当 Inventory Service 因 Redis 集群某分片 CPU 持续 >95%(由大 Key 扫描触发)响应延迟飙升至 800ms 后,Order Service 默认 1.5s 超时无法覆盖该场景,导致下游 Payment Service 被同步阻塞,形成跨服务线程池耗尽。
稳定性加固落地项
- 数据库:为
t_order(user_id, status, created_at)新增联合索引,覆盖全部高频查询条件; - 缓存层:将
OrderCacheService全量迁移至 Caffeine + Redis 两级缓存,本地最大容量设为 5000,远程 TTL 统一为 30 分钟; - 熔断策略:在 Resilience4j 中为 Inventory Service 调用新增
timeLimiterConfig.timeoutDuration = 300ms,并启用半开状态自动探测; - 容器资源:将 Order Service Pod 的 requests.cpu 从 500m 提升至 1200m,避免 K8s QoS 降级导致的 CPU 节流。
