Introduce to Kafka

Introduce to Kafka

- 12 mins

Trong các hệ thống Microservices và Reactive System, một trong những thứ không thể thiếu đó là các message broker/stream. Hiện tại team mình đang dùng Kafka cho mục đích chính cho integration, decouple hệ thống cũng như implement Event Sourcing partern.

Với bài viết này mình sẻ ghi lại một chút kiến thức cũng như kinh nghiệm trong thời gian ngắn vừa rồi làm việc với Kafka. Mình sẻ giới thiệu hai nội dung chính sau:

A. Why Kafka?

Trong thực tế Kafka có rất nhiều lợi ích, mình chỉ liệt kê ra một số usecases mà tụi mình đã và đang dùng cho dự án.

1. Dùng Kafka làm message queue

Chúng ta bắt đầu bằng việc có một bài toán như sau: Nếu có 1 triệu người đặt hàng trên trang web của bạn cùng lúc, nó có thể tạo ra một số vấn đề concurrency như deadlock, race condition,…. lúc đó ý tưởng về việc sử lí các yêu cầu theo thứ tự ngay lập tức mang lại hiệu quả. Chúng ta sẻ sắp xếp các order vào một message queue để có thể đảm bảo thứ tự của chúng và kiểm soát được số lượng order được xử lí đồng thời, Kafka với một kiến trúc scalable là một trong những lựa chọn tốt giúp chúng ta xử lí được bài toán trên.

2. Là công cụ thiết yếu cho việc Decouple hệ thống và Integration

Khi làm việc với các hệ thống phức tạp chúng ta bắt buộc phãi thiết kế thành một hệ thống Low Coupling, High Cohesion. Kafka là công cụ cực kì mạnh mẻ cho việc decouple hệ thống, nó cho phép chúng ta phân tách các thành phần của hệ thống thành các thành phần độc lập, low coupling đồng thời đảm bảo High Cohesion.

Trong một hệ thống lớn chúng ta thường sử dụng nhiều ngôn ngữ và công nghệ khác nhau tuỳ thuộc vào mục đích từng thành phần, hơn nữa việc duy trì dependencies cứng nhắc giữa các thành phần đôi lúc không cần thiết và đôi lúc phãi trả một cái giá lớn cho dependencies. Bằng cách giao tiếp thông qua kafka giữa các services với các format hợp lí bạn có có thể tách biệt được dependencies giữa các service, mỗi thành phần của hệ thống đều có thể phát triển, mở rộng một cách độc lập.

Đặc biệt đối với Microservice Approach, chúng ta thường đối mặt với các vấn đề liên quan đến Internal Communication và xử lí lỗi trong communication giữa các services. Những dạng giao tiếp thường thấy có thể là call trực tiếp(thông qua http rest/gRPC/thrift….), giao tiếp thông qua việc sử dụng Asynchronous Messaging. Tuy nhiên đối với các hệ thống phãi xử lí một lượng lớn request trên kiến trúc Microservices cũng như kiến trúc Monolic truyền thống thì những lỗi như lỗi mạng, timeout, lỗi do bug….là điều không thể tránh khỏi. Message Streaming nói chung và Kafka nói riêng giúp hệ thống của chúng ta xử lí các lỗi trên một cách hiệu quả bằng cách implement các Circuit Breaker, một cách đơn giản nhất đó là lưu lại trạng thái các request lúc nào xử lí xong mới xoá request đó đi bất cứ lỗi nào xãy ra thì chúng ta đều có thể xử lí lại request đó.

3. Dùng cho việc implement các RPC function và Reactive System/Event Driven System

Các ngôn ngữ lập trình truyền thống thường dựa trên thread và threadpools để xử lí các tác vụ đồng thời đơn cử như là Java theo mặc định mỗi thread thường chiếm 1MB trong stack size(trên JVM 64bit) và chúng ta có thể config stack size tuỳ ý. Việc cấp phát một thread thường khá đắt đỏ trong CPU cho nên chúng ta thường sẻ phãi config size của threadpools trước, mỗi khi có request tới, web server sẻ pick một thread trong threadpools và kêu nó xử lí reuqest, các thread trên sẻ xử lí các tác vụ một cách song song. Nếu bạn giữ các giá trị như mặc định thì nếu config 1k thread trong thread pool đồng nghĩa với việc bạn sẻ dùng hết gần 1G Ram.

