设计一个能在一小时内稳定发送一千万条短信的线程池,绝不仅仅是设置几个参数那么简单。这是一个典型的高并发、IO密集型任务,需要从架构层面进行系统性设计,以确保高性能、高可靠和系统稳定。
以下是完整的设计方案:
首先,明确我们的目标:
10,000,000 / 3600 ≈ 2778 条/秒这意味着我们的系统需要稳定地维持近 2800 QPS 的发送能力。
在生产环境中,严禁使用 Executors.newFixedThreadPool() 等方式创建线程池,因为它们使用无界队列,在海量任务下极易导致内存溢出(OOM)。我们必须手动创建 ThreadPoolExecutor 并进行精细化配置。
短信发送是典型的 IO 密集型 任务(主要耗时在网络调用),而非 CPU 密集型。对于 IO 密集型任务,可以使用以下经验公式来估算初始线程数:
Nthreads = Ncpu * Ucpu * (1 + W/C)
Ncpu: CPU 核心数Ucpu: 目标 CPU 利用率 (通常设为 1)W/C: 等待时间与计算时间的比值。对于网络请求,W远大于C,因此这个值很大。在实际工程中,一个常用的简化策略是将线程数设置为 CPU 核心数的 2 倍作为起点。例如,一台 8 核的机器,可以从 16 个核心线程开始。但这只是一个初始值,最终需要通过压力测试来确定最优配置。
必须使用有界队列(如 LinkedBlockingQueue 并指定容量)来限制等待任务的数量,这是防止 OOM 的关键防线。队列的大小需要根据可用内存和单个任务占用的内存来估算。
当线程池和队列都满了之后,新提交的任务如何处理?默认的 AbortPolicy 会直接抛出异常,导致任务丢失,这在我们的场景中是不可接受的。
强烈推荐使用 CallerRunsPolicy。
仅有线程池是不够的,必须构建一套完整的可靠性保障体系。
线上流量是变化的,硬编码的参数无法应对所有情况。
corePoolSize, maxPoolSize, queueCapacity)配置在 Apollo、Nacos 等配置中心,支持运行时动态调整,无需重启服务。为了防止应用宕机导致内存队列中的任务丢失,必须有兜底方案。
你的线程池再强大,也必须考虑下游短信网关的承受能力。如果网关有 QPS 限制(例如每秒最多接收 3000 条),那么你的发送速率就不能超过这个限制。
此时,可以在任务执行逻辑中引入限流器,如 Guava 的 RateLimiter。
// 创建一个每秒放行 2800 个令牌的限流器
final RateLimiter rateLimiter = RateLimiter.create(2800.0);
// 在线程池的任务中
public void sendSmsTask() {
// 获取令牌,如果速率超限则会阻塞等待
rateLimiter.acquire();
// 执行真正的短信发送逻辑
smsGateway.send(...);
}
通过这种方式,可以确保发送给网关的流量是平滑且受控的,避免因瞬时流量过大而被网关拒绝。
综上所述,一个健壮的千万级短信推送方案应该是多层次的:
| 层级 | 策略 | 目的 |
|---|---|---|
| 基础层 | 手动创建 ThreadPoolExecutor,使用有界队列和 CallerRunsPolicy |
避免 OOM,实现背压,保证单机稳定性 |
| 业务层 | 基于 IO 密集型公式设定初始线程数,并通过压测调优 | 最大化资源利用率和吞吐能力 |
| 保障层 | 任务状态持久化 + 离线补偿任务 | 确保任务在任何情况下都不丢失 |
| 治理层 | 动态线程池 + 全链路监控 | 实时感知系统状态,灵活应对流量变化 |
| 协同层 | 使用 RateLimiter 等工具进行限流 |
保护下游依赖,遵守外部系统约束 |