Những thuật toán mô phỏng cuộc sống

Kể từ khóa học khoa học máy tính đầu tiên của tôi ở trường đại học, tôi đã quan tâm đến cách các thuật toán diễn ra trong thế giới thực. Khi chúng ta nghĩ về một số điều xảy ra trong thế giới thực, chúng ta có thể đưa ra các thuật toán mô phỏng những điều đó. Đặc biệt tôi thường làm điều này khi phải xếp hàng, chẳng hạn như khi xếp hàng trong cửa hàng tạp hóa, khi tắc đường hoặc tại sân bay. Tôi nhận thấy sự nhàm chán khi xếp hàng mang đến những cơ hội tuyệt vời để suy ngẫm về lý thuyết xếp hàng.

Hơn một thập kỷ trước, tôi đã dành một ngày làm việc tại trung tâm hoàn thiện đơn hàng của Amazon. Tôi được hướng dẫn bởi một thuật toán, lấy các mặt hàng từ kệ, di chuyển các mặt hàng từ hộp này sang hộp khác, di chuyển các thùng xung quanh. Làm việc song song cùng rất nhiều người khác, tôi thấy thật tuyệt vời khi được là một phần của hình thức về cơ bản là hợp nhất vật lý hài hòa một cách đáng kinh ngạc.

Trong lý thuyết xếp hàng, hành vi của hàng đợi ngắn thường không mấy thú vị. Tóm lại, khi hàng đợi ngắn, mọi người đều vui vẻ. Chỉ khi hàng đợi bị ùn đống, khi dòng người xếp hàng đến một sự kiện tràn ra ngoài cửa và tới tận góc đường, mọi người mới bắt đầu nghĩ về thông lượng và ưu tiên.

Trong bài viết này, tôi sẽ thảo luận về các chiến lược mà chúng tôi sử dụng tại Amazon để giải quyết các kịch bản tồn đọng hàng đợi - phương pháp thiết kế mà chúng tôi thực hiện để rút ngắn hàng đợi một cách nhanh chóng và ưu tiên khối lượng công việc. Quan trọng nhất, tôi sẽ mô tả cách ngăn chặn tồn đọng hàng đợi hình thành ngay từ đầu. Trong nửa đầu, tôi sẽ mô tả các kịch bản dẫn đến tồn đọng và trong nửa sau, tôi sẽ mô tả nhiều cách tiếp cận được sử dụng tại Amazon để tránh tồn đọng hoặc xử lý tồn đọng một cách hài hòa.

Bản chất hai mặt của hàng đợi

Hàng đợi là công cụ mạnh mẽ để xây dựng các hệ thống bất đồng bộ đáng tin cậy. Hàng đợi cho phép một hệ thống chấp nhận tin nhắn từ một hệ thống khác và duy trì tin nhắn đó cho đến khi tin nhắn được xử lý hoàn chỉnh, ngay cả khi xảy ra tình trạng ngừng hoạt động kéo dài, lỗi máy chủ hoặc gặp sự cố với các hệ thống phụ thuộc. Thay vì việc bỏ tin nhắn khi xảy ra lỗi, hàng đợi sẽ đẩy lại các tin nhắn cho đến khi tin nhắn được xử lý thành công. Cuối cùng, hàng đợi sẽ làm tăng độ bền và tính khả dụng của hệ thống, đổi lại là độ trễ đôi khi tăng do hoạt động thử lại.
 
Tại Amazon, chúng tôi xây dựng nhiều hệ thống bất đồng bộ tận dụng hàng đợi. Một vài trong số các hệ thống này xử lý các quy trình công việc có thể đòi hỏi nhiều thời gian và liên quan đến những đối tượng chuyển động thực trên thế giới, như việc hoàn thành đơn hàng trên amazon.com. Các hệ thống khác điều phối các bước cũng có thể đòi hỏi một khoảng thời gian không hề nhỏ. Ví dụ: Amazon RDS yêu cầu phiên bản EC2, đợi các phiên bản này khởi chạy, rồi đặt cấu hình cho cơ sở dữ liệu cho bạn. Các hệ thống khác tận dụng việc chia lô. Ví dụ: Các hệ thống liên quan đến việc tải nhập số liệu và nhật ký CloudWatch sẽ thu thập một loạt dữ liệu, rồi tổng hợp và “làm phẳng” dữ liệu thành các khúc dữ liệu.
 
Trong khi có thể dễ dàng thấy được lợi ích của hàng đợi đối với việc xử lý tin nhắn theo cách không đồng bộ, rủi ro của việc sử dụng hàng đợi lại khó thấy hơn. Trong nhiều năm qua, chúng tôi nhận thấy việc xếp hàng nhằm mục đích nâng cao tính khả dụng có thể gây tác dụng ngược. Trên thực tế, hàng đợi có thể làm tăng đáng kể thời gian phục hồi sau trạng thái ngừng hoạt động.
 
Trong hệ thống dựa trên hàng đợi, khi quá trình xử lý dừng nhưng tin nhắn vẫn đến, tin nhắn chờ xử lý có thể tích lũy thành một lượng tồn đọng lớn, làm tăng thời gian xử lý. Công việc có thể được hoàn thành quá muộn dẫn đến kết quả không còn hữu ích, về cơ bản gây ảnh hưởng đến tính khả dụng là mục tiêu mà việc xếp hàng đang hướng đến.
 
Nói theo một cách khác, hệ thống dựa trên hàng đợi có hai chế độ vận hành, hay là hành vi hai mốt. Khi không có tồn đọng trong hàng đợi, độ trễ của hệ thống thấp và hệ thống ở chế độ nhanh. Nhưng nếu lỗi hoặc kiểu tải ngoài dự kiến làm cho tốc độ đến vượt quá tốc độ xử lý, hệ thống sẽ nhanh chóng chuyển sang chế độ vận hành bất lợi hơn. Ở chế độ này, độ trễ đầu cuối tăng lên ngày càng cao và có thể mất rất nhiều thời gian để giải quyết tồn đọng trước khi trở về chế độ nhanh.

