ข้ามไปยังเนื้อหา

Performance & Tuning

การ tune Kafka คือการเลือกจุดที่คุณจะยืนบนเส้น throughput เทียบกับ latency และตัวเลขทุกตัวในบทนี้มาจาก Docker บนเครื่อง broker เดียว บน Mac เครื่องนี้ — จริง ทำซ้ำได้ และไม่ใช่ตัวแทนของการ benchmark workload จริงของคุณบน hardware จริงของคุณ

พิสูจน์: จำนวน partition วัดจริง ไม่ใช่แค่กล่าวอ้าง

หัวข้อที่มีชื่อว่า “พิสูจน์: จำนวน partition วัดจริง ไม่ใช่แค่กล่าวอ้าง”

สอง topic, broker เดียวกัน, ตั้งค่า producer เหมือนกัน ต่างกันแค่จำนวน partition

scripts/performance-and-tuning/partition-count-perf.sh
/opt/kafka/bin/kafka-topics.sh --bootstrap-server "$BOOTSTRAP" \
--create --topic perf-1p --partitions 1 --replication-factor 1
/opt/kafka/bin/kafka-topics.sh --bootstrap-server "$BOOTSTRAP" \
--create --topic perf-12p --partitions 12 --replication-factor 1
echo "=== 1 partition ==="
/opt/kafka/bin/kafka-producer-perf-test.sh --topic perf-1p \
--num-records 200000 --record-size 200 --throughput -1 \
--producer-props bootstrap.servers="$BOOTSTRAP" acks=1 2>&1 | tail -1
echo "=== 12 partitions ==="
/opt/kafka/bin/kafka-producer-perf-test.sh --topic perf-12p \
--num-records 200000 --record-size 200 --throughput -1 \
--producer-props bootstrap.servers="$BOOTSTRAP" acks=1 2>&1 | tail -1

output จริงแบบไม่แก้ไข:

=== 1 partition ===
200000 records sent, 204708.290686 records/sec (39.05 MB/sec), 91.32 ms avg latency, 197.00 ms max latency, 99 ms 50th, 189 ms 95th, 192 ms 99th, 193 ms 99.9th.
=== 12 partitions ===
200000 records sent, 328407.224959 records/sec (62.64 MB/sec), 1.37 ms avg latency, 175.00 ms max latency, 1 ms 50th, 3 ms 95th, 5 ms 99th, 11 ms 99.9th.

broker เดียวกัน disk เดียวกัน config producer เดียวกัน — throughput สูงกว่าประมาณ 60% และ latency เฉลี่ยต่ำกว่าประมาณ 66 เท่า ด้วย 12 partition เทียบกับ 1 partition นี่คือ Docker บนเครื่อง Mac เครื่องเดียว ไม่ใช่ตัวเลข capacity-planning สำหรับ cluster ของคุณ แต่ทิศทางเป็นจริงและอธิบายได้: มี partition เดียว การ append ทุกครั้ง serialize ผ่าน log-writer path เดียวของ partition นั้น; มี 12 partition I/O thread ของ broker (8 ตัวโดย default พิสูจน์ด้านล่าง) append เข้า log อิสระ 12 ตัวพร้อมกันได้ แทนที่จะต่อคิวรออันเดียว

scripts/performance-and-tuning/consumer-perf-test.sh
/opt/kafka/bin/kafka-consumer-perf-test.sh --bootstrap-server "$BOOTSTRAP" \
--topic perf-12p --num-records 200000
# Clean up both scratch topics this lesson created above.
/opt/kafka/bin/kafka-topics.sh --bootstrap-server "$BOOTSTRAP" --delete --topic perf-1p
/opt/kafka/bin/kafka-topics.sh --bootstrap-server "$BOOTSTRAP" --delete --topic perf-12p
start.time, end.time, data.consumed.in.MB, MB.sec, data.consumed.in.nMsg, nMsg.sec, rebalance.time.ms, fetch.time.ms, fetch.MB.sec, fetch.nMsg.sec
2026-09-29 02:42:04:251, 2026-09-29 02:42:07:878, 38.1470, 10.5175, 200000, 55141.9906, 3459, 168, 227.0653, 1190476.1905

