Cache stampede—Hiện tượng chất đống cache
Caching là một kĩ thuật tăng tốc mà hầu như mọi kĩ sư phần mềm đều cần biết. Tuy vậy, đôi khi caching vẫn có thể mang lại cho bạn những vấn đề phiền toái khác như cache stampede.
Varnish Cache là một reverse HTTP proxy, đôi khi còn được xem là một web accelerator (tăng tốc web). Varnish đóng vai trò đứng giữa client và server trong một web service, cache lại HTTP response files trong memory, giúp giảm thời gian response và hao tốn network bandwidth của những request tương tự.
Như hình bên trên bạn thấy Quần Cam đang xem phim XXX, .. à nhầm, đang xem phim từ dịch vụ của XXX. Ở lần request đầu tiên, Varnish không tìm thấy response tương ứng trong memory cache store, nó sẽ forward request đến server XXX và lưu response object vào cache store. Ở các request tương tự tiếp theo, thay vì phải request XXX, nó sẽ tự serve client bằng response object trong cache store.
Như app bên mình có đến 80% request là read, nên việc có một Varnish đứng đầu ngọn sóng đã giúp giảm một lượng lớn tải cho các backend server.
Trong bài viết này, mình sẽ giới thiệu một chút về cách bọn mình thu thập metrics và monitor Varnish cluster.
Về mặt tổng quan thì bên mình dùng influxdb để lưu trữ, Telegraf để thu thập, Kapacitor để cảnh báo, và Grafana cho việc hiển thị metrics. Bạn có thể đọc bài viết này để biết thêm chi tiết.
Telegraf có hỗ trợ sẵn Varnish input plugin giúp thu thập Varnish metrics. Tuy nhiên có lẽ vì Varnish bên mình dùng khá cổ (2.x), không có metrics nào được report khi bật thử plugin.
Sau khi đọc code một hồi thì lý do Telegraf không nhận thông số là do output của varnishstat
đã thay đổi từ version 3. Xem tham số output của hai version bên dưới ta thấy Varnish 4 gán category prefix cho tên của các thông số như MAIN.*
, Telegraf dựa vào format này để đọc và group các tham số.
Các tham số output của varnishstat version 2.
uptime 14234778 . Child uptime
client_conn 2703079202 189.89 Client connections accepted
client_req 2839064624 199.45 Client requests received
cache_hit 2267678359 159.31 Cache hits
cache_hitpass 0 0.00 Cache hits for pass
cache_miss 478890598 33.64 Cache misses
Còn của version 4.
NAME CURRENT CHANGE AVERAGE AVG_10 AVG_100 AVG_1000
MAIN.uptime 171488 1.00 1.00 1.00 1.00 1.00
MAIN.sess_conn 1055 7.98 . 8.35 4.49 2.11
MAIN.client_req 1055 7.98 . 8.35 4.49 2.11
MAIN.cache_hit 1052 7.98 . 8.35 4.49 2.10
MAIN.cache_miss 3 0.00 . 0.00 0.00 0.00
MAIN.backend_conn 4 0.00 . 0.00 0.00 0.01
May mắn là Telegraf cho phép tùy chỉnh file thực thi của varnishstat
, nên tụi mình đã tự viết lại một cái varnishstat riêng. Nói thì ghê gớm chứ thực chất chỉ gắn thêm MAIN.*
vào tên của các tham số rồi output lại, chỉ với một bash script nhỏ dưới đây.
# /usr/bin/varnishstat2to4
#!/bin/bash
/usr/bin/varnishstat -1 | sed -e '/^[a-z]/ s/^/MAIN./'
Công việc còn lại chỉ là cài đặt file này vào Telegraf config.
# A plugin to collect stats from Varnish HTTP Cache
[[inputs.varnish]]
## The default location of the varnishstat binary can be overridden with:
binary = "/usr/bin/varnish2to4"
## By default, telegraf gathers stats for 3 metric points.
## Setting stats will override the defaults shown below.
## stats may also be set to ["all"], which will collect all stats
stats = ["all"]
Đây là thông số quan trọng nhất để đánh giá performance của một Varnish cluster. Nôm na nếu hit rate đạt 80% có nghĩa phần lớn lượng request đến web service đã được served bởi cache mà không cần request đến backend. Nó cũng đồng nghĩa thời gian response nhanh hơn và giảm tải rất nhiều cho backend server (điều này có thể không đúng, xem thêm giải thích cache_hitpass
ở bên dưới).
Ba thông số đáng chú ý nhất là:
cache_hit
: Lũy tích số lần mà Varnish serve request từ cache store.
cache_miss
: Lũy tích số lần mà Varnish không thể serve request từ cache store, mà phải request từ backend.
cache_hitpass
: Có một số request không thể cache được mà phải luôn request đến backend, như POST /users/create
chẳng hạn. Những request này sẽ được tính là cache_hitpass
chứ không là cache_miss
.
Công thức tính hit rate khá đơn giản.
hit_rate = cache_hit / (cache_hit + cache_pass)
Hit rate càng cao thì càng tốt, cụ thể như bên mình luôn giữ hit rate ở mức tối thiểu 60% cho thời điểm bình thường và 80% cho peak. Quan trọng nhất là ở peak hours, khi web service của bạn đang chịu tải lớn, việc giữ cho hit rate của Varnish càng cao sẽ giảm càng nhiều tải cho backend servers.
Varnish hit rate graph của Wikimedia
Đây là cơ sở để bạn tiến hành nâng hit rate, dựa vào các dữ kiện như LRU objects đã được evicted (dọn) như thế nào, dung lượng bộ nhớ ra sao.
Các thông số nên chú ý ở đây là:
n_expired
: Số lượng object expired vì TTL từ Cache-Control
header.
n_lru_nuked
: Số lượng object đã bị evicted vì thiếu memory.
sma_nbytes
: Tổng số allocated memory.
sma_nobj
: Tổng số allocated objects.
Có ba cách để một object bị xóa/dọn khỏi Varnish:
1) Expired: object bị xóa vì TTL được set trong Cache-Control
đã hết hạn. Cách xóa này được xem là lành mạnh, vì vòng đời của object diễn ra theo đúng ý nguyện của backend server.
2) Purged: object bị xóa vì admin muốn thế, như đôi khi bạn muốn content được refresh ngay lập tức (chẳng hạn sau khi cập nhật CSS, JS). Cách xóa này cũng có thể xem là lành mạnh.
3) Evicted: object bị xóa khỏi Varnish bị thiếu memory. Varnish là một LRU (Least Recently Used) cache, nên các object ít được dùng nhất sẽ bị lấy chỗ cho object mới.
Khi monitor Varnish, nếu n_lru_nuked
tăng một cách chóng mặt (thường tỉ lệ nghịch với hit rate), bạn nên xem xét tăng memory cho Varnish để các cache object cần thiết không bị xóa đi.
Các thông số về client cũng khá thú vị.
client_req
: Số lượng request Varnish nhận được.
client_conn
: Số lượng connection Varnish nhận được.
Bạn có thể dùng client_req
để biết số request/s và setup một số alerts khi thông số này tăng/giảm đột ngột.
HTTP request graph của Wikimedia
Xin khẳng định là bài viết không giúp bạn tăng lương. Nhưng mình có một số tips để giúp bạn nâng hit rate.
n_lru_nuked
Như mình đã viết, việc các object bị evicted theo cách không tự nhiên đồng nghĩa với việc miss rate cao hơn -> hit rate giảm đi. Trong trường hợp này bạn có thể xem xét tăng memory cho Varnish.
Rất khó để mình liệt kê hết tất cả HTTP caching mechanism trong này, mình sẽ nêu một số ví dụ.
Giả sử bạn đang dùng Varnish để host một cái S3 bucket (Cloudfront đắt lắm), hãy đảm bảo bạn có set Cache-Control
hay Expires
cho các S3 objects. TTL có thể được cài đặt mặc định khi Varnish được chạy nhưng đặt các cache headers sẽ giúp bạn điều khiển được vòng đời của Varnish object theo ý muốn.
Mặc định Varnish sẽ pass các request sử dụng cookies vì lý do security, nên hãy bỏ Cookies
header cho các request không cần cookies, để Varnish cache và tự serve client mà không cần backend request.
Varnish dựa vào Vary
header để cache các version khác nhau của cùng một request.
Như Varnish sẽ xem hai request dưới đây là khác nhau, từ đó lưu thành hai version khác nhau.
Vary: Accept-Encoding
Accept-Encoding: gzip,deflate
Vary: Accept-Encoding
Accept-Encoding: deflate,gzip
Vì vậy bạn nên normalize lại để tránh lãng phí memory, để có thêm không gian cho Varnish lưu những object khác.
Nếu không normalize, trường hợp xấu nhất là một số server trả về Vary: User-Agent
trong response headers tùy theo browser/client, mà User-Agent
thì chỉ riêng Google Chrome đã có chừng chục cái, tỉ lệ hit rate của bạn rơi xuống vực thẳm là rất cao.
Chỉ nên làm việc này nếu bạn hiểu rõ bạn đang làm gì.
Varnish lưu object với key là một hash, sau đó nó sẽ dùng hash này khi cần tìm lại object. Mặc định hash sẽ được tính toán dựa trên request url và Host
header.
Tuy vậy bạn có thể viết lại hàm hash theo ý của mình để nâng hiệu suất cache của Varnish.
sub vcl_hash {
set req.hash += req.url;
set req.hash += req.http.host;
set req.hash += req.http.Accept;
return (hash);
}
Caching là một kĩ thuật tăng tốc mà hầu như mọi kĩ sư phần mềm đều cần biết. Tuy vậy, đôi khi caching vẫn có thể mang lại cho bạn những vấn đề phiền toái khác như cache stampede.
Giả sử server tự dưng lăn đùng ra chết không còn gửi được request nữa, bạn sẽ làm gì?
Có một thủ pháp thường hay được sử dụng khi deploy app là chạy database migration ngay khi deploy, nhưng liệu đó có phải là một good practice (tam dịch: cách làm tốt)?