把 API 效能提高近 10 倍的故事

最近在調公司專案的 HTTP API 效能。背景是 Ruby on Rails 和 RESTful API,第一天就上 bullet 防 SQL N+1,所以主要問題不在 N+1。API Spec 使用 OpenAPI 寫的,測試全部都通過 OpenAPI 的 JSON Schema validator(詳見我的演講簡報),所以可以放大膽 refactoring。

最初用了 Rails 推薦的 JBuilder,沒考慮到效能,QA 也不覺得慢,就沒注意。結果 APM 統計出來數字很難看,某特定 endpoint 的 rps < 2(requests per second),就開始認真調查了。

首先是發現 SQL 花的時間很長,極端狀況下甚至會 timeout。仔細一看才發現是 schema 的 nesting 太深,四五層左右,還有重複內容的 array。這是 OpenAPI Schema 互相嵌套的潛在問題,錯在當初 review API spec 沒仔細審 schema。這種四五個表格互相 join 我是不會調 SQL 效能啦。但實際上看前端 UI 也沒用到,就跟前端工程師協調把那些 array 直接放空,終於可以拉到 rps 3 以上了。

但還是很慢,就想說改用別的 JSON Serializer。首先選的是 ActiveModel::Serializer,速度立刻翻倍。但 AMS 的 API 有點醜,bug 也不少,他們自己甚至把整個 gem 砍掉重練;我的話是要在 root 加料的時候很麻煩。但看在速度的份上還是用下去了。這樣子 rps 拉到了 5 以上。直接擺脫 ActionView 的 Template 果然是正確的選擇 — — JBuilder 似乎每 render 一次 partial 就會重新開一次檔案,時間都浪費在 disk I/O;而 class 的話一啟動就存在記憶體裡面了。

不過在找 JSON Serializer 的時候,固然發現到其他 gem。其中一個叫做 panko_serializer,是用 C 寫的,號稱有特別的優化(關於 type casting)。效能是 AMS 的兩倍,該當 endpoint 的 rps 拉到了 10 左右,要在 root 加料也很簡單。缺點是嚴重依賴 db column 的 raw 值,只能輸出 UTC 時間(除非改用 method 叫),而且有隱藏的「如果不是 db column 就會變成 nil」的問題。加上專案本身還很新,感覺維護成本很大,就放棄了。

最近試的是 ActiveModel::Serialization::JSON#as_json,就是真正內建在 Rails 裡面的那個。搭配 Oj encoder 的 Rails 優化功能的話,速度只差 panko_serializer 一點點(5% 左右)。該 endpoint 測出來大概 8 rps 而已。

原本覺得他的 API 很難看,沒有 Serializer 好讀;但是仔細看了一下其實也沒什麼難懂,基本上就是事先用 hash 寫靜態的 structure。指定 association 的方式也很簡單,也因此比較容易發現多曾嵌套問題。

panko_serializer 的 attribute-only 問題他也有,但是 API 文件寫的比較好讀,事前指定 call instance method 就行了。缺點是不能設定 attribute alias,必須 method alias 寫在 Model 裡面,然後當作 method 去拉。

此外沒有對 association 加工的功能(前文提到我需要把某個深深的 array 清空),其他 serializer 或 Jbuilder (view) 都可以寫 if-else 來處理。不過好在 as_json 拉出來就是 hash,反正對這個 hash 用 yield_selftap 加料就好了。

另一個隱藏的問題是,如果 association 是 nil,那麼他連 key 都不會寫進去,JSON Schema validator 就在唉了。我不知道這是 bug 還是 feature 啦,總之是 hash 嘛,對他加料即可。

測試的工具是 wrk,之前還用過 Apache Bench (ab),但覺得 wrk 直接指定單線程刷 n 秒看 rps 比較直觀。都不喜歡的話可以參考 awesome-http-benchmark 這個列表找自己喜歡的即可。

至於最近很紅的 Netflix 的 fast_jsonapi,我沒用它,因為有個要件:你的 API response 必須是 JSON:API 格式(有固定的 meta attributes 要套上)。我們的 API 很不巧並不是 JSON:API 格式,所以不能直接上。曾經我參加過一個 workshop 號稱改用 fast_jsonapi 就可以效能翻不知道幾倍,結果我看他的 code 根本就一堆 N+1 問題,修掉 N+1 就翻倍再翻倍了。更別說原本他的 API 也不是 JSON:API 規格,效能翻倍之前先逼死前端吧。

以上就是我把 API 效能提高近 10 倍的故事。結論是

  1. API Spec 的 schema 多層嵌套是 red flag
  2. 直接刷 API endpoint 測效能
  3. 慎選 JSON Generator
  4. 效能不佳,先解 SQL 效能
  5. 世界上沒有 silver bullet。別人的場景跟你不見得相同