1. 项目背景与核心价值
短信批量发送是电商促销、系统告警、身份验证等场景中的高频需求。传统同步发送模式在面对数万级任务时,往往面临吞吐量低、响应延迟高、资源占用大等问题。我们团队最近重构了短信发送模块,采用Go语言的协程并发机制,将发送效率提升12倍,服务器资源消耗降低60%。本文将分享具体实现方案和踩坑实录。
2. 技术方案设计
2.1 架构选型对比
我们对比了三种实现方案:
- 同步单线程模式:简单但性能差
- 线程池模式:Java常用方案,但上下文切换成本高
- Go协程模式:轻量级并发,内存占用低
最终选择方案3的原因:
- 单个goroutine仅需2KB栈内存
- 调度器在用户态实现,减少内核态切换
- 原生channel实现安全通信
2.2 核心组件设计
go复制type SMSWorker struct {
taskChan chan *SMSTask // 任务队列
resultChan chan *SendResult // 结果回调
wg sync.WaitGroup // 任务同步
poolSize int // 并发度
}
关键参数设计原则:
- poolSize = CPU核心数 × 2 (经验值)
- taskChan容量 = 预期QPS × 平均处理时间(秒)
- 采用buffered channel避免阻塞
3. 核心实现细节
3.1 协程池初始化
go复制func (w *SMSWorker) Init() {
w.taskChan = make(chan *SMSTask, 10000)
w.resultChan = make(chan *SendResult, 10000)
for i := 0; i < w.poolSize; i++ {
go w.worker()
}
}
func (w *SMSWorker) worker() {
defer w.wg.Done()
for task := range w.taskChan {
resp, err := sendSMS(task)
w.resultChan <- &SendResult{task, resp, err}
}
}
3.2 流量控制实现
通过令牌桶算法防止突发流量:
go复制type RateLimiter struct {
bucket chan struct{}
ticker *time.Ticker
}
func NewRateLimiter(rate int) *RateLimiter {
r := &RateLimiter{
bucket: make(chan struct{}, rate),
ticker: time.NewTicker(time.Second),
}
go r.fillBucket()
return r
}
func (r *RateLimiter) fillBucket() {
for range r.ticker.C {
for i := 0; i < cap(r.bucket); i++ {
select {
case r.bucket <- struct{}{}:
default:
break
}
}
}
}
4. 性能优化实践
4.1 连接池优化
短信API调用90%时间消耗在TCP连接建立上。我们复用HTTP连接:
go复制var transport = &http.Transport{
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
}
client := &http.Client{
Transport: transport,
Timeout: 5 * time.Second,
}
4.2 内存优化技巧
避免在循环中创建对象:
go复制// 错误做法
for _, task := range tasks {
buf := new(bytes.Buffer) // 每次循环都分配
// ...
}
// 正确做法
var buf bytes.Buffer
for _, task := range tasks {
buf.Reset()
// ...
}
5. 生产环境问题排查
5.1 协程泄漏检测
使用runtime包监控:
go复制go func() {
for {
log.Printf("goroutines: %d", runtime.NumGoroutine())
time.Sleep(5 * time.Second)
}
}()
常见泄漏场景:
- channel未关闭导致goroutine阻塞
- waitGroup未正确Done
5.2 性能瓶颈分析
使用pprof工具:
bash复制go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile
典型优化案例:
- 将JSON序列化从json.Marshal改为jsoniter
- 减少sync.Mutex竞争粒度
6. 完整示例代码
go复制package main
import (
"context"
"log"
"sync"
"time"
)
type SMSService struct {
workers []*SMSWorker
rateLimiter *RateLimiter
}
func (s *SMSService) SendBatch(ctx context.Context, tasks []*SMSTask) {
var wg sync.WaitGroup
for _, task := range tasks {
<-s.rateLimiter.bucket // 限流控制
wg.Add(1)
go func(t *SMSTask) {
defer wg.Done()
select {
case s.workers[t.Partition()].taskChan <- t:
case <-ctx.Done():
log.Println("canceled")
}
}(task)
}
wg.Wait()
}
7. 关键注意事项
-
通道关闭原则:
- 永远由发送方关闭channel
- 关闭后禁止再发送
-
上下文传递:
go复制func worker(ctx context.Context, taskChan <-chan Task) { for { select { case task := <-taskChan: // process case <-ctx.Done(): return } } } -
错误处理建议:
- 使用errors.Wrap保存调用栈
- 通过channel返回错误而非panic
8. 性能对比数据
测试环境:4核8G云服务器
| 方案 | QPS | 内存占用 | CPU使用率 |
|---|---|---|---|
| 同步模式 | 120 | 1.2GB | 15% |
| 线程池(Java) | 1800 | 3.5GB | 75% |
| Go协程池 | 8500 | 800MB | 65% |
实际生产环境中,我们实现了:
- 日均发送量从50万提升到600万
- 95分位响应时间从1.2s降到200ms
- 服务器实例数从20台缩减到3台
9. 扩展优化方向
-
动态扩缩容:
go复制func (w *WorkerPool) AdjustSize(newSize int) { if newSize > w.size { // 扩容 for i := w.size; i < newSize; i++ { go w.worker() } } else { // 缩容 for i := 0; i < w.size-newSize; i++ { w.taskChan <- nil // 特殊终止信号 } } w.size = newSize } -
智能路由策略:
- 根据运营商分片
- 失败自动切换通道
- 基于历史成功率权重分配
-
异步回调优化:
- 使用Redis Stream存储结果
- 批量回调减少IOPS
在实现过程中,我们发现Go的sync.Pool对临时对象复用效果显著,特别是在高并发场景下可减少40%的内存分配。但要注意pool.Get()返回的对象可能是脏数据,必须重置后再使用