Đối với hệ thống lớn chúng ta sẻ gặp phãi hai vấn đề về thread đó là stack size và threadpools size. Nếu config stack size mỗi thread nhỏ xuống thì sẻ tiết kiệm được nhiều bộ nhớ nhưng khi một thread nào đó đòi hỏi tính toán nhiều thì dễ gây ra stack overflow. Cũng như vậy đối với threadpools size, khi chúng ta giữ một lượng lớn thread trong threadpool thì memory cost và switching context cost sẻ là rất cao và hệ thống sẻ trở nên khó kiểm soát, ngược lại khi giữ một số lượng thread nhỏ hơn thì lại làm giảm tính available của hệ thống.

Nếu hệ thống chúng ta có các tác vụ tính toán mất thời gian, tốn tài nguyên, khó thể ước tính thời gian và tài nguyên cho mỗi yêu cầu nhưng lại không nhất thiết phãi response ngay lập tức thì chúng ta có thể sử dụng mô hình RPC.

Các hệ thống ngày càng phát triển đòi hỏi một tốc độ phản hồi cao, đối mặt tốt với các lỗi cũng như đòi hỏi mức downtime = 0, đồng thời đáp ứng khả năng mở rộng tuyệt vời với không một điểm thắt cổ chai. Đó là mục tiêu người ta xây dựng nên Reactive System với mọi thứ hướng theo Event. Kafka với khả năng mở rộng tuyệt vời và đáng tin cậy do đó là một lựa chọn khá tốt để xử lí các event trong mô hình này.

4. Đối với Stream Proccessing data realtime.

Trong các hệ thống hướng Data, các yêu cầu report một con số gì đó trong thời gian ngắn để make một decision là rất phổ biến. Trong một hệ thống lớn với lượng Data khổng lồ thì việc xử lí một lượng lớn Data như vậy bằng Batch Proccessing là điều rất tốn thời gian cũng như tài nguyên. Do đó sử dụng Kafka đổ reduce ra một kết quả sẻ giúp chúng ta dễ hiện thực tính năng đó hơn.

Bên cạnh đó cũng sẻ có những yêu cầu về việc phân tích Data Realtime, đưa ra report gần như chính xác tại mỗi thời điểm,…lúc đó những Message Stream như Kafka cùng với các tool liên quan đến Big Data như Hadoop, Spark là giải pháp tuyệt vời.

B. Kiến trúc kafka:

Cơ chế hoạt động cơ bản của Kafka hình dung đơn giản giống như Logs system, các log record được lưu lên đĩa và sắp xếp theo thứ tự thời gian, Kafka tổ chức phân loại các messages theo khái niệm Topic hình dung giống như tag trong việc quản lí Log của chúng ta.

Mỗi khi cài đặt Kafka một trong những việc ta cần làm là setup nơi mà log sẻ được lưu xuống-url của log.dir, mỗi topic sẻ được map với thư mục con bên trong thư mục log của chúng ta và cũng có thêm nhiều thư mục con nữa đó là các thư mục topic partitions với tên thư mục format là topic-name_partition-number, bên trong mỗi thư mục sẻ là file log nơi mà message tới sẻ được append vào.

Mỗi Topic có một hoặc nhiều Partitions và mỗi Partition là một list các messages, khi một message được chia sẻ lên Kafka theo Topic, message sẻ được gửi vào một Partition của Topic, việc lưu message xuống Partition nào được quyết định bởi các producers. Nếu ta muốn message xuống Partition cụ thể nào đó thì ta phãi set partition_key cho message đó ứng với Partition ta muốn message xuống, nếu không mặc định message sẻ được phân phối theo giải thuật round-robin.

/logs
    /logs/topicA_0
    /logs/topicB_0
    /logs/topicB_1
    /logs/topicC_0
    /logs/topicC_1
    /logs/topicC_2

Như trên chúng ta có thể thấy rằng topic A có 1 partition, topic B có 2 partitions và C có 3 partitions. Vậy partition đó là gì, dùng để làm gì?

Partitions là một phần quan trọng trong thiết kế của kafka. Các partitions chia luồng message đến một topic vào các luồng song song và đó là chìa khoá giúp Kafka đạt được một lượng message khổng lồ. Tuy nhiên thứ tự message đến từ các partitions khác nhau không được đảm bảo, Kafka chỉ đảm bảo thứ tự message trong cùng một partition, do đó trong trường hợp chúng ta cần đảm bảo thứ tự message cho nhiều tác vụ nào đó, chắc chắn các message xử lí các tác vụ đó phãi được sắp xếp vào cùng 1 topic và cùng một partion_key.

Markdowm Image

Mỗi Partition có thể được phân tán trên nhiều máy và mỗi máy như thế được gọi là một Broker, mỗi Broker có thể có 0 1 hoặc nhiều Partitions của cùng 1 Topic hoặc thậm chí các topic khác nhau.

Markdowm Image

Kafka hổ trợ Replication để đảm bảo tính high-availability và tính fault-tolerance. Mỗi partition có thể có nhiều bản replicas tuỳ thuộc vào số replication factor mà ta config.

Markdowm Image

Tuy nhiên chỉ có các leader partitions mới được nhận message, leader partitions nhận message sau đo mới đồng bộ lên các replicas partitions khác. Trong trường hợp một leader partition chết thì một trong những replica của partition được chọn để làm leader partition. Đến khi partition kia sống lại thì nó trở thành replica và phãi fetch lại tất cả data chưa đồng bộ được trong quá trình chết đi từ các partition khác. Kafka dùng ZOOKEEPER để handle chuyện đó, Kafka dùng nó để thực hiện việc leadership electron các kafka broker và các cặp Topic Partition, ZOOKEEPER cũng đứng vai trò là Service Discovery cho các Kafka Brokers từ các cluster, xử lí các trường hợp broker nào join vào, broker nào chết đi, topic nào được add vào topic nào bị remove đi….

Rõ ràng nhìn vào kiến trúc của Kafka chúng ta thấy rằng nó hoàn toàn thoã mãn hai đặc điểm đó là High-availability và Durability.

Cách hoạt động của Kafka producers khá đơn giản, ban đầu các producers phãi fetch các metadata lên ví dụ như chúng cần biết có các topics nào đang tồn tại? có bao nhiêu partition mỗi topic đang có? leader broker hiện tại của mỗi partition là node nào? host và port của mỗi broker đó là sao? Sau đó chúng làm việc trực tiếp với các broker khác nhau mà không có một sự điều phối nào.

Markdowm Image

Với thiết kế đó kafka producers đã loại bỏ triệt để vấn đề write-bottleneck, đồng thời giúp cho mỗi broker nhận được một số lượng messages hợp lí, dễ dàng mở rộng một cách tuyến tính, khi muốn mở rộng chỉ cần thêm partition và broker vào.

Như vậy nhìn chung thì Kafka producers tương đối đơn giản tuy nhiên đối với Kafka consumers thì phức tạp hơn nhiều. Cũng giống như Kafka producer, Kafka consumers bắt đầu các hoạt động của chúng bằng việc fetch các metadata, mỗi consumer có thể kết nối tới nhiều brokers và đọc từ nhiều replicas. Mỗi broker xử lí một tập hợp các partition của các topics mà consumer subsribe tới cùng lúc.

Markdowm Image

Nhờ thiết kế đó mà workload giữa các broker được cân bằng đồng thời giảm thiểu vấn đề read-bottleneck. Hơn nữa mỗi consumer có nhiều replicas để đọc giúp tăng tính availability và cân bằng workload giữa các consumer.

Markdowm Image

Tuy nhiên chỉ với thiết kế đó thì vẫn chưa đảm bảo tính scalable, khái niệm consumer group sinh ra giúp chúng ta dễ dàng scale các consumers. Theo đó mỗi consumer sẻ thuộc về một consumer group hay nói cách khác một consumer group sẻ chứa các consumer và message sẻ được phân phát đến tất cả consumer group, mỗi member của group sẻ xử lí message từ một hoặc nhiều partitions.

Markdowm Image

Với thiết kế này, mỗi consumer có thể xử lí message từ nhiều partitions, đảm bảo cover hết tất cả các partition cũng như thứ tự xử lí message trong mỗi partition đồng thời cân bằng tải giữa các consumers trong cùng một group. Khi chúng ta muốn scale lên chỉ cần tăng số brokers, tăng số partitions và tăng số consumers. Nhìn lại một lần nữa vào ví dụ trên, chúng ta có thể thấy rằng thiết kế trên cho phép mỗi consumer có thể xử lí nhiều partitions cùng lúc chứ không cho phép nhiều consumers cùng lúc xử lí một partition cho nên giả dụ như chúng ta có 5 partitions nhưng có đến 6 cunsumers thì hiển nhiên 1 consumer ngồi chơi làm phí phạm tài nguyên hệ thống, cho nên khi scale thì nên lưu ý rằng số consumers luôn bé hơn hoặc bằng số partitions mà tốt nhất là bằng nhau thì hơn.

Updating….

comments powered by Disqus
rss facebook twitter github gitlab youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora