跳转到正文
zeno's blog
返回

Go 服务:容量评估与扩容决策

Table of contents

Open Table of contents

TL;DR

Go 服务本身几乎不是瓶颈(纯 CPU 可达 10 万+ RPS),真正的瓶颈在数据库查询、RPC 调用等 I/O。决策核心是:用 CPU 利用率 × P99 延迟的组合判断该优化代码还是加机器,用连接池指标定位隐藏瓶颈。


Go 服务的核心指标

流量指标

指标含义关注点
RPS (Requests Per Second)每秒处理的请求数服务的吞吐能力上限
P99 Latency99% 请求的响应时间用户体验的真实反映,比平均值重要得多
P999 Latency99.9% 请求的响应时间尾部延迟,暴露 GC、锁竞争等隐藏问题
Error Rate错误率(5xx / 总请求)超过 0.1% 就该报警
Inflight Requests当前正在处理的请求数反映并发压力,接近上限时延迟会急剧上升

资源指标

指标含义危险阈值
CPU UtilizationCPU 使用率> 70% 就该扩容,不是等到 100%
Goroutine Count活跃 goroutine 数持续增长 = goroutine 泄漏;突增 = 下游阻塞
Heap Memory堆内存使用量持续增长 = 内存泄漏
GC Pause (STW)GC 停顿时间Go 1.19+ 通常 < 1ms,> 10ms 说明有问题
GC CPU%GC 消耗的 CPU 占比> 25% 说明内存分配过于频繁
File Descriptors打开的文件描述符数接近 ulimit = 无法建立新连接
Thread CountOS 线程数正常 ≈ GOMAXPROCS + 少量,暴涨 = syscall 阻塞

连接指标

指标含义关注点
Active Connections当前活跃连接数HTTP/gRPC 连接池是否健康
DB Pool Active/Idle/Wait数据库连接池状态Wait > 0 = 连接池不够,最常见的瓶颈
Redis Pool Active/IdleRedis 连接池状态同上
Upstream Latency调用下游服务的延迟你的 P99 = 你最慢的下游依赖的 P99

单实例能力量级

┌─────────────────────────┬───────────────┬──────────────┐
│ 场景                     │ 单实例 RPS     │ P99 延迟      │
├─────────────────────────┼───────────────┼──────────────┤
│ 纯计算 JSON 序列化        │ 5万~20万       │ < 1ms        │
│ 简单 CRUD(有缓存)       │ 1万~5万        │ 2~10ms       │
│ 业务逻辑 + DB + Redis    │ 3K~15K        │ 10~50ms      │
│ 复杂业务(多次 DB + RPC) │ 1K~5K         │ 50~200ms     │
│ 文件上传 / 大 payload     │ 500~2K        │ 100ms~1s     │
└─────────────────────────┴───────────────┴──────────────┘

加上一次 5ms 的数据库查询,RPS 就从 10 万+ 降到万级。瓶颈在 I/O,不在 Go。


决策框架

决策一:需要多少实例?

所需实例数 = (峰值 RPS × 3) ÷ 单实例 RPS

× 3 的原因:

决策二:优化代码 vs 加机器?

CPU < 40%,P99 高 → 不是 CPU 问题,优化 I/O(连接池、批量查询、缓存)
CPU > 70%,P99 低 → CPU 密集,加机器或优化算法
CPU < 40%,P99 低 → 余量充足,不用动
CPU > 70%,P99 高 → 两头都到极限,必须加机器 + 优化

决策三:GOMAXPROCS 设置

容器环境:必须用 automaxprocs(uber-go/automaxprocs)
  → 否则 Go 读到宿主机 CPU 核数,goroutine 调度混乱

裸机/VM:默认值(= CPU 核数)通常最优,不用调

决策四:连接池大小

// 数据库连接池 — 关键参数
db.SetMaxOpenConns(N)     // 最大连接数
db.SetMaxIdleConns(N)     // 最大空闲连接数(设成和 Open 一样)
db.SetConnMaxLifetime(h)  // 连接最大存活时间

// N 的计算:
// N = 目标 RPS × 平均查询耗时(秒)
// 例:5000 RPS × 0.005s = 25 个连接 → 留 buffer 设 50

