騰訊業務百萬數據 6s 響應,APIJSON 性能優化背後的故事

最近發生了一件大事兒,APIJSON 再也不用擔心被人質疑性能問題了哈哈!

 

某週三騰訊 CSIG 某項目組(已經用 APIJSON 做完一期)突然反饋了查詢大量數據性能急劇下降的情況:

某張表 2.3KW 記錄,用 APIJSON 萬能通用接口 /get 查數據 LIMIT 100 要 10s,LIMIT 1000 要 98s!

可把我嚇了一跳,這麼慢還怎麼用。。。想了會先讓他們 Log.DEBUG = false 關掉日誌看看:

還同樣那張表,還是同樣的同域 CURL 請求 /get,LIMIT 100 降爲 2s,LIMIT 1000 降爲 30s。

看起來勉強夠用,一般都是分頁查 10 條 20 條的樣子,但他們的業務“LIMIT 1000 的需求還挺多的”。

 

這可是騰訊內第一個用 APIJSON 的團隊及項目,不僅用了好幾個月,還在內網寫了多篇文章幫忙推廣,

怎麼都不能辜負他們的信任和期望,更不能讓這個事件成爲 APIJSON 性能差的污點從而影響用戶口碑。

 

image

我看着日誌尋思着應該是數組內主表生成了 1000 個 SQLConfig 並調用 getSQL 等過程導致解析耗時過長,

而除了第 0 條,剩下的 999 條記錄都完全沒必要重複這個過程,尤其是期間大量打印日誌非常耗時。

當時是爲了兼容多表關聯查詢,自己的業務又幾乎沒有 LIMIT 100 以上的需求,所以影響不大就忽略了。

現在如果把第 0 條數據的解析結果(ObjectParser, SQLConfig 等類及變量狀態)緩存起來給後面的複用

那不就可以把 2s 壓縮到只比 SQL 執行耗時 133ms 多出少量 APIJSON 解析過程耗時 的時長了?

 

“我有方案了,應該可以把 2s 優化到 200ms 左右” “我這週末優化下” 我拍着腦門給出一個承諾。

這個組的後端同事職業本能地探究細節:“現在的 30s 主要耗時在哪裏?” “怎麼搞?”

“執行 SQL 133ms 耗時正常,整體慢是 日誌(大頭) 和 解析(小頭) 的鍋” “SQLExecutor 應該一次返回整個列表給 Parser”

看起來這個同事得到了些許安慰:“這個可以優化的話,LIMIT 1000 耗時 30s 估計也只要幾秒了”

 

週末除了 APIAuto-機器學習 HTTP 接口工具 的簡單更新:

  • 零代碼迴歸測試:用經驗法解決冷啓動問題,在沒有校驗標準時也能進行斷言
  • 自動生成註釋:新增根對象全局關鍵詞鍵值對的語法提示

剩下重點都是 APIJSON 的性能優化,我分成了兩步:

1.查數組主表時把 SQLExecutor.execute 查到的原始列表全部返回,緩存到新增的 arrayMainCacheMap

https://gitee.com/Tencent/APIJSON/commit/bb4896d208e813a3f5eec21f60e1c083621b1f92

 

2.查數組主表時把第 0 個 ObjectParser 實例緩存到新增的 arrayObjectParserCacheMap

https://gitee.com/Tencent/APIJSON/commit/a406242a81f2b303a1c55e6a4f5c3c835e62e53a

 

期間嘗試修改返回類型 JSONObject 爲 List<JSONObject> 發現改動範圍過大,而且只有這個場景需要,

於是就改爲在 JSONObject 中加個肯定不會是表字段的特殊字段 @RAW@LIST 來帶上 ResultSet 對應的 List;

改完後性能確實大幅提升,但發現除了第 0 條,後面的數組項全都丟了副表記錄,於是反覆調試修改多次終於解決;

接着按第 2 步優化,性能再次大幅提升,手動測試也沒問題,用 APIAuto 一跑回歸測試發現計數字段 total 沒返回,

這次運氣好點,因爲之前也碰到過同樣問題,我解決後加了比較詳細的註釋說明,所以回顧審視後沒多久就解決了。

 