อ่านคอลัมน์ตรง ๆ nMsg.sec (55,142/sec รวมเวลา group-join เข้าไปด้วย) ดูช้ากว่า fetch.nMsg.sec (1,190,476/sec แค่เวลา fetch) มาก — ต่างกันประมาณ 21 เท่าใน run เดียวกัน เพราะ rebalance.time.ms (3,459 ms — การ handshake กับ group coordinator) มากกว่า fetch.time.ms (168 ms — การส่งข้อมูลจริง) มากบน test เล็ก ๆ 200,000 record ตัวเลข consumer-perf-test จริงที่ผสมเวลา rebalance เข้าไปใน “throughput” จะทำให้ Kafka ดูช้ากว่าความจริงมากสำหรับ benchmark สั้น ๆ สำหรับอะไรที่มากกว่าการเช็กแบบเร็ว ให้ดู fetch.MB.sec/fetch.nMsg.sec เฉพาะเจาะจง หรือรันด้วยจำนวน record ที่มากพอให้ต้นทุน rebalance ครั้งเดียวไม่ครอบงำค่าเฉลี่ย

Batching & Compression วัด linger.ms/batch.size/compression.type ลงลึกไปแล้วบน broker ของคอร์สนี้เอง (ตัวเลขจริงของ kafka-producer-perf-test.sh: 317,460 records/sec ที่ linger/batch setting หนึ่ง เทียบ 303,951 records/sec ที่อีกค่าหนึ่ง พร้อมพิสูจน์ compression ratio เต็มรูปแบบผ่าน kafka-dump-log.sh) ไม่พูดซ้ำที่นี่ run วัดจำนวน partition ของบทนี้เองด้านบนใช้ tool เดียวกัน broker family เดียวกัน Mac เดียวกัน ยืนยันว่าตัวเลขเหล่านั้นไม่ใช่เรื่องบังเอิญครั้งเดียว: throughput ของ kafka-producer-perf-test.sh ในช่วงหลักแสนต้น ๆ ถึงกลาง ๆ record/sec บน broker เดียว Docker ในเครื่อง คือ order of magnitude ที่สม่ำเสมอของ environment นี้ข้ามหลายบทที่เป็นอิสระจากกัน

Thread pool: num.io.threads, num.network.threads, num.replica.fetchers, และสัญญาณจริงที่ต้องดู

หัวข้อที่มีชื่อว่า “Thread pool: num.io.threads, num.network.threads, num.replica.fetchers, และสัญญาณจริงที่ต้องดู”

default จริง อ่านตรงจาก broker ของบทนี้เอง

$ kafka-configs.sh --bootstrap-server localhost:9092 --describe --entity-type brokers --entity-name 1 --all | grep -E "num\.(io|network|replica\.fetchers)"
num.io.threads=8 sensitive=false synonyms={DEFAULT_CONFIG:num.io.threads=8}
num.network.threads=3 sensitive=false synonyms={DEFAULT_CONFIG:num.network.threads=3}
num.replica.fetchers=1 sensitive=false synonyms={DEFAULT_CONFIG:num.replica.fetchers=1}
  • num.io.threads (8) — thread ที่ทำ disk I/O จริงสำหรับ produce/fetch request หลังจาก network layer อ่านมาจาก wire แล้ว น้อยเกินไปแล้ว request จะต่อคิวรอ disk operation แม้จะมี network capacity เหลือเฟือ
  • num.network.threads (3) — thread ที่อ่าน request จาก socket และเขียน response กลับ น้อยเกินไปแล้ว client จะเห็นความช้าระดับ connection แม้ disk I/O เร็วอยู่แล้ว
  • num.replica.fetchers (1) — thread ที่ broker follower ใช้ fetch จาก leader ไม่เกี่ยวกับ setup broker เดียวของคอร์สนี้ (RF=1 ไม่มี follower) แต่เป็น knob ตัวแรกที่ควรเพิ่มบน cluster หลาย broker จริงถ้า replication เองตามหลัง (เห็นเป็น UnderReplicatedPartitions ที่ไต่ขึ้น พิสูจน์ในบทก่อนหน้า)

