#174 - Xử lý bài toán reliable reprocessing với kafka tại Uber
Những bài viết hay
Building and Scaling Data Lineage at Netflix to Improve Data Infrastructure Reliability, and Efficiency — netflixtechblog.com
Các doanh nghiệp hiện nay thường dựa trên các dữ liệu khác nhau để đưa ra các quyết định chiến lược cho doanh nghiệp. Tuy nhiên, để hiểu được các dữ liệu này được lấy từ đâu và xử lý như thế nào thì thường chúng ta sẽ hỏi các data engineers hay data scientists làm nên các dữ liệu đấy. Việc này thường sẽ giải quyết được vấn đề cho các công ty vừa và nhỏ, nhưng sẽ không áp dụng được hiệu quả cho các công ty lớn hoặc các công ty có những data pipelines khá là rắc rối.
Bài viết sau đây được các kỹ sư ở Netflix nói về quá trình và chặng đường họ đã đi để xây dựng một hệ thống data lineage hiệu quả cho công ty họ khi mà Netflix dựa rất nhiều vào các dữ liệu họ thu thập được. Mặc dù bài viết không nói quá sâu về chi tiết hệ thống data lineage của họ như thế nào. Tuy nhiên, từ bài viết này thì chúng ta vẫn hiểu được độ phức tạp của hệ thống khi mà một hệ thống data lineage hiệu quả cần phải được integrate với nhiều hệ thống khác nhau để lấy được các dữ liệu cần thiết.
Kubernetes requests vs limits: Why adding them to your Pods and Namespaces matters
Khi k8s schedule một Pod, các containers cần có đủ tài nguyên để chạy. Nếu bạn schedule một ứng dụng lớn trên một node với lượng tài nguyên hạn chế, thì node đó có thể hết bộ nhớ hoặc CPU và mọi thứ ngừng hoạt động.
Các ứng dụng cũng có thể chiếm nhiều tài nguyên hơn mức cần thiết. Điều này có thể do một team scale up ứng dụng với nhiều replicas hơn (việc này thường dễ dàng hơn là làm cho code ứng dụng chạy hiệu quả và chỉ cần ít replicas hơn). Dẫn đến một thay đổi cấu hình không tốt có thể khiến chương trình hoạt động theo một cách không được kiểm soát và sử dụng 100% CPU có sẵn. Bất kể nguyên nhân là gì đi chăng nữa (developer không hiệu quả, code lỗi, hay một sự cố không may, ...), điều quan trọng là bạn phải luôn kiểm soát được tình hình.
Qua bài viết, Google đưa ra những lời khuyên hữu ích giúp chúng ta có thể giải quyết những vấn đề này bằng cách sử dụng một cách hiệu quả resource requests và limits. Resource request là lượng tài nguyên container được đảm bảo nhận được. Resource limit là lượng tài nguyên tối đa mà container có thể nhận được.
Resource requests và limits được xác định trên mỗi container. Mỗi Pod có thể có nhiều hơn một container. Requests và limits cần cho mỗi Pod được xác định bằng tổng giá trị tương ứng của các containers trong Pod đó. Để kiểm soát được requests và limits của một container, bạn cần hiểu rõ và thiết lập các mức quotas ở cả container và namespace level. Ngoài ra, giá trị resource requests được k8s scheduler xác định để chạy ứng dụng của bạn, vì vậy chúng ta cần nắm rõ lifecycle của một Pod để có thể điều chỉnh container một cách chính xác. Đặc biệt trong các k8s cluster có tính năng Node Autoscaler như GKE, việc nắm rõ mối quan hệ giữa resource requests, limits và cơ chế node autoscaling sẽ giúp bạn kiểm soát k8s cluster một cách tối ưu hơn.
The things I hate about Apache Cassandra
Apache Cassandra là một trong những NoSQL databases phổ biến được sử dụng khá rộng rãi cho nhiều hệ thống và ứng dụng khác nhau. Dù cho Cassandra được nhiều lập trình viên yêu thích và biết tới bởi nhiều ưu điểm về hệ thống và hệ sinh thái của Cassandra. Tuy nhiên, cũng như những cộng đồng và hệ thống khác thì Cassandra cũng có vài hạn chế cần được cải thiện hơn.
Bài viết sau đây được tác giả nói về những hạn chế đó. Một vài ví dụ điển hình được tác giả nêu lên như là:
Cassandra được viết bằng Java cho nên các hạn chế về performance của hệ thống khi GC chạy là điều khó tránh khỏi mặc dù các kỹ sư viết lên Cassandra đã tìm nhiều cách giới hạn việc này
Cassandra sử dụng Java Metrics Extension (JMX) để thu thập một số metrics của hệ thống. Tuy nhiên, JMX có một vài nhược điểm về JMX port và Remote Method Invocation (RMI) có thể dẫn tới vài hạn chế lúc phân tích các dữ liệu metrics này
Cassandra sử dụng Log-Structured Merge Tree (LSM Tree) như là storage engine chính. Việc Cassandra được viết khá là chặt chẽ với storage engine này dẫn tới một vài hạn chế khi mà chúng ta cần sử dụng các mô hình storage engine khác cho một vài trường hợp sử dụng
Một vài hạn chế về các lựa chọn cho việc compact dữ liệu của LSM Tree
So với các database engines nổi tiếng khác thì cộng đồng Cassandra có khá là ít các công cụ về mặt quản lý và vận hành Cassandra do Cassandra cũng khá là mới so với các databases khác
Một vài hạn chế về việc xử lý và phân chia các partitions lớn
Một vài hạn chế về performance của Lightweight Transactions trong Cassandra
Góc Distributed System
Xử lý bài toán reliable reprocessing với kafka tại Uber
Trong các hệ thống phân tán, vấn đề retry hay các vấn đề về network error, server crash là không thể tránh khỏi. Uber sử dụng Kafka cho hệ thống event messaging của họ. Cùng xem Uber xử lý bài toán reprocessing trong Kafka như thế nào?
Bài toán đặt ra là khi một service ở phía consumer có hiện tượng high latency, service phía publisher có thể sẽ thực hiện retry (kèm backoff) hành động publish event vào kafka cho tới khi có phản hồi thành công hoặc đạt điều kiện dừng. Vấn đề với cách tiếp cận retry đơn giản như vậy là:
Làm tắc nghẽn cơ chế batch processing của Kafka: xảy ra khi có quá nhiều retried message trong Kafka và consumer liên tục consume retried message
Metadata của các bản tin retry có thể gây phiền phức cho việc lập trình như lấy timestamp hay đếm số lần retry
Uber xử lý bài toán này thông qua việc sử dụng Dead-letter queue (DLQ), tức là xây dựng các retry queues (ở trên các topic khác nhau) để lưu trữ các event lỗi, phục vụ bài toán retry. Cụ thể như sau:
Khi consumer không thể response thành công một request event X nhận từ queue, sau số lần retry nhất định (config sẵn), consumer sẽ publish event X đó qua 1 retry topic tương ứng để reprocessing về sau. Đồng thời publish offset của X đó vào một topic cố định khác để đánh dấu là event X đã bị retry nhiều lần. Rồi consumer tiếp tục thực hiện với các event khác
Các consumer khác được thiết lập từ trước sẽ consume từ các retry topic để lấy event (X) cho việc thực hiện lại request (reprocessing). Nếu còn bị lỗi thì lại lặp lại thao tác như bước 1
Sau một số lần reprocessing không thành công, event X sẽ được đẩy vào Dead-letter topic
Các event trong dead-letter được thực hiện lại bằng cách được gửi trở lại topic nguồn
Cứ như vậy các event lỗi sẽ dần được retry. Cách làm này không những giúp Uber giải quyết được vấn đề tắc nghẽn trong queue mà còn có một số điểm lợi khác:
Tránh việc consuming bị tắc khi cứ phải xử lý message retry lỗi liên tục, thay vào đó consumer tiếp tục xử lý các message khác và quay lại xử lý message lỗi sau. Nguyên nhân vì khi một message bị delay khi xử lý sẽ ảnh hưởng tới toàn bộ message khác trên cùng một partition
Việc tách ra nhiều topic cho retry message (thay vì chỉ một retry topic) cũng có lợi hơn khi bản thân retry message vẫn tiếp tục bị delay trên retry topic
Dễ dàng cấu hình, tinh chỉnh cho các topic retry
Tăng khả năng observability
Và một số điểm lợi khác. Mời bạn đọc bài viết cụ thể ở đây. Để hiểu thêm về DLQ trong Kafka, bạn tham khảo bài viết này
Góc Database
Monitoring là một nhu cầu quan trọng trong các hệ thống ngày nay. Việc liên tục phải theo dõi xem CPU của bạn đang ở mức nào, API này đang serve bao nhiêu QPS, tỉ lệ lỗi của API này là bao nhiêu, ... là điều cần thiết để có thể đưa ra các cảnh bảo.
Tuy nhiên bạn có tự hỏi một hệ thống database dùng để lưu trữ dữ liệu tracking như vậy sẽ có những đặc tính gì, hay có những kỹ thuật gì đã được áp dụng để xây dựng nên nó?
Trong số kỳ này, mời các bạn tham khảo bài báo về Gorilla, một in-memory time series database được phát triển bởi Facebook nhằm phục vụ cho việc theo dõi traffic và performance của các hệ thống khác ở Facebook. Bài báo sẽ đề cập đến một vài kỹ thuật được sử dụng Timestamp Compression, Value Compression cũng như cấu trúc dữ liệu trên memory được dùng để tổ chức các dữ liệu này.
Góc Lập Trình
Đề ra tuần này:
Cho một cây nhị phân, hãy thiết kế thuật toán để tạo 1 list chứa tất cả các node của cây theo từng độ sâu. Nếu cây có độ sâu bằng n, thuật toán sẽ trả lại 1 list với n list con.
Input:
Output: [[1], [2,3], [4, 5, 6], [7, 8, 9, 10]]
Lời giải tuần trước:
Một phương án ta có thể nghĩ tới ngay đó là tính giá trị của một chiếc bánh theo tỉ lệ value/weight. Ví dụ với input cake_tuples = [ (1, 10), (5, 100)], ta thấy loại bánh thứ nhất có ratio là 10/1 = 10, loại thứ 2 có ratio là 100/5 = 20. Ta sẽ lần lượt lấy loại bánh thứ 2 nhét đầy vào túi, sau đó tới loại bánh thứ 1.
Tuy nhiên phương án này không phải lúc nào cũng cho ra kết quả tối ưu nhất. Giả sử với trường hợp cake_tuples = [ (3, 40), (5, 70) ], capacity = 9. Loại bánh thứ 2 có ratio lớn hơn loại thứ 1 (70/5 > 40/3), do đó với cách làm ở trên, ta sẽ ưu tiên loại bánh thứ 2 trước. Vậy với capacity = 9, ta sẽ cần 1 bánh loại 2 và 1 bánh loại 1, cho ta tổng giá trị là 110. Tuy nhiên đáp án chính xác sẽ là 3 bánh loại 1 với tổng giá trị là 120. Vậy cách tính value/weight sẽ không cho ta phương án tối ưu nhất. Ta có thể dùng quy hoạch động để giải bài toán này.
Chúng ta sẽ bắt đầu với việc chia nhỏ bài toán, bắt đầu bằng việc giả sử capacity = 1. Nếu capacity = 1, chúng ta chỉ cần duyệt qua tất cả loại bánh và chỉ quan tâm tới các loại bánh có weight = 1, chọn ra loại bánh nào có value lớn nhất. Ta tạm lưu giá trị value này là maxValueAtCapacity1.
Tiếp theo, tương tự nếu trường hợp capacity = 2. Ta cũng muốn tìm ra giá trị value lớn nhất và lưu vào maxValueAtCapacity2. Chúng ta cũng làm tương tự bằng cách duyệt lại toàn bộ loại bánh, và sẽ chỉ cần lưu ý tới các loại bánh có weight = 1 hoặc 2 (rõ ràng 1 chiếc bánh có trọng lượng lớn hơn 2 sẽ không để vừa túi).
Nếu bánh có weight = 2, ta chỉ cần kiểm tra value của loại bánh này có lớn hơn maxValueAtCapacity2 hay không để cập nhật lại giá trị maxValueAtCapacity2.
Nếu bánh có weight = 1, nếu ta bỏ 1 chiếc bánh có trọng lượng 1 vào túi thì ta cũng sẽ cần bỏ thêm 1 chiếc bánh khác có trọng lượng 1 vào để lấp đầy túi. Rõ ràng ta có thể sử dụng giá trị maxValueAtCapacity1 đã tính trước đó để có giá trị tối ưu và không cần phải xử lý lại. Do đó, ta sẽ so sánh với maxValueAtCapacity2 để tìm ra giá trị lớn nhất cho maxValueAtCapacity2.
Tới đây chắc bạn đọc đã có thể hình dung được một cách tổng quát, chúng ta có thể dùng maxValueAtCapacity1 để tính maxValuleAtCapacity2, thì cũng có thể dùng maxValueAtCapacity1 và maxValueAtCapacity2 để tính maxValueAtCapacity3. Để tránh việc phải tính lại, ta sẽ lưu các giá trị maxValueAtCapacity vào một mảng. Khi đó gía trị value sau cùng sẽ là maxValueAtCapacityK (capacity = k).
Độ phức tạp của bài toán là O(k*n) time complexity và O(k) space complexity, với k là trọng lượng tối đa chiếc túi mang được, n là số loại bánh ta có.
Nhận xét:
Đây là một bài toán tương đối kinh điển trong quy hoạch động. Tuy nhiên lời giải tối ưu cũng đòi hỏi độ phức tạp cao O(k*n) và sẽ cần nhiều thời gian hơn khi k hoặc n quá lớn. Lời giải ban đầu với giá trị ratio của value/weight có thể kém tối ưu hơn nhưng lại nhanh hơn với độ phức tạp O( n lg n) (Cần thực hiện thao tác sort). Trong thực tế, ta có thể tùy theo tình huống để lựa chọn phương án phù hợp. Không phải lúc nào lời giải tối ưu cũng sẽ là lời giải hiệu quả nhất (sẽ thế nào nếu tên trộm phải chạy đua với thời gian và kịp tẩu thoát trước khi bị phát hiện?)
Trong lời giải trên, chúng tôi vẫn chưa xử lý cho tình huống weight = 0 hoặc value = 0. Bên cạnh đó các bạn đọc có thể điều chỉnh lời giải trên để trả về số lượng chính xác của mỗi loại bánh ta mang được.
Ngoài ra, sẽ thế nào nếu thay vì có một kho vô tận, ta chỉ có một số lượng giới hạn các loại bánh? Có những loại bánh rất nặng hoặc rất kém giá trị và gần như không bao giờ nằm trong phương án tối ưu, vậy ta thể dùng ý tưởng này để xử lý không?
Hãy coi những câu hỏi trên như những bài tập nhỏ dành cho bạn đọc trong việc khám phá những bài tóan lập trình thú vị, và đừng quên chia sẻ với chúng tôi nhé.
Code & Tools
This Week Sponsors
KMS Technology luôn nằm trong Top các nơi làm việc lý tưởng trong ngành IT. Điều gì đang chờ đón bạn tại đây?
Nhiều phúc lợi không phải ở đâu cũng có (thử việc, bảo hiểm xã hội 100% lương; bảo hiểm sức khoẻ cao cấp cho KMSer ngay từ ngày đầu tiên, và cho người thân sau thử việc; linh động, thoải mái về thời gian và nơi làm việc, thưởng theo năng lực,...)
Gia nhập đội ngũ trẻ, giỏi, năng động trong các dự án lớn, quy mô toàn cầu
Nhiều cơ hội đào tạo, học tập, luân chuyển dự án, làm việc on-site
Văn hoá làm việc rõ ràng, tôn trọng, minh bạch
Vẫn còn nhiều điều có 1-0-2 tại KMS. Bạn hãy đích thân khám phá nhé!
Góc Tuyển Dụng
Các vị trí khác tại KMS Technology Careers
Quotes
The Analytical Engine has no pretensions whatever to originate any thing. It can do whatever we know how to order it to perform.
- Ada Lovelace