最近在开发一个打卡接口,其实只需要做些判断,保存一下打卡结果即可,预计同时段1000多人在线打卡,但是第一次写完之后,压测效果非常糟糕,可以看到只有十几的并发,喝下的水都要喷出来了,那么简单的接口都能耗时那么久的吗,我预估100ms以内准可以的,那还有上百的并发才对。于是开始了我的优化之路。呕血回顾一次提高接口并发的经历,很实用-LMLPHP

看看主要代码,controller接收参数进来之后,执行一下服务方法就返回了,就是查询活动,判断活动时间,累计积分,保存结果,并发这么差我实属是想不到,让人摇头摇头郁闷,我应该没问题啊,部署的机器有问题?但是也都是还可以的服务器的。

  @Override
  public RelationActivityRecord clock(String userId, ClockDto clockDto) {
    // 判断活动是否有效
    RelationActivity relationActivity =
        relationActivityService.getRelationActivity(clockDto.getActivityId(),
                clockDto.getActivityType());
    Date now = new Date();
    if (now.before(relationActivity.getStartTime())) {
      throw new BadRequestException(ErrorEnum.WRONG_ARGUMENTS, "活动还未开始,请耐心等待~");
    }
    if (now.after(relationActivity.getEndTime())) {
      throw new BadRequestException(ErrorEnum.WRONG_ARGUMENTS, "活动已结束,可在主页查看个人作品哦~");
    }
    // 本次打卡获得积分
    int score= 0;
    // 当天的0点时间
    Date toDay = DateUtil.beginOfDay(now);
    if (Objects.equals(clockDto.getActivityType(), 1)
        && Objects.equals(clockDto.getClockType(), 1)) {
      // 查询累计打卡次数
      CurrentClockRecordVo clockRecordStat = this.currentRecord(userId,
              clockDto.getActivityId(), clockDto.getActivityType(), 
              );
      // 当天第一次的打卡,可以攒一个积分
      if (clockRecordStat.getLastTime() != null && clockRecordStat.getLastTime().before(toDay)
              && clockRecordStat.getNum() < 100) {
        score = 1;
      }
    } else if (Objects.equals(clockDto.getActivityType(), 1)) {
      // 其他的活动之类...
      throw new BadRequestException(ErrorEnum.WRONG_ARGUMENTS, "活动暂未开放");
    } else {
      throw new BadRequestException(ErrorEnum.WRONG_ARGUMENTS, "未知活动");
    }
    // 保存打卡记录
    RelationActivityRecord newClockRecord = new RelationActivityRecord();
    newClockRecord.setStudentId(userId);
    newClockRecord.setActivityId(clockDto.getActivityId());
    newClockRecord.setActivityType(clockDto.getActivityType());
    newClockRecord.setClockType(clockDto.getClockType());
    newClockRecord.setCreateTime(now);
    newClockRecord.setContent(clockDto.getContent());
    newClockRecord.setEnergy(energy);
    newClockRecord.setVideoId(clockDto.getVideoId());
    newClockRecord.setIntro(clockDto.getIntro());
    this.save(newClockRecord);
    // 有积分可以攒到学生账户上
    if (score> 0) {
      activityStudentService.addScore(clockDto.getActivityId(), userId, score);
    }
    return newClockRecord;
  }

冷静冷静再次喝口水,遇到问题先找找外部原因,是不是springboot默认的tomcat最大连接数之类的默认值太小,项目里是没有重新配置的,但是一想前些天看过一个视频说,默认的都是8192最大连接数,那不改配置也是嘎嘎够用的。谨慎起见,写一个随便执行几条语句,不连接数据库的接口压测一下,一开始有1000多并发,然后积攒的排队请求多了之后,并发逐渐下降到600,正常合理,处理不过的请求会排队没办法,除非加大线程数的配置:

呕血回顾一次提高接口并发的经历,很实用-LMLPHP

突然想起来,项目启动不是用的tomcat,而是更换成了undertow,这家伙有问题吗?但是求助搜索引擎,说它要比tomcat更彪悍,至少默认是有8000并发的,看来也排除了。

那会不会是JVM的堆栈设置的太小,处理卡顿,看了下gc数是有点多。其他项目拷贝过来的启动配置,才512M我去,马上改成4G试试。然而,实际效果并无变化。其实gc虽然很多,但实际耗费的时间加起来都没3s,可以排除是它的问题。(这里还遇到了一个问题,就是即使改大了之后,项目一次启动还是会有3次full-gc和几次的young-gc,一下子搞不懂为什么会这样,有经验的朋友可以给些排查方向)

为此,我判断大概率是接口耗时太久导致并发起不来。

代码除了一些判断语句,对象转换,查询插入更新数据库,就没有其他操作了。对象转换有问题吗,但是通篇都有对象转换,这都有问题可能吗。估计真的是数据库有问题了。为了提高查询速度,我只加了一个复合索引,这都那么慢是很不符合常理的。

