Bài viết này sẽ giải thích cho bạn graceful shutdown là gì, lợi ích khi sử dụng nó, cách cài đặt graceful shutdown trên ứng dụng Kubernetes. Ngoài ra chúng ta sẽ cùng thảo luận về cách validate, cách benchmark, và cả cách tránh các lỗi thường thấy.
Graceful shutdown
Chúng ta bắt đầu bàn về graceful shutdown của ứng dụng khi tất cả tài nguyên nó sử dụng/ cũng như tất cả các tiến trình dữ liệu nó xử lý đã đóng và được giải phóng.
Nó có nghĩa là không có bất kì kết nối database nào còn mở và không có bất cứ request đang gửi nào bị fail bởi vì chúng ta đã dừng ứng dụng.
Graceful shutdown: khi tất cả tài nguyên nó sử dụng/ cũng như tất cả các tiến trình dữ liệu nó xử lý đã đóng và được giải phóng. @RisingStack
Kịch bản khả dĩ cho graceful shutdown của web server:
- Ứng dụng nhận được notification yêu cầu dừng (nhận được tín hiệu
SIGTERM
) - Ứng dụng cho bộ cân bằng tải (load balancer) biết rằng nó chưa sẵn sàng cho các request mới hơn
- Ứng dụng phục vụ tất cả các request đang được đưa tới
- Ứng dụng giải phóng tất cả tài nguyên: database, hàng đợi, ...
- Ứng dụng thoát ra với thông báo thành công (sử dụng hàm process.exit() ).
Bài viết này đi sâu vào cách shutting down các web server theo cách đúng đắn. Tuy nhiên hoàn toàn có thể áp dụng những kỹ thuật này vào công việc của bạn: tôi đề xuất bạn dừng các hàng đợi chiếm nhiều thời gian và hoàn thành các task/job hiện thời.
Tại sao graceful shutdown lại quan trọng đến thế?
Nếu không dừng ứng dụng đúng cách, chúng ta sẽ làm tiêu tốn tài nguyên như các kết nối DB và có thể sẽ làm gián đoạn các request đang gửi tới. Một HTTP request sẽ không tự phục hồi được - nếu gặp lỗi trong việc nhận và xử lý các request thì đơn giản là ta đã lỡ mất nó.
Nếu không dừng ứng dụng đúng cách, chúng ta sẽ làm tiêu tốn tài nguyên như các kết nối DB và có thể sẽ làm gián đoạn các request đang gửi tới.
Khởi động graceful
Chúng ta chỉ nên khởi động ứng dụng khi tất cả các dependency và kết nối database đã sẵn sàng để kiểm soa các request.
Kịch bản khả dĩ khi khởi động một graceful web server:
- Ứng dụng khởi động (dùng câu lệnh npm start).
- Ứng dụng mở các kết nối DB.
- Ứng dụng lắng nghe ở một cổng.
- Ứng dụng nói với load balancer rằng nó đã sẵn sàng cho việc nhận các request.
Graceful shutdown trong ứng dụng Node.js
Điều đầu tiên là bạn cần lắng nghe tín hiệu SIGTERM
và bắt nó:
process.on('SIGTERM', function onSigterm () {
console.info('Got SIGTERM. Graceful shutdown start', new Date().toISOString())
// start graceul shutdown here
shutdown()
})
Sau đó, bạn có thể đóng server, rồi đến các tài nguyên và thoát các tiến trình:
function shutdown() {
server.close(function onServerClosed (err) {
if (err) {
console.error(err)
process.exit(1)
}
closeMyResources(function onResourcesClosed (err) {
// error handling
process.exit()
})
})
}
Khá đơn giản đúng không?
Vậy còn load balancer thì sao? Làm thế nào mà nó có thể biết ứng dụng của bạn chưa sẵn sàng để nhận thêm các request? Và còn các kết nối keepalive (keepalive connection)? Chúng sẽ được server duy trì trạng thái mở trong thời gian bao lâu? Nếu cùng lúc đó server SIGKILL
ứng dụng thì sẽ ra sao?
Graceful shutdown với Kubernetes
Nếu bạn muốn biết thêm về Kubernetes, hãy tìm hiểu ở đây. Còn bây giờ thì hãy tập trung vào shutdown thôi.
Kubernetes cung cấp một tài nguyên tên Service. Công việc của nó là điều hướng các luồng dữ liệu đến các pod (thể hiện của ứng dụng). Kubernetes còn có một thứ là Deployment
sẽ miêu tả cách hành xử của ứng dụng trong quá trình thoát, mở rộng và triển khai - bạn có thể định nghĩa cho một tác vụ health check. Sau cùng thì ta sẽ kết hợp những tài nguyên đó cho graceful shutdown để hoàn thiện và bàn giao trong khi triển khai một cái mới ở luồng dữ liệu kết nối cao.
Biểu đồ dưới thể hiện rpm nhất quán và không có hiệu ứng triển khai hỗ trợ nào - không có sự thay đổi khi triển khai:
Ok, hãy tìm hướng để giải quyết bài toán này.
Cài đặt graceful shutdown
Với Kubernetes, để tạo một graceful shutdown đúng chuẩn, ta cần thêm một readinessProbe
đến định dạng dữ liệu yaml Deployment
và để cho load balancer Service
biết rằng trong khi shutdown ta sẽ không phục vụ bất cứ một request nào. Do đó readinessProbe sẽ ngừng gửi. Ta có thể đóng server, đóng các kết nối DB và thoát ra sau khi hoàn thành những tác vụ đó.
Nó làm việc như thế nào?
- Pod nhận tín hiệu
SIGTERM
vì Kubernetes muốn dừng - các lý do như triển khai, mở rộng, ... - Ứng dụng (pod) khởi động và trả về thông báo
500
choGET/health
và choreadinessProbe
(service) biết rằng nó chưa sẵn sàng để nhận thêm request. - Kubernetes
readinessProbe
kiểm traGET/health
và sau khi (failureThreshold * periodSecond) nó dừng và tái điều hướng luồng dữ liệu đến ứng dụng (vì nó tiếp tục trả về 500). - Ứng dụng chờ (failureThreshold * periodSecond) trước khi nó bắt đầu shutdown - để chắc chắn rằng service nhận được notify thông qua việc
readinessProbe
lỗi. - Ứng dụng khởi động graceful shutdown.
- Ứng dụng đóng database sau khi server đóng.
- Ứng dụng giải phóng các tiến trình.
- Kubernetes ép buộc việc dừng ứng dụng sau 30s (SIGKILL) nếu ứng dụng vẫn chạy.
Trong trường hợp của chúng ta, Kubernetes livenessProbe
sẽ không dừng ứng dụng trước khi graceful shutdown xảy ra bởi vì nó cần phải chờ (failureThreshold * periodSecond) làm điều đó. Có nghĩa là, ngưỡng livenessProbe
nên lớn hơn ngưỡng readinessProbe
. Theo cách này (gracefull dừng hoạt động khoảng 4s, trong khi lệnh ép buộc dừng sẽ xảy ra 30s sau SIGTERM).
Làm cách nào để đạt được điều đó?
Ta cần làm hai việc. Đầu tiên cần để readinessProbe
biết rằng sau khi SIGTERM
, ta vẫn chưa sẵn sàng:
'use strict'
const db = require('./db')
const promiseTimeout = require('./promiseTimeout')
const state = { isShutdown: false }
const TIMEOUT_IN_MILLIS = 900
process.on('SIGTERM', function onSigterm () {
state.isShutdown = true
})
function get (req, res) {
// SIGTERM already happened
// app is not ready to serve more requests
if (state.isShutdown) {
res.writeHead(500)
return res.end('not ok')
}
// something cheap but tests the required resources
// timeout because we would like to log before livenessProbe KILLS the process
promiseTimeout(db.ping(), TIMEOUT_IN_MILLIS)
.then(() => {
// success health
res.writeHead(200)
return res.end('ok')
})
.catch(() => {
// broken health
res.writeHead(500)
return res.end('not ok')
})
}
module.exports = {
get: get
}
Thứ hai, ta phải trì hoãn việc dừng process - bạn có thể sử dụng thời gian cần thiết cho hai quá trình hủy: readinessProbe
: failureThreshold: 2
* periodSeconds: 2
= 4s.
process.on('SIGTERM', function onSigterm () {
console.info('Got SIGTERM. Graceful shutdown start', new Date().toISOString())
// Wait a little bit to give enough time for Kubernetes readiness probe to fail
// (we are not ready to serve more traffic)
// Don't worry livenessProbe won't kill it until (failureThreshold: 3) => 30s
setTimeout(greacefulStop, READINESS_PROBE_DELAY)
})
Ví dụ cụ thể: https://github.com/RisingStack/kubernetes-graceful-shutdown-example
Cách validate
Kiểm tra graceful shutdown bằng cách gửi lượng lớn luồng dữ liệu đến các pod và giải phóng phiên bản mới cùng lúc (tạo lại tất cả các pod).
Test case
Bạn cần thay đổi biến môi trường trong Deployment
để tạo lại toàn bộ các pod trong quá trình benchmark ab
.
AB output
Document Path: /
Document Length: 3 bytes
Concurrency Level: 20
Time taken for tests: 172.476 seconds
Complete requests: 100000
Failed requests: 0
Total transferred: 7800000 bytes
HTML transferred: 300000 bytes
Requests per second: 579.79 [#/sec] (mean)
Time per request: 34.495 [ms] (mean)
Time per request: 1.725 [ms] (mean, across all concurrent requests)
Transfer rate: 44.16 [Kbytes/sec] received
Log output của ứng dụng
Got SIGTERM. Graceful shutdown start 2016-10-16T18:54:59.208Z
Request after sigterm: / 2016-10-16T18:54:59.217Z
Request after sigterm: / 2016-10-16T18:54:59.261Z
...
Request after sigterm: / 2016-10-16T18:55:00.064Z
Request after sigterm: /health?type=readiness 2016-10-16T18:55:00.820Z
HEALTH: NOT OK
Request after sigterm: /health?type=readiness 2016-10-16T18:55:02.784Z
HEALTH: NOT OK
Request after sigterm: /health?type=liveness 2016-10-16T18:55:04.781Z
HEALTH: NOT OK
Request after sigterm: /health?type=readiness 2016-10-16T18:55:04.800Z
HEALTH: NOT OK
Server is shutting down... 2016-10-16T18:55:05.210Z
Successful graceful shutdown 2016-10-16T18:55:05.212Z
Kết quả benchmark
Success!
Không có request lỗi: bạn có thể thấy trong log ứng dụng service đã dừng gửi các data traffic đến pod trước khi chúng ta disconnect từ DB và dừng ứng dụng.
Các kết nối keepalive
Kubernetes không bàn giao các kết nối keepalive một cách đúng đắn.
Điều này có nghĩa là các request từ agent với header keepalive vẫn được điều hướng đến pod.
Tôi đã có sự sai sót khi lần đầu tiên benchmark với autocannon hay Google Chrome
(họ sử dụng các kết nối keepalive).
Kết nối keepalive ngăn chặn việc đóng server đúng lúc. Để ép một process thoát ra, bạn có thể sử dụng module server-destroy. Một khi nó đã chạy, bạn có thể chắc chắn rằng tất cả các ongoing request được phục vụ. Bạn cũng có thể thêm logic timeout vào server.close(cb)
Tín hiệu Docker
Việc ứng dụng của bạn bỗng nhiên không nhận được các tín hiệu đúng từ ứng dụng docker-hóa hoàn toàn có thể xảy ra.
Ví dụ trong ảnh Alpine: CMD ["node", "src"]
chạy, nhưng CMD ["npm", "start"]
thì không. Lý do vì SIGTERM
không được truyền vào trong node process. Kết quả gần như tương đồng với PR: https://github.com/npm/npm/pull/10868
Để sửa lỗi break tín hiệu Docker bạn có thể sử dụng dumb-init như một giải pháp thay thế.
Kết luận
Hãy luôn luôn tâm niệm rằng ứng dụng cần phải được dừng đúng cách - nó sẽ giải phóng tất cả tài nguyên và giúp ta kiểm soát được khối lượng dữ liệu gửi tới phiên bản mới của ứng dụng.
Xem repository ví dụ với Node.js và Kubernetes: https://github.com/RisingStack/kubernetes-graceful-shutdown-example
Một ứng dụng được dừng đúng cách khi nó giải phóng tất cả tài nguyên và chuyển giao được khối lượng data được gửi tới phiên bản ứng dụng mới. @RisingStack
Bài viết nguồn: https://blog.risingstack.com/graceful-shutdown-node-js-kubernetes/?utm_source=nodeweekly&utm_medium=email
Bình luận