RequestHandlerAvgIdlePercent (แนะนำในบทเดียวกันนั้น) คือสัญญาณจริงว่า num.io.threads size ถูกหรือไม่ — ไม่ใช่การเดา เป็นการวัดตรง รัน load ของ producer ที่หนักที่สุดของบทนี้ (record หลักล้าน acks=1 batching default) กับ client kafka-producer-perf-test.sh แบบ single-threaded นี้ แล้ว sample กลาง run

kafka.server:type=KafkaRequestHandlerPool,name=RequestHandlerAvgIdlePercent:OneMinuteRate=0.9810855172428229

ยังว่างอยู่ ~98% ภายใต้ load ต่อเนื่องที่หนักที่สุดของบทนี้ ข้อสรุปที่ซื่อสัตย์ไม่ใช่ “8 thread ใหญ่เกินไป” — แต่คือ process producer แบบ single-thread ตัวเดียวบน Mac เครื่องหนึ่งสร้าง concurrent request volume ไม่พอที่จะโหลด I/O thread 8 ตัวบน broker ในเครื่องได้จริง การทำให้ metric นี้อิ่มตัวจริง ๆ ต้องการ client connection พร้อมกันหลายตัว ซึ่งเป็นเหตุผลว่าทำไม RequestHandlerAvgIdlePercent ที่ไต่ลงใน production จริง (client จริงหลายตัว) มีความหมาย ในขณะที่ metric เดียวกันที่นิ่งในการ benchmark ในเครื่องแบบ client เดียวไม่ใช่หลักฐานอะไรเลยทั้งสองทาง

เอกสาร design ของ Kafka เองพูดตรง ๆ เรื่องนี้ (quote จาก kafka.apache.org/43/design/design/): “rather than maintain as much as possible in-memory and flush it all out to the filesystem in a panic when we run out of space, we invert that” — Kafka เขียนผ่านเข้า filesystem ทันที และให้ OS page cache ไม่ใช่ JVM heap เก็บข้อมูลร้อนในหน่วยความจำ การเสิร์ฟ fetch request ที่อยู่ใน page cache แล้วใช้ sendfile และเอกสารเดียวกันบอกตรง ๆ: “Using sendfile, this re-copying is avoided by allowing the OS to send the data from pagecache to the network directly” — kernel ย้าย byte จาก page cache ไป network socket โดยไม่เคย copy เข้า JVM user-space heap เลย หน้าเดียวกันยังบอกผลลัพธ์ด้าน durability สำหรับ consumer ที่ตามทันแล้ว: “you will see no read activity on the disks whatsoever as they will be serving data entirely from cache” Storage & Segments ครอบคลุม format ของ segment บนดิสก์ที่ zero-copy path นี้อ่านตรงจากมัน — ไม่พูดซ้ำที่นี่

flowchart LR
  disk["Disk"] -->|"read once"| cache["OS page cache"]
  cache -->|"sendfile() -- zero-copy"| socket["Network socket"]
  socket --> consumer["Consumer"]
  cache -.->|"NOT copied into"| heap["JVM heap (bypassed)"]
Zero-copy: page cache ไปยัง network socket ไม่มีการ copy เข้า JVM heap

นี่ยังเป็นเหตุผลเชิงปฏิบัติที่ JVM heap ของ broker Kafka เล็กโดยตั้งใจ (ไม่กี่ GB เป็นปกติ แม้บนเครื่องที่มี RAM มากกว่านั้นมาก) — heap ใหญ่แย่ง RAM ที่ page cache จะใช้เก็บข้อมูลเดียวกันที่ Kafka พยายามเสิร์ฟให้เร็ว และ heap ใหญ่แปลว่า GC pause นานขึ้นบน path ประมวลผล request ของ broker เอง

socket.send.buffer.bytes (default จริง 102400 — ยืนยันบน broker ของบทนี้ คำสั่งเดียวกับด้านบน) กำหนดขนาด TCP send buffer ระดับ OS ต่อ connection การเพิ่มมันช่วย network path ที่ latency สูง/bandwidth สูงได้ แต่บน network ในเครื่องที่สุขภาพดี มันไม่ต่างอย่างมีนัยสำคัญ และตั้งสูงเกินไปก็แค่จอง memory ต่อ connection ที่ broker ที่มีหลาย connection จะรู้สึกได้

