Go 应用性能问题排查报告:从通用诊断到 GC 专项优化 一、总体排查思路 Go 应用的性能问题大致可以分为五类,排查前先做归类,能避免"一上来就调 GC 参数"的误区: 类型 典型症状 常见根因 CPU 密集 CPU 使用率高、吞吐下降 算法效率低、序列化/反序列化开销、正则表达式、加密计算 内存/GC 相关 延迟毛刺(latency spike)、CPU 中 GC 占比高、内存持续增长 高分配速率、堆过大、对象逃逸多 并发/调度问题 goroutine 数暴涨、P 争抢、上下文切换多 goroutine 泄漏、锁竞争、channel 阻塞 I/O /网络问题 请求延迟高但 CPU 不高 数据库慢查询、下游服务慢、连接池不足 系统资源问题 容器被 OOMKill、CPU throttling cgroup limit 与 GOMAXPROCS/GOMEMLIMIT 不匹配 诊断的第一步永远是 用数据说话 ,而不是凭经验猜测。Go 官方工具链在这方面非常完善,建议按下面的顺序排查。 二、核心诊断工具 1. net/http/pprof :性能剖析的入口 在服务中引入: import _ "net/http/pprof" go func () { log.Println(http.ListenAndServe( "localhost:6060" , nil )) }() 然后可以采集: # CPU profile(默认采样 30 秒) go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 # 堆内存 profile(查看当前存活对象的分配来源) go tool pprof http://localhost:6060/debug/pprof/heap # goroutine 数量与堆栈(排查泄漏) go tool pprof http://localhost:6060/debug/pprof/goroutine # 锁竞争 go tool pprof http://localhost:6060/debug/pp...
Redis 五种基本数据类型的底层实现 Redis 对外暴露的是 String、Hash、List、Set、Sorted Set 五种类型,但内部每种类型往往有 两种(甚至多种)底层编码 ,根据数据量大小自动切换。核心设计哲学是: 小数据用紧凑、连续内存的结构省内存;数据量大了再切换成哈希表/跳表这类效率更高但更耗内存的结构 。 1. String —— SDS(Simple Dynamic String) Redis 没有直接用 C 语言原生字符串,而是自己实现了 SDS。 结构(以 sdshdr8 为例): len : 已使用长度 alloc : 总分配空间 flags : 类型标记 buf[] : 实际字节数组(以 \ 0 结尾,但不依赖它判断长度) 根据字符串长度不同,还细分为 sdshdr5/8/16/32/64 ,用最小的头部类型存放,进一步省内存。 为什么这样设计: - C 字符串要遍历到 \0 才能拿到长度,SDS 直接读 len 字段, O(1) 。 - C 字符串不是二进制安全的(遇到 \0 就截断),SDS 靠 len 判断边界,可以存图片、序列化数据等任意二进制内容。 - 拼接前会检查剩余空间,杜绝了缓冲区溢出。 - 空间预分配 :字符串变长时,若新长度 < 1MB,分配双倍空间;若 ≥ 1MB,每次只多分配 1MB,避免频繁 realloc 。 - 惰性空间释放 :字符串变短时不立即释放内存,留着以后用。 优缺点: 换来了安全性、O(1) 长度获取、减少内存重分配次数,代价是比原生 C 字符串多占一点元数据空间。 2. Hash —— listpack / hashtable 小对象 :用 listpack (Redis 7.0 之前叫 ziplist ),本质是一段连续内存,紧凑存放 key1 value1 key2 value2... ,没有指针开销。 大对象 :转为 hashtable (Redis 自己实现的 dict ),本质是数组 + 链地址法解决哈希冲突,支持 渐进式 rehash 。 触发转换的阈值(可配置): - hash-max-listpack-entries (默认 128) - hash-max-listpack-value (默认 64 字节) 任一超限...