最近在開發一個打卡介面,其實只需要做些判斷,儲存一下打卡結果即可,預計同時段1000多人線上打卡,但是第一次寫完之後,壓測效果非常糟糕,可以看到只有十幾的併發,喝下的水都要噴出來了,那麼簡單的介面都能耗時那麼久的嗎,我預估100ms以內準可以的,那還有上百的併發才對。於是開始了我的最佳化之路。
看看主要程式碼,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,正常合理,處理不過的請求會排隊沒辦法,除非加大執行緒數的配置:
突然想起來,專案啟動不是用的tomcat,而是更換成了undertow,這傢伙有問題嗎?但是求助搜尋引擎,說它要比tomcat更彪悍,至少預設是有8000併發的,看來也排除了。
那會不會是JVM的堆疊設定的太小,處理卡頓,看了下gc數是有點多。其他專案複製過來的啟動配置,才512M我去,馬上改成4G試試。然而,實際效果並無變化。其實gc雖然很多,但實際耗費的時間加起來都沒3s,可以排除是它的問題。(這裡還遇到了一個問題,就是即使改大了之後,專案一次啟動還是會有3次full-gc和幾次的young-gc,一下子搞不懂為什麼會這樣,有經驗的朋友可以給些排查方向)
為此,我判斷大機率是介面耗時太久導致併發起不來。
程式碼除了一些判斷語句,物件轉換,查詢插入更新資料庫,就沒有其他操作了。物件轉換有問題嗎,但是通篇都有物件轉換,這都有問題可能嗎。估計真的是資料庫有問題了。為了提高查詢速度,我只加了一個複合索引,這都那麼慢是很不符合常理的。
那我就用skywalking探測一下!
這。。真的是介面耗時符非常久,雖然壓測情況下,分配資源出來等了70ms,但介面執行的時候卻花費了308ms,在skywalking怎麼只能看到查詢語句的耗時,可是查詢語句只花了幾毫秒而已。
那我加下p6spy看下,每條語句的執行耗時吧。
吃驚,嚇人,這兩插入更新語句居然需要兩百毫秒。
咋回事,我想想,想不通,求助搜尋引擎和大模型了:
留意到第2點,沒有想到,我本來認為介面可以忍受不加事務帶來資料不一致性,以提高併發。既然這麼說了,那就加個事務試試。不加不知道,一加嚇一跳,加了基本0耗時了。
@Transactional(rollbackFor = Exception.class) @Override public RelationActivityRecord clock(String userId, ClockDto clockDto) { .... }
再搜尋一下這是為什麼,有人說是加了事務之後,不會像之前一樣插入一次寫一次盤,然後在事務的干預下,攢夠一批再寫盤,這樣效率就高了。那這裡或者還能最佳化這一批能不能攢多點,也要衡量涉及到寫盤的時候,當時的單個介面請求就會慢了。
然後有了另一個疑問,我事務是不是加得太大了,畢竟是先查詢,再插入和更新資料,可以將它們分開的,那就分開試試,查詢不加事務,插入更新提出一個方法加事務,但是,奇了怪了,結果併發效果更差了。看來這裡面還有事。 同時發現這裡一開始壓測都壓同一個使用者,按理說真實情況是不同使用者的,同一個使用者可能競爭資源更加劇烈,所以對獲取使用者userId改了一下隨機生成一個。
這個時候感覺到8.x版本的skywalking實在是很難提供到更全面的消耗資訊,想到不久前看到skywalking9.x版本來了,那就下載一個試試吧。這裡我一開始下載了最新的版本,結果死活啟動不了一直閃退,什麼埠占用啊都改了也不行,大坑。然後直接選9.0版本,就正常啟動了。果然9.0版本就不一樣了,介面佈局舒服了,監控資料也活了,很贊。
而且,trace鏈路的耗時釋出總算是全面了很多!!資料庫連線都出來了。可以看到,看了下沒有加事務之前,每次查詢,寫入,都要單獨獲取一次資料庫連結!!!插入語句監控也能正常出來了!
查詢不加事務,查詢和更新用了事務,得獲取三次資料庫連線,第一次連線是為了查selelct 1判斷資料庫是否正常,可以去掉。
查詢和插入更新在同一個大事務,只獲取一次資料庫連線,效率自然更高了。
這裡基本也確定了,放一個事務裡,效率要高,減少獲取資料庫連線。
此外,透過檢視 skywalking連結追蹤,發現是getConnection的時候特別耗時,獲取一個連線要好幾秒。
目前連線池用的是druid,引數是預設配置,查了下預設配置的最大連線數並不高,那問題估計就是這裡了,加大試試。目前pg資料庫設定是250個連結,如果是生產環境,資料庫獨享,yml和pg配置的最大連線數還能大些,目前先試試200,如果併發能提高說明這裡也是能最佳化的點,當然實際配置還得看情況,不然會把資料庫拉胯。
設定一秒200併發,持續30秒,效果還不錯,平均700多ms一個請求,最後併發到250左右。
這裡的undertow和資料庫連線配置還需要逐步按實際情況不斷調整,以達到單機最佳效果。
總結一下:
-
併發低,有配置的問題,也有程式碼的問題。
-
事務的新增需要留意一下新增之後的效果,有時候並非是自己想的那樣,一開始覺得事務範圍大不合適,結果它效果比不加事務還要好。
-
一個好工具太重要了,例如skywalking9.0。
-
多點分析。
-
好好總結這次經驗,基本單機併發就這些問題比較多了。