再度最佳化推薦景點的回應時間

現在大家使用 Funliday 在「景點瀏覽」輸入城市,或者是移動地圖出現的推薦景點,應該都是即時出現,也是最近花了一些時間的成果。下面先筆記一下推薦景點開發的歷程,之後如果有機會應該可以到 PostgreSQL.TW 分享一下完整的內容。

簡單來說,就是一個從 70000ms 的 response time,提升到 200ms 的 response time 過程。


2019/02 推薦景點第一版

如何用 PostgreSQL 的 advisory lock 實作推薦景點

2019/09 late 加上使用者查詢的 poi history 到推薦景點

  1. 如果該 city 最近 30 天的 poi history 景點超過 10 個,則直接使用
  2. 如果該 city 最近 30 天的 poi history 景點小於等於 10 個,則加上 KNN+rating 算法所取出的 poi 一起回傳給使用者
  3. 使用網路上找到的 uniq,將 poi history 的景點及 KNN+rating 算法的景點去除重覆資料
  4. 會將計算後取出的 poi id 存入 Redis,TTL 為 1 天

2020/06 late 加快查詢速度

  1. 將原本要從 poi_data (超過 2500 萬筆的 POI) 取得 city 的 bbox,改從 city_data (約 200 萬筆的 city) 取得,加快查詢速度

2020/07 late 刪除不正確的 poi

  1. 刪除該 city 經過 poi history 計算後 count 為 0 的 poi
  2. 刪除部分不該出現在推薦景點上的景點類型,如:飲水機、球場、停車場…等
  3. 將 KNN+rating 的景點 TTL 設定為 14 天,而從 poi history 計算出來的景點 TTL 一樣為 1 天

2020/09 late 重構推薦景點功能,並加上 L2, L3 cache

2020/09/22 late 重構推薦景點功能

  1. 先從 Redis 取出確認該 city 是否有 cache,有的話就直接回傳,沒有的話寫入 refresh = true 的 flag 到 city_data table,表示該 city 資料必須更新
  2. 從 DB 取出確認該 city 是否有 cache,有的話就直接回傳,沒有的話就必須即時計算該 city 的推薦景點
  3. 推薦景點的算法與之前相同,包括了 poi history 及 KNN+rating,但移除 uniq,改用 Set 的寫法去除重覆資料
  4. 計算完該 city 的推薦景點之後,直接將資料存回 Redis 及 DB,並且回傳計算完的推薦景點
  5. 期望這樣跑下來後,之後使用者就算在 Redis 找不到 cache (因為過期),也可以從 DB 找到 cache (因為不會消失)
  6. 每天固定跑 scheduler,掃出 refresh flag 為 true 的 city,在 background job 將最新的推薦景點寫回 Redis 及 DB,並將 refresh flag 設定為 false

2020/09/23 early 加快預熱 cache 到 DB 的速度

  1. 因為目前在 Redis 還有部分 city 的 cache,如果要等一天 (TTL) 之後這些 cache 失效後才能啟動 L2 cache 機制會有點浪費
  2. 所以增加若在 Redis 確認該 city 有 cache 且 DB 沒 cache 的話,就將該 cache 寫回 DB,並且回傳

2020/09/23 middle 確認是否有最佳化空間

  1. 延長 city Redis cache 的 TTL 從 1 天增加到 14 天,避免部分活動過於熱門,導致該區域的推薦景點都被該活動洗掉

2020/09/24 early 加上 new relic

  1. 加上 new relic 的 custom attributes,記錄該 city 的 IO 相關讀取時間

2020/09/24 middle 加上 local cache

  1. 因為從 Redis 或 DB 取回來的 id 要再經過一次 DB 查詢,才能組合完整的 response 回傳
  2. 所以加上 city id 及使用者語言做為 local cache key,加快同 city id 且同語言的資料回傳速度
  3. 如果 city id 或使用者語言在該 cache 找不到,再回 DB 查詢
  4. 加上 new relic 的 custom segment,方便判讀 request 的瓶頸是在哪一段

2020/09/24 late 調整 poi history 的執行速度

  1. 將原本使用 where + group 取得最近 30 的 poi history 語法,改為先做 where,再做 group,將原本放在同一個 CTE 的 query 拆成兩個 CTE 執行
  2. 速度從原本的 40 ~ 50 秒,甚至到 70 秒,下降到約 10 秒左右

2020/09/25 early 移除不必要的 middleware

  1. 在整整一年前將 poi recommend 及 search 拆開成不同檔案的時候,沒有仔細確認,所以每次 request 都要到 Redis 做兩次的 auth check
  2. 所以將其中一個 auth check 拿掉,加快約 1ms

2020/09/27 early 再次檢視 poi history 計算方式是否有最佳化空間

  1. 原本在使用經緯度計算是哪個 city 時,查詢速度大約都 400ms 左右,加上 GiST index 之後,查詢速度提升 1000x,變成只要 0.4ms 就能算出來
  2. 整整一年前導入 poi history 到推薦景點時,資料量還沒這麼大,所以在計算時速度都蠻快的
  3. 但 poi history 現在資料筆數已經超過 2500 萬筆,都變成 Seq Scan,雖然 planner 會判斷成 Parallel Seq Scan,但速度還是很慢
  4. 因為是使用近 30 天的資料做計算,所以將 created_at 加入 index,速度從 10 秒左右降到 1 秒左右