本书未发布

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
发起讨论 只看当前版本


暂无话题~