Kafka คืออะไร
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”Kafka คือ log แบบ distributed, append-only ที่แยกระบบฝั่งที่ผลิตข้อมูลออกจากระบบฝั่งที่บริโภคข้อมูล producer เขียน event ครั้งเดียว แล้ว consumer อิสระกี่ตัวก็ได้อ่าน event นั้นตามลำดับ ตามจังหวะของแต่ละตัวเอง
ไม่ใช่ queue แต่เป็น log
หัวข้อที่มีชื่อว่า “ไม่ใช่ queue แต่เป็น log”message queue แบบเดิม ลบ message ทิ้งทันทีที่ consumer ack แล้ว มี reader เชิงตรรกะแค่ตัวเดียว และ message หายไปหลังส่งเสร็จ Kafka ต่างออกไป เป็น log เมื่อ producer เขียน record เข้ามา Kafka จะ append record นั้นเข้า partition แล้วเก็บไว้ตาม retention period ที่ตั้งไว้ การอ่าน record ไม่ได้ ทำให้ record หายไป consumer แต่ละตัว track ตำแหน่งของตัวเอง (คือ offset) ใน log ดังนั้นสองทีมอ่าน stream เดียวกันเพื่อจุดประสงค์คนละอย่างได้เลยโดยไม่กวนกัน
การตัดสินใจออกแบบข้อเดียวนี้แหละ คือเหตุผลที่ Kafka ถูกใช้ทำหลายอย่างพร้อมกัน
- Messaging แยก service ออกจากกันด้วย buffer ที่ทนทานคั่นกลาง
- Event sourcing ตัว log คือ source of truth ส่วน state เป็นแค่ projection ของตัวเอง
- Stream processing transform และ join stream ตอนที่ข้อมูลไหลเข้ามา
- Data integration pipeline เดียวป้อนข้อมูลให้ database, search index และ warehouse
flowchart LR p1["Producer: orders service"] --> log["Topic (append-only log)"] p2["Producer: payments service"] --> log log --> c1["Consumer group: fraud detection"] log --> c2["Consumer group: analytics"] log --> c3["Consumer group: email notifications"]
ทำไมไม่ใช้ database ไปเลย
หัวข้อที่มีชื่อว่า “ทำไมไม่ใช้ database ไปเลย”คุณ อาจจะ poll ตาราง database เพื่อหา row ใหม่ก็ได้ แต่นั่นคือการสร้าง Kafka ขึ้นมาใหม่แบบห่วย ๆ ไม่มี ordering guarantee ต่อ key ในตัว, ไม่มี fan-out ไปหา reader หลายตัวอย่างมีประสิทธิภาพ, ไม่มี back-pressure, replay จากจุดใดก็ได้ไม่ได้ และ scale throughput ตามแนวนอนไม่ได้ Kafka ถูกออกแบบมาเพื่อ event stream ที่ throughput สูง, มีลำดับ และ replay ได้ โดยเฉพาะ ทั้งการเขียนดิสก์แบบ sequential, การอ่านแบบ zero-copy และ partitioning ที่ scale การอ่านเขียนกระจายทั้ง cluster
ศัพท์หลักที่ต้องรู้
หัวข้อที่มีชื่อว่า “ศัพท์หลักที่ต้องรู้”คุณจะเจอศัพท์พวกนี้ตลอด เลยต้องปักหมุดไว้ตั้งแต่ตอนนี้
- Broker เซิร์ฟเวอร์ Kafka หนึ่งตัว ส่วน cluster คือ broker หลายตัวรวมกัน
- Topic stream ของ record ที่มีชื่อ (เช่น
orders) เชิงตรรกะคือ log หนึ่งอัน - Partition topic ถูกแบ่งเป็น partition แต่ละ partition คือ log ที่มีลำดับจริง ๆ
- Offset ตำแหน่งของ record ภายใน partition consumer track offset เพื่อรู้ว่าตัวเองอ่านถึงไหนแล้ว
- Producer / Consumer client ที่เขียนเข้าและอ่านออกจาก topic
พิสูจน์: produce → consume round trip จริง
หัวข้อที่มีชื่อว่า “พิสูจน์: produce → consume round trip จริง”ทุกอย่างข้างบนเป็นแค่คำกล่าวอ้างจนกว่าจะรันกับ broker จริง นี่คือ single-broker Kafka 4.3.1 KRaft container บนเครื่องนี้ สร้าง topic, produce 3 record ด้วย CLI, list, describe แล้ว consume จากจุดเริ่มต้น
/opt/kafka/bin/kafka-topics.sh --bootstrap-server $BOOTSTRAP \ --create --if-not-exists --topic orders-demo --partitions 3 --replication-factor 1
/opt/kafka/bin/kafka-console-producer.sh --bootstrap-server $BOOTSTRAP --topic orders-demo <<'MSGS'order-101order-102order-103MSGS
echo "=== list topics ==="/opt/kafka/bin/kafka-topics.sh --bootstrap-server $BOOTSTRAP --list
echo "=== describe orders-demo ==="/opt/kafka/bin/kafka-topics.sh --bootstrap-server $BOOTSTRAP --describe --topic orders-demo
echo "=== consume from the beginning ==="timeout 8 /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server $BOOTSTRAP \ --topic orders-demo --from-beginning --max-messages 3output จริงไม่ได้แก้ไข จากสคริปต์เดียวกันนี้ (ชื่อ topic ตรงนี้คือ orders รันคำสั่งเดียวกันกับ broker ของคอร์สนี้เอง)
Created topic orders.=== list topics ===orders=== describe orders-demo ===Topic: orders TopicId: c8AQc2zSSeOo5q0EC94zFA PartitionCount: 3 ReplicationFactor: 1 Configs: min.insync.replicas=1 Topic: orders Partition: 0 Leader: 1 Replicas: 1 Isr: 1 Elr: LastKnownElr: Topic: orders Partition: 1 Leader: 1 Replicas: 1 Isr: 1 Elr: LastKnownElr: Topic: orders Partition: 2 Leader: 1 Replicas: 1 Isr: 1 Elr: LastKnownElr:=== consume from the beginning ===The consumer rebalance protocol (KIP-848) is production-ready! Set group.protocol=consumer to try it out. See https://kafka.apache.org/documentation/#consumer_rebalance_protocolorder-101order-102order-103Processed a total of 3 messagesมีสองอย่างที่ควรสังเกตจาก output จริง ไม่ใช่แค่ diagram: topic กลับมาพร้อม ReplicationFactor: 1 และ 3 partition อิสระ แต่ละตัวมี Leader ของตัวเอง ตรงกับโมเดล “topic ถูกแบ่งเป็น partition กระจายทั้ง cluster” จากศัพท์ข้างบนเป๊ะ พิสูจน์แล้ว ไม่ใช่แค่กล่าวอ้าง และ console consumer เองก็ print banner เรื่อง KIP-848 ซึ่งเป็น consumer rebalance protocol รุ่นใหม่ล่าสุด สัญญาณว่า broker build นี้ (4.3.1) ทันสมัยแค่ไหน โมดูลถัด ๆ ไปในคอร์สนี้ใช้มันโดยตรง
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”🐛 หา bug ในโค้ดนี้
import { Kafka } from 'kafkajs';
const kafka = new Kafka({ clientId: 'notifier', brokers: ['localhost:9092'] });const consumer = kafka.consumer({ groupId: 'notifier-group' });
async function main() {await consumer.connect();await consumer.subscribe({ topic: 'orders' });
await consumer.run({ eachMessage: async ({ message }) => { await sendNotification(message.value!.toString()); // AI comment: "message handled and removed from the queue" },});}
main();เฉลย
comment คือตัว bug ไม่ใช่โค้ด มันอธิบาย Kafka ว่าเป็น queue (“removed from the queue” หลังจัดการเสร็จ) ซึ่งเป็น mental model ที่ผิด orders ไม่ได้ถูก consume จนหายไปจาก handler ตัวนี้ record ที่พิสูจน์ไว้ข้างบน (order-101) ยังอยู่ใน log ตลอด retention period ของมัน consumer ตัวนี้เลื่อนแค่ offset ที่ commit ของ ตัวมันเอง สำหรับ consumer group notifier-group เท่านั้น consumer group อื่น ไม่ว่าจะเป็น analytics job, fraud-detection service, หรือรันโค้ดเดียวกันนี้ซ้ำด้วย groupId คนละตัว จะอ่าน record ชุดเดียวกันเป๊ะตั้งแต่ต้น โดยไม่ถูกกระทบจากสิ่งที่ notifier-group ทำไปแล้ว ตัวโค้ดเองทำงานถูกต้อง แต่ mental model ที่อยู่หลัง comment นั้น ถ้าหลุดไปเป็น design decision (เช่น สมมติว่า consumer ตัวที่สองของ topic เดียวกันจะไม่เห็นอะไรเลย) จะผิด
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ลองนึกถึง กระดานไวท์บอร์ดที่เขียนได้แค่ด้านล่างสุด ไม่เคยถูกลบ producer เขียนบรรทัดใหม่ต่อท้ายเรื่อย ๆ ส่วนผู้อ่านแต่ละคนมี sticky note ของตัวเอง ทำเครื่องหมายว่าอ่านลงมาถึงไหนแล้ว sticky note ของใครก็ไม่กระทบของคนอื่น และการอ่านของใครก็ไม่ลบบรรทัดไหนทิ้ง นั่นคือ mental model ทั้งหมด ศัพท์ “broker”, “topic”, “partition”, “offset” ข้างบนก็แค่ตั้งชื่อส่วนต่าง ๆ ของกระดานนี้ ห้องที่กระดานอยู่ (broker/cluster) กระดานไหน (topic) กระดานจริงแผ่นไหนถ้ามีมากกว่าหนึ่งแผ่นคุยเรื่องเดียวกัน (partition) และ sticky note อยู่ตรงไหน (offset)
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”- log แทนที่จะเป็น queue — consumer group แต่ละตัวได้ view อิสระที่ replay ได้ แลกกับที่ broker ต้องเก็บข้อมูลไว้ (ดิสก์ และถ้า topic แบบ compacted หรือ retention ยาว ก็มีต้นทุน storage จริง) แทนที่จะทิ้งทันทีที่อ่านเสร็จ
- replay ทำง่าย แต่ redeliver แค่ message เดียวที่ fail ทำยาก — queue แบบ per-message ack/nack retry แค่ message ที่มีปัญหาตัวเดียวได้ ส่วนหน่วย “ทำใหม่” ของ Kafka คือ offset ของ consumer คือ “replay จากจุดนี้ไปข้างหน้า” ซึ่งบท offsets and commits อธิบายลึกกว่านี้
- partitioning ให้ throughput และ ordering ภายใน partition แต่ไม่ได้ให้ total order เดียวข้ามทั้ง topic บทถัดไปในโมดูลนี้จะอธิบาย trade-off นี้ตรง ๆ
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “Kafka consumer ลบ message ทิ้ง เหมือน queue” ผิด พิสูจน์แล้วข้างบนและใน SpotTheBug ว่า record ยังอยู่ใน log ตลอด retention window ของมัน มีแค่ offset ของ consumer group ที่อ่านเท่านั้นที่เลื่อน
- “ถ้า service หนึ่ง consume topic ไปแล้ว service ใหม่ที่อ่านตั้งแต่ต้นจะไม่ได้อะไรเลย” ผิด consumer group ใหม่ที่ใช้
fromBeginning: true(หรือยังไม่เคย commit offset) อ่านประวัติทั้งหมดที่ยังถูกเก็บไว้ได้ ไม่เกี่ยวกับว่า group อื่นทำอะไรไปแล้ว - “Kafka ก็แค่ message queue ที่เร็วกว่า” ใช้แบบนั้นได้ แต่ log abstraction (retention, replay, reader อิสระหลายตัว, partition ที่มีลำดับ) เป็น primitive ที่ต่างจาก delete-on-ack ของ queue จริง ๆ ความต่างนี้แหละที่ทำให้ Kafka ทำ event sourcing และ stream processing ได้ ซึ่ง queue ทำไม่ได้