本书未发布

Go - 记录一次性能分析的全过程

未匹配的标注

前因#

刚到这个公司,因为有很多热点账户之间的转账,所以需要处理这个问题。
基本解决思路:DB 的批量处理,减少锁的等待时长。

方案#

第一个方案#

将 reqs 分配到不同的 group,group 的 reqs 满了之后传给 executor 的 worker,worker 批量处理这些请求。示意图如下:

Go - 记录一次性能分析的全过程

因为是用的 Golang 写的,所以这里都是异步处理的 req。

  1. 用户的请求会经过 grouper 分发给 group,group 之间如果有重复的 id 会 merge 到一个 group。减少锁冲突。
  2. group 的 reqs 会传给 executor
  3. executor 的 reqs 会传给 worker
  4. worker 通过事务批量处理 reqs

第二步 group 触发的时机有两个:

  1. reqs 的长度达到阈值
  2. 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(...)
        }
    }
}

缺点:

  1. Ticker 不受控制,group 的 reqs 在不满的请求下也会发送给 executor 处理。
  2. reqs 的长度过长锁时长难以避免
  3. 参数调优
  4. 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
}

优势:

  1. 避免了 ticker 不受控制的因素
  2. group 的 reqs 空间充分利用
  3. 单条请求响应更快
  4. 调优参数减少了

缺点:

  1. reqs 的长度过长锁时长难以避免

性能测试#

性能测试包括两部分:

  1. 模拟 100 个用户随机转账
  2. 点对点的密集转账

测试过程中,出现过几个现象:

  1. 优化后的响应时间改善,但是 QPS 没有显著提升
  2. 事务锁出现死锁(select .. where id in (?) for update),虽然 id 已经正序,但是 in 条件不保证顺序。(第一个失误)改成 for 循环一个个的加锁。

QPS 没有显示提升:

  1. 事务内的 DB 操作批量处理,10 个请求,每个请求都要 insert 2 个 bill 记录,那么就要处理 20 次。每个 SQL 处理大约在 1.5-2ms, 最大也就节省大概 40ms-6ms=34ms。第二个失误
  2. 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):

  1. 10 * 2(bill) * 2 = 40ms
  2. 10 * 1.2 = 12ms

优化后:

  1. insert 大 bill SQL:8ms
  2. insert 大 txInfo SQL:2ms

思考:

  1. 是因为减少了 response 的响应时间吗?
  2. IO 密集型优化优先考虑减少 IO 处理时长,如 MySQL 的处理时长。
  3. 忽略了大 SQL 处理带来的性能优化
  4. 加锁时不能用 in 条件。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
讨论数量: 0
发起讨论 查看所有版本


暂无话题~