本书未发布
Go - 记录一次性能分析的全过程
前因#
刚到这个公司,因为有很多热点账户之间的转账,所以需要处理这个问题。
基本解决思路:DB 的批量处理,减少锁的等待时长。
方案#
第一个方案#
将 reqs 分配到不同的 group,group 的 reqs 满了之后传给 executor 的 worker,worker 批量处理这些请求。示意图如下:
因为是用的 Golang 写的,所以这里都是异步处理的 req。
- 用户的请求会经过 grouper 分发给 group,group 之间如果有重复的 id 会 merge 到一个 group。减少锁冲突。
- group 的 reqs 会传给 executor
- executor 的 reqs 会传给 worker
- worker 通过事务批量处理 reqs
第二步 group 触发的时机有两个:
- reqs 的长度达到阈值
- ticker 的时间已经到了
事务处理部分:
func (w *worker) do(reqs []Request) {
var err error
response := func(req Request,ret Response, ch RespCh) {
if err != nil {
DB操作
ch <- ret
} else {
DB操作
ch <- errorResp(...)
}
}
Transaction(func(tx *gorm.DB) error) {
for k,v := range reqs {
1. Select .. Where account_id = ? For Update
2. DB 操作
3. defer response(...)
}
}
}
缺点:
- Ticker 不受控制,group 的 reqs 在不满的请求下也会发送给 executor 处理。
- reqs 的长度过长锁时长难以避免
- 参数调优
- ticker 时长影响 response
第二个方案#
改变触发时机:如果 grouper.reqs 为空,则触发其他 group 的 reqs 提交给 executor。这样就可以省去 ticker 了。
伪代码:
var isEmpty = true
for {
if len(grouper.reqs) == 0 || isEmpty {
req <- grouper.reqs
isEmpty = false
execGroup,poped := handleReq(req)
if poped {
executor.reqs <- execGroup.reqs
execGroup.Reset()
// 为了防止有些group饿死,增加补偿机制
// 找到reqs最少的group,给他的len+1
}
}
for _,v := range grouper.Group {
if len(v.reqs) > 0 {
executor.reqs <- v.reqs
v.Reset()
}
}
isEmpty = true
}
优势:
- 避免了 ticker 不受控制的因素
- group 的 reqs 空间充分利用
- 单条请求响应更快
- 调优参数减少了
缺点:
- reqs 的长度过长锁时长难以避免
性能测试#
性能测试包括两部分:
- 模拟 100 个用户随机转账
- 点对点的密集转账
测试过程中,出现过几个现象:
- 优化后的响应时间改善,但是 QPS 没有显著提升
- 事务锁出现死锁(select .. where id in (?) for update),虽然 id 已经正序,但是 in 条件不保证顺序。(第一个失误)改成 for 循环一个个的加锁。
QPS 没有显示提升:
- 事务内的 DB 操作批量处理,10 个请求,每个请求都要 insert 2 个 bill 记录,那么就要处理 20 次。每个 SQL 处理大约在 1.5-2ms, 最大也就节省大概 40ms-6ms=34ms。第二个失误
- defer response 内的 DB 操作,同样 10 个请求,处理 10 次 insert,大概需要 1.5+ms。每个
respCh <- response
也会多等 15ms。最大也能节省大概 15-3+ms=12ms。第三个失误
改完这 3 个失误之后,从原来的 QPS=80,飙到了 QPS=240~300,提升了大概 3 + 倍。奇怪的还有:没处理第二个失误之前,QPS 并没有变化,锁等待时间也没变化。但是处理完第二个失误之后,锁等待时间从原来的 120ms 减到了 40ms。提升了 3 倍。
分析原因#
原来的锁等待时长 120+ms 的原因 (10 个 req):
- 10 * 2(bill) * 2 = 40ms
- 10 * 1.2 = 12ms
优化后:
- insert 大 bill SQL:8ms
- insert 大 txInfo SQL:2ms
思考:
- 是因为减少了 response 的响应时间吗?
- IO 密集型优化优先考虑减少 IO 处理时长,如 MySQL 的处理时长。
- 忽略了大 SQL 处理带来的性能优化
- 加锁时不能用 in 条件。
推荐文章: