Kinh nghiệm viết mã (cho hệ thống) yêu cầu độ khả dụng cao


Dịch từ bài viết Lessons learned writing highly available code của Jacob Greenleaf

Sau hơn hai năm làm việc ở Imgur, tôi đã phải học rất nhiều về các nguyên lý trong việc viết các hệ thống có độ khả dụng cao (không phải AP) và khả năng phục hồi sau sự cố. Thỉnh thoảng khi một vài hệ thống gặp sự cố, tôi vẫn thức dậy như mọi ngày và chỉ đến lúc tới công ty mới nhận ra rằng đêm qua, một công cụ tự vệ do chúng tôi cài đặt đã tự động kích hoạt, hoặc hệ thống bị lỗi và đã khôi phục thành công. Những lúc như vậy tôi cảm thấy thật sự biết ơn các nguyên tắc thiết kế đúng đắn. Và đây là một vài trong số đó mà tôi đặc biệt lưu ý:

1. Đặt giới hạn cho mọi thứ

Bạn có một queue cho việc xử lý hàng loạt các items? Nó có nhất thiết dài vô hạn? Hãy đặt một giới hạn trên cho nó - 1 triệu items chẳng hạn, và bắt đầu loại bỏ các items mới nhất hoặc cũ nhất. Khi kết nối đến một dịch vụ khác qua mạng, bạn có nhất định phải giữ kết nối vô thời hạn? Hãy thêm thời gian chờ! Các kết nối đến máy chủ của bạn có cần phải duy trì vĩnh viễn, hay chỉ 5 phút là quá đủ để kết thúc chúng? Chắc bạn đã có câu trả lời cho mình.

Imgur có một cron job gọi là "long query killer", nó làm nhiệm vụ quét các queries đang hoạt động trên MySQL được tạo ra từ các request của người dùng, kiểm tra xem chúng chạy trong bao lâu, và xóa những queries vượt qua một ngưỡng nhất định. Vì PHP times out (max_execution_time) sau 30 giây, không nên có query nào vẫn chạy sau vài phút. Có thể chỉ một mình công cụ query killer đó đã ngăn chặn giúp chúng tôi nhiều đêm không ngủ. Nếu hệ thống của bạn chưa có, hãy xây dựng một cái. Tương lai, bạn sẽ biết ơn quyết định của bản thân lúc này.

2. Thử lại, với thời gian chờ tăng theo cấp số nhân

Jim Gray viết trong Why Do Computers Stop and What Can Be Done About It rằng "hầu hết các sự cố khi chạy phần mềm mang tính nhất thời. Thực hiện lại tác vụ bị lỗi thường sẽ thành công." Với những lỗi chỉ tồn tại trong chốc lát này chúng ta có thể tăng độ khả dụng của hệ thống bằng cách thêm tính năng thử lại (các tác vụ bị lỗi). Tuy nhiên, nếu bất cẩn, bạn có thể DDOS chính mình! Hãy làm theo nguyên tắc (1), chỉ thử lại một số lần nhất định và tăng dần khoảng thời gian giữa các lần thử để đảm bảo việc phân tải mang tính liên tục.

3. Sử dụng các tiến trình supervisors và giám sát

Erlang, ngôn ngữ thường được dùng cho các phần mềm yêu cầu độ khả dụng rất cao như viễn thông, có một design pattern về supervisor (giám sát viên): mọi tác vụ mà chương trình thực hiện được cấu trúc theo cách mà tác vụ đó luôn nằm dưới sự theo dõi của một tác vụ chủ quản. Nếu supervisor phát hiện một tác vụ dừng ngoài ý muốn, nó sẽ khởi động lại tác vụ đó (tương tự như nguyên tắc (2)) từ một trạng thái được biết chắc là tốt. Sẽ là vô nghĩa nếu chạy lại một thứ chắc chắn sẽ hỏng! Monit là một công cụ tuyệt vời có khả năng tự động khởi động lại web server hay daemon process nếu chúng bị crash. Monit cũng là một giải pháp thay thế (cho supervisor của Erlang) tuyệt vời cho hầu hết các ngôn ngữ, và cả Erlang VM nếu như bạn không muốn dùng heart.

4. Thêm health checks, và sử dụng chúng để tái định tuyến requests hoặc tự động hóa rollbacks

Là một nhà phát triển, hãy nghĩ về việc đưa tất cả các biến số trong hệ thống của bạn thành một biến boolean duy nhất "hệ thống có đang khỏe mạnh không? có đang hoạt động không?" Tại Imgur, chúng tôi sử dụng tính năng tuyệt vời có sẵn của ELB: giám sát tình trạng sức khỏe của các cổng, để nhanh chóng và tự động điều hướng (requests) tránh các instances đã ngừng hoạt động.

5. Redundancy không phải chỉ là có-thì-tốt, mà là yêu cầu bắt buộc

Nếu bạn đang sử dụng các dịch vụ điện toán đám mây thì các instances của bạn có thể chết bất cứ lúc nào. Vì vậy, redundancy không phải chỉ là có-thì-tốt, mà là một yêu cầu bắt buộc cho các hệ thống có khả năng chịu lỗi.

Email yêu thích của tôi. Cảm ơn Amazon.

Đôi khi, Amazon có lòng tốt gửi cho bạn thông báo trước hai tuần như ảnh trên. Những lần khác, bạn sẽ không may mắn như vậy; có một lần, chúng tôi chỉ nhận được thông báo như trên sau khi instance đã bị xóa sổ (sau cả cảnh báo của PagerDuty và Nagios).

6. Ưu tiên các công cụ đã được kiểm chứng hơn "những ngôi sao mới"

Sẽ thật là hấp dẫn khi tiếp cận với các công cụ mới nhất, được hứa hẹn nhiều tính năng tân tiến. Chẳng hạn như CoreOS đang thổi một làn gió mới vào cộng đồng DevOps bằng việc sử dụng các LXC-style containers gọn nhẹ, nhưng trong thử nghiệm của tôi, một thành phần cốt lõi (FleetD) có một lỗ hổng đáng kinh ngạc trong cách nó xử lý việc thiết lập lịch trình trong một số trường hợp và có thể dẫn tới việc gián đoạn dịch vụ. Trong trường hợp của mình, tôi đã thôi làm việc với CoreOS sau hàng giờ tìm hiểu nguyên nhân khiến server ngừng hoạt động, vào lúc 2:00 sáng. Các công nghệ mới thường có nhiều thiếu sót mà cả bạn và họ (những người tạo ra và phát triển công nghệ đó) chưa biết tới.

Các công cụ mới cũng thường thiếu các cơ sở vững chắc để hoạt động trong production. Ví dụ, Golang thiếu một debugger chính thức, và cho tới cách đây vài tháng, không có một debugger nào, ngay cả trong cộng đồng mã nguồn mở. Các công cụ tracing và giám sát trong Go runtime còn cách một khoảng xa so với Java JMX và Erlang.

Còn bạn? Bí quyết tăng thời gian uptime yêu thích của bạn là gì?