Hệ thống dựa trên hàng đợi

Để minh họa cho các hệ thống dựa trên hàng đợi trong bài viết này, tôi sẽ giải thích cách thức hoạt động của hai dịch vụ AWS: AWS Lambda, dịch vụ thực thi mã của bạn để phản hồi cho các sự kiện trong khi bạn không cần quan tâm về cơ sở hạ tầng mà mã đó hoạt động; và AWS IoT Core, dịch vụ được quản lý cho phép các thiết bị được kết nối tương tác dễ dàng và an toàn với các ứng dụng đám mây cũng như các thiết bị khác.

Với AWS Lambda, bạn tải lên mã hàm, rồi gọi các hàm theo một trong hai cách:

• Đồng bộ: trả về đầu ra cho hàm của bạn trong phản hồi HTTP
• Bất đồng bộ: trả về phản hồi HTTP ngay lập tức và hàm của bạn được thực thi và thử lại ở phía sau

Lambda đảm bảo rằng hàm của bạn được chạy, ngay cả khi gặp lỗi máy chủ, vì vậy cần có một hàng đợi bền vững để lưu trữ yêu cầu của bạn. Với một hàng đợi bền vững, yêu cầu của bạn có thể được đẩy lại nếu hàm của bạn thất bại lần đầu tiên.

Với AWS IoT Core, các thiết bị và ứng dụng của bạn sẽ kết nối và có thể đăng ký theo dõi các chủ đề tin nhắn PubSub. Khi một thiết bị hoặc ứng dụng phát hành tin nhắn, các ứng dụng có đăng ký phù hợp sẽ nhận được bản sao riêng của tin nhắn đó. Phần lớn việc nhắn tin PubSub này xảy ra không đồng bộ, vì thiết bị IoT bị ràng buộc không muốn sử dụng các tài nguyên giới hạn của mình để chờ và đảm bảo rằng tất cả các thiết bị, ứng dụng cũng như hệ thống đã đăng ký đều nhận được một bản sao. Điều này đặc biệt quan trọng vì một thiết bị đã đăng ký có thể đang ngoại tuyến khi một thiết bị khác phát hành tin nhắn mà thiết bị đó quan tâm. Khi thiết bị ngoại tuyến đó kết nối lại, thiết bị dự kiến sẽ được tăng tốc trở lại trước, rồi mới nhận tin nhắn được gửi đến (để biết thông tin về việc mã hóa hệ thống nhằm quản lý hoạt động gửi tin nhắn sau khi kết nối lại, xem Phiên liên tục MQTT trong Hướng dẫn dành cho nhà phát triển AWS IoT). Có nhiều kiểu liên tục và xử lý bất đồng bộ diễn ra phía sau để thực hiện việc này.

Các hệ thống dựa trên hàng đợi như thế này thường được triển khai với một hàng đợi bền vững. SQS cung cấp ngữ nghĩa gửi tin nhắn bền vững, có thể mở rộng, ít nhất một lần, vì vậy đội ngũ Amazon bao gồm Lambda và IoT thường xuyên sử dụng SQS khi xây dựng các hệ thống bất đồng bộ có thể mở rộng. Trong hệ thống dựa trên hàng đợi, một thành phần sẽ tạo dữ liệu bằng cách đưa tin nhắn vào hàng đợi và một thành phần khác sẽ tiêu thụ dữ liệu đó bằng cách định kỳ yêu cầu tin nhắn, xử lý tin nhắn và cuối cùng xóa tin nhắn sau khi xử lý xong.

Các lỗi trong hệ thống bất đồng bộ

Trong AWS Lambda, nếu hành động gọi hàm của bạn chậm hơn bình thường (ví dụ như do phụ thuộc) hoặc nếu hành động không thành công trong thời gian ngắn, không có dữ liệu nào bị mất và Lambda sẽ thử thực hiện lại hàm của bạn. Lambda xếp hàng các hành động gọi của bạn và khi hàm này bắt đầu hoạt động trở lại, Lambda sẽ giải quyết công việc tồn đọng của hàm. Nhưng hãy xem mất bao lâu để giải quyết tồn đọng và trở lại bình thường.

Hãy tưởng tượng hệ thống gặp sự cố ngừng hoạt động kéo dài một giờ trong khi đang xử lý tin nhắn. Không liên quan đến tốc độ và khả năng xử lý đã có, việc phục hồi sau khi ngừng hoạt động đòi hỏi gấp đôi công suất hệ thống trong một giờ sau khi phục hồi. Trong thực tế, hệ thống có thể có công suất khả dụng nhiều hơn gấp đôi, đặc biệt là với các dịch vụ linh hoạt như Lambda và việc phục hồi có thể diễn ra nhanh hơn. Mặt khác, các hệ thống khác mà hàm của bạn tương tác có thể không được chuẩn bị để xử lý sự gia tăng lớn trong quá trình xử lý khi bạn giải quyết tồn đọng. Khi điều này xảy ra, có thể mất nhiều thời gian hơn để bắt kịp. Các dịch vụ bất đồng bộ sẽ tích lũy tồn đọng trong thời gian ngừng hoạt động, dẫn đến thời gian phục hồi kéo dài, không giống như các dịch vụ đồng bộ thường loại bỏ yêu cầu trong thời gian ngừng hoạt động nhưng có thời gian phục hồi nhanh hơn.

Trong nhiều năm, khi nghĩ đến việc xếp hàng, đôi khi chúng tôi đã bị cám dỗ khi nghĩ rằng độ trễ không quan trọng đối với các hệ thống bất đồng bộ. Các hệ thống bất đồng bộ thường được xây dựng để đảm bảo độ bền vững hoặc tách người gọi tức thì khỏi độ trễ. Tuy nhiên, trong thực tế, chúng tôi thấy rằng thời gian xử lý cũng quan trọng và thường thì ngay cả các hệ thống bất đồng bộ cũng được kỳ vọng sẽ có độ trễ dưới một giây hoặc ít hơn. Khi hàng đợi được áp dụng để mang đến độ bền vững, mọi người dễ dàng quên đi họ phải đánh đổi với độ trễ xử lý cao trong trường hợp gặp phải tồn đọng. Rủi ro tiềm ẩn của các hệ thống bất đồng bộ là phải đối mặt với các tồn đọng lớn.

Cách chúng tôi đo lường tính khả dụng và độ trễ

Cuộc thảo luận về việc đánh đổi độ trễ với tính khả dụng này đặt ra một câu hỏi thú vị: Làm thế nào để đo lường và đặt mục tiêu về độ trễ và tính khả dụng cho dịch vụ bất đồng bộ? Việc đo lường tỷ lệ lỗi từ góc độ nhà sản xuất giúp chúng tôi quan sát được phần nào bức tranh về tính khả dụng, nhưng không phải là tất cả. Tính khả dụng của nhà sản xuất tỷ lệ thuận với tính khả dụng hàng đợi của hệ thống mà chúng tôi đang sử dụng. Vì vậy, khi chúng tôi xây dựng dựa trên SQS, tính khả dụng của nhà sản xuất phù hợp với tính khả dụng của SQS.

Mặt khác, nếu chúng tôi đo lường tính khả dụng ở phía người tiêu dùng thì tính khả dụng của hệ thống có vẻ tệ hơn thực tế, bởi vì các lỗi có thể được thử lại và rồi thành công trong lần thử tiếp theo.

Chúng tôi cũng nhận được số liệu đo lường về tính khả dụng từ hàng đợi thư chết (DLQ). Nếu một tin nhắn hết số lần thử lại, tin nhắn đó sẽ bị loại bỏ hoặc đưa vào DLQ. DLQ chỉ đơn giản là một hàng đợi riêng được dùng cho việc lưu trữ tin nhắn không thể xử lý để điều tra và can thiệp sau này. Tỷ lệ tin nhắn bị loại bỏ hoặc tin nhắn DLQ là một phép đo tốt về tính khả dụng, nhưng có thể phát hiện vấn đề quá muộn. Mặc dù báo động về khối lượng DLQ là ý hay, nhưng thông tin DLQ đến quá muộn nên chúng tôi không thể dựa hoàn toàn vào thông tin này để phát hiện vấn đề.

Còn độ trễ thì sao? Một lần nữa, độ trễ do nhà sản xuất quan sát phản ánh độ trễ của chính dịch vụ hàng đợi của chúng tôi. Do đó, chúng tôi tập trung nhiều hơn vào việc đo lường thời gian đã tồn tại của các tin nhắn trong hàng đợi. Việc này giúp nhanh chóng nắm bắt các trường hợp hệ thống bị trễ hoặc thường xuyên báo lỗi ngay lập tức và dẫn đến các lần thử lại. Các dịch vụ như SQS cung cấp nhãn thời gian khi mỗi tin nhắn đến hàng đợi. Với thông tin nhãn thời gian, mỗi khi chúng tôi đưa tin nhắn ra khỏi hàng đợi, chúng tôi có thể ghi lại và đưa ra số liệu về độ trễ của hệ thống.

Tuy nhiên, vấn đề độ trễ có thể đa dạng hơn một chút. Tóm lại, tồn đọng sẽ xuất hiện và trên thực tế, điều đó là bình thường đối với một số tin nhắn. Ví dụ: Trong AWS IoT, có đôi khi thiết bị được dự kiến là sẽ ngoại tuyến hoặc chậm đọc tin nhắn. Điều này là do nhiều thiết bị IoT có công suất thấp và có kết nối internet không ổn định. Với vai trò là nhà vận hành AWS IoT Core, chúng tôi cần phân biệt được tồn đọng nhỏ nằm trong dự kiến do các thiết bị ngoại tuyến hoặc chọn đọc tin nhắn chậm gây ra và tồn đọng ngoài dự kiến trên toàn hệ thống.

Trong AWS IoT, chúng tôi điều phối dịch vụ bằng một số liệu khác: AgeOfFirstAttempt. Phép đo này ghi lại thời gian hiện tại trừ đi thời gian tin nhắn được xếp vào hàng đợi, nhưng chỉ khi đây là lần đầu tiên AWS IoT tìm cách gửi tin nhắn đến thiết bị. Bằng cách này, khi các thiết bị được sao lưu, chúng tôi có một số liệu sạch không bị ảnh hưởng bởi các thiết bị đang cố gửi lại tin nhắn hoặc đưa tin nhắn vào hàng đợi. Để làm số liệu này sạch hơn nữa, chúng tôi phát hành số liệu thứ hai – AgeOfFirstSubscriberFirstAttempt. Trong hệ thống PubSub như AWS IoT, không có giới hạn thực tế về số lượng thiết bị hoặc ứng dụng có thể đăng ký theo dõi một chủ đề cụ thể, do đó độ trễ khi gửi tin nhắn đến một triệu thiết bị sẽ cao hơn so với khi gửi đến một thiết bị. Để tạo cho mình một số liệu ổn định, chúng tôi phát hành số liệu đo thời gian cho lần thử phát hành tin nhắn đầu tiên đến đối tượng đầu tiên đăng ký theo dõi chủ đề đó. Sau đó, chúng tôi có các số liệu khác để đo lường tiến trình hệ thống phát hành các tin nhắn còn lại.

Số liệu AgeOfFirstAttempt đóng vai trò cảnh báo sớm cho vấn đề toàn hệ thống, chủ yếu vì số liệu này sẽ lọc độ nhiễu đến từ các thiết bị đang chọn đọc tin nhắn một cách chậm hơn. Điều đáng nói là các hệ thống như AWS IoT được trang bị nhiều số liệu hơn thế này. Dù với tất cả các số liệu có sẵn liên quan đến độ trễ, chiến lược phân loại độ trễ của lần thử đầu tiên tách biệt với độ trễ của các lần thử lại vẫn thường được sử dụng tại Amazon.

Đo độ trễ và tính khả dụng của các hệ thống bất đồng bộ là một thách thức và việc gỡ lỗi cũng có thể khó khăn, bởi vì các yêu cầu diễn ra thất thường giữa các máy chủ và có thể bị trì hoãn ở những vị trí nằm ngoài mỗi hệ thống. Để giúp theo dõi phân tán, chúng tôi phân bổ một id yêu cầu trong các tin nhắn được xếp hàng để có thể xâu chuỗi mọi thứ lại với nhau. Chúng tôi cũng thường sử dụng những hệ thống như X-Ray để trợ giúp việc này.

Tồn đọng trong các hệ thống bất đồng bộ nhiều đối tượng thuê

Nhiều hệ thống bất đồng bộ là hệ thống nhiều đối tượng thuê, xử lý công việc thay mặt cho nhiều khách hàng khác nhau. Điều này làm tăng tính phức tạp cho việc quản lý độ trễ và tính khả dụng. Nhiều đối tượng thuê giúp chúng tôi tiết kiệm chi phí hoạt động khi không phải vận hành riêng nhiều thiết bị và cho phép chúng tôi vận hành kết hợp các khối lượng công việc với mức tận dụng tài nguyên cao hơn nhiều. Tuy nhiên, khách hàng kỳ vọng hệ thống sẽ hoạt động giống như hệ thống đối tượng thuê riêng của họ, với độ trễ có thể dự đoán và tính khả dụng cao, bất kể khối lượng công việc của các khách hàng khác.

Dịch vụ AWS không hiển thị hàng đợi nội bộ của mình trực tiếp cho người gọi gửi tin nhắn đến. Thay vào đó, các dịch vụ này triển khai API gọn nhẹ để xác thực người gọi và nối thông tin người gọi vào từng tin nhắn trước khi đưa vào hàng đợi. Điều này tương tự với kiến trúc Lambda được mô tả trước đó: Khi bạn gọi một hàm không đồng bộ, Lambda sẽ đặt tin nhắn của bạn vào hàng đợi thuộc sở hữu của Lambda và trả về ngay, thay vì hiển thị trực tiếp hàng đợi nội bộ của Lambda cho bạn.

Các API gọn nhẹ này cũng cho phép chúng tôi điều chỉnh thêm về sự công bằng. Cần phải duy trì sự công bằng trong hệ thống nhiều đối tượng sao cho khối lượng công việc của khách hàng này không ảnh hưởng đến khách hàng khác. Cách phổ biến để AWS duy trì sự công bằng là đặt ra những giới hạn dựa trên tỷ lệ cho mỗi khách hàng, với một số khả năng linh hoạt để vượt mức cơ bản. Trong nhiều hệ thống của chúng tôi, ví dụ như trong chính SQS, chúng tôi tăng giới hạn cho mỗi khách hàng khi khách hàng tăng trưởng một cách hữu cơ. Giới hạn đóng vai trò là hàng rào bảo vệ trước những đột biến bất ngờ, cho phép chúng tôi có thời gian để thực hiện điều chỉnh dự phòng ở phía sau.

Ở một mức độ nào đó, tính công bằng trong các hệ thống bất đồng bộ hoạt động giống như việc điều tiết trong các hệ thống đồng bộ. Tuy nhiên, chúng tôi nghĩ rằng điều này quan trọng hơn nhiều đối với các hệ thống bất đồng bộ vì những tồn đọng lớn có thể hình thành rất nhanh.

Để minh họa, hãy xem xét những gì sẽ xảy ra nếu một hệ thống bất đồng bộ không được tích hợp đủ biện pháp bảo vệ trước môi trường nhiễu xung quanh. Nếu một khách hàng của hệ thống đột nhiên tăng lưu lượng truy cập mà không được điều tiết và tạo ra tồn đọng trên toàn hệ thống, có thể mất đến 30 phút để nhà điều hành tham gia, tìm hiểu những gì đang xảy ra và để giảm thiểu vấn đề này. Trong 30 phút đó, phía nhà sản xuất của hệ thống có thể đã mở rộng nhanh chóng và đưa tất cả tin nhắn vào hàng đợi. Nhưng nếu khối lượng tin nhắn được đưa vào hàng đợi gấp 10 lần dung lượng mà phía người tiêu dùng được mở rộng, điều này có nghĩa là sẽ mất 300 phút để hệ thống giải quyết tồn đọng và phục hồi. Ngay cả những đột biến khối lượng ngắn cũng có thể gây ra thời gian phục hồi kéo dài nhiều giờ, và do đó dẫn đến việc ngừng hoạt động trong nhiều giờ.

Trong thực tế, các hệ thống trong AWS có nhiều yếu tố bù để giảm thiểu hoặc ngăn ngừa các tác động tiêu cực từ những tồn đọng hàng đợi. Ví dụ: Tự động thay đổi quy mô giúp giảm thiểu các vấn đề khi khối lượng tăng. Tuy vậy, cũng cần xem xét riêng những ảnh hưởng của việc xếp hàng mà không xét đến các yếu tố bù, vì điều này sẽ hỗ trợ việc thiết kế các hệ thống đáng tin cậy theo nhiều lớp. Dưới đây là một vài mẫu thiết kế mà chúng tôi nhận thấy có thể giúp tránh những tồn đọng hàng đợi lớn và thời gian phục hồi kéo dài:

Bảo vệ tại mỗi lớp rất quan trọng đối với các hệ thống bất đồng bộ. Vì hệ thống đồng bộ không có xu hướng hình thành tồn đọng, nên chúng tôi sẽ bảo vệ các hệ thống này với việc điều tiết cửa trước và kiểm soát đầu vào. Trong hệ thống bất đồng bộ, mỗi thành phần trong hệ thống của chúng tôi cần tự bảo vệ mình khỏi sự quá tải và ngăn không để một khối lượng công việc tiêu thụ phần tài nguyên không hợp lý. Sẽ luôn có một số khối lượng công việc lách được qua kiểm soát tiếp nhận cửa trước, vì vậy chúng tôi cần một vành đai, bộ chặn và bộ bảo vệ nhỏ gọn để giữ cho các dịch vụ không bị quá tải.
Sử dụng nhiều hàng đợi giúp định hình lưu lượng. Ở một mức độ nào đó, một hàng đợi duy nhất và nhiều đối tượng thuê sẽ bất hòa với nhau. Khi công việc được xếp hàng trong một hàng đợi dùng chung, rất khó để tách biệt khối lượng công việc này với khối lượng công việc khác.
Các hệ thống thời gian thực thường được triển khai với hàng đợi FIFO (vào trước, ra trước), nhưng ưu tiên hành vi LIFO (vào sau, ra trước). Theo khách hàng của chúng tôi thì khi đối mặt với tồn đọng, họ muốn thấy dữ liệu mới được xử lý ngay lập tức. Mọi dữ liệu được tích lũy trong thời gian ngừng hoạt động hoặc tăng đột biến đều có thể được xử lý khi có khả năng.

Chiến lược của Amazon để tạo các hệ thống bất đồng bộ nhiều đối tượng thuê linh hoạt

Các hệ thống tại Amazon sử dụng một số mẫu để giúp các hệ thống bất đồng bộ nhiều đối tượng thuê trở nên linh hoạt trước những thay đổi về khối lượng công việc. Có nhiều kỹ thuật nhưng cũng có nhiều hệ thống được sử dụng trên khắp Amazon, mỗi hệ thống đều có yêu cầu riêng về độ sẵn sàng và độ bền vững. Trong phần sau, tôi sẽ mô tả một số mẫu chúng tôi sử dụng cũng như mẫu mà khách hàng AWS cho biết họ sử dụng trong hệ thống của mình.

Tách khối lượng công việc thành các hàng đợi riêng

Thay vì chia sẻ một hàng đợi cho tất cả khách hàng, trong một số hệ thống, chúng tôi cung cấp cho mỗi khách hàng một hàng đợi riêng. Việc thêm hàng đợi cho từng khách hàng hoặc khối lượng công việc không phải lúc nào cũng hiệu quả về chi phí, vì các dịch vụ sẽ cần phải tiêu tốn tài nguyên để kiểm soát vòng tất cả hàng đợi. Nhưng trong những hệ thống có ít khách hàng hoặc các hệ thống liền kề, giải pháp đơn giản này có thể giúp ích. Trái lại, nếu một hệ thống có hàng chục hoặc hàng trăm khách hàng thì hàng đợi riêng biệt có thể bộc lộ tính khó sử dụng. Ví dụ: AWS IoT không sử dụng hàng đợi riêng cho mỗi thiết bị IoT trong vũ trụ. Chi phí kiểm soát vòng sẽ không được điều chỉnh hợp lý trong trường hợp đó.

Phân mảnh xáo trộn

AWS Lambda là ví dụ về một hệ thống phải chịu chi phí cao nếu kiểm soát vòng hàng đợi riêng cho mỗi khách hàng Lambda. Tuy nhiên, việc có một hàng đợi duy nhất có thể dẫn đến một số vấn đề được mô tả trong bài viết này. Vì vậy, thay vì sử dụng một hàng đợi, AWS Lambda cung cấp một số lượng hàng đợi cố định và phân bổ mỗi khách hàng vào một số lượng nhỏ hàng đợi. Trước khi đưa tin nhắn vào hàng đợi, hệ thống sẽ kiểm tra xem hàng đợi nào trong số các hàng đợi được nhắm mục tiêu có chứa ít tin nhắn nhất và đưa tin nhắn vào hàng đợi đó. Khi khối lượng công việc của một khách hàng tăng sẽ dẫn đến tồn đọng trong hàng đợi được ánh xạ của khách hàng đó, nhưng khối lượng công việc khác sẽ tự động được chuyển khỏi các hàng đợi đó. Không cần quá nhiều hàng đợi để tích hợp một vài sự tách biệt tài nguyên tuyệt vời. Đây chỉ là một trong nhiều biện pháp bảo vệ được tích hợp trong Lambda, nhưng cũng là một kỹ thuật được sử dụng trong các dịch vụ khác tại Amazon.

Sắp xếp lưu lượng vượt quá vào một hàng đợi riêng

Ở một mức độ nào đó, khi tồn đọng hình thành trong hàng đợi thì đã quá muộn để ưu tiên lưu lượng. Tuy nhiên, nếu việc xử lý tin nhắn tương đối tốn kém hoặc mất thời gian thì vẫn tốt hơn nếu có thể di chuyển tin nhắn sang một hàng đợi tràn riêng biệt. Trong một số hệ thống ở Amazon, dịch vụ người tiêu dùng sẽ thực hiện việc điều tiết phân tán và khi các hệ thống này đưa tin nhắn của khách hàng đã vượt quá tốc độ được đặt cấu hình ra khỏi hàng đợi, hệ thống sẽ đưa các tin nhắn vượt quá đó vào các hàng đợi tràn riêng biệt và xóa tin nhắn khỏi hàng đợi chính. Hệ thống vẫn xử lý các tin nhắn trong hàng đợi tràn ngay khi có sẵn tài nguyên. Về bản chất, hàng đợi này gần giống hàng đợi ưu tiên. Đôi khi, phía nhà sản xuất cũng triển khai logic tương tự. Theo cách này, nếu hệ thống chấp nhận khối lượng lớn các yêu cầu từ một khối lượng công việc duy nhất thì khối lượng công việc đó sẽ không chiếm chỗ của các khối lượng công việc khác trong hàng đợi đường dẫn nhanh.