每步完成後,我都對 TestRecord[], 朋友圈多層嵌套列表 等查詢 LIMIT 100 用優化前後代碼各跑 10 次以上,

最終無論是 都取最小值,還是 去掉幾個最小和最大值後其餘取平均值,得出的前後性能對比結論基本一致:

步驟 1 將單表列表耗時降低爲原來 42%,朋友圈多層嵌套列表降爲原來的 82%;

步驟 2 將單表列表耗時降低爲原來 60%,朋友圈多層嵌套列表降爲原來的 77%;

算出總體上將單表列表耗時降低爲原來 25%,朋友圈多層嵌套列表降爲原來的 63%

 

MacBook Pro 英寸 2015 年初期 13 Intel i5 雙核 2.9 GHz,250G SSD,OSX EI Capitan 10.11.6

Docker Community Edition 18.03.1-ce-mac65 (24312)

MySQL Community Server 5.7.17 - 跑在 Docker 裏,性能會比直接安裝的差不少

Eclipse Java  EE Neon.1a Release (4.6.1) - 全量保留日誌,比用有限保留日誌的 IntelliJ IDEA 2018.2.8 (IU-182.5262.2) 慢一個數量級

實測結果如下:

TestRecord[] 查詢前後對比 耗時降至 35%,性能提升 186% 爲原來 2.9 倍:

朋友圈列表查詢前後對比 耗時降至 34%,性能提升 192% 爲原來 2.9 倍:

 

改爲 [], []/User[], []/[] 組合且每個數組長度 count 都改爲 0(對應 100 條),最多返回 100*(1 + 100 + 100*2) = 30100 條記錄:

注:找不到 5651ms 對應那張圖了,用最接近的圖替代

模擬 to C 應用的這個極端情況下 生成 SQL 數從 1397 減少至 750,耗時降至 83%,性能提升 20% 爲原來 1.2 倍。

 

最多才降低耗時爲原來 25%?那估計他們組原來 2s 只能優化到  500ms(承諾 200ms 左右),30s 只能優化到 7.5s。

天,牛都吹出去了,這下怎麼辦?... 算了算了,都一點了,先睡吧,先發版明天讓他們試試看,唉...

於是我就打着哈欠把 APIJSON 和 APIAuto 都部署到 apijson.cn,簡單自測沒問題,然後到兩點多就洗洗睡了...

(後面又反覆多次自測,雖然結果都和以上接近,但 Release 上用了更保守低調的數值)

 

第二天上午通知騰訊 CSIG 某項目組的同事更新 APIJSON,下午同事在 APIJSON 諮詢羣 大讚“給力!”併發了幾張截屏。

一看 LIMIT 1000000 我虎軀一震:好傢伙,上來就線上生產環境百萬數據暴力測試,年輕人不講武德了麼?

---------- | ---------- | ---------- | ---------- | -------------
   Total   |  Received  | Time Total | Time Spent | Current Speed
   72.5M   |    72.5M   |   0:00:05  |   0:00:05  |     20.0M

/get >> http請求結束:5624

一時沒控記住記幾,在公司連續大喊 “我靠!一百萬五秒!一百萬五秒!查一百萬條記錄耗時五秒!”

差點還喊出洗腦廣告 “OMG,頂它頂它頂它!!!”

把周邊同事嚇了一跳,過了一會有幾個後端同事反應過來紛紛點贊!

 

冷靜下來我仔細分析:

由於他們這次用 CURL 測試接口時,對應的 APIJSON 服務關掉了日誌,不能直接看到服務從接收參數到響應結果的耗時,

所以按以上數據計算是 總耗時 5624ms - 數據 72.5M/下載速度 20.0M每秒 = 1999ms,也就是服務執行過程耗時僅僅 2s!

顯然 APIJSON 這次性能優化效果顯著遠超預期,同時 2.3KW 大表查 100W 記錄僅 1s 左右也反應了 騰訊 TDSQL 的迅猛!

然而後來同事反饋,這次帶條件查出來實際上不到 100W,只有 12W 多數據。

瞬間把我從豐滿的理想拉回了骨感的現實,嗯,革命尚未成功,仍需再接再厲!

經過了幾個版本迭代,4.8.0 終於迎來再一次大幅性能提升,這次終於做到了!

 

APIJSON 4.7.0 和 4.8.0 騰訊 CSIG 某項目性能測試結果

1. 測試環境

1.1 機器配置

騰訊雲tke docker pod, 4 核 / 16G。

1.2 db機器配置

騰訊雲8核32000MB內存,805GB存儲空間

1.3 測試表建表DML、數據量(mysql 5.7)

CREATE TABLE `t_xxxx_xxxx` (
    `x_id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
    `x_xxxx_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT 'xID',
    `x_xid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT 'xxID',
    `x_xx_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT 'xID',
    `x_xxxx_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT 'xxxID',
    `x_xxxxx_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT 'xxID',
    `x_xxxx_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT 'xxID',
    `x_uin` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT 'xxuin',
    `x_send_time` datetime DEFAULT NULL COMMENT '推送消息時間',
    `x_xxxx_result` int(11) unsigned NOT NULL DEFAULT '0' COMMENT 'xx結果',
    `x_xxx_xxxx_result` varchar(255) DEFAULT '' COMMENT 'xx結果',
    `x_result` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '0錯誤 1正確 2未設置',
    `x_create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間, 落地時間',
    `x_credit` int(11) unsigned NOT NULL DEFAULT '0' COMMENT 'xx數量',
    `x_xxxxxx_xxx_id` varchar(32) NOT NULL COMMENT '公共參數, 上報應用',
    `x_xxxxxx_source` varchar(32) NOT NULL COMMENT '公共參數, 上報服務名',
    `x_xxxxxx_server` varchar(32) NOT NULL COMMENT '公共參數, 上報服務端ip',
    `x_xxxxxx_event_time` datetime NOT NULL COMMENT '公共參數, 上報時間',
    `x_xxxxxx_client` varchar(32) NOT NULL COMMENT '公共參數, 客戶端ip',
    `x_xxxxxx_trace_id` varchar(64) NOT NULL COMMENT '公共參數',
    `x_xxxxxx_sdk` varchar(16) NOT NULL COMMENT '公共參數, sdk版本',
    PRIMARY KEY (`x_id`, `x_uin`),
    UNIQUE KEY `udx_uid_xxxxid` (`x_uin`, `x_xxxx_id`),
    KEY `idx_xid` (`x_xid`)
  ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4  COMMENT = 'xx事件表'; 
  • 數據量:18558903

  • mysql版本:5.7

  • 數據分佈:使用group by 統計,基於其中x_xid來group by,得到以下表格:

select x_xid, count(x_id) counter from t_xxxx_xxxx group by x_xid order by counter desc limit 10;

x_xid counter
xxxx36 696376
xxxx38 418576
xxxx63 384503
xxxx40 372080
xxxx41 301364
xxxx08 248243
xxxx46 223820
xxxx07 220234
xxxx44 207721
xxxx02 152795

1.4 日誌打印設置

Log.DEBUG = false;

AbstractParser.IS_PRINT_REQUEST_STRING_LOG = false;
AbstractParser.IS_PRINT_REQUEST_ENDTIME_LOG = false;
AbstractParser.IS_PRINT_BIG_LOG = false;

2. 測試腳本 (使用Table[]: {Table: {}}格式)

腳本統計方式:

  • 基於linux time命令輸出的realtime來統計。

  • 分2個場景測試:一個不帶where條件、一個帶x_xid in (xxxx36,xxxx38)的條件,該條件能匹配出100W+數據,方便覆蓋10W-100W之間的任何數據量場景,這裏事先用select x_xid, count(x_id) c from t_xxxx_xxxx group by x_xid order by c desc;這樣的語句對錶做了統計,x_xid=xxxx36有696376條記錄,x_xid=xxxx38有418576條記錄。

腳本:apitest.sh

#!/bin/bash
printf -- '--------------------------\n開始不帶where條件的情況測試\n'
time curl  -X POST -H 'Content-Type:application/json' 'http://x.xxx.xx.xxx:xxxx/get' -d '{"T_xxxx_xxxx[]":{"count":100000, "T_xxxx_xxxx":{"@column":"x_uin,x_send_time,x_xxxx_id,x_xid,x_xx_id,x_xxxxx_id,x_xxxx_result,x_result,x_credit"}}}' > 10w_no_where.log

time curl  -X POST -H 'Content-Type:application/json' 'http://x.xxx.xx.xxx:xxxx/get' -d '{"T_xxxx_xxxx[]":{"count":200000, "T_xxxx_xxxx":{"@column":"x_uin,x_send_time,x_xxxx_id,x_xid,x_xx_id,x_xxxxx_id,x_xxxx_result,x_result,x_credit"}}}' > 20w_no_where.log

time curl  -X POST -H 'Content-Type:application/json' 'http://x.xxx.xx.xxx:xxxx/get' -d '{"T_xxxx_xxxx[]":{"count":500000, "T_xxxx_xxxx":{"@column":"x_uin,x_send_time,x_xxxx_id,x_xid,x_xx_id,x_xxxxx_id,x_xxxx_result,x_result,x_credit"}}}' > 50w_no_where.log

time curl  -X POST -H 'Content-Type:application/json' 'http://x.xxx.xx.xxx:xxxx/get' -d '{"T_xxxx_xxxx[]":{"count":800000, "T_xxxx_xxxx":{"@column":"x_uin,x_send_time,x_xxxx_id,x_xid,x_xx_id,x_xxxxx_id,x_xxxx_result,x_result,x_credit"}}}' > 80w_no_where.log

time curl  -X POST -H 'Content-Type:application/json' 'http://x.xxx.xx.xxx:xxxx/get' -d '{"T_xxxx_xxxx[]":{"count":1000000, "T_xxxx_xxxx":{"@column":"x_uin,x_send_time,x_xxxx_id,x_xid,x_xx_id,x_xxxxx_id,x_xxxx_result,x_result,x_credit"}}}' > 100w_no_where.log



printf -- '--------------------------\n開始帶where條件的情況測試\n'
time curl  -X POST -H 'Content-Type:application/json' 'http://x.xxx.xx.xxx:xxxx/get' -d '{"T_xxxx_xxxx[]":{"count":100000, "T_xxxx_xxxx":{"x_xid{}":[xxxx36,xxxx38],"@column":"x_uin,x_send_time,x_xxxx_id,x_xid,x_xx_id,x_xxxxx_id,x_xxxx_result,x_result,x_credit"}}}' > 10w_with_where.log

time curl  -X POST -H 'Content-Type:application/json' 'http://x.xxx.xx.xxx:xxxx/get' -d '{"T_xxxx_xxxx[]":{"count":200000, "T_xxxx_xxxx":{"x_xid{}":[xxxx36,xxxx38],"@column":"x_uin,x_send_time,x_xxxx_id,x_xid,x_xx_id,x_xxxxx_id,x_xxxx_result,x_result,x_credit"}}}' > 20w_with_where.log

time curl  -X POST -H 'Content-Type:application/json' 'http://x.xxx.xx.xxx:xxxx/get' -d '{"T_xxxx_xxxx[]":{"count":500000, "T_xxxx_xxxx":{"x_xid{}":[xxxx36,xxxx38],"@column":"x_uin,x_send_time,x_xxxx_id,x_xid,x_xx_id,x_xxxxx_id,x_xxxx_result,x_result,x_credit"}}}' > 50w_with_where.log

time curl  -X POST -H 'Content-Type:application/json' 'http://x.xxx.xx.xxx:xxxx/get' -d '{"T_xxxx_xxxx[]":{"count":800000, "T_xxxx_xxxx":{"x_xid{}":[xxxx36,xxxx38],"@column":"x_uin,x_send_time,x_xxxx_id,x_xid,x_xx_id,x_xxxxx_id,x_xxxx_result,x_result,x_credit"}}}' > 80w_with_where.log

time curl  -X POST -H 'Content-Type:application/json' 'http://x.xxx.xx.xxx:xxxx/get' -d '{"T_xxxx_xxxx[]":{"count":1000000, "T_xxxx_xxxx":{"x_xid{}":[xxxx36,xxxx38],"@column":"x_uin,x_send_time,x_xxxx_id,x_xid,x_xx_id,x_xxxxx_id,x_xxxx_result,x_result,x_credit"}}}' > 100w_with_where.log

也就是 MySQL 5.7 共 1.9KW 記錄的大表,統計 CRUL 10-20M/s 網速從發起請求到接收完回包的總時長

數量級 4.7.0(5次取平均值) 4.8.0(5次取平均值) 是否正常回包 where條件 性能提升
10W 1.739s 1.159s 50%。即((1/1.159-1/1.739)/(1/1.739))*100%
20W 3.518s 2.676s 31.5%
50W 9.257s 6.952s 33.2%
80W 16.236s 10.697s -Xmx=3192M時無法正常回包,OOM錯誤,調大-Xmx參數後ok。 51.8%
100W 19.748s 14.466s -Xmx=3192M時無法正常回包,OOM錯誤,調大-Xmx參數後ok 36.5%
10W 1.928s 1.392s "x_xid{}":[xxxx36,xxxx38],覆蓋數據超過100W數據。 38.5%
20W 4.149s 2.852s "x_xid{}":[xxxx36,xxxx38] 45.5%
50W 10.652s 7.231s "x_xid{}":[xxxx36,xxxx38] 47.3%
80W 16.975s 12.465s 調整了-Xmx後正常回包 "x_xid{}":[xxxx36,xxxx38] 36.2%
100W 20.632s 16.481s 調整了-Xmx後正常回包 "x_xid{}":[xxxx36,xxxx38] 25.2%

 

感謝同事給的詳細測試報告,在他已脫敏的基礎上二次脫敏,並和他確認後對外公開~

 

以之前的 APIJSON 4.6.0 連接 2.3KW 大表帶條件查出 12W+ 數據來估計:

---------- | ---------- | ---------- | ---------- | -------------
   Total   |  Received  | Time Total | Time Spent | Current Speed
   72.5M   |    72.5M   |   0:00:05  |   0:00:05  |     20.0M

/get >> http請求結束:5624

4.6.0-4.7.2 都沒有明顯的性能優化,所以 4.7.0 只花了約 2s 應該是因爲換了張表,平均每行數據量減少了約 65% 爲原來的 35%。
APIJSON 4.6.0 查原來 2.3KW 大表中 100W 數據按新舊錶數據量比例估計耗時 = 20.632s / 65% = 31.7s;
APIJSON 4.6.0 查原來 2.3KW 大表中 100W 數據按同表查出數據量比例估計耗時 = 100W*72.5M/(12-13W)/(20M/s) = 27.9s-30.2s。

兩種方式估算結果基本一致,也可以按這個 35% 新舊錶平均每行數據量比例估算排除網絡耗時後的整個服務耗時:

APIJSON 4.6.0 查原來 2.3KW 大表中 12W+ 數據量服務耗時 = 總耗時 5.624s - 數據 72.5M/下載速度 20.0Mbps = 2.00s;
APIJSON 4.7.0 查現在 1.9KW 大表中 10W 數據量服務耗時 = 總耗時 1.928s - 數據 72.5M*35%(10/12)/(下載速度 10-20.0Mbps) = 0.00-0.87s;
APIJSON 4.8.0 查現在 1.9KW 大表中 10W 數據量服務耗時 = 總耗時 1.392s - 數據 72.5M*35%
(10/12)/(下載速度 10-20.0Mbps) = 0.00-0.33s,降低 62%;
APIJSON 4.7.0 查現在 1.9KW 大表中 100W 數據量服務耗時 = 總耗時 20.632s - 數據 725M*35%(10/12)/(下載速度 10-20.0Mbps) = 0.00-10.06s;
APIJSON 4.8.0 查現在 1.9KW 大表中 100W 數據量服務耗時 = 總耗時 16.481s - 數據 725M*35%
(10/12)/(下載速度 10-20.0Mbps) = 0.00-5.91s,降低 41%。

也就是 1.9KW 大表帶條件查出 100W 條記錄,APIJSON 4.8.0 的從 完成接收參數 到 開始返回數據 的整個服務耗時不到 6s!

 

 

APIJSON - 零代碼接口和文檔

🏆 騰訊開源前十、內外五個獎項 🚀 零代碼、熱更新、全自動 ORM 庫,

後端接口和文檔零代碼,前端(客戶端) 定製返回 JSON 的數據和結構!

https://gitee.com/Tencent/APIJSON

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章