那我就用skywalking探测一下!

呕血回顾一次提高接口并发的经历,很实用-LMLPHP

这。。真的是接口耗时符非常久,虽然压测情况下,分配资源出来等了70ms,但接口执行的时候却花费了308ms,在skywalking怎么只能看到查询语句的耗时,可是查询语句只花了几毫秒而已。

那我加下p6spy看下,每条语句的执行耗时吧。

呕血回顾一次提高接口并发的经历,很实用-LMLPHP

呕血回顾一次提高接口并发的经历,很实用-LMLPHP

吃惊,吓人,这两插入更新语句居然需要两百毫秒。

咋回事,我想想,想不通,求助搜索引擎和大模型了:

呕血回顾一次提高接口并发的经历,很实用-LMLPHP

留意到第2点,没有想到,我本来认为接口可以忍受不加事务带来数据不一致性,以提高并发。既然这么说了,那就加个事务试试。不加不知道,一加吓一跳,加了基本0耗时了。

  @Transactional(rollbackFor = Exception.class)
  @Override
  public RelationActivityRecord clock(String userId, ClockDto clockDto) {
   ....
  }

呕血回顾一次提高接口并发的经历,很实用-LMLPHP

再搜索一下这是为什么,有人说是加了事务之后,不会像之前一样插入一次写一次盘,然后在事务的干预下,攒够一批再写盘,这样效率就高了。那这里或者还能优化这一批能不能攒多点,也要衡量涉及到写盘的时候,当时的单个接口请求就会慢了。

然后有了另一个疑问,我事务是不是加得太大了,毕竟是先查询,再插入和更新数据,可以将它们分开的,那就分开试试,查询不加事务,插入更新提出一个方法加事务,但是,奇了怪了,结果并发效果更差了。看来这里面还有事。 同时发现这里一开始压测都压同一个用户,按理说真实情况是不同用户的,同一个用户可能竞争资源更加剧烈,所以对获取用户userId改了一下随机生成一个。

呕血回顾一次提高接口并发的经历,很实用-LMLPHP

呕血回顾一次提高接口并发的经历,很实用-LMLPHP

这个时候感觉到8.x版本的skywalking实在是很难提供到更全面的消耗信息,想到不久前看到skywalking9.x版本来了,那就下载一个试试吧。这里我一开始下载了最新的版本,结果死活启动不了一直闪退,什么端口占用啊都改了也不行,大坑。然后直接选9.0版本,就正常启动了。果然9.0版本就不一样了,界面布局舒服了,监控数据也活了,很赞。

呕血回顾一次提高接口并发的经历,很实用-LMLPHP

而且,trace链路的耗时发布总算是全面了很多!!数据库连接都出来了。可以看到,看了下没有加事务之前,每次查询,写入,都要单独获取一次数据库链接!!!插入语句监控也能正常出来了!

呕血回顾一次提高接口并发的经历,很实用-LMLPHP

查询不加事务,查询和更新用了事务,得获取三次数据库连接,第一次连接是为了查selelct 1判断数据库是否正常,可以去掉。

呕血回顾一次提高接口并发的经历,很实用-LMLPHP

查询和插入更新在同一个大事务,只获取一次数据库连接,效率自然更高了。

呕血回顾一次提高接口并发的经历,很实用-LMLPHP

这里基本也确定了,放一个事务里,效率要高,减少获取数据库连接。

此外,通过查看 skywalking链接追踪,发现是getConnection的时候特别耗时,获取一个连接要好几秒。

呕血回顾一次提高接口并发的经历,很实用-LMLPHP

目前连接池用的是druid,参数是默认配置,查了下默认配置的最大连接数并不高,那问题估计就是这里了,加大试试。目前pg数据库设置是250个链接,如果是生产环境,数据库独享,yml和pg配置的最大连接数还能大些,目前先试试200,如果并发能提高说明这里也是能优化的点,当然实际配置还得看情况,不然会把数据库拉胯。

呕血回顾一次提高接口并发的经历,很实用-LMLPHP

设置一秒200并发,持续30秒,效果还不错,平均700多ms一个请求,最后并发到250左右。呕血回顾一次提高接口并发的经历,很实用-LMLPHP

这里的undertow和数据库连接配置还需要逐步按实际情况不断调整,以达到单机最佳效果。

总结一下:

  1. 并发低,有配置的问题,也有代码的问题。

  2. 事务的添加需要留意一下添加之后的效果,有时候并非是自己想的那样,一开始觉得事务范围大不合适,结果它效果比不加事务还要好。

  3. 一个好工具太重要了,例如skywalking9.0。

  4. 多点分析。

  5. 好好总结这次经验,基本单机并发就这些问题比较多了。

09-26 11:15