Sắp xếp lưu lượng cũ vào một hàng đợi riêng

Tương tự như việc sắp xếp lưu lượng vượt quá, chúng tôi cũng có thể sắp xếp lưu lượng cũ. Khi chúng tôi đưa tin nhắn ra khỏi hàng đợi, chúng tôi có thể kiểm tra thời gian tồn tại của tin nhắn đó. Thay vì chỉ ghi lại thời gian đã tồn tại, chúng tôi có thể sử dụng thông tin này để quyết định có nên chuyển tin nhắn vào hàng tồn đọng chỉ được giải quyết sau khi chúng tôi bắt kịp hàng đợi trực tiếp hay không. Nếu có tăng tải đột biến khi chúng tôi tải nhập nhiều dữ liệu và bị tụt lại phía sau, chúng tôi có thể sắp xếp làn sóng lưu lượng đó vào một hàng đợi khác trong khả năng đưa lưu lượng ra khỏi và vào lại hàng đợi nhanh nhất có thể. Việc này sẽ giải phóng để tài nguyên của người tiêu dùng xử lý các tin nhắn mới nhanh hơn so với việc chúng tôi chỉ giải quyết tồn đọng theo thứ tự. Đây là một cách để tiến gần với thứ tự LIFO.

Bỏ tin nhắn cũ (thời gian tin nhắn tồn tại)

Một số hệ thống có thể chấp nhận bỏ các tin nhắn quá cũ. Ví dụ: Một số hệ thống xử lý những biến đổi dữ liệu đối với hệ thống một cách nhanh chóng nhưng cũng thực hiện đồng bộ hóa hoàn toàn theo định kỳ. Chúng tôi thường gọi các hệ thống đồng bộ hóa định kỳ này là máy quét chống entropy. Trong những trường hợp này, thay vì sắp xếp lưu lượng đã xếp hàng cũ ra riêng, chúng tôi có thể bỏ qua lưu lượng này, không hề tốn kém, nếu nó đến trước lần quét gần đây nhất.

Giới hạn luồng (và các tài nguyên khác) trên mỗi khối lượng công việc

Giống như trong các dịch vụ đồng bộ của chúng tôi, chúng tôi thiết kế các hệ thống bất đồng bộ để ngăn chặn một khối lượng công việc sử dụng nhiều luồng hơn so với phần luồng hợp lý của khối lượng công việc đó. Một khía cạnh của AWS IoT mà chúng ta chưa nhắc đến là công cụ quy tắc. Khách hàng có thể đặt cấu hình để AWS IoT định tuyến tin nhắn từ thiết bị đến cụm Amazon Elaticsearch, Kinesis Stream, v.v. của họ. Nếu độ trễ đối với các tài nguyên thuộc sở hữu của khách hàng đó bị chậm, nhưng tốc độ tin nhắn đến không đổi, lượng tương tranh trong hệ thống sẽ tăng. Và do lượng tương tranh mà hệ thống có thể xử lý bị hạn chế ở mọi thời điểm, công cụ quy tắc sẽ ngăn chặn bất kỳ khối lượng công việc nào tiêu thụ nhiều hơn phần tài nguyên liên quan đến tương tranh hợp lý của mình.

Nguyên lý ở đây được mô tả trong Luật Little, theo đó tương tranh trong hệ thống bằng với tốc độ gửi đến nhân với độ trễ trung bình của mỗi yêu cầu. Ví dụ: Nếu máy chủ xử lý 100 tin nhắn/giây ở mức trung bình 100 ms thì máy chủ đó sẽ tiêu thụ trung bình 10 luồng. Nếu độ trễ đột ngột tăng lên 10 giây, máy chủ sẽ đột ngột sử dụng 1.000 luồng (mức trung bình, trên thực tế có thể nhiều hơn), có thể dễ dàng sử dụng hết một vòng luồng.

Công cụ quy tắc sử dụng một số kỹ thuật để ngăn điều này xảy ra. Công cụ này dùng I/O không chặn để tránh sử dụng hết luồng, mặc dù vẫn còn các giới hạn khác đối với số lượng công việc của một máy chủ nhất định (ví dụ: trình mô tả tệp và bộ nhớ khi máy khách chuyển qua các kết nối và hết thời gian phụ thuộc). Phương pháp bảo vệ tương tranh thứ hai có thể được sử dụng là mã hiệu. Mã hiệu sẽ đo lường và giới hạn lượng tương tranh có thể được sử dụng cho mọi khối lượng công việc đơn lẻ tại bất kỳ thời điểm nào. Công cụ quy tắc cũng sử dụng giới hạn công bằng dựa trên tỷ lệ. Tuy nhiên, vì khối lượng công việc hoàn toàn có thể thay đổi theo thời gian nên công cụ quy tắc cũng tự động điều chỉnh giới hạn theo thời gian để thích ứng với những thay đổi trong khối lượng công việc. Và vì công cụ quy tắc được dựa trên hàng đợi nên công cụ này hoạt động như một bộ đệm giữa các thiết bị IoT và khả năng tự động điều chỉnh quy mô tài nguyên cũng như giới hạn bảo vệ an toàn ở phía sau.

Trên các dịch vụ tại Amazon, chúng tôi sử dụng các vòng luồng riêng biệt cho mỗi khối lượng công việc để tránh một khối lượng công việc tiêu thụ tất cả các luồng hiện có. Chúng tôi cũng sử dụng AtomicInteger cho mỗi khối lượng công việc để hạn chế tương tranh cho phép cho từng khối lượng công việc và các phương pháp điều chỉnh dựa trên tỷ lệ để tách riêng các tài nguyên dựa trên tỷ lệ.

Gửi ngược dòng

