项目技术背景 :
旧系统 : php(Yii) + mysql + redis。
重构后新系统 : go( gin + gorm + redigo ) + mysql + redis
上线遇到的问题:
以首页为例, 没使用缓存的情况下 :
[GIN] 2021/03/26 - 10:12:09 | 200 | 22.3642791s | 192.168.8.216 | GET "/xxx"
使用缓存的情况下 :
[GIN] 2021/03/26 - 10:12:59 | 200 | 1.8741071s | 192.168.8.216 | GET "/xxx"
发现的问题 :
- 在使用 redis 缓存的时候, 每次调用 conn := RedisConn.Get() 都需要花费大概 169.0097ms 去获取连接,而首页要获取 11次连接。。
- 查询数据库的时候, 基本上都是命中了索引, 但是一个接口查询数据库的次数高达19次。
- 同一条sql语句,gorm中开启debug 用时392.02ms,在navicate用时0.075s,关闭debug用时88.0051ms
- 在命中缓存的情况下, 获取缓存用时689.0394ms(数据量太大,154.82KB) 【这个主要是网络传输数据】
为什么之前的旧系统没这个问题?
1、旧的系统做得主从, 新的系统做的双主,
2、旧的系统表没有拆分的这么细(主要数据集中在两三个表里面), 新的系统表拆分得太细了(六七个主表), 所以旧的系统查询数据库的次数没有新的系统那么多。
友盟数据显示昨日最多同时在线人数 1080 人, 昨日活跃用户 484005 人。
迁移之后发现原来的其他模块查询数据库变得非常慢(查询的是同一个库中不同的表)
用 explain 分析 sql 语句, 发现基本上都命中了索引, 一些包含 in 的数据 类型是 range , 其他基本都是 ref 、 eq_ref 。
以前主表就有上千万数据,联表查就得生成临时表, 内存得挂,然后就单个单个表去查,再在逻辑层去拼接。 结果就导致一个接口查询19次数据库。
通过对 gorm 的debug 日志分析发现 :
最初的接口查询了19次 数据库, 总计用时大概 5700ms。
但是接口总用时 10.6936116s。 通过sql的执行时间分析, 发现多出来的时间都是在执行 redis 读写。
进行的优化 :
- 将首页一些版块中列表页的数据细化(只获取图标、id、应用名等必要的字段,其他信息等点进详情页的时候再去获取, 这样用户在点击进入详情的时候没那么快可以得到详情页的一些数据了,但是可能可以减少不必要的查询等。)
- 适当的做联表查询(数据库里面的数据删除了不少)
- 某些表需要做拆分(比如用户收藏表等)
- 接口里有一些旧的数据现在没有使用了,但是之前没有把这部分数据砍掉,导致现在查询的时候还是会去查询这部分数据。
修改后的接口查询 12 次数据库(使用了联表查询), 且都命中了索引, 查询总用时 400ms , 接口用时 23.9453696s。

分析日志发现两条查询语句之间间隔 11 秒, 这 11 秒都是在循环中设置 redis 缓存。
其他地方也有类似情况。
进行的优化 :
- 将查询中的 select * 改成 select xxx, xxx (具体需要的字段)。
- 写redis 缓存的时候采用异步(另外开一个协程)
- 接口在获取各模块数据的时候采用并发(每个板块开一个协程去获取数据, 最终组装数据)
例如 :
func setName(result *NameResult, chLock chan<- struct{}) {
defer func() {
chLock <- struct{}{}
}()
time.Sleep(1 * time.Second)
result.Name = "xiaoming"
}
func setAge(result *AgeResult, chLock chan<- struct{}) {
defer func() {
chLock <- struct{}{}
}()
time.Sleep(2 * time.Second)
result.Age = 18
}
func setSex(result *SexResult, chLock chan<- struct{}) {
defer func() {
chLock <- struct{}{}
}()
time.Sleep(3 * time.Second)
result.Sex = true
}
type NameResult struct {
Name string
}
type AgeResult struct {
Age int
}
type SexResult struct {
Sex bool
}
type UserResult struct {
NameResult
AgeResult
SexResult
}
func getUserInfo() {
result := UserResult{}
const num = 3
chLock := make(chan struct{}, num)
go setName(&result.NameResult, chLock)
go setAge(&result.AgeResult, chLock)
go setSex(&result.SexResult, chLock)
for i := 0; i < num; i++ {
<-chLock
}
fmt.Println(result)
}
func main() {
for i := 0; i < 100; i++ {
go getUserInfo()
}
time.Sleep(100 * time.Second)
}
优化完成之后上线的时候发现还是不行, 查看日志发现执行了大量sql语句, 数据库8核cpu跑满了, 数据库16个G用了10个G。
正常情况下查询速度 [1.29ms]的一个 sql 语句现在1到3秒才能返回。
日志中出现大量错误 : redigo: connection pool exhausted 。
MaxIdle = 30
MaxActive = 30
IdleTimeout = 200
CacheTimeOut = 300
最后找到的原因是获取redis连接的时候使用了异步(如果没有可用连接直接报错)。导致在并发读取缓存的时候, 一部分请求获取redis连接失败之后去直接读数据库了。
解决办法 :
1、增加redis连接池大小。
2、设置成阻塞获取redis连接。
另外在上线的时候发现内存溢出错误导致宕机, 最后原因是接口需要解析云端图片的尺寸, 每次都把图片加载到内存中导致内存溢出。
最终解决办法是修改数据库将图片尺寸存起来。
还有一些内存方面的优化比如函数传参的时候结构体类型尽量使用指针进行传参。
MaxIdl
MaxActive = 30
IdleTimeout = 200
CacheTimeOut = 300

本文探讨了从PHP到Go的系统重构中遇到的性能问题,重点聚焦于Redis缓存连接耗时、数据库查询频繁及解决方案,包括连接池优化、异步操作和数据结构调整等。
915

被折叠的 条评论
为什么被折叠?



