現在大家使用 Funliday 在「景點瀏覽」輸入城市,或者是移動地圖出現的推薦景點,應該都是即時出現,也是最近花了一些時間的成果。下面先筆記一下推薦景點開發的歷程,之後如果有機會應該可以到 PostgreSQL.TW 分享一下完整的內容。
簡單來說,就是一個從 70000ms 的 response time,提升到 200ms 的 response time 過程。
2019/02 推薦景點第一版
如何用 PostgreSQL 的 advisory lock 實作推薦景點
2019/09 late 加上使用者查詢的 poi history 到推薦景點
- 如果該 city 最近 30 天的 poi history 景點超過 10 個,則直接使用
- 如果該 city 最近 30 天的 poi history 景點小於等於 10 個,則加上 KNN+rating 算法所取出的 poi 一起回傳給使用者
- 使用網路上找到的 uniq,將 poi history 的景點及 KNN+rating 算法的景點去除重覆資料
- 會將計算後取出的 poi id 存入 Redis,TTL 為 1 天
2020/06 late 加快查詢速度
- 將原本要從 poi_data (超過 2500 萬筆的 POI) 取得 city 的 bbox,改從 city_data (約 200 萬筆的 city) 取得,加快查詢速度
2020/07 late 刪除不正確的 poi
- 刪除該 city 經過 poi history 計算後 count 為 0 的 poi
- 刪除部分不該出現在推薦景點上的景點類型,如:飲水機、球場、停車場…等
- 將 KNN+rating 的景點 TTL 設定為 14 天,而從 poi history 計算出來的景點 TTL 一樣為 1 天
2020/09 late 重構推薦景點功能,並加上 L2, L3 cache
2020/09/22 late 重構推薦景點功能
- 先從 Redis 取出確認該 city 是否有 cache,有的話就直接回傳,沒有的話寫入 refresh = true 的 flag 到 city_data table,表示該 city 資料必須更新
- 從 DB 取出確認該 city 是否有 cache,有的話就直接回傳,沒有的話就必須即時計算該 city 的推薦景點
- 推薦景點的算法與之前相同,包括了 poi history 及 KNN+rating,但移除 uniq,改用 Set 的寫法去除重覆資料
- 計算完該 city 的推薦景點之後,直接將資料存回 Redis 及 DB,並且回傳計算完的推薦景點
- 期望這樣跑下來後,之後使用者就算在 Redis 找不到 cache (因為過期),也可以從 DB 找到 cache (因為不會消失)
- 每天固定跑 scheduler,掃出 refresh flag 為 true 的 city,在 background job 將最新的推薦景點寫回 Redis 及 DB,並將 refresh flag 設定為 false
2020/09/23 early 加快預熱 cache 到 DB 的速度
- 因為目前在 Redis 還有部分 city 的 cache,如果要等一天 (TTL) 之後這些 cache 失效後才能啟動 L2 cache 機制會有點浪費
- 所以增加若在 Redis 確認該 city 有 cache 且 DB 沒 cache 的話,就將該 cache 寫回 DB,並且回傳
2020/09/23 middle 確認是否有最佳化空間
- 延長 city Redis cache 的 TTL 從 1 天增加到 14 天,避免部分活動過於熱門,導致該區域的推薦景點都被該活動洗掉
2020/09/24 early 加上 new relic
- 加上 new relic 的 custom attributes,記錄該 city 的 IO 相關讀取時間
2020/09/24 middle 加上 local cache
- 因為從 Redis 或 DB 取回來的 id 要再經過一次 DB 查詢,才能組合完整的 response 回傳
- 所以加上 city id 及使用者語言做為 local cache key,加快同 city id 且同語言的資料回傳速度
- 如果 city id 或使用者語言在該 cache 找不到,再回 DB 查詢
- 加上 new relic 的 custom segment,方便判讀 request 的瓶頸是在哪一段
2020/09/24 late 調整 poi history 的執行速度
- 將原本使用 where + group 取得最近 30 的 poi history 語法,改為先做 where,再做 group,將原本放在同一個 CTE 的 query 拆成兩個 CTE 執行
- 速度從原本的 40 ~ 50 秒,甚至到 70 秒,下降到約 10 秒左右
2020/09/25 early 移除不必要的 middleware
- 在整整一年前將 poi recommend 及 search 拆開成不同檔案的時候,沒有仔細確認,所以每次 request 都要到 Redis 做兩次的 auth check
- 所以將其中一個 auth check 拿掉,加快約 1ms
2020/09/27 early 再次檢視 poi history 計算方式是否有最佳化空間
- 原本在使用經緯度計算是哪個 city 時,查詢速度大約都 400ms 左右,加上 GiST index 之後,查詢速度提升 1000x,變成只要 0.4ms 就能算出來
- 整整一年前導入 poi history 到推薦景點時,資料量還沒這麼大,所以在計算時速度都蠻快的
- 但 poi history 現在資料筆數已經超過 2500 萬筆,都變成 Seq Scan,雖然 planner 會判斷成 Parallel Seq Scan,但速度還是很慢
- 因為是使用近 30 天的資料做計算,所以將 created_at 加入 index,速度從 10 秒左右降到 1 秒左右