📨🧠 Kafka – Hiểu Cốt Lõi Trong 3 Phút - Kafka 101 P1
Kafka 101 – Hiểu Cốt Lõi Trong 3 Phút
1. "Cái bẫy" của REST API và sự kết nối chặt chẽ (Tight Coupling)
Mọi hệ thống phần mềm lớn trên thế giới khi bắt đầu phát triển thường lựa chọn một mô hình giao tiếp đơn giản và trực quan nhất giữa các dịch vụ: REST API đồng bộ (Synchronous REST API). Hãy tưởng tượng bạn đang xây dựng một nền tảng thương mại điện tử (E-commerce). Ban đầu, hệ thống của bạn chỉ có ba dịch vụ cơ bản:
- Order Service (Dịch vụ Đơn hàng): Tiếp nhận yêu cầu mua sắm từ khách hàng.
- Inventory Service (Dịch vụ Kho hàng): Kiểm tra và giữ hàng (lock item).
- Shipping Service (Dịch vụ Vận chuyển): Lập đơn giao nhận.
Khi khách hàng bấm nút "Mua ngay", Order Service nhận request, gửi một HTTP POST request sang Inventory Service để trừ kho, sau đó gửi tiếp một HTTP POST sang Shipping Service để đóng gói. Mọi thứ diễn ra theo trình tự tuần tự (sequential) và đồng bộ. Dữ liệu trả về tức thì, lập trình viên dễ viết code, dễ debug.
+---------------+ Sync HTTP +-------------------+
| | ----------------------> | Inventory Service |
| Order Service | +-------------------+
| | ---------\
+---------------+ \ Sync HTTP +-------------------+
\----------> | Shipping Service |
+-------------------+
Tuy nhiên, sự thành công của doanh nghiệp kéo theo việc hệ thống phải phình to. Để tối ưu doanh thu và trải nghiệm người dùng, ban giám đốc yêu cầu tích hợp thêm một loạt tính năng mới. Hệ thống nhanh chóng xuất hiện thêm:
- Notification Service: Gửi email và SMS xác nhận đơn hàng.
- Loyalty Service: Tích điểm thành viên cho khách hàng.
- Recommendation Service: Phân tích hành vi mua sắm để gợi ý sản phẩm liên quan.
- Analytics Service: Đưa dữ liệu về kho dữ liệu (Data Warehouse) để bộ phận kinh doanh theo dõi dashboard thời gian thực.
Lúc này, quy trình xử lý của Order Service trở thành một cơn ác mộng. Để hoàn tất một đơn hàng, Order Service phải thực hiện chuỗi gọi API đồng bộ liên tiếp tới 6 dịch vụ khác nhau.
+-------------------+
/----------> | Inventory Service |
/ +-------------------+
/ +-------------------+
/-------------> | Shipping Service |
/ +-------------------+
/ +-------------------+
+---------------+ /-----------------> | Notification Serv |
| | ---/ +-------------------+
| Order Service | ---\ +-------------------+
| | \-----------------> | Loyalty Service |
+---------------+ \ +-------------------+
\ +-------------------+
\--------------> | Recommend Service |
\ +-------------------+
\ +-------------------+
\-----------> | Analytics Service |
+-------------------+
Đây chính là lúc hệ thống rơi vào cái bẫy kết nối chặt chẽ (Tight Coupling), kéo theo hàng loạt hậu quả chí mạng trên production:
- Độ trễ tích lũy (Latency Accumulation): Thời gian phản hồi API của Order Service bằng tổng thời gian phản hồi của tất cả 6 dịch vụ gọi chéo. Chỉ cần Loyalty Service bị nghẽn mạng và phản hồi mất 2 giây, toàn bộ quá trình thanh toán của khách hàng sẽ bị treo 2 giây. Khách hàng thấy màn hình load liên tục, tỉ lệ bỏ giỏ hàng tăng vọt.
- Sụp đổ dây chuyền (Cascading Failure): Điều gì xảy ra nếu Notification Service gặp sự cố và sập hoàn toàn? Vì lời gọi API là đồng bộ, Order Service nhận về lỗi
500 Internal Server Errorhoặc504 Gateway Timeouttừ Notification Service. Kết quả là toàn bộ giao dịch mua hàng của khách hàng bị thất bại, dù kho vẫn còn hàng và dịch vụ vận chuyển vẫn sẵn sàng hoạt động. - Quá tải hệ thống: Khi một dịch vụ phía sau chạy chậm, các request của Order Service bị dồn ứ lại. Các connection pool và thread pool trên Order Service nhanh chóng bị cạn kiệt, khiến bản thân Order Service cũng bị tê liệt và sụp đổ theo.
Để thoát khỏi thảm họa này, kiến trúc sư hệ thống buộc phải tìm cách cắt đứt sự kết nối chặt chẽ (decoupling). Thay vì gọi trực tiếp, các dịch vụ cần giao tiếp với nhau một cách bất đồng bộ thông qua các sự kiện (Events). Và giải pháp đầu tiên mà hầu hết các kỹ sư nghĩ đến là sử dụng một Message Queue.
2. Niềm tin phổ biến: "Kafka chỉ là một Message Queue như RabbitMQ"
Khi tìm kiếm giải pháp truyền tin bất đồng bộ, các kỹ sư thường đặt Apache Kafka chung hàng với RabbitMQ, ActiveMQ hay Redis Pub/Sub. Họ coi chúng là những công cụ có thể thay thế hoàn hảo cho nhau với cùng một mục đích.
Tư duy này hoàn toàn dễ hiểu và nghe có vẻ rất hợp lý. Suy cho cùng:
- Cả hai đều hỗ trợ mô hình Publish/Subscribe.
- Cả hai đều có Producer gửi tin nhắn (message) và Consumer nhận tin nhắn.
- Cả hai đều hoạt động như một bộ đệm (buffer) trung gian, giúp Order Service giải phóng khỏi việc phải chờ đợi các dịch vụ phía sau xử lý xong.
Với RabbitMQ, Order Service chỉ cần đẩy một sự kiện "Order Created" vào Broker, sau đó lập tức trả về phản hồi thành công cho khách hàng. Các dịch vụ như Inventory, Shipping, Notification... tự động subscribe vào hàng đợi (queue) để lấy tin nhắn về xử lý độc lập. Hệ thống chạy cực nhanh, tính cô lập cao.
Khi chuyển sang Apache Kafka, mã nguồn của ứng dụng cũng được viết tương tự: cấu hình Producer gửi tin nhắn vào Kafka Topic, cấu hình Consumer đọc tin nhắn từ Kafka Topic. Mọi thứ hoạt động trơn tru. Từ đó, một niềm tin phổ biến được hình thành: "Kafka chỉ là một Message Queue thông thường, chẳng qua nó được quảng cáo là nhanh hơn và có khả năng scale tốt hơn nhờ viết bằng ngôn ngữ Java/Scala thay vì Erlang như RabbitMQ".
Bên cạnh đó, nhiều người cũng mặc định rằng dữ liệu trong Kafka, giống như RabbitMQ hay bất kỳ hệ thống hàng đợi nào khác, sẽ tự động bị xóa bỏ khỏi hệ thống ngay sau khi Consumer đọc và xác nhận (acknowledge) xử lý thành công.
Nhưng thực tế, hai công cụ này được thiết kế dựa trên hai triết lý kiến trúc hoàn toàn khác biệt. Việc áp dụng tư duy "hàng đợi truyền thống" vào Kafka không chỉ làm lãng phí sức mạnh thực sự của nó mà còn dẫn đến những quyết định thiết kế sai lầm nghiêm trọng trên môi trường production.
3. Tại sao niềm tin này sụp đổ trên Production: Giới hạn của mô hình Queue truyền thống
Khi quy mô dữ liệu và số lượng service tăng lên hàng chục hoặc hàng trăm, mô hình Message Queue truyền thống (như RabbitMQ) bắt đầu bộc lộ những giới hạn vật lý và kiến trúc không thể khắc phục:
A. Triết lý "Smart Broker, Dumb Consumer" và Gánh nặng trạng thái
Trong các Message Queue truyền thống, Broker là thành phần "thông minh". Nó chịu trách nhiệm theo dõi chi tiết trạng thái của từng tin nhắn: tin nhắn nào đã được gửi cho consumer nào, tin nhắn nào đang chờ xử lý, tin nhắn nào đã được xác nhận thành công (ACK) để xóa bỏ hoàn toàn khỏi đĩa hoặc RAM.
Khi hệ thống xử lý hàng chục nghìn message mỗi giây, việc quản lý trạng thái này đòi hỏi chi phí CPU và RAM rất lớn. Broker liên tục phải thực hiện các thao tác cập nhật cấu trúc dữ liệu trong bộ nhớ và ghi đĩa để theo dõi vị trí của từng consumer. Khi số lượng consumer tăng lên, hiệu năng của Broker sẽ sụt giảm nghiêm trọng.
B. Thảm họa nhân bản dữ liệu (Fan-out Overhead)
Nếu bạn có 5 dịch vụ khác nhau cùng muốn nhận sự kiện "Order Created", RabbitMQ phải tạo ra 5 queue độc lập. Khi một sự kiện được gửi tới, Broker phải nhân bản tin nhắn đó ra làm 5 bản vật lý và đẩy vào 5 queue này.
- Nếu kích thước tin nhắn là 1MB, hệ thống sẽ tốn 5MB bộ nhớ.
- Nếu có 100 dịch vụ quan tâm đến sự kiện đó, chi phí lưu trữ và băng thông mạng nội bộ của Broker lập tức tăng gấp 100 lần. Đây là một sự lãng phí tài nguyên khủng khiếp ở quy mô lớn.
C. Không thể quay ngược thời gian (Lack of Replayability)
Đây là điểm yếu chí mạng nhất. Vì triết lý của hàng đợi truyền thống là xóa tin nhắn ngay sau khi đọc xong để tiết kiệm tài nguyên, dữ liệu lịch sử sẽ biến mất vĩnh viễn. Nếu ngày hôm nay, đội ngũ Business Analyst yêu cầu triển khai một dịch vụ chạy phân tích dữ liệu mua sắm của toàn bộ tháng trước để phục vụ chiến dịch khuyến mãi mới, bạn hoàn toàn bó tay. Bạn không thể yêu cầu Message Queue gửi lại các tin nhắn của tháng trước vì chúng đã bị xóa từ lâu. Giải pháp duy nhất là phải viết code kéo ngược dữ liệu từ Database chính (PostgreSQL/MySQL), gây quá tải cho hệ thống database đang chạy production.
4. Góc nhìn mới: Event Streaming Platform và Nhật ký ghi phân tán (Distributed Commit Log)
Để giải quyết triệt để những giới hạn trên, Apache Kafka đã định nghĩa lại cuộc chơi bằng cách chuyển dịch từ mô hình Message Queue sang Event Streaming Platform (Nền tảng truyền dòng sự kiện), được xây dựng trên một cấu trúc dữ liệu cực kỳ đơn giản nhưng mạnh mẽ: Distributed Commit Log (Nhật ký ghi phân tán).
Bản chất của Commit Log là gì?
Hãy quên đi cấu trúc hàng đợi phức tạp với các cơ chế định tuyến lằng nhằng. Một Commit Log thực chất chỉ là một file nhật ký được ghi tuần tự (append-only).
- Dữ liệu mới luôn được ghi vào cuối file.
- Dữ liệu đã ghi là bất biến (immutable). Bạn không thể sửa đổi, không thể chèn vào giữa, và không thể xóa bỏ một bản ghi cụ thể nằm ở giữa file.
[ KAFKA COMMIT LOG ]
+---------+---------+---------+---------+---------+---------+
| Event 0 | Event 1 | Event 2 | Event 3 | Event 4 | Event 5 | ---> Ghi tuần tự vào cuối log
+---------+---------+---------+---------+---------+---------+
^ ^
| (Read) | (Read)
Consumer A Consumer B
(Offset = 0) (Offset = 3)
Triết lý "Dumb Broker, Smart Consumer"
Khác biệt hoàn toàn với RabbitMQ, Kafka Broker hoạt động như một cỗ máy lưu trữ log cực kỳ "ngốc nghếch". Nó không quan tâm consumer nào đang đọc dữ liệu, cũng không chủ động theo dõi xem tin nhắn nào đã được đọc thành công để xóa đi. Nó chỉ đơn giản là giữ lại toàn bộ file log trên đĩa cứng theo một chính sách cấu hình thời gian (retention policy - ví dụ: lưu trữ 7 ngày, 30 ngày hoặc vô hạn).
Việc theo dõi vị trí đọc thuộc về trách nhiệm của Consumer. Mỗi consumer tự quản lý một con trỏ số nguyên chỉ vị trí bản ghi tiếp theo mà nó muốn đọc, gọi là Offset.
- Nếu Consumer A muốn đọc từ đầu, nó đặt
Offset = 0. - Nếu Consumer B đã xử lý đến tin nhắn thứ 4, nó giữ
Offset = 3. - Khi Consumer đọc xong tin nhắn, nó chỉ cần tăng giá trị Offset của mình lên. Hành động đọc này hoàn toàn không làm thay đổi hay xóa dữ liệu trong file log của Kafka Broker.
Nhờ thay đổi góc nhìn này, Kafka mang lại những khả năng phi thường trên production:
- Khả năng Replay dữ liệu vô hạn: Nếu dịch vụ của bạn bị lỗi hoặc bạn muốn deploy một dịch vụ phân tích dữ liệu mới, bạn chỉ cần reset Offset của consumer quay lại một thời điểm bất kỳ trong quá khứ (ví dụ: quay lại 3 ngày trước). Consumer sẽ đọc lại toàn bộ dòng sự kiện lịch sử như thể nó vừa mới xảy ra.
- Không tốn chi phí nhân bản tin nhắn: Dù bạn có 1 hay 100 consumer cùng đọc một Topic, dữ liệu vật lý trên Kafka Broker vẫn chỉ duy nhất có một bản. Các consumer chỉ đơn giản là mở file log ra, đọc tuần tự từ các Offset khác nhau. Việc thêm consumer mới không hề làm tăng chi phí RAM hay đĩa cứng của Broker.
- Hiệu năng cực đại ổn định: Vì Broker không phải quản lý trạng thái tin nhắn phức tạp, nó giải phóng toàn bộ tài nguyên CPU để tập trung vào việc đọc/ghi file log tuần tự với tốc độ chóng mặt.
5. Ba mảnh ghép cốt lõi: Message, Topic, và Partition
Để làm chủ Kafka, chúng ta cần thấu hiểu cấu trúc vật lý và logic đằng sau ba khái niệm nền tảng: Message, Topic, và Partition.
+-------------------------------------------------------------+
| TOPIC |
| +-------------------------------------------------------+ |
| | Partition 0: [Msg 0][Msg 1][Msg 2][Msg 3] | |
| +-------------------------------------------------------+ |
| +-------------------------------------------------------+ |
| | Partition 1: [Msg 0][Msg 1][Msg 2] | |
| +-------------------------------------------------------+ |
| +-------------------------------------------------------+ |
| | Partition 2: [Msg 0][Msg 1][Msg 2][Msg 3][Msg 4] | |
| +-------------------------------------------------------+ |
+-------------------------------------------------------------+
A. Message (Event)
Message là đơn vị dữ liệu nhỏ nhất trong Kafka. Một message gửi vào Kafka không đơn thuần chỉ chứa nội dung (payload) mà có cấu trúc chuẩn hóa bao gồm:
- Key (Khóa): Dùng để xác định partition mà message sẽ được ghi vào, đồng thời đóng vai trò quan trọng trong việc đảm bảo thứ tự xử lý dữ liệu.
- Value (Giá trị): Nội dung thực tế của sự kiện, có thể lưu trữ dưới dạng chuỗi JSON, XML, hoặc các định dạng nhị phân tối ưu như Apache Avro, Protobuf.
- Header (Tiêu đề): Chứa metadata dạng key-value, thường dùng để lưu ID truy vết hệ thống (Tracing ID/Correlation ID), định dạng dữ liệu, hoặc thông tin bảo mật.
- Timestamp: Thời gian tạo sự kiện hoặc thời gian Broker nhận sự kiện.
B. Topic
Topic là một khái niệm logic, hoạt động giống như một thư mục (folder) chứa các file log. Nó giúp phân loại dữ liệu trong hệ thống. Ví dụ: chúng ta có topic orders chứa toàn bộ sự kiện liên quan đến đơn hàng, topic clicks chứa logs hành vi nhấp chuột của người dùng, topic inventory-updates chứa thông tin thay đổi tồn kho.
C. Partition (Phân vùng) - Bí mật của khả năng Scale cực đại
Đây là nơi phép thuật thực sự diễn ra. Một Topic trong Kafka không được lưu trữ như một file log khổng lồ duy nhất trên một máy chủ. Nếu làm thế, kích thước của Topic sẽ bị giới hạn bởi dung lượng đĩa cứng của cỗ máy đó, đồng thời tốc độ ghi/đọc sẽ bị nghẽn cổ chai bởi băng thông của một CPU duy nhất.
Để giải quyết vấn đề này, Kafka chia nhỏ một Topic thành nhiều Partition vật lý độc lập. Mỗi Partition là một Commit Log tuần tự riêng biệt.
[ TOPIC: orders ]
|
+-------------------------+-------------------------+
| |
v v
[ Partition 0 ] [ Partition 1 ]
(Nằm trên Broker 1) (Nằm trên Broker 2)
+---+---+---+---+---+ +---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | | 0 | 1 | 2 | 3 |
+---+---+---+---+---+ +---+---+---+---+
Cơ chế phân chia Partition mang lại hai giá trị to lớn cho thiết kế hệ thống phân tán:
- Mở rộng lưu trữ vật lý: Các partition của cùng một Topic có thể được phân tán nằm trên các máy chủ khác nhau (gọi là các Kafka Broker) trong cụm cluster. Ví dụ, Topic
orderscó 100 partitions, 50 partitions nằm ở Broker 1, 50 partitions nằm ở Broker 2. Nhờ đó, tổng dung lượng lưu trữ của Topic này bằng tổng dung lượng đĩa cứng của cả cụm máy chủ cộng lại, phá vỡ mọi giới hạn vật lý của một đơn máy chủ. - Xử lý song song cực đại (Parallelism): Vì mỗi partition hoạt động độc lập, Kafka cho phép nhiều Consumer khác nhau đọc đồng thời từ nhiều partition khác nhau. Càng nhiều partition, thông lượng (throughput) ghi và đọc của toàn hệ thống càng tăng tuyến tính.
6. Góc nhìn Senior: Các Đánh đổi lớn (Trade-offs)
Một kỹ sư thiết kế hệ thống thực thụ luôn hiểu rằng không có giải pháp nào mang lại toàn bộ ưu điểm mà không phải trả giá. Việc lựa chọn cấu trúc Partition của Kafka giúp hệ thống đạt throughput hàng triệu message/giây, nhưng nó cũng đi kèm những sự đánh đổi lớn về mặt thiết kế ứng dụng.
Đánh đổi 1: Thứ tự thông điệp (Ordering Guarantee)
Kafka chỉ đảm bảo thứ tự tuyệt đối của các sự kiện trong phạm vi một Partition duy nhất, KHÔNG đảm bảo thứ tự trên toàn bộ Topic.
- Nếu bạn gửi Event A và Event B vào cùng một Partition 0, Kafka cam kết Event A ghi trước sẽ được Consumer đọc trước.
- Nếu bạn gửi Event A vào Partition 0 và Event B vào Partition 1, không có cách nào kiểm soát được Consumer nào sẽ đọc Event nào trước.
Giải pháp: Để giải quyết bài toán này, bạn phải sử dụng Message Key. Kafka sẽ dùng thuật toán băm (hashing) dựa trên Key để đưa toàn bộ sự kiện có chung Key về duy nhất một partition. Ví dụ: dùng customer_id hoặc order_id làm Key, toàn bộ lịch sử thanh toán của khách hàng đó sẽ luôn nằm trên một partition, đảm bảo thứ tự xử lý luôn chuẩn xác.
Đánh đổi 2: Tính bất biến của số lượng Partition (Partition Immutability)
Bạn có thể dễ dàng tăng số lượng partition của một Topic đang chạy trên production. Nhưng bạn không thể giảm số lượng partition đi.
Hơn nữa, khi bạn tăng số lượng partition, công thức băm Key (hash(key) % partition_count) sẽ bị thay đổi hoàn toàn.
- Trước khi tăng: Tin nhắn của khách hàng X được đẩy vào Partition 2.
- Sau khi tăng: Tin nhắn mới của khách hàng X bị tính toán ra Partition 5.
- Hậu quả: Thứ tự xử lý giữa tin nhắn cũ (ở Partition 2) và tin nhắn mới (ở Partition 5) của cùng khách hàng X bị đảo lộn hoàn toàn trên production.
Đánh đổi 3: Chi phí vận hành hạ tầng
Quản lý một cụm Kafka Cluster đòi hỏi đội ngũ DevOps/SRE có kiến thức chuyên chuyên sâu. Từ việc quản lý cơ chế đồng thuận (ZooKeeper hoặc KRaft), cấu hình các thông số nhân bản dữ liệu (Replication Factor, In-Sync Replicas - ISR) cho đến việc xử lý sự cố đứt mạng giữa các Broker. Chi phí này lớn hơn rất nhiều so với việc deploy một database hay một RabbitMQ instance đơn giản.
7. Các trường hợp thất bại thực tế (Failure Cases)
Dưới đây là những sai lầm kinh điển mà các đội ngũ kỹ sư thường gặp phải khi áp dụng Kafka vào thực tế:
Lỗi 1: Thiết kế Key không phân phối đều (Hot Partition)
Một ứng dụng chọn country_code làm Message Key cho topic giao dịch. Tuy nhiên, 90% lượng người dùng của ứng dụng nằm ở Việt Nam (VN), 10% còn lại rải rác ở các nước khác.
Khi chạy thuật toán băm, toàn bộ các transaction của Việt Nam bị dồn hết vào duy nhất Partition 0, trong khi các partition khác hầu như trống rỗng. Kết quả là Kafka Broker chứa Partition 0 liên tục bị quá tải đĩa cứng và nghẽn CPU, trong khi toàn bộ hệ thống không thể scale ngang dù đã tăng số lượng partition lên 100.
Lỗi 2: Lạm dụng số lượng partition quá mức
Nghĩ rằng càng nhiều partition hệ thống càng nhanh, một đội ngũ kỹ sư tạo ra hàng nghìn partition cho từng topic nhỏ. Hậu quả là hệ thống bị cạn kiệt File Descriptors của hệ điều hành, thời gian khôi phục hệ thống khi Broker gặp sự cố (failover time) kéo dài từ vài giây lên vài chục phút do Kafka phải thực hiện bầu chọn Leader cho hàng vạn partition cùng một lúc.
8. Tổng kết và Tư duy Kỹ sư (Key Takeaways)
- Kafka không phải là Message Queue nâng cấp: Kafka là một Event Streaming Platform được thiết kế trên nền tảng Distributed Commit Log. Hãy dùng RabbitMQ nếu bạn cần một hàng đợi phức tạp có cơ chế routing linh hoạt và dữ liệu cần xóa ngay sau khi đọc. Hãy dùng Kafka nếu bạn cần xử lý thông lượng lớn, phân tích dữ liệu thời gian thực và cần khả năng replay dữ liệu lịch sử.
- Partition là đơn vị mở rộng: Hãy thiết kế số lượng partition dựa trên thông lượng ghi/đọc mong muốn của doanh nghiệp trong tương lai và luôn nhớ giới hạn về mặt thứ tự dữ liệu.
- Lựa chọn Message Key cẩn thận: Message Key quyết định tính phân phối tải của dữ liệu trên các partition và đảm bảo tính tuần tự của nghiệp vụ. Hãy chọn các Key có độ phân tán cao (High Cardinality) như
customer_idhoặctransaction_id.
9. Lời kết và Open Loop
Hiểu được bản chất tĩnh của Message, Topic và Partition mới chỉ là bước khởi đầu. Trên môi trường production thực tế, dữ liệu liên tục dịch chuyển. Làm cách nào để các Producer đẩy hàng triệu message vào các partition một cách an toàn mà không bị nghẽn? Làm thế nào để các Consumer tự động chia sẻ tải đọc dữ liệu mà không dẫm chân lên nhau, và cơ chế Rebalance hoạt động ra sao khi có một Consumer instance bị crash?
Chúng ta sẽ cùng nhau đi sâu và mổ xẻ những bí mật vận hành động này trong Episode 02: Kafka Deep Dive – Sức Mạnh Thực Sự Đến Từ Đâu?.
🚀 Tiếp tục hành trình cùng TechCraft
Nếu bài viết này giúp bạn nhìn Kafka như một hệ thống dữ liệu thời gian thực thay vì chỉ là một message queue, thì đây mới chỉ là điểm bắt đầu.
TechCraft đang tiếp tục xây dựng Dev Insider như nơi tập trung các series chuyên sâu hơn về Backend Engineering, Database, Distributed Systems và tư duy thiết kế production systems.
🚀 Dev Insider https://www.patreon.com/techcraft_official/posts/vi-sao-dev-ra-161163881?collection=2220113
📘 Facebook https://www.facebook.com/techcraft.official
🎥 YouTube https://www.youtube.com/@techcraft.official
🎵 TikTok https://www.tiktok.com/@techcraft.official
Hiểu hệ thống. Không chỉ framework.
All rights reserved