Đánh giá và tối ưu hiệu suất cụm Pulsar

Giới thiệu

Trong thời gian gần đây, tôi đang thực hiện công việc liên quan đến quản lý hệ thống MQ (Pulsar), bao gồm một phần là nâng cấp hàng loạt các chức năng liên quan đến hàng đợi tin nhắn như:

  • Tạo tự động một cụm kiểm tra.
  • Chạy một số bài kiểm thử để kiểm tra các chức năng mà chúng ta sử dụng trên môi trường sản xuất, đồng thời tạo báo cáo kết quả.
  • Mô phỏng kiểm tra tải, thu thập và phân tích kết quả.

Mục tiêu chính là xác định xem việc nâng cấp phiên bản mới có ảnh hưởng gì đến hoạt động hiện tại hay không.

Việc tạo cụm và chạy bài kiểm thử khá đơn giản, được thực hiện bằng cách sử dụng helm và SDK của k8s client.

Kiểm tra tải

Cái khó khăn hơn là kiểm tra tải. Mặc dù Pulsar cung cấp sẵn công cụ kiểm tra tải, nhưng tính năng còn hạn chế, chỉ hỗ trợ kiểm tra giới hạn cho một nhóm topic và đưa ra báo cáo kết quả. Tôi đã tham khảo quy trình kiểm tra tải từ phía nhà phát triển, đồng thời bổ sung thêm các chỉ số theo dõi thời gian thực để dễ dàng phân tích sự biến đổi về hiệu suất trong suốt quá trình kiểm tra.

Lỗi timeout ở client

Khi áp lực kiểm tra tăng lên, ví dụ như tăng thời gian kiểm tra hoặc số lượng luồng, client sẽ ném ra lỗi timeout khi gửi tin nhắn.

org.apache.pulsar.client.api.PulsarClientException$TimeoutException: 
The producer pulsar-test-212-20 can not send message to the topic persistent://my-tenant/my-ns/perf-topic-0 within given timeout : createdAt 82.964 seconds ago, firstSentAt 8.348 seconds ago, lastSentAt 8.348 seconds ago, retryCount 1

Lỗi này từng xuất hiện trong môi trường sản xuất vào giờ cao điểm, gây mất dữ liệu. Vì vậy, tôi đã tái hiện tình huống này và tìm hiểu nguyên nhân cũng như giải pháp khắc phục.

Phân tích mã nguồn client

Vì lỗi bắt nguồn từ client nên tôi bắt đầu xem xét từ điểm xảy ra lỗi. Quá trình và nguyên nhân không quá phức tạp, như sau:

Quy trình client:

  1. Khi client gửi tin nhắn, nó sẽ đưa tin nhắn vào một hàng đợi pending địa phương.
  2. Khi broker xử lý (ghi vào bookkeeper) và trả về ACK, hàng đợi pending sẽ xóa tin nhắn đầu tiên.
  3. Một nhiệm vụ định kỳ sẽ kiểm tra hàng đợi pending, nếu tin nhắn ở đầu hàng đợi đã quá thời gian (được cấu hình, mặc định là 30 giây), thì sẽ ném lỗi timeout.
  4. Nếu tin nhắn đầu hàng đợi đã hết hạn, toàn bộ hàng đợi sẽ bị xóa.

Quy trình broker:

  1. Khi nhận tin nhắn, broker sẽ gọi API của bookkeeper để ghi tin nhắn.
  2. Trong khi ghi tin nhắn, callback sẽ được kích hoạt.
  3. Sau khi ghi thành công, callback sẽ được thực thi, lúc này sẽ ghi lại độ trễ ghi tin nhắn và thông báo client rằng đã nhận ACK.
  4. Độ trễ ghi có thể được theo dõi qua chỉ số metric pulsar_broker_publish_latency.

Từ quy trình trên, nếu client không có biện pháp dự phòng, thì ở bước thứ tư sẽ dẫn đến mất tin nhắn. Đây không phải là sự cố của broker, mà do client cho rằng broker không thể xử lý kịp, do đó bỏ qua tin nhắn chưa được gửi.

Phân tích hiệu năng

Dựa trên phân tích trên, đặc biệt là quy trình ghi dữ liệu của broker, ta thấy rằng hiệu năng ghi dữ liệu chủ yếu phụ thuộc vào bookkeeper. Trong trường hợp lý tưởng, nếu độ trễ ghi của bookkeeper là 0ms, hiệu năng ghi của cụm gần như không giới hạn. Do đó, tôi tập trung vào các chỉ số liên quan đến bookkeeper trong quá trình kiểm tra.

CPU

Đầu tiên là CPU:

Từ biểu đồ, ta thấy CPU tăng rõ rệt trong quá trình kiểm tra. Vì vậy, tôi cần tìm ra nơi nào chiếm nhiều CPU nhất?

Tôi muốn cảm ơn công cụ arthas của Alibaba, nó giúp tạo biểu đồ flame rất dễ dàng.

Cách đơn giản nhất để tìm ra điểm nóng là xem hàm nào rộng nhất trên biểu đồ, thường là nơi gây ra vấn đề lớn nhất.

Trong biểu đồ này, không có hàm nào nổi bật về độ rộng, vì vậy không có hàm nào chiếm nhiều CPU.

Sau đó, dựa vào giám sát từ nhà cung cấp đám mây, tôi biết rằng CPU không bị giới hạn (giới hạn là 8 lõi).

Trong quá trình sử dụng arthas, tôi gặp một lỗi nhỏ: trong môi trường k8s, ứng dụng có thể không ghi pid vào đĩa, khiến không thể tìm thấy tiến trình Java.

$ java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.6.7
[INFO] Can not find java process. Try to pass <pid> in command line.
Please select an available pid.

Lúc này, bạn có thể dùng ps để lấy ID tiến trình, rồi truyền vào khi khởi động:

$ java -jar arthas-boot.jar 1

Thông thường, ID tiến trình là 1.

Đĩa cứng

Nếu CPU không phải là vấn đề, thì ta hãy xem xét đĩa cứng.

Trong quá trình kiểm tra, thời gian chờ IO tăng đáng kể so với yêu cầu bình thường. Để xác minh xem có phải là vấn đề của đĩa hay không, tôi thay thế loại đĩa sang SSD.

Thực tế cho thấy, dù là kiểm tra tải, SSD vẫn có độ trễ thấp hơn nhiều so với đĩa thường.

Khi độ trễ của đĩa giảm, theo phân tích trước đó, hiệu năng tổng thể của cụm nên tăng đáng kể. Tôi so sánh chỉ số TPS ghi trước và sau khi nâng cấp:

Sau khi nâng cấp, tốc độ ghi mỗi giây tăng từ 40k lên khoảng 80k, gần như gấp đôi (thật ra tiền là cách nhanh nhất để giải quyết vấn đề).

Tuy nhiên, ngay cả khi làm như vậy, trong kiểm tra tải cực đoan vẫn có thể xảy ra lỗi timeout, vì dù có cải thiện hiệu năng server, vẫn không thể tránh hoàn toàn độ trễ trong mọi giai đoạn.

Lỗi timeout trong quá trình nâng cấp

Một bước quan trọng khác cần kiểm tra là mô phỏng việc nâng cấp cụm khi có rất nhiều producer và consumer đang hoạt động. Điều này giúp đánh giá tác động đối với client.

Theo hướng dẫn của nhà phát triển, quy trình nâng cấp như sau:

  • Nâng cấp Zookeeper.
  • Tắt autorecovery.
  • Nâng cấp Bookkeeper.
  • Nâng cấp Broker.
  • Nâng cấp Proxy.
  • Bật autorecovery.

Bước quan trọng nhất là nâng cấp Broker và Proxy, vì đây là hai thành phần tương tác trực tiếp với client.

Thực chất, quy trình nâng cấp là dừng dịch vụ một cách an toàn, sau đó khởi động lại bằng phiên bản mới. Do đó, client sẽ nhận biết Broker bị ngắt kết nối và cố gắng kết nối lại. Nếu có thể kết nối nhanh, client sẽ không gặp vấn đề.

Trong quá trình kiểm tra của tôi, có khoảng 2000 producer gửi tin nhắn với tốc độ 1k/giây, trong vòng 30 phút, tất cả các thành phần đều được nâng cấp. Trong quá trình này, client có thể kết nối lại nhanh chóng mà không gặp lỗi hay mất tin nhắn.

Tuy nhiên, khi tốc độ gửi tăng, trong quá trình dừng Broker, lỗi timeout sẽ xuất hiện. Nguyên nhân là do client không thể kết nối lại trong 30 giây mặc định, dẫn đến tin nhắn bị tích tụ và vượt quá thời gian chờ.

Sau khi phân tích mã nguồn, tôi thấy rằng khi kết nối giữa client và Broker bị gián đoạn, client sẽ tự động kết nối lại. Việc chọn Broker cụ thể để kết nối lại do LookUpService xử lý, nó sẽ lấy metadata của topic để xác định IP + port của Broker.

Lý thuyết cho thấy nếu quá trình này diễn ra nhanh, client sẽ không nhận thấy sự gián đoạn.

Trong metadata chứa thông tin về bundle liên quan đến topic, giúp client kết nối lại và gửi tin nhắn.

Bundle là một nhóm topic được gán với một Broker.

Khi một Broker bị tắt, tất cả bundle của nó sẽ được chuyển sang các Broker khác thông qua cân bằng tải. Lúc này, client sẽ kết nối lại với Broker mới.

Có hai trường hợp làm chậm quá trình lấy metadata từ LookUpService:

Do tất cả các Broker đều là node có trạng thái, khi nâng cấp, chúng sẽ được khởi động từ node mới. Giả sử node được nâng cấp là broker-5, bundle của nó sẽ được chuyển sang broker-4. Khi đó, client sẽ kết nối lại với broker-4. Trong lúc đó, nếu node tiếp theo được nâng cấp là broker-4, client sẽ phải chờ cho đến khi bundle được chuyển sang node mới, ví dụ như broker-3, điều này làm kéo dài thời gian gửi lại tin nhắn, dẫn đến timeout nếu vượt quá thời gian chờ.

Một trường hợp khác là khi số lượng bundle lớn, thời gian cập nhật metadata vào zookeeper sẽ lâu hơn.

Vì vậy, tôi suy nghĩ xem liệu trong quá trình nâng cấp, có thể ưu tiên chuyển bundle sang Broker-0, sau đó khi tất cả nâng cấp xong, thực hiện cân bằng tải một lần nữa để giảm thiểu khả năng client phải kết nối lại.

Giải pháp

Để giải quyết lỗi timeout, có một số phương án như sau:

  1. Thay thế đĩa bookkeeper bằng SSD, giảm độ trễ ghi.
  2. Tăng số lượng node bookkeeper, tuy nhiên do bookkeeper có trạng thái nên việc mở rộng theo chiều ngang khá phức tạp, và việc thu nhỏ lại cũng khó khăn.
  3. Tăng thời gian chờ ghi của client, có thể cấu hình.
  4. Client cần có biện pháp dự phòng, bắt lỗi, ghi log hoặc lưu trữ, sau đó gửi lại tin nhắn.
  5. Thêm cảnh báo cho độ trễ ghi của bookkeeper.
  6. Spring vừa ra mắt Pulsar-starter, đã tích hợp sẵn các metrics liên quan đến producer, client có thể theo dõi và cảnh báo.

Trong số các giải pháp trên, phương án 4 là tốt nhất, hiệu quả cao, chi phí thấp. Tôi khuyến khích những ai chưa thực hiện nên bắt đầu viết try catch ngay.

Quá trình kiểm tra này mất khoảng một đến hai tuần, đây là lần đầu tiên tôi đánh giá toàn diện một hệ thống trung gian, và học được rất nhiều điều. Từ mã nguồn đến kiến trúc, tôi hiểu sâu hơn về Pulsar.

Thẻ: Pulsar Bookkeeper Kubernetes Java PerformanceOptimization

Đăng vào ngày 8 tháng 9 lúc 02:12