# Go程序性能优化

## Go 应用性能优化

1. 减少变量逃逸，尽量使用局部变量，在栈上分配对象
2. 切片等结构提前分配足够容量
3. 减少对象分配数量，复用对象；或使用 sync.Pool
4. 减少协程数量，复用协程处理多个任务
5. 减小锁粒度，减少锁竞争
6. pprof 分析程序性能瓶颈

### 寻找性能瓶颈

在压测时，我们通过以下步骤来逐渐提升接口的整体性能：

1. 使用固定 QPS 压测，以阶梯形式逐渐增加压测 QPS，如 1000 -> 每分钟增加 1000 QPS
2. 压测过程中观察系统的延迟是否异常
3. 观察系统的 CPU 使用情况
4. 如果 CPU 使用率在达到一定值之后不再上升，反而引起了延迟的剧烈波动，这时大概率是发生了阻塞，进入 pprof 的 web 页面，点击 goroutine，查看 top 的 goroutine 数，这时应该有大量的 goroutine 阻塞在某处，比如 Semacquire
5. 如果 CPU 上升较快，未达到预期吞吐就已经过了高水位，则可以重点考察 CPU 使用是否合理，在 CPU 高水位进行 profile 采样，重点关注火焰图中较宽的“平顶山”

重复上述步骤，直至系统性能达到或超越我们设置的性能目标。

## 一些优化案例

#### gc mark 占用过多 CPU

在 Go 语言中 gc mark 占用的 CPU 主要和运行时的对象数相关，也就是我们需要看 inuse\_objects。

定时任务，或访问流量不规律的应用，需要关注 alloc\_objects。

优化主要是下面几方面：

**1. 减少变量逃逸**

尽量在栈上分配对象

**2. 使用 sync.Pool 复用堆上对象**

`sync.Pool` 用出花儿的就是 `fasthttp` 了，可以看看我之前写的这一篇：`fasthttp` 为什么快。

最简单的复用就是复用各种 `struct, slice`，在复用时 `put` 时，需要判断 `size` 是否已经扩容过头，小心因为 `sync.Pool` 中存了大量的巨型对象导致进程占用了大量内存。

#### 调度占用过多 CPU

`goroutine` 频繁创建与销毁会给调度造成较大的负担，如果我们发现 CPU 火焰图中 `schedule，findrunnable` 占用了大量 CPU，那么可以考虑使用开源的 `workerpool` 来进行改进，比较典型的有 `fasthttp worker pool`。

如果客户端与服务端之间使用的是短连接，那么我们可以使用长连接来减少连接创建的开销，这里就包含了 `goroutine` 的创建与销毁。

#### 锁冲突严重，导致吞吐量瓶颈

进行锁优化的思路无非就一个 **拆** 和一个 **缩** 字：

* 拆：将锁粒度进行拆分，比如全局锁，我能不能把锁粒度拆分为连接粒度的锁；如果是连接粒度的锁，那我能不能拆分为请求粒度的锁；在 logger fd 或 net fd 上加的锁不太好拆，那么我们增加一些客户端，比如从 1-> 100，降低锁的冲突是不是就可以了。
* 缩：缩小锁的临界区，业务允许的前提下，可以把 syscall 移到锁外面；有时只是想要锁 map 的读写逻辑，但是却不小心锁了连接读写的逻辑，或许简单地用 sync.Map 来代替 map Lock，defer Unlock 就能简单地缩小临界区了。

#### timer 相关函数占用大量 CPU

同样是在网关和海量连接的应用中较常见，优化手段：

* 使用时间轮/粗粒度的时间管理，精确到 ms 级一般就足够了
* 升级到 Go 1.14+，享受官方的升级红利

### 模拟真实工作负载

在前面的论述中，我们对问题进行了简化。真实世界中的后端系统往往不只一个接口，压测工具、平台往往只支持单接口压测。

公司的业务希望知道的是后端系统整体性能，即这些系统作为一个整体，在限定的资源条件下，能够承载多少业务量(如并发创建订单)而不崩溃。

虽然大家都在讲微服务，但单一服务往往也不只有单一功能，如果一个系统有 10 个接口(已经算是很小的服务了)，那么这个服务的真实负载是很难靠人肉去模拟的。

这也就是为什么互联网公司普遍都需要做全链路压测。像样点的公司会定期进行全链路压测演练，以便知晓随着系统快速迭代变化，系统整体是否出现了严重的性能衰退。

通过真实的工作负载，我们才能发现真实的线上性能问题。讲全链路压测的文章也很多，本文就不再赘述了。

**参考**

[曹春晖 - Go 应用性能优化指北](https://mp.weixin.qq.com/s/45L1lIgZxXjyld23D9hZzw)
