用Nginx做微快取

#apollo-graphql #cache-control #nginx #nuxt

用 nginx 設定一個極短的 cache ttl,搭配 proxy_cache_use_staleproxy_cache_background_update,不僅可以大幅度提高網站反應速度、負載能力,還能大幅度降低 upstream 的 CPU使用率
但還是要看場景。

起因

最近工作上遇到一個架構上很單純的網站,它透過 nuxt 產出動態網站,它會打 API 拉資料並做套版除此之外沒有登入需求、沒有驗證需求,很單純



效能瓶頸

眾所皆知,nginx 的效能非常好,但在這個架構中,nginx 的效能受限於位於 upstream 的 Nuxt。簡單說,整個網站的效能 = Nuxt的效能

nginx 官方部落格中有一張很棒的圖,可以用來說明,一般來說哪些內容可以透過 cache 提高效能

而我們的 Nuxt 網站雖然是動態的,但沒有登入需求,所以 Nuxt 的 response 沒有 user data。

It’s dynamic content, but cacheable.



實作Cache

底下內容都參考自nginx 官方部落格

同時程式碼放在Github

現狀

再複習一次現在的架構圖,圖中線的粗度代表流量

因為每次 request,nginx 都必須把 request 交給 upstream 處理,所以當流量一大,Nuxt、GraphQL就會吃掉不少CPU,最終的效能如下,一秒鐘可以負擔 536 筆 request

Requests per second:    536.09 [#/sec] (mean)
Time per request:       18.653 [ms] (mean)
Time per request:       1.865 [ms] (mean, across all concurrent requests)
Transfer rate:          1853.29 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:    10   19   5.9     18     213
Waiting:       10   19   5.9     18     213
Total:         10   19   5.9     18     213



加上微快取

現在在 nginx 在 proxy_pass 後面加上這幾句,來啟用快取

proxy_cache my_cache;

proxy_cache_valid 200 1s;

如同下面這張圖:

效能就提高非常多,來到49885

Requests per second:    49885.86 [#/sec] (mean)
Time per request:       0.200 [ms] (mean)
Time per request:       0.020 [ms] (mean, across all concurrent requests)
Transfer rate:          172505.70 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:     0    0   1.1      0      82
Waiting:        0    0   1.1      0      82
Total:          0    0   1.1      0      82



快取穿透

不過這邊有個潛在的問題,是在 nginx 官方部落格有提到的,就是快取穿透問題

每 1 秒 nginx 認定快取過期之後,所有使用者的 request 都會被轉發到 upstream,直到 upstream 再次回傳網頁讓 nginx 快取起來,因此會造成雖然效能已經很好,但CPU佔用率仍然很高的問題



優化微快取

官方提供的優化方法是加上下列兩行

proxy_cache_lock on;

proxy_cache_use_stale updating;

這樣子的話,就算有大量使用者同時瀏覽已經快取過期的網頁,也只有第一筆連線會需要等到 request finished,其他連線則是直接回傳目前版本

等到 upstream 完成第1筆連線,重新快取之後,下次所有人連線就可以拿到新版本了。這樣一來,upstream 的壓力肯定低不少。

就像下面這樣