// 铁律:MaxIdleConns = MaxOpenConns
// 否则高峰过后连接被回收,下次请求又要重建,导致延迟尖刺

常见坑:MySQL 默认 max_connections = 151。10 个实例各设 50 连接 = 500,直接超了。连接池大小必须和数据库侧对齐。

决策五:Goroutine 控制

HTTP 服务:不需要 goroutine 池
  → net/http 已经是 per-connection goroutine,调度器能处理百万级

后台任务(消费 MQ、定时任务):需要控制并发
  → 用 semaphore(chan struct{} 或 errgroup.SetLimit)
  → 不用第三方 goroutine 池库,channel 就够了

WebSocket / 长连接:goroutine 数 = 连接数 × 2(读 + 写)
  → 10 万连接 = 20 万 goroutine ≈ 2GB 栈内存(每个 8KB 初始栈)
  → 百万连接需要考虑 epoll 方案(gnet/nbio)

决策六:HTTP vs gRPC

对外 API(给前端/App)     → HTTP + JSON(通用性、调试方便)
内部服务间通信              → gRPC + Protobuf
  → 序列化速度快 5~10 倍
  → HTTP/2 多路复用,不用连接池
  → 强类型契约(proto 文件即文档)
  → 流式传输(server streaming 替代轮询)

性能差异:高 QPS 场景(> 1 万 RPS)时 JSON 序列化占 10~30% CPU,
         Protobuf 序列化开销几乎可忽略

监控体系

Go 运行时指标 → runtime/metrics(Go 1.16+)或 prometheus/client_golang
  → goroutine_count, gc_pause, heap_inuse, cpu_usage

HTTP 指标 → middleware 埋点
  → request_duration_histogram(按路由、状态码分桶)
  → inflight_requests_gauge

连接池指标 → database/sql 自带 DBStats
  → db.Stats() → OpenConnections, InUse, WaitCount, WaitDuration
  → WaitCount > 0 = 连接池不够,最早的预警信号

链路追踪 → OpenTelemetry → Jaeger/Tempo
  → 定位"P99 高是因为哪个下游慢"

实战例子:DAU 50 万 IM 应用的 Go 服务规划

峰值在线 10 万用户,每用户 1 req/s → 10 万 RPS

流量分布:
  - 心跳/在线状态:6 万 RPS → 纯 Redis,Go 处理极轻
  - 发消息:2 万 RPS → 写 MQ + 写 DB
  - 拉消息/历史:2 万 RPS → 读 Redis 缓存 + 少量 DB

单实例能力(4C8G 容器):
  - 心跳类:~3 万 RPS(几乎纯内存操作)
  - 消息类:~5000 RPS(有 Redis + MQ I/O)

所需实例:
  - 心跳服务:6万 ÷ 3万 × 3 = 6 台
  - 消息服务:4万 ÷ 5000 × 3 = 24 台
  - 总计约 30 个 Pod

WebSocket 网关:
  - 10 万连接 ÷ 单实例 2 万连接 = 5 台
  - 每台 4 万 goroutine ≈ 320MB 栈内存,轻松

Pitfalls

  1. 不要用平均延迟做决策 — P50 = 5ms 的服务可能 P99 = 500ms,10% 的用户体验极差但你看不到
  2. Goroutine 数不等于并发能力 — 100 万 goroutine 但都阻塞在 DB 查询上 = 没有并发,真正的并发受限于最慢的 I/O
  3. 连接池是最常被忽略的瓶颈db.Stats().WaitCount 如果 > 0,说明请求在排队等连接,加再多机器也没用
  4. 别在 Go 里做 CPU 密集型计算 — 图片处理、加密、压缩交给专门服务或 C/Rust 库,否则 GC 压力和延迟都不可控
  5. automaxprocs 在 K8s 环境是必须的 — 不加这个库,4 核容器里 Go 以为自己有 64 核,调度器行为完全错乱

分享这篇文章:

上一篇
Go 服务:从接口契约到分层实现
下一篇
Go 网络(一):goroutine-per-connection 模型与生产实践