log.flush.* เป็นกรณีที่ต่างออกไป — ไม่ใช่ “ไม่ค่อยมีประโยชน์” แต่ อันตรายจริงถ้าแตะ บน workload ส่วนใหญ่ ค่าจริงของ broker ของบทนี้เอง

log.flush.interval.messages=9223372036854775807 sensitive=false synonyms={DEFAULT_CONFIG:log.flush.interval.messages=9223372036854775807}
log.flush.scheduler.interval.ms=9223372036854775807 sensitive=false synonyms={DEFAULT_CONFIG:log.flush.scheduler.interval.ms=9223372036854775807}

9223372036854775807 คือ Long.MAX_VALUE — default จริงของ Kafka คือไม่เคยบังคับ fsync ตามจำนวน message หรือช่วงเวลาคงที่ durability มาจาก replication (หลาย broker เก็บข้อมูล) และ acks/min.insync.replicas พิสูจน์ทั่วทั้งproducersและdelivery-and-transactions ไม่ใช่จากการบังคับเขียนทุกครั้งลง disk ก่อน ack การตั้ง log.flush.interval.messages ต่ำเพิ่ม fsync จริงบน hot write path ของ broker สำหรับทุก batch ซึ่งลด throughput อย่างมีนัยสำคัญและยังไม่แทนที่ replication จริงได้ — broker เดียวที่ไม่มี replication ที่ fsync ทุก message ก็ยังเป็นจุดเดียวที่ข้อมูลหายได้ถ้า disk นั้นพัง

ขีดจำกัดของ OS ที่สำคัญตอน scale — อ้างอิง ไม่ได้รัน

หัวข้อที่มีชื่อว่า “ขีดจำกัดของ OS ที่สำคัญตอน scale — อ้างอิง ไม่ได้รัน”

partition มากขึ้นแปลว่า open file มากขึ้นและ memory-mapped region มากขึ้น และ OS default จริงสองตัวกลาย load-bearing ทันทีที่ broker host มันจำนวนมาก จากเอกสาร operations ของ Kafka เอง (kafka.apache.org/43/operations/hardware-and-os/)

Kafka recommends at least 100,000 allowed file descriptors for the broker processes as a starting point. If a broker hosts many partitions, the broker needs at least (number_of_partitions) * (partition_size / segment_size) file descriptors to track all log segments, in addition to the number of connections the broker makes.

By default, on a number of Linux systems, the value of vm.max_map_count is somewhere around 65,535. Each log segment… requires a pair of index/timeindex files, and each of these files consumes 1 map area… Creating 50,000 partitions on a broker will result in allocation of 100,000 map areas and likely cause broker crash with OutOfMemoryError (Map failed) on a system with default vm.max_map_count.

container Docker ของคอร์สนี้เองรันด้วยขีดจำกัดที่กว้างขวางอยู่แล้ว — ยืนยันตรง ไม่ใช่จากเอกสาร

$ docker exec broker sh -c 'ulimit -n; cat /proc/sys/vm/max_map_count'
1048576
262144

open file 1,048,576 ตัวและ vm.max_map_count 262,144 — ทั้งคู่สูงกว่า default ของ Linux เปล่า ๆ ที่เอกสารเตือนไว้มาก ซึ่งเป็นเหตุผลเป๊ะว่าทำไมบทที่ broker เดียว partition น้อยของคอร์สนี้ไม่เคยชนขีดจำกัดทั้งสองเลย host production จริงที่ตั้งด้วย OS default เปล่า ๆ รัน broker ที่มี partition หลักหมื่น สามารถชนทั้งคู่ได้จริง — วิธีแก้คือค่า sysctl/ulimit ที่ quote ไว้ด้านบน ใส่ไว้ก่อนที่ broker จะเจอ load นั้น ไม่ใช่หลังจาก OutOfMemoryError ที่เอกสารอธิบาย

🐛 หา bug ในโค้ดนี้

# section: broker
# AI comment: "Tune for maximum durability and consistency"
log.flush.interval.messages=1
log.flush.interval.ms=1000
num.io.threads=64
เฉลย