Nếu khối lượng công việc đang gây ra tồn đọng không hợp lý mà người tiêu dùng không thể theo kịp, nhiều hệ thống của chúng tôi sẽ tự động bắt đầu từ chối công việc một cách mạnh mẽ hơn ở phía nhà sản xuất. Không khó để một khối lượng công việc gây ra tồn đọng dài cả ngày. Ngay cả khi đã tách biệt khối lượng công việc thì để vượt qua được cũng có thể cần đến sự ngẫu nhiên và tốn kém. Việc triển khai phương pháp này có thể đơn giản chỉ là thỉnh thoảng đo độ sâu hàng đợi của một khối lượng công việc (giả sử khối lượng công việc nằm trên hàng đợi của chính nó) và điều chỉnh giới hạn điều tiết trong (tỷ lệ nghịch) theo kích thước tồn đọng.

Trong trường hợp chúng tôi chia sẻ hàng đợi SQS cho nhiều khối lượng công việc, phương pháp này sẽ trở nên khó khăn. Mặc dù có API SQS trả về số lượng tin nhắn trong hàng đợi, nhưng không có API nào có thể trả về số lượng tin nhắn với một thuộc tính cụ thể trong hàng đợi. Chúng tôi vẫn có thể đo độ sâu của hàng đợi và áp dụng áp lực ngược tương ứng nhưng việc này sẽ đặt áp lực ngược không công bằng lên những khối lượng công việc không liên quan tình cờ nằm trong cùng một hàng đợi. Các hệ thống khác như Amazon MQ có khả năng quan sát tồn đọng chi tiết hơn.

Áp lực ngược không phù hợp với tất cả hệ thống tại Amazon. Ví dụ: Trong các hệ thống thực hiện xử lý đơn hàng cho amazon.com, chúng tôi có xu hướng thích chấp nhận đơn hàng ngay cả khi hình thành tồn đọng, thay vì chặn không cho chấp nhận đơn hàng mới. Nhưng tất nhiên điều này sẽ đi kèm với rất nhiều ưu tiên phía sau sao cho những đơn hàng khẩn cấp nhất sẽ được xử lý trước.

Sử dụng hàng đợi trì hoãn để trì hoãn công việc xử lý đến lúc khác

Khi các hệ thống thấy rằng cần giảm thông lượng cho một khối lượng công việc cụ thể, chúng tôi cố gắng sử dụng chiến lược tạm tránh cho khối lượng công việc đó. Để thực hiện điều này, chúng tôi thường sử dụng tính năng SQS có tác dụng trì hoãn việc gửi tin nhắn đến lúc khác. Khi chúng tôi xử lý tin nhắn và quyết định lưu tin nhắn đó để xử lý sau, đôi khi chúng tôi xếp lại tin nhắn đó vào một hàng đợi phát sinh riêng biệt, nhưng đặt tham số độ trễ để tin nhắn đó bị ẩn trong hàng đợi trễ trong vài phút. Điều này cho phép hệ thống có cơ hội xử lý dữ liệu mới hơn.

Tránh quá nhiều tin nhắn đang di chuyển

Một số dịch vụ hàng đợi như SQS có giới hạn về số lượng tin nhắn đang di chuyển có thể được gửi đến người tiêu dùng của hàng đợi. Đây không phải là số lượng tin nhắn có thể có trong hàng đợi (số lượng đó không có giới hạn thực tế) mà là số lượng tin nhắn mà nhóm người tiêu dùng đang xử lý cùng một lúc. Số lượng này có thể bị thổi phồng nếu hệ thống đưa các tin nhắn ra khỏi hàng đợi nhưng rồi lại không xóa được các tin nhắn đó. Ví dụ: Chúng tôi đã gặp các lỗi khi đó mã không phát hiện được lỗi trong khi xử lý tin nhắn và quên xóa tin nhắn. Trong trường hợp này, tin nhắn vẫn ở chế độ đang di chuyển từ góc nhìn của SQS đối với VisibilityTimeout của tin nhắn. Khi chúng tôi thiết kế chiến lược xử lý lỗi và quá tải, chúng tôi luôn ghi nhớ các giới hạn này và có xu hướng ưu tiên chuyển các tin nhắn vượt quá sang một hàng đợi khác thay vì tiếp tục hiển thị các tin nhắn đó.

Hàng đợi FIFO của SQS có giới hạn tương tự nhưng tinh vi hơn. Với FIFO của SQS, các hệ thống tiêu thụ tin nhắn theo thứ tự đối với một nhóm tin nhắn nhất định, nhưng tin nhắn của các nhóm khác nhau lại được xử lý theo thứ tự bất kỳ. Vì vậy, nếu chúng tôi có tồn đọng nhỏ trong một nhóm tin nhắn, chúng tôi sẽ tiếp tục xử lý tin nhắn trong các nhóm khác. Tuy nhiên, FIFO của SQS chỉ kiểm soát vòng 20k tin nhắn chưa được xử lý gần đây nhất. Vì vậy, nếu có hơn 20k tin nhắn chưa được xử lý trong một tập hợp con các nhóm tin nhắn, các nhóm tin nhắn khác có tin nhắn mới sẽ bị bỏ qua.

Sử dụng hàng đợi thư chết cho các tin nhắn không thể xử lý

Tin nhắn không thể xử lý có thể góp phần làm hệ thống quá tải. Nếu hệ thống đưa tin nhắn không thể xử lý vào hàng đợi (có thể vì tin nhắn kích hoạt trường hợp biên xác thực đầu vào) thì SQS có thể hỗ trợ bằng cách tự động di chuyển các tin nhắn này vào một hàng đợi riêng biệt với tính năng hàng đợi thư chết (DLQ). Chúng tôi sẽ báo động nếu có bất kỳ tin nhắn nào trong hàng đợi này, vì điều đó có nghĩa là có lỗi chúng tôi cần phải sửa. Lợi ích của DLQ là cho phép chúng tôi xử lý lại các tin nhắn này sau khi khắc phục được lỗi.

Đảm bảo bộ đệm bổ sung trong các luồng kiểm soát vòng trên mỗi khối lượng công việc

Nếu khối lượng công việc tạo ra đủ thông lượng để các luồng kiểm soát vòng luôn bận rộn ngay cả khi ở trạng thái ổn định thì hệ thống có thể đã đạt đến điểm không có bộ đệm để hấp thụ lưu lượng tăng đột biến. Ở trạng thái này, lưu lượng tăng đột biến nhỏ sẽ dẫn đến một lượng tồn đọng chưa được xử lý kéo dài, dẫn đến độ trễ cao hơn. Chúng tôi lập kế hoạch cho bộ đệm bổ sung trong các luồng kiểm soát vòng để hấp thụ những đột biến như vậy. Một phép đo là theo dõi số lần thử kiểm soát vòng dẫn đến phản hồi trống. Nếu mỗi lượt thử kiểm soát vòng truy xuất thêm một tin nhắn thì chúng tôi sẽ có vừa đúng số lượng luồng kiểm soát vòng hoặc có thể không có đủ để theo kịp lưu lượng đến.

Tạo xung cho những tin nhắn kéo dài

Khi hệ thống xử lý tin nhắn SQS, SQS sẽ cung cấp cho hệ thống đó một khoảng thời gian nhất định để hoàn tất việc xử lý tin nhắn trước khi giả định rằng hệ thống bị sập và gửi tin nhắn cho người tiêu dùng khác để thử lại. Nếu mã tiếp tục chạy và quên thời hạn này thì một tin nhắn có thể được gửi nhiều lần song song. Trong khi bộ xử lý đầu tiên vẫn đang chuyển tin nhắn đi sau khi hết thời gian chờ, bộ xử lý thứ hai sẽ nhận tin nhắn đó và tương tự sẽ chuyển đi sau thời gian chờ, và sau đó là bộ xử lý thứ ba, v.v.. Khả năng xảy ra yếu nguồn phân tầng này là lý do tại sao chúng tôi triển khai logic xử lý tin nhắn để ngừng hoạt động khi tin nhắn hết hạn hoặc tiếp tục tạo xung cho tin nhắn đó để nhắc nhở SQS rằng chúng tôi vẫn đang xử lý tin nhắn. Khái niệm này tương tự như cho thuê trong khi chọn đơn vị chỉ huy.

Đây là một vấn đề nan giải, bởi vì chúng tôi thấy rằng độ trễ của hệ thống có thể tăng lên khi quá tải, có thể từ các truy vấn đến cơ sở dữ liệu mất nhiều thời gian hơn hoặc từ các máy chủ chỉ đơn thuần đảm nhận nhiều công việc hơn so với khả năng xử lý của mình. Việc độ trễ hệ thống vượt qua ngưỡng VisibilityTimeout sẽ làm cho dịch vụ đã bị quá tải rơi vào tình trạng về bản chất là tự fork-bomb chính mình.

Lập kế hoạch gỡ lỗi chéo máy chủ

Chỉ riêng việc hiểu được lỗi trong hệ thống phân tán đã là rất khó rồi. Bài viết liên quan về việc điều phối mô tả một số cách tiếp cận của chúng tôi để điều phối các hệ thống bất đồng bộ, từ việc ghi lại độ sâu hàng đợi theo định kỳ, đến việc truyền bá “id theo dõi” và tích hợp với X-Ray. Hoặc, khi các hệ thống của chúng tôi có quy trình làm việc không đồng bộ phức tạp vượt ra ngoài hàng đợi SQS tầm thường, chúng tôi thường sử dụng dịch vụ quy trình công việc bất đồng bộ khác như Step Functions, dịch vụ cung cấp khả năng quan sát quy trình công việc và đơn giản hóa việc gỡ lỗi phân tán.

Kết luận

Trong hệ thống bất đồng bộ, chúng ta có thể dễ dàng bỏ quên tầm quan trọng của độ trễ. Tóm lại, các hệ thống bất đồng bộ được cho là đôi khi sẽ mất nhiều thời gian hơn, bởi vì chúng có một hàng đợi để thực hiện những thử lại đáng tin cậy. Tuy nhiên, các kịch bản quá tải và lỗi có thể tạo ra các tồn đọng lớn không thể vượt qua, từ đó dịch vụ không thể phục hồi trong một khoảng thời gian hợp lý. Các tồn đọng này có thể đến từ việc đưa vào hàng đợi một khối lượng công việc hoặc khách hàng với tốc độ cao bất ngờ, từ những khối lượng công việc trở nên tốn kém ngoài dự kiến khi xử lý hoặc từ độ trễ hay lỗi trong sự phụ thuộc.

Khi xây dựng hệ thống bất đồng bộ, chúng ta cần tập trung và dự đoán các kịch bản tồn đọng này và giảm thiểu chúng bằng cách sử dụng các kỹ thuật như ưu tiên, sắp xếp và áp lực ngược.

Đọc thêm

Thuyết hàng đợi
Luật Little
Luật Amdahl
• Little A Proof for the Queuing Formula: L = λW, Case Western, 1961
• McKenney, Stochastic Fairness Queuing, IBM, 1990
• Nichols and Jacobson, Controlling Queue Delay, PARC, 2011

Về tác giả

David Yanacek là Kỹ sư chính cấp cao làm việc với AWS Lambda. David là nhà phát triển phần mềm tại Amazon từ năm 2006, trước đây đã làm việc với Amazon DynamoDB và AWS IoT, cũng như các khung dịch vụ web nội bộ và các hệ thống tự động hóa nghiệp vụ nhóm. Một trong những hoạt động yêu thích của David tại nơi làm việc là thực hiện phân tích nhật ký và sàng lọc các số liệu hoạt động để tìm cách làm cho hệ thống vận hành ngày càng trơn tru hơn.

Chọn đơn vị chỉ huy trong các hệ thống phân tán Đo lường hệ thống phân tán để hiểu rõ về hoạt động vận hành