文章 · 2026-02-26

柔性降级的艺术:过载保护中的概率博弈与延迟清理

在构建高性能分布式系统时,我们往往追求极致的吞吐量。然而,现实世界的流量并非总是温顺的。当瞬时并发涌入,系统 CPU 飙升至临界点时,最危险的动作不是"处理太慢",而是"试图处理所有请求"导致的雪崩效应。

最近我在分析某工业级七层负载均衡器的源码时,发现了 CPU 保护逻辑的一套设计方案。它没有采用简单的硬截断(Hard Drop),而是通过一种"柔性"的方式,在保障系统核心稳定性的同时,尽可能地维持了服务的可用性。

核心挑战:过载时的"次生灾害"

当 CPU 负载过高时,简单地关闭连接(Close Connection)往往并不足以平息风暴。在高并发场景下,频繁的 accept 后立即 close 依然会消耗大量的内核态 CPU 资源用于处理系统调用和 TCP 状态机维护。若客户端有重试机制,即时失败可能会引发更猛烈的请求回浪。

系统采用两项互补的技术:概率性拒绝延迟清理

权衡一:从"开关"到"滑块"

系统通过指数移动平均(EMA)跟踪 CPU 使用率。与单一硬性阈值相比,设计者定义了低水位(Lo)与高水位(Hi)两条线,将运行空间划分为三个区间:

硬性阈值会导致严重的震荡——负载在临界值附近游走时,系统在"全量接受"与"全量拒绝"之间剧烈切换。线性斜坡则产生平滑、稳定的卸载曲线,对瞬时波动有更好的抑制。

权衡二:物理降频与延迟清理

被拒绝的连接不会立刻销毁。系统维护一个清理器(Cleaner),将被拒绝的连接送入缓冲队列,强制持有一段时间(例如 10 秒)。在此期间,连接占用文件描述符,但没有任何业务逻辑处理。

这带来两个效果:

  1. 物理降频:通过占用连接,减缓激进客户端(或攻击者)建立新连接的速度。
  2. 削峰填谷:将资源释放的开销推迟到系统压力较小的时刻。

净室重构:Go 语言演示

为了表达这一设计思想,我选择了 Go 语言。Go 的协程模型和 Channel 特性直接对应"持有并延迟清理"的并发意图。

package main

import (
	"math/rand"
	"net"
	"sync"
	"time"
)

type CpuLimiterConfig struct {
	UsageCoeff       float64       // EMA 平滑系数
	ConnRejectLo     float64       // 开始拒绝的阈值 (如 0.8)
	ConnRejectHi     float64       // 100% 拒绝的阈值 (如 0.95)
	ConnHoldDuration time.Duration // 拒绝后的持有时长
}

type CpuLimiter struct {
	config    CpuLimiterConfig
	cpuUsage  float64
	mu        sync.RWMutex
	rejections chan net.Conn
}

func NewCpuLimiter(config CpuLimiterConfig) *CpuLimiter {
	l := &CpuLimiter{
		config:    config,
		rejections: make(chan net.Conn, 10000),
	}
	go l.runCleaner()
	return l
}

// 模拟 RejectedConnsCleaner:持有资源以平滑清理开销
func (l *CpuLimiter) runCleaner() {
	for conn := range l.rejections {
		go func(c net.Conn) {
			// 在此持有连接,起到“降频”作用
			time.Sleep(l.config.ConnHoldDuration)
			_ = c.Close()
		}(conn)
	}
}

func (l *CpuLimiter) UpdateUsage(instantUsage float64) {
	l.mu.Lock()
	defer l.mu.Unlock()
	// 指数移动平均,平滑掉瞬时抖动
	l.cpuUsage = (1-l.config.UsageCoeff)*l.cpuUsage + l.config.UsageCoeff*instantUsage
}

func (l *CpuLimiter) ShouldReject() bool {
	l.mu.RLock()
	usage := l.cpuUsage
	l.mu.RUnlock()

	if usage <= l.config.ConnRejectLo {
		return false
	}
	if usage >= l.config.ConnRejectHi {
		return true
	}

	// 线性概率模型:(usage - lo) / (hi - lo)
	probability := (usage - l.config.ConnRejectLo) / (l.config.ConnRejectHi - l.config.ConnRejectLo)
	return rand.Float64() < probability
}

架构洞察:工程思维 vs 理论完美

这段逻辑展示了工业级系统的典型特征:在现实约束下工作。

在理论上,我们希望有能够完美预测负载并精确控制流量的算法。但在真实的负载均衡器中,计算开销本身就是成本。使用简单的 EMA 配合随机数概率,以极低的指令成本换取了系统在大规模过载下的生存能力。

概率拒绝的代价是引入了非确定性:在某些罕见情况下,重要的探测请求——健康检查、配置加载器——可能意外地被随机拒绝。工业级系统通常通过"白名单"或优先级通道来缓解这一问题,让核心基础设施流量完全绕过概率门控,确保其不会成为卸载流量的牺牲品。

此外,延迟清理机制体现了对底层资源(文件描述符、内核缓存)的尊重——有时候,"持有资源"比"快速释放"更能保护系统。


说明:本演示仅用于解析设计思想,生产环境建议结合具体的操作系统指标(如 loadavg 或特定的处理器遥测数据)进行采样。

© 2026 Yuxu Ge ·