อ่านดูเหมือนการ tune ที่รอบคอบเน้น durability — flush ตลอดเวลาไม่ให้อะไรค้างนานเกินไป แล้วโยน I/O thread เพิ่มเข้าไปให้ broker เผื่อไว้ ปัญหาจริง พิสูจน์ด้านบน: mechanism durability จริงของ Kafka คือ replication (acks, min.insync.replicas) — การบังคับ fsync ทุก message เดียว (log.flush.interval.messages=1) เพิ่มต้นทุนจริงที่วัดได้บน write path ทุก batch สำหรับการันตีที่ replication ให้ได้แข็งแรงกว่าอยู่แล้ว (รอดจาก disk ของ broker ทั้งตัวพัง ซึ่ง fsync discipline ของ broker เดียวทำไม่ได้เลย) num.io.threads=64 ก็ไม่ใช่ “tune เพิ่ม” เหมือนกัน — พิสูจน์ด้านบน broker ของคอร์สนี้เองไม่สามารถผลัก thread default 8 ตัวให้ต่ำกว่า ~98% idle ได้ด้วย load client เดียวที่หนักจริง การเพิ่มจำนวน thread บน broker ที่ไม่ได้ bound ด้วย I/O thread เสีย memory และ context-switch overhead ให้ thread ที่จะนั่งว่างเปล่า และไม่ช่วยคอขวดจริงเลย (ซึ่งบน disk เดียว มีโอกาสสูงว่าเป็น disk เองหรือ — พิสูจน์ด้านบน — จำนวน partition ที่มีให้ parallelize ตั้งแต่แรก)

ตัวเลข performance ของ Kafka ทุกตัวอยู่บนหนึ่งในสามแกนอิสระ: ฝั่ง producer (batching, compression, จำนวน partition — คุณรับ byte ได้เร็วแค่ไหน), ฝั่ง broker (thread pool, page cache, disk — broker ประมวลผลและเก็บสิ่งที่รับมาได้เร็วแค่ไหน), และ ฝั่ง consumer (ขนาด fetch, parallelism, overhead ของ group rebalance — คุณดึง byte กลับออกมาได้เร็วแค่ไหน) บทนี้พิสูจน์ชัยชนะจริงฝั่ง producer (partition), ผลลัพธ์-ไม่-เกิดขึ้นจริงฝั่ง broker (thread pool ไม่อิ่มตัวในเครื่อง), และ gotcha จริงฝั่ง consumer (เวลา rebalance ครอบงำ benchmark สั้น) การ tune แกนผิดสำหรับคอขวดจริงของคุณคือความผิดพลาดที่พบบ่อยที่สุดตัวเดียว — พิสูจน์ตรงโดย SpotTheBug ของบทนี้เอง ที่ “tune” durability และ I/O thread ในขณะที่ mechanism durability จริง (replication) และ lever throughput จริงที่บทนี้วัดไว้ (จำนวน partition) ไม่ถูกแตะเลย

  • partition มากขึ้นเพื่อ parallelism ของ producer/consumer — พิสูจน์ด้านบน ชัยชนะ throughput และ latency จริงบน broker เดียวกัน — เทียบกับต้นทุนจริงที่พิสูจน์ใน Brokers, Topics & Partitions: open file มากขึ้น controller metadata มากขึ้น rebalance นานขึ้น มี ceiling จริงอยู่ บทนี้ไม่ได้หามันเจอ พิสูจน์แค่ทิศทางที่อยู่ต่ำกว่ามัน
  • การเพิ่มขนาด thread pool — config ได้ฟรี แต่พิสูจน์ด้านบนว่าไม่มีความหมายเมื่อ workload ไม่เคยเข้าใกล้การทำให้ default อิ่มตัว ต้นทุนจริงคือ memory และ context-switching overhead ที่ต้องจ่ายไม่ว่า thread จะทำงานมีประโยชน์หรือไม่
  • การแตะ log.flush.* เพื่อ “ความปลอดภัยเพิ่ม” — ให้ความรู้สึกระมัดระวัง แต่พิสูจน์ได้ (ผ่าน default จริงด้านบน) ว่าไม่ใช่วิธีที่ durability model ของ Kafka เองทำงาน และมีต้นทุน throughput จริงตรง ๆ โดยไม่มี durability benefit คู่กันเลยเมื่อ replication ตั้งค่าถูกต้องแล้ว
  • Benchmark Docker broker เดียวในเครื่อง — ถูก ทำซ้ำได้ มีประโยชน์จริงสำหรับการเทียบแบบสัมพัทธ์ (ผลลัพธ์ partition-count และ batching cross-link ของบทนี้) — และไม่ใช่ตัวแทนของการ load-test cluster จริงของคุณภายใต้ load client จริงพร้อมกันก่อนจะเชื่อตัวเลขสมบูรณ์ใด ๆ
  • “num.io.threads มากขึ้นแปลว่า throughput ดีขึ้นเสมอ” ไม่ — พิสูจน์ด้านบน: broker ของคอร์สนี้นิ่งอยู่ที่ ~98% idle บน RequestHandlerAvgIdlePercent ภายใต้ test ในเครื่องที่หนักที่สุด thread มากขึ้นช่วยก็ต่อเมื่อ pool ปัจจุบันเป็นคอขวดจริง ซึ่งบทนี้สร้างไม่ได้เลยในเครื่องด้วย client เดียว
  • “log.flush.interval.messages บ่อยขึ้นทำให้ Kafka durable ขึ้น” ไม่ — พิสูจน์ด้านบน: default จริงคือ Long.MAX_VALUE (แทบไม่เกิดเลย) เพราะ durability มาจาก replication และ acks ไม่ใช่จาก fsync ต่อ message การบังคับ flush บ่อย ๆ เพิ่มต้นทุนจริงสำหรับการันตีที่มันไม่ได้ทำให้แข็งแรงขึ้นจริง
  • “ตัวเลข nMsg.sec หัวเรื่องของ kafka-consumer-perf-test.sh คือ fetch throughput จริงของ tool” ไม่ — พิสูจน์ด้านบน: ตัวเลขนั้นผสม rebalance.time.ms ครั้งเดียวเข้าไปในค่าเฉลี่ย; fetch throughput จริงล้วน ๆ (fetch.nMsg.sec) สูงกว่า 20 เท่าใน run เดียวกัน
  • “Kafka เร็วทั้งที่เขียนทุก message ลง disk” กลับกัน — พิสูจน์ด้านบนด้วย quote ตรงจากเอกสาร design ของ Kafka เอง: Kafka เร็วเพราะมันเขียนลง disk ทันทีและให้ OS page cache บวก sendfile zero-copy ทำการเสิร์ฟ ตั้งใจหลีกเลี่ยง in-process JVM cache ที่แข่งกัน ไม่ใช่หาทางเลี่ยงการเขียน disk
พิสูจน์ด้านบนด้วยสอง topic บน broker เดียวกัน จำนวน partition (1 เทียบ 12) ทำให้ throughput และ latency ของ producer-perf-test ต่างกันจริงอย่างไร?
พิสูจน์ด้านบนใน run จริงของ kafka-consumer-perf-test.sh ทำไมคอลัมน์หัวเรื่อง nMsg.sec (55,142/sec) ต่ำกว่า fetch.nMsg.sec (1,190,476/sec) มากใน run เดียวกันเป๊ะ?
พิสูจน์ด้านบน เกิดอะไรขึ้นกับ RequestHandlerAvgIdlePercent ตอนบทนี้รัน load producer client เดียวที่หนักที่สุดกับ num.io.threads default 8 ตัวของ broker?
quote ด้านบนจากเอกสาร design ของ Kafka เอง (kafka.apache.org/43/design/design/) ทำไม Kafka ถึงพึ่ง OS page cache และ sendfile แทน JVM heap cache ขนาดใหญ่?
พิสูจน์ด้านบนด้วยค่า config จริงของ broker ของบทนี้เอง log.flush.interval.messages default เป็นอะไร และทำไมมันสำคัญ?
อ้างอิงด้านบนจากเอกสาร operations จริงของ Kafka เกิดอะไรขึ้นถ้า broker host partition 50,000 ตัวบน Linux ที่ปล่อย vm.max_map_count ไว้ที่ default (~65,535)?