layout: post title: GraphQL api performance tuning紀錄 date: 2020-10-17 11:11 +0800
這是一個監控交易狀況和計算損益的服務,每當程式進行交易後更新目前的持倉和損益。
要求與交易系統狀態同步的延遲時間盡可能縮短。 (交易發生後到服務更新完成的時間)
本文紀錄從最初開發到現在的演變過程。
成交紀錄和系統資料皆存放在遠端機房的MySQL DB中,
為了減少讀寫和網路壓力,使用在本地機房建立的slave MySQL DB讀取資料來進行計算。
這一個版本的實作只有一個api process。
在啟動時從slave讀取成交紀錄,計算出損益後開始serve HTTP。
為了更方便做JOIN和aggregate query,首先分拆出了同步服務,將資料從slave複製到本地DB。
使用本地DB的Primary Key,就能拿來當損益紀錄對應的成交紀錄Foreign Key。
損益的計算改為先從本地DB拿出成交紀錄,計算出損益紀錄後寫到DB中,
方便直接從DB中查詢資料而不用將所有功能寫在api裡。
api則使用GROUP BY拿出需要的資料
最初的版本使用RESTful提供資料,但系統的資料是樹狀呈現,
一個user可能擁有多個account,一個account底下可以擁有一個portfolio,
一個portfolio可以有多個subscription。
而portfolio和subscription又有各自的損益計算方式。
user又想看到所有portfolio加起來的損益。
因此API變成:
GET /user
GET /user/profit
GET /account
GET /portfolio
GET /portfolio/profit
GET /subscription
GET /subscription/profit
結果API常常需要回傳一部分相同的資料或功能非常類似,因此我們開始加入GraphQL,
將schema定義成:
profit {
date
netbalance
}
subscription {
portfolio
profits []profit
}
portfolio {
subscriptions []subscription
account
profits []profit
}
account {
user
portfolio
}
user {
accounts []account
profits []profit
}
查詢則類似:
user {
accounts {
portfolio {
subscription {
profit
}
profit
}
}
profit
}
GraphQL雖然簡化了API設計,但是完全沒有減少計算複雜度,而且容易引起大量查詢,
以上面的schema來說,如果有100個user,每個user有2個portfolio,每個portfolio有2個subscription
就會觸發100 + 200 + 400次profit的resolver去查詢資料
另外如果profit裡面指定需要回傳每日的損益合計,一共有一年份的歷史紀錄,
就會有700*365個profit summary object,會變成一個MB等級的巨型回應。
最初的profit查詢方式是
SELECT deal_date, SUM(profit)
FROM profits
WHERE account_id = ? AND subscription_id = ?
GROUP BY deal_date
顯而易見的這會花一定時間來計算,儘管加了index還是需要50ms~300ms,
再加上海量查詢次數使得回應時間最慢來到5s~20s。
實際上這個計算是可以cache起來的,但放在in-memory或redis又會失去DB查詢的方便性,
所以加入了Materialized View,這是Postgresql提供的功能,可以將View做一個snapshot。
另外MView也可以加上INDEX,以及UNIQUE INDEX來支援REFRESH MVIEW CONCURRENTLY,
再增加查詢和更新的速度。
加入以後降低到1~2s的回應時間
v1的設計主要是配合vue前端可以在每個component寫自己想要的schema,
因此支援了四種query:
query {
users()
accounts()
portfolios()
subscriptions()
}
但這個做法事實上違反了SSOT,每個component都會各自發出query,相同的資料存在多個地方。
當透過websocket通知前端:持倉有變化時,大量的component都在監聽event。
為了解決這個問題,在v2中的query只留下users()一個root。同時刪減欄位減少不必要的傳輸。
前端則將GraphQL的結果存在vuex store中。
回應速度降低到了300~500ms
在V2的基礎上,我們做了更多細部調校
postgresql用docker啟動可以另外指定shared_memory的大小,可以考量資料庫大小和機器ram大小做設定
shm_size: 4G
細部設定可以修改 postgresql.conf
主要是增加可以使用的memory大小,以及紀錄耗費時間太長的查詢、需要檔案來暫存的查詢
temp_buffers = 128MB # 預設8MB
work_mem = 128MB # 預設4MB
log_min_duration_statement= '500' # 0.5s
log_temp_files = '4' # 4k
使用的package是 github.com/graph-gophers/graphql-go
使用option
graphql.UseFieldResolvers(),
graphql.Tracer(trace.NoopTracer{}),
graphql.MaxParallelism(runtime.GOMAXPROCS(0)),
graphql.MaxDepth(20),
graphql.DisableIntrospection(),
drop in replacement
替代std的encoding/json github.com/json-iterator/go
使用 jsoniter.ConfigFastest
替代std的gzip github.com/klauspost/pgzip
使用 gzip.BestSpeed
使用 Object.freeze
鎖定不會更新的資料