1. 项目背景与核心价值
短信批量发送是电商促销、用户通知、验证码下发等场景中的高频需求。传统同步发送模式在面对数万级手机号时,往往面临响应慢、吞吐量低、资源占用高等问题。我在某跨境电商平台的促销活动中就遇到过这样的困境——当需要向20万用户发送限时优惠短信时,PHP同步脚本跑了近3小时才完成,期间还触发了运营商频控。
Go语言的轻量级协程(goroutine)和通道(channel)机制为这个问题提供了优雅的解决方案。通过实测对比,使用协程池技术的Go程序能在5分钟内完成同等量级的发送任务,且内存占用稳定在200MB以内。这种效率提升主要来自三个方面:
- 协程创建成本极低(仅2KB栈内存)
- IO等待期间自动释放线程资源
- 内置调度器实现多核负载均衡
2. 技术架构设计
2.1 核心组件拆解
系统由四个关键模块构成:
go复制type SMSGateway struct {
workerPool chan struct{} // 协程池大小限制
retryQueue chan *Message // 失败重试队列
apiClient *http.Client // 自定义HTTP客户端
stats *Statistics // 实时发送统计
}
2.1.1 协程池控制
通过带缓冲的channel实现工作协程数量控制:
go复制pool := make(chan struct{}, 100) // 最大100并发
for _, msg := range messages {
pool <- struct{}{} // 阻塞直到有空位
go func(m *Message) {
defer func(){ <-pool }()
sendMessage(m)
}(msg)
}
2.1.2 短信API客户端
需要特别处理:
- 连接复用(Enable Keep-Alive)
- 超时控制(总超时3s,含DNS解析)
- 重试机制(503状态码时延时重试)
go复制client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 1 * time.Second,
},
Timeout: 3 * time.Second,
}
2.2 流量控制策略
为避免触发运营商限制,需要实现三级流控:
- 协程池大小限制(硬件资源层面)
- 令牌桶算法(业务层面QPS控制)
- 动态退避(根据错误码调整速率)
go复制// 令牌桶实现示例
rateLimiter := time.Tick(time.Second / 50) // 50QPS
for range rateLimiter {
go processMessage()
}
3. 关键实现细节
3.1 消息处理流水线
采用生产者-消费者模型提升吞吐量:
- 主协程读取CSV文件生成原始消息
- 清洗协程校验手机号格式
- 发送协程调用API接口
- 日志协程持久化结果
go复制rawChan := make(chan *RawMessage, 1000)
cleanChan := make(chan *Message, 1000)
// 生产者
go func() {
for raw := range csvReader {
rawChan <- raw
}
}()
// 清洗消费者
go func() {
for raw := range rawChan {
if valid := validate(raw.Phone); valid {
cleanChan <- &Message{Phone: formatPhone(raw.Phone)}
}
}
}()
3.2 错误处理机制
3.2.1 实时重试
对可恢复错误(如网络超时)立即重试:
go复制for retry := 0; retry < 3; retry++ {
err := sendAPI(msg)
if err == nil || !isRecoverable(err) {
break
}
time.Sleep(time.Second << retry) // 指数退避
}
3.2.2 死信队列
将最终失败的消息写入Redis供人工处理:
go复制failedMsg, _ := json.Marshal(msg)
redisClient.RPush("sms:dead-letter", failedMsg)
4. 性能优化技巧
4.1 内存复用技术
通过sync.Pool减少GC压力:
go复制var messagePool = sync.Pool{
New: func() interface{} {
return new(Message)
},
}
// 获取对象
msg := messagePool.Get().(*Message)
defer messagePool.Put(msg)
4.2 批量提交优化
对支持批量接口的运营商,合并多个请求:
go复制func batchSend(messages []*Message) {
body := make([]string, 0, 50)
for _, msg := range messages {
if len(body) == cap(body) {
flushBatch(body)
body = body[:0]
}
body = append(body, encodeMessage(msg))
}
flushBatch(body)
}
5. 生产环境注意事项
5.1 监控指标埋点
必须监控的关键指标:
- 实时吞吐量(messages/s)
- 成功率(200响应占比)
- 平均延迟(从入队到完成)
- 协程数量(runtime.NumGoroutine)
推荐使用Prometheus客户端暴露指标:
go复制requestsTotal := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "sms_requests_total",
Help: "Total SMS requests",
},
[]string{"status"},
)
prometheus.MustRegister(requestsTotal)
5.2 优雅停机方案
处理系统终止信号时:
- 停止接收新任务
- 等待进行中的请求完成
- 持久化未处理消息
- 关闭统计协程
go复制func (s *SMSGateway) Shutdown() {
close(s.incomingChan) // 停止接收
ctx, cancel := context.WithTimeout(30*time.Second)
defer cancel()
for {
select {
case <-ctx.Done():
s.savePendingMessages()
return
default:
if s.activeWorkers == 0 {
return
}
time.Sleep(100 * time.Millisecond)
}
}
}
6. 实测性能对比
在4核8G云服务器上测试:
| 方案 | 10万条耗时 | CPU占用 | 内存峰值 |
|---|---|---|---|
| PHP同步 | 183分钟 | 25% | 1.2GB |
| Python多线程 | 47分钟 | 90% | 800MB |
| Go协程池(本方案) | 4.8分钟 | 65% | 210MB |
关键发现:
- Go方案吞吐量达到347条/秒
- 内存占用稳定无泄漏
- 错误率低于0.2%
7. 扩展优化方向
7.1 多通道负载均衡
对接多个短信供应商时:
go复制type ProviderSelector struct {
providers []*Provider
current uint32
}
func (p *ProviderSelector) Next() *Provider {
n := atomic.AddUint32(&p.current, 1)
return p.providers[(int(n)-1)%len(p.providers)]
}
7.2 智能路由策略
根据历史数据动态选择:
- 成功率最高的通道
- 当前延迟最低的节点
- 成本最优的供应商
go复制func selectProvider(phone string) *Provider {
if isInternational(phone) {
return globalProvider
}
if hour := time.Now().Hour(); hour > 22 {
return nightShiftProvider
}
return defaultProvider
}
在实际部署中,建议配合Kubernetes的HPA实现自动扩缩容。当消息队列积压超过阈值时,自动增加Pod副本数。我们通过这种方案成功应对了双11期间300万/小时的短信发送峰值。