最近發生了一件大事兒,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 性能差的污點從而影響用戶口碑。
我看着日誌尋思着應該是數組內主表生成了 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