1 คะแนน โดย GN⁺ 2024-11-14 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ในการตรวจสอบ Bufstream 0.1.0~0.1.3 ซึ่งเป็นระบบสตรีมมิงที่เข้ากันได้กับ Kafka พบปัญหาด้าน availability ของ Bufstream เอง 2 รายการ และปัญหาด้าน safety 3 รายการ โดยทั้ง 5 รายการได้รับการแก้ไขแล้ว ณ เวอร์ชัน 0.1.3
  • การทดสอบอิงจาก Java Kafka Client 3.8.0 และชุดทดสอบ Jepsen เดิมสำหรับ Kafka/Redpanda โดยใช้ การตั้งค่าที่ให้ความสำคัญกับ safety เช่น acks = all, enable.idempotence = true, enable.auto.commit = false, read_committed
  • ปัญหาของ Bufstream รวมถึง consumer·producer หยุดทำงาน, การตอบกลับ offset 0 ที่ผิดพลาด, การสูญหายของ transaction commit และ การสูญหายของ write ที่ถูกรับรองแล้ว จากบั๊กการกรองขนาด response ของ fetch API
  • ระหว่างการสืบสวน ยังพบปัญหาใน Kafka Java client และโปรโตคอล transaction ของ Kafka ด้วย ได้แก่ Consumer.close() บล็อกแบบไม่มีกำหนด, consumer offset ที่คาดเดาไม่ได้, และปัญหา aborted read·lost write·torn transaction
  • Jepsen เห็นว่าโปรโตคอล transaction ของ Kafka ไม่ได้ให้การรับประกันอย่างชัดเจนต่อทั้งลำดับคำขอจาก client และหมายเลข transaction ทำให้เมื่อใช้ Java client อย่างเป็นทางการ ความปลอดภัยของ transaction ใน Kafka และระบบที่เข้ากันได้กับ Kafka อาจถูกทำลายได้

โครงสร้างของ Bufstream และขอบเขตการตรวจสอบ

  • Kafka เป็นระบบสตรีมมิงที่ให้บริการ log แบบ append-only ที่มีการ replicate และ shard ส่วน Bufstream เป็น implementation ทางเลือกแทน Kafka ที่ให้ความสำคัญกับ data governance และความคุ้มค่าด้านต้นทุนในสภาพแวดล้อมคลาวด์
  • Bufstream ให้บริการ topic และ partition เหมือน Kafka และทำงานกับ Kafka client มาตรฐานได้
    • producer append record ด้วย producer.send()
    • consumer bind กับ partition ด้วย consumer.assign() หรือ consumer.subscribe() แล้วอ่าน record ด้วย consumer.poll()
    • consumer group แบ่งหน้าที่กันประมวลผล record ของชุด topic
  • เมื่อผสานกับ Buf Schema Registry จะสามารถตรวจสอบ record แบบ Protocol Buffer เพื่อรองรับการตรวจสอบ record, field-level access control และการแปลงรูปแบบข้อมูลกับระบบอื่นได้
  • ต่างจาก Kafka ที่ใช้ดิสก์ในเครื่องและโปรโตคอล replication ของตัวเอง Bufstream เขียนข้อมูลลง object storage โดยตรง
    • มุ่งลดต้นทุนโดยใช้ประโยชน์จากโครงสร้างต้นทุนของทราฟฟิก replication ใน object storage
    • node ของ Bufstream สามารถทำงานเป็น VM แบบ stateless ที่ auto-scale ได้
  • Bufstream ประกอบด้วย subsystem 3 ส่วน
    • agent: บริการ stateless ที่ให้ Kafka API
    • object store: เก็บ record chunk และให้บริการแก่ reader
    • coordination service: ปัจจุบันใช้ etcd ทำหน้าที่กำหนดว่า chunk ใดถูก commit แล้ว และกำหนดลำดับของ record
  • ณ เดือนตุลาคม 2024 Bufstream ถูก deploy ให้ลูกค้าบางรายเท่านั้น และเอกสารระบุว่าเป็น “drop-in replacement for Apache Kafka” พร้อมชูความเข้ากันได้กับ Kafka transactions และ exactly-once semantics แต่ไม่ได้มีข้อกล่าวอ้างด้าน safety ที่เป็นรูปธรรมมากนัก

การตั้งค่า client และสมมติฐานของ transaction

  • Jepsen ปรับการตั้งค่า client เพื่อให้ได้พฤติกรรมที่ปลอดภัยขึ้น เช่นเดียวกับการทดสอบระบบที่เข้ากันได้กับ Kafka ก่อนหน้านี้
  • การตั้งค่า Producer

    • ใช้ค่าเริ่มต้น acks = all
    • ใน Bufstream ค่า acks = 0 อาจรับรอง write โดยไม่รอ storage ทำให้ write ที่ commit แล้วอาจสูญหายได้
    • acks = 1 และ acks = all จะ block จนกว่า Bufstream จะมั่นใจว่า persist แบบ durable แล้ว
    • ใช้ค่าเริ่มต้น enable.idempotence = true เพื่อป้องกันการ append ซ้ำจากการ retry อัตโนมัติของ Kafka producer
  • การตั้งค่า Consumer

    • โดยทั่วไปใช้ enable.auto.commit = false เพราะมีเอกสารหลายแห่งระบุว่า auto-commit อาจนำไปสู่การสูญหายของข้อมูล
    • เมื่อไม่มี committed offset ค่าเริ่มต้นของ auto.offset.reset จะเริ่มจาก offset ล่าสุด จึงไม่รับประกัน at-least-once delivery
    • ใช้ auto.offset.reset = earliest เพื่อให้ consumer สามารถสังเกต log ทั้งหมดได้
    • transaction ของ Kafka ประกอบด้วยชุด record ที่ producer ส่ง และ map ของ offset สูงสุดแยกตาม partition ที่ consumer poll มา
    • เฉพาะเมื่อ transaction ถูก commit แล้วเท่านั้น record ที่ส่งจึง durable และสุดท้ายจะมองเห็นได้โดย consumer แบบ read_committed และ committed offset ก็จะเพิ่มขึ้นไปอย่างน้อยถึง offset ที่ระบุใน transaction
    • หาก transaction ไม่ถูก commit committed offset จะไม่เดินหน้า และ visibility ของ write อาจแตกต่างไปตามการตั้งค่า consumer
    • ปรากฏการณ์ที่ consumer แบบ read_uncommitted อ่านค่าจาก transaction ที่ abort แล้ว ถูกจัดประเภทเป็น aborted read(G1a)
    • เอกสาร Kafka ระบุว่า read_committed ป้องกัน G1a และรับประกันคุณสมบัติในระดับหนึ่งว่าทุก write ของ transaction จะมองเห็นทั้งหมดหรือไม่มองเห็นเลย แต่ในการทดสอบ Kafka·Redpanda·Bufstream ของ Jepsen พบ write cycle(ปรากฏการณ์คล้าย G0) และรูปแบบ G1c บางส่วน

การออกแบบการทดสอบ

  • Jepsen ทดสอบ Bufstream ตั้งแต่ 0.1.0 ถึง 0.1.3 รวมถึง release candidate build หลายรายการ
  • test harness ใช้ Bufstream test harness, Jepsen testing library และ Java Kafka Client 3.8.0
  • สภาพแวดล้อมการรัน

    • ใช้ node Debian Bookworm 3~5 เครื่อง ทั้งบน LXC container และ EC2 VM
    • ใช้ node 1 เครื่องสำหรับ etcd, node 1 เครื่องสำหรับ Minio และที่เหลือเป็น Bufstream agent
    • producer, consumer และ admin client ถูก initialize โดยใส่ node เพียงเครื่องเดียวใน bootstrap_servers แต่ไม่ได้ปิดกั้น smart client discovery
  • การตั้งค่าหลักด้าน safety

    • ปิด auto-commit
    • acks = all
    • retries 1,000
    • เปิดใช้ idempotence
    • isolation level read_committed
    • auto_offset_reset = earliest
    • ปิดการสร้าง topic อัตโนมัติฝั่ง server
    • การฉีดความขัดข้องรวมถึง process pause(SIGSTOP), crash(SIGKILL), clock skew(clock_settime), network partition(iptables)
    • เนื่องจาก Bufstream แบ่งเป็น agent, object store และ coordination service จึงสร้างเครื่องมือ Jepsen ใหม่ที่สามารถฉีดความขัดข้องโดยเจาะจงเฉพาะ subsystem บางส่วนได้
    • ตัวอย่างเช่น ปรับชุดผสมตามเวลา เช่น crash เฉพาะ node ของ Bufstream หรือ pause เฉพาะ etcd coordinator

Queue workload และ Abort workload

  • Queue workload วิเคราะห์ความปลอดภัยให้สอดคล้องกับโมเดลข้อมูลของ Kafka
    • logical process แต่ละตัวรัน producer, consumer และ admin client
    • numeric key ใช้ระบุ topic-partition เฉพาะรายการหนึ่ง
    • key ถูกเลือกด้วย exponential frequency ทำให้บาง key ถูกเข้าถึงบ่อย และบาง key ถูกเข้าถึงนาน ๆ ครั้ง
  • ใช้ operation พื้นฐานสามแบบ
    • crash: ยุติ logical process แล้วแทนที่ด้วย client ใหม่
    • subscribe หรือ assign: เปลี่ยนชุด topic หรือ partition ที่ consumer จะ poll
    • txn, poll, send: ดำเนินการ sequence ของ micro-operation แบบ poll หรือ send
  • ใน workload แบบ non-transactional แต่ละ send หรือ poll จะมี micro-operation เพียงหนึ่งรายการเท่านั้น
  • ใน workload แบบ transactional จะครอบ micro-operation หลายรายการไว้ด้วย Kafka transaction
  • การวิเคราะห์สร้าง mapping จาก offset เป็น value แยกตาม key แล้วค้นหาข้อผิดพลาด
    • หากเห็น value หลายค่าที่ offset เดียวกัน คือ inconsistent offset
    • หากเห็น value เดียวกันที่หลาย offset คือ duplicate error
    • หากไม่พบ record ที่ได้รับการยืนยันเลย จะถือว่า lost หรือ unseen
    • หาก poll คืนค่า value ที่ operation ซึ่งถูก abort ส่งไว้ คือ aborted read
    • ตรวจสอบด้วยว่า transaction สังเกตเห็นการเขียนของตัวเองหรือไม่
  • หลัง main test จะเคลียร์ความขัดข้องแล้วเข้าสู่ขั้นตอน final reads
    • แต่ละ process อ่านทุก topic-partition ตั้งแต่ offset 0 และ poll จนถึง offset สูงสุดที่ทราบว่าเขียนแล้ว
    • หาก final reads timeout และยังไม่พบ record ที่ได้รับการยืนยัน จะจัดประเภทเป็น unseen
  • Abort workload ถูกเพิ่มเข้ามาเพื่อติดตามพฤติกรรมของ poll offset หลัง transaction abort
    • จำกัด topic ให้มี partition, process, producer และ consumer อย่างละหนึ่ง
    • หลังจาก transaction poll record แล้ว จะตั้งใจ abort จากนั้นจัดประเภท poll offset เป็น advance, rewind, rewind-further หรือ other

ปัญหา 5 รายการที่พบใน Bufstream

  • Consumer ค้าง (#1)

    • ตั้งแต่ 0.1.0 ถึง 0.1.3-rc.8 ขั้นตอน final read มักค้างบ่อยครั้ง
    • consumer.poll() คืนผลลัพธ์ว่างทันที แต่ใน log ยังมี record ที่ได้รับการยืนยันแล้วหลายพันรายการค้างอยู่
    • สถานะนี้กินเวลาตั้งแต่หลายสิบวินาทีไปจนถึงเกิน 1 ชั่วโมง
    • ในการทดสอบหนึ่ง ส่ง record ที่ได้รับการยืนยันแล้ว 691 รายการในช่วง 120 วินาทีแรก และ ณ ตอนเริ่ม final reads มี 40 รายการที่ไม่มี poller ใดสังเกตเห็น
    • หลังจากนั้น consumer.poll() ไม่คืนผลลัพธ์เป็นเวลากว่า 1 ชั่วโมง ทำให้การทดสอบ timeout
    • สาเหตุคือ node ของ Bufstream ที่รีสตาร์ตแล้วอาจคืนค่า stale cached value ของ last stable offset และ high watermark ได้
    • client library บางตัวตัดสินว่าไม่มี record ที่อยู่ถัดไปอีกแล้วจึง stall และ Bufstream ได้ออก patch ใน 0.1.3-rc.6 ให้ refresh cache ตอน startup
  • Producer และ consumer ค้าง (#2)

    • แม้ใน 0.1.3-rc.6 ก็ยังพบปัญหา unseen write หลัง pause, crash, partition ต่อ coordinator, storage และ node ของ Bufstream อย่างต่อเนื่อง
    • ในบางกรณี หลัง pause coordinator แล้ว แม้ node ของ Bufstream ทั้งหมดกำลังทำงานอยู่ client ก็เข้าสู่สถานะรอ InitProducerId จน timeout
    • ในกรณีอื่น listOffsets ล้มเหลวด้วย node ... being disconnected หรือ timed out waiting for a node assignment และ poll เสร็จสิ้นแต่ไม่คืนผลลัพธ์
    • เมื่อ kill node ของ Bufstream แล้ว restart ปัญหาก็หายไป
    • สาเหตุเกี่ยวข้องกับ etcd lease
    • Bufstream agent ใช้ etcd leases เพื่อติดตาม active agent
    • เนื่องจาก pause หรือ partition ช่วงสั้น ๆ ทำให้ etcd ลบ key ที่ผูกกับ lease ของ agent แต่ update การลบอาจไม่ถูกส่งไปยัง agent
    • agent จึงอยู่ในสถานะที่ไม่รู้ว่าตนเองเสีย lease ไปแล้ว
    • ทีม Bufstream เพิ่ม polling logic เข้าไป และใน 0.1.3-rc.8 ปัญหา unseen write ส่วนใหญ่ได้รับการแก้ไข
  • Offset ศูนย์ที่เกิดขึ้นอย่างผิดปกติ (#3)

    • ตั้งแต่ 0.1.0 ถึง 0.1.3-rc.2 ค่า sent value อาจได้รับ offset 0 แล้วไปปรากฏที่ offset จริงซึ่งสูงกว่าได้
    • เกิดขึ้นแม้ offset 0 จะถูกจัดสรรไปนานมากแล้วก็ตาม
    • มีเพียง sender เท่านั้นที่เห็น offset 0 ส่วน poller เห็น offset ที่สูงกว่า
    • ในการทดสอบ 2 นาทีที่มี node ของ Bufstream เดี่ยวและใส่ pause ให้ process ของ etcd พบว่า write 6 รายการได้รับ offset 0 แล้วไปปรากฏที่ offset ที่สูงกว่า
    • สาเหตุคือ error response ของ Bufstream ขาด field ที่จำเป็น
    • Bufstream ส่งคำขอ log commit ไปยัง etcd และ etcd ประมวลผลแล้ว แต่เนื่องจาก pause หรือ partition ทำให้ Bufstream อาจ timeout ระหว่างรอ response
    • Bufstream ส่ง error code ให้ client แต่ไม่ได้ตั้งค่า offset ของ sent record เป็น -1 ซึ่งเป็น error signal
    • Java Kafka client จึงตีความว่าเป็น success response ของ offset 0
    • Franz-go ที่ test suite ของ Bufstream ใช้ ตีความ message นี้เป็น error ทำให้ปัญหานี้ไม่ปรากฏในการทดสอบ
    • Bufstream แก้ไขใน 0.1.3-rc.6 และหลังจากนั้น Jepsen ก็ไม่พบซ้ำอีก
  • Transaction writes สูญหาย (#4)

    • ใน 0.1.2 record บางส่วนของ transaction ที่ commit แล้วหายไปและไม่ถูกพบเห็นอีก เกิด write loss บ่อยครั้ง
    • ในการทดสอบหนึ่ง record 240 รายการที่เขียนโดย transaction ที่ commit แล้วสูญหาย ภายใน 100 วินาทีและ write transaction 6,761 รายการ
    • ในตัวอย่าง value 141 ของ key 5 ถูกคืนกลับมาว่าเขียนสำเร็จที่ offset 274 แต่ consumer.poll() ทั้งหมดข้าม offset นั้นไป
    • สาเหตุคือ bug ใน concurrency safety mechanism ที่เพิ่มเข้ามาใน 0.1.2
    • mechanism นี้ให้ unique number แก่แต่ละ transaction ภายใน producer epoch เพื่อลดผลกระทบจากการขาด idempotence ของ Kafka transaction protocol
    • เนื่องจาก bug ใน transaction number tracking logic เมื่อมีหลาย transaction ถูก commit ข้ามหลาย epoch commit บางรายการจึงถูกเพิกเฉยอย่างผิดพลาด
    • transaction ที่ดูเหมือน commit แล้วอาจถูก abort จริง ๆ หรือกลับกันก็ได้
    • Jepsen พบ bug นี้ได้เพราะตั้ง transaction timeout ไว้ต่ำที่ 1 วินาที
    • Bufstream ตรวจพบปัญหาภายในไม่กี่ชั่วโมงหลัง release 0.1.2 และป้องกันไม่ให้ลูกค้า upgrade โดยลูกค้าไม่ได้ upgrade เป็น 0.1.2
    • การแก้ไขรวมอยู่ใน 0.1.3-rc2
  • Lost writes จาก server-side filtering (#5)

    • ใน 0.1.3-rc.8 มักเกิด window ของ write loss สั้น ๆ หลังความขัดข้องเล็กน้อย เช่น process ของ Bufstream หรือ coordinator pause หรือ partition ระหว่างทั้งสองฝ่าย
    • data loss เกิดขึ้นไม่ว่าจะใช้ transaction หรือไม่
    • ในการทดสอบ 5 นาทีหนึ่ง จาก record 16,770 รายการ มี 22 รายการที่ถูก acknowledge แล้วแต่ไม่มี consumer ใด poll ได้
    • record บางรายการเคยปรากฏให้ poller เห็นอยู่ช่วงหนึ่ง แต่ภายหลังกลับหายไปจาก poll
    • สาเหตุคือ logic จำกัดขนาด response ของ fetch API ที่เพิ่มใน 0.1.3-rc.8 เพื่อหลีกเลี่ยง bug ของ Kafka web GUI ยอดนิยม
    • bug ใน filtering logic ซ่อน record จาก lagging consumer ทำให้ดูเหมือน write loss
    • Bufstream แก้ไขใน 0.1.3-rc.12

ปัญหาระหว่าง Kafka Java client กับโปรโตคอล Kafka

  • KIP-588: ProducerFencedException ที่ชวนให้เข้าใจผิด

    • ระหว่างการทดสอบ มักเกิดข้อผิดพลาด ProducerFencedException: There is a newer producer with the same transactionalId which fences the current one. บ่อยครั้ง
    • ข้อผิดพลาดนี้เกิดขึ้นแม้ในการทดสอบที่ producer ทุกตัวได้รับ transactional ID ที่ไม่ซ้ำกัน ทำให้ต้องใช้เวลาหาสาเหตุ
    • KIP-588 ระบุว่า ProducerFencedException อาจถูกโยนเมื่อเกิด transaction timeout ได้ด้วย
    • Kafka Java client ใช้ TimeoutException เฉพาะสำหรับ timeout ส่วนใหญ่ แต่ในกรณีนี้กลับโยน ProducerFencedException
    • ทั้งที่จริงไม่มี producer ที่ชนกัน แต่ข้อความ error กลับบอกว่ามี producer instance ตัวที่สองอยู่
    • KIP-588 เปิดค้างมา 2 ปีแล้ว และ Jepsen แนะนำให้ทีม Kafka เปลี่ยน error message
  • KAFKA-17734: Consumer.close() อาจ block ได้ไม่มีกำหนด

    • ในการทดสอบทั้งฝั่ง Bufstream และ Kafka การทดสอบหยุดค้างทุก ๆ ไม่กี่ชั่วโมงเพราะ bug ของ Java client
    • โดยค่าเริ่มต้น Consumer.close() จะ block อยู่ที่ network IO
    • timeout parameter ของ close() ควรป้องกันการ block ไม่มีกำหนด แต่กลับไม่ทำงาน
    • วิธีเรียก consumer.wakeup() จาก thread แยกเพื่อ interrupt consumer ที่ stuck อยู่ใน IO ก็ไม่ได้ผลเช่นกัน
    • Jepsen เห็นว่าโปรแกรมที่รันระยะยาวควรสามารถปล่อย resource เช่น client, connection, thread และ memory ได้ภายในเวลาที่สมเหตุสมผลแม้มี network error จึงได้เปิด KAFKA-17734
  • KAFKA-17582: หลัง transaction ล้มเหลว consumer offset คาดเดาไม่ได้

    • เอกสารทางการของ Kafka แทบไม่ได้บอกว่า consumer offset ควรเป็นอย่างไรเมื่อ transaction commit ล้มเหลว
    • Kafka design documentation ของ Confluent บอกว่าเมื่อ transaction ถูก abort แล้ว consumer position จะกลับไปยังค่าเดิม แต่ Java client จริงไม่ได้ทำงานเช่นนั้นเสมอไป
    • จากผลของ abort workload แม้ใน healthy cluster พฤติกรรมหลัง abort ก็ยังคาดเดาได้ยาก
    • transaction pair ส่วนใหญ่ advance ไปยัง offset ที่ไกลกว่า
    • บางส่วนถูก rewind ไปยัง offset ก่อนหน้า
    • การ rewind ทั้งหมดเกี่ยวข้องกับ rebalance event และการ advance ทั้งหมดไม่มี rebalance
    • ตามคำตอบจากฝั่ง Kafka พฤติกรรมนี้เป็น intentional
    • consumer จะ advance ต่อไปเรื่อย ๆ
    • เมื่อเกิด rebalance อาจ rewind ไปยังจุดใดก็ได้ตาม committed offset
    • ผู้ใช้ต้อง rewind consumer position เองเมื่อ transaction abort
    • Jepsen เปิด KAFKA-17582 และเสนอให้จัดทำเอกสารพฤติกรรมนี้ รวมถึงพิจารณาเปลี่ยนค่าเริ่มต้นให้ rewind เมื่อ transaction abort
    • Queue workload ก็ถูกแก้ให้ rewind consumer อย่างชัดเจนเช่นกัน
  • KAFKA-17754: write loss, aborted read, torn transaction

    • ใน Bufstream 0.1.0~0.1.3 พบ aborted read, lost write และ atomicity violation ได้จากเพียงการ pause process ของ Bufstream, pause coordinator, crash และ network partition
    • การวิเคราะห์นำไปสู่ข้อบกพร่องพื้นฐานของ Kafka transaction protocol
    • ในตัวอย่าง client รัน transaction ด้วย unique transactional ID jt1234 และส่ง committed = false ไปยัง EndTxn เพื่อ abort แต่การเรียก poll() 15 ครั้งกลับสังเกตเห็น write ของ transaction ที่ถูก abort ไปแล้ว
    • write อื่นใน transaction เดียวกันไม่มี poller ใดสังเกตเห็น
    • เมื่อดู packet capture ร่วมกับ log ของ Bufstream พบว่าสาเหตุคือ commit message ที่ล่าช้า
    • commit EndTxn ที่ส่งไปเมื่อหลาย transaction ก่อน ถูกประมวลผลช้าที่ node หนึ่ง
    • client ได้ดำเนิน transaction ถัด ๆ ไปแล้ว
    • commit ที่ล่าช้าถูกนำไปใช้กับ transaction ปัจจุบัน ทำให้เฉพาะส่วนต้นของ transaction ถูก commit ส่วนที่เหลือถูกจัดการเหมือนเป็น transaction แยกและถูก abort
    • Kafka protocol ถูกออกแบบให้ client ส่ง request ไปยังหลาย TCP connection และหลาย node ได้ แต่ไม่มี sequence number ที่กำหนดลำดับ request ของ client เดียวกัน
    • และไม่มีแนวคิดเรื่อง transaction number ทำให้ server ไม่รู้ว่า client ตั้งใจจะจบ transaction ใดเมื่อได้รับ commit หรือ abort message
    • ผลคือสถานการณ์ต่อไปนี้เป็นไปได้
      • transaction ที่ดูเหมือนถูก commit แล้ว กลับถูก abort จริง
      • transaction ที่ถูก abort แล้ว กลับถูก commit จริง
      • เกิด torn transaction ที่มีเพียง write บางส่วนของ transaction ถูกเก็บไว้ ส่วนบางส่วนสูญหาย
    • Java Kafka client ทางการถือว่า timeout เป็น retryable และอาจส่ง EndTxn message หลายรายการโดยอัตโนมัติ ดังนั้นปัญหาอาจเกิดขึ้นได้แม้ผู้ใช้จะเรียก commit หรือ abort เพียงครั้งเดียวต่อ transaction
    • Jepsen พบ aborted read และ torn transaction ใน Kafka ด้วยจากการ pause process และเปิด KAFKA-17754
    • วิศวกร Kafka เห็นว่า KIP-890 อาจแก้ปัญหานี้ได้
    • KIP-890 เปลี่ยน transaction protocol โดยเพิ่ม producer epoch ในแต่ละ transaction
    • เนื่องจาก server ปฏิเสธ message จาก epoch ก่อนหน้า จึงป้องกันไม่ให้ commit message ของ transaction ในอดีตรั่วไหลไปยัง transaction ภายหลังได้
    • Bufstream เพิ่ม mechanism ใน 0.1.3 ที่ใช้ etcd revision เป็น logical clock เพื่อลดความถี่ แต่ยังไม่สามารถป้องกัน reorder ระหว่าง client กับ Bufstream ได้
    • Jepsen ยังคงพบ aborted read, lost write และ torn transaction ใน 0.1.3 และเห็นว่าจำเป็นต้องมีการแก้ไขฝั่ง client

สรุปผลโดยรวม

  • ปัญหาของ Bufstream เองทั้ง 5 รายการได้รับการแก้ไขแล้วทั้งหมด
    • #1: consumer ค้างเนื่องจาก lagging highest stable offset, ไม่ต้องมีความขัดข้องก็เกิดได้, แก้ไขใน 0.1.3-rc.6
    • #2: producer/consumer ค้างเนื่องจาก etcd lease expiry, ต้องมี pause, แก้ไขใน 0.1.3-rc.8
    • #3: spurious zero offsets, ต้องมี pause, แก้ไขใน 0.1.3-rc.6
    • #4: lost transaction writes, ไม่ต้องมีความขัดข้องก็เกิดได้, แก้ไขใน 0.1.3-rc.2
    • #5: lost writes เนื่องจาก server-side filtering, ต้องมี pause, แก้ไขใน 0.1.3-rc.12
  • ปัญหาที่เกี่ยวข้องกับ Kafka ยังหลงเหลืออยู่
    • KIP-588: error message ไม่ถูกต้องเมื่อเกิด transaction timeout, ยังไม่แก้ไข
    • KAFKA-17734: ConsumerClient.close() อาจ block ได้ไม่มีกำหนด, ยังไม่แก้ไข
    • KAFKA-17582: consumer offset หลัง transaction ล้มเหลวคาดเดาไม่ได้, ยังไม่แก้ไข
    • KAFKA-17754: write loss, aborted read, torn transaction, ยังไม่แก้ไข
  • Jepsen เตือนว่าการตรวจสอบความปลอดภัยเชิงทดลองสามารถพิสูจน์การมีอยู่ของ bug ได้ แต่พิสูจน์การไม่มีอยู่ไม่ได้
  • โดยเฉพาะเพราะ KAFKA-17754 ทำให้มองว่ายากต่อการตัดสินว่า Bufstream มีกรณี write loss อื่นอีกหรือไม่

คำแนะนำสำหรับผู้ใช้และการปฏิบัติการ Bufstream

  • ผู้ใช้ที่ใช้ transaction ของ Bufstream ด้วย Java Kafka client ทางการควรคำนึงว่า transaction ในขณะนี้อาจไม่ปลอดภัย
    • transaction ที่ถูก abort อาจถูก commit จริงได้
    • transaction ที่ถูก commit อาจถูก abort จริงได้
    • transaction อาจถูกฉีกเป็นสองส่วนและคงเหลือผลลัพธ์ไว้เพียงบางส่วน
  • Bufstream มองว่า Franz-go client มีความเปราะบางต่อปัญหานี้น้อยกว่า แต่ Jepsen ไม่ได้ทดสอบ Franz-go ด้วยเทคนิคเดียวกับงานนี้
  • client อื่นอาจเปราะบางหรือไม่ก็ได้
  • ผู้ใช้ก่อน Bufstream 0.1.3 อาจพบปัญหาต่อไปนี้
    • producer.send() คืนค่า offset เป็น 0 อย่างผิดพลาดแทน offset จริง
    • metastable availability issue ที่ทำให้ client ค้าง
  • Jepsen แนะนำให้ upgrade เป็น 0.1.3
  • ประเมินว่า architecture โดยรวมของ Bufstream ดู sound
    • วิธีใช้ coordination service อย่าง etcd เพื่อกำหนดลำดับของ immutable data chunk เป็นแนวทางที่ค่อนข้างเรียบง่ายและมีตัวอย่างมาก่อนใน OLTP และ streaming system
  • ในด้านการปฏิบัติการ มีการแนะนำการปรับปรุง 2 อย่าง
    • หากคำขอ shared file ของ storage ล้มเหลวตอน startup cluster อาจ crash ได้ จึงแนะนำให้เพิ่ม retry และ Bufstream ได้เพิ่ม retry layer แล้ว
    • แนะนำให้ agent ทำงานต่อไปเมื่อ dependency unavailable แทนที่จะตายทันที พร้อมให้ backpressure และ system status และ recover ได้อย่างนุ่มนวลขึ้น
  • ณ เวอร์ชัน 0.1.3 Bufstream ได้เพิ่ม retry logic สำหรับ etcd แล้ว แต่ยังต้องมี constant supervision เพื่อรักษาสถานะ online
  • ผู้ใช้ควรทดสอบว่ามี process supervisor และยังทำงานโดยไม่ยอมแพ้ระหว่าง outage ระยะยาวหรือไม่

จำเป็นต้องจัดทำเอกสารและแก้ไขโปรโตคอล Kafka transaction

  • เอกสารทางการของ Kafka แทบไม่ได้กล่าวถึง transaction ทำให้ผู้ใช้ต้องนำ source หลายแห่งที่คลุมเครือและขัดแย้งกันมาประกอบกันเอง
  • Jepsen แนะนำให้ทีม Kafka สร้างเอกสารกลางที่สรุป transaction semantics อย่างชัดเจน และกล่าวถึง KAFKA-17671
  • เอกสารดังกล่าวอย่างน้อยควรระบุเรื่องต่อไปนี้
    • consumer จะสังเกตเห็น offset ที่เพิ่มขึ้นแบบ monotonically increasing เมื่อใด
    • consumer สามารถข้าม record ที่ถูก acknowledge แล้วได้เมื่อใด
    • rebalance สามารถส่งผลระหว่าง transaction ได้หรือไม่
    • producer write offset เพิ่มขึ้นแบบ monotonic เมื่อใด
    • G0, G1a, G1b, G1c, fractured read, และการอ่าน transaction write ของตัวเอง ถูกต้องตามกติกาเมื่อใด
    • หลัง transaction ที่ถูก abort ค่า return ของ poll() และ offset มีความหมายอย่างไร
    • ควรจัดการ transaction error, error ระหว่าง abort, และ error ระหว่าง rewind อย่างไร
  • แม้เอกสาร Confluent จะกล่าวซ้ำ ๆ ว่าค่าเริ่มต้นของ Kafka ให้ at-least-once delivery แต่ Jepsen ชี้ว่าสิ่งนี้ดูเหมือนจะไม่เป็นความจริง
    • auto.offset.reset = latest อาจทำให้ record ที่ยังไม่ได้ประมวลผลดูเหมือน “committed” ได้
    • เอกสาร offset management ของ Confluent เองก็กล่าวถึงความเสี่ยงที่ message progress จะสูญหายเมื่อ crash ภายใต้ค่าเริ่มต้น auto-commit
    • เอกสารที่ระบุว่า consumer จะถูก rewind เมื่อ transaction abort ก็ไม่ตรงกับพฤติกรรมจริง
  • Jepsen มองว่า Kafka transaction protocol จำเป็นต้องได้รับการแก้ไขในระดับพื้นฐาน
    • protocol สมมติโดยนัยว่ามี ordered reliable delivery แต่ในความจริงมี process pause, network unreliability, non-zero latency และ unordered delivery ระหว่าง TCP socket หลายตัว
    • Kafka protocol กระจาย message ไปยังหลาย node และ TCP socket และ client จะ retry message โดยอัตโนมัติ
    • ไม่มี sequence number สำหรับกู้คืนลำดับ message ของ client เดียวกัน และไม่มี transaction number สำหรับยืนยันเป้าหมายของ transaction
  • KIP-890 พยายามรับประกันลำดับที่เข้มงวดยิ่งขึ้นด้วยการเพิ่ม epoch ทุกครั้งที่ transaction commit
  • client library ก็ช่วยได้เช่นกันด้วยวิธี re-initialize producer เพื่อเพิ่ม epoch เมื่อ message ไม่ถูก acknowledge
  • Java Kafka Client 3.8.0 เปราะบางต่อปัญหานี้
  • Jepsen มองว่า Franz-go อาจบรรเทาหรือป้องกันปัญหาได้ด้วยการ re-initialize เมื่อ timeout แต่ไม่ได้ตรวจสอบ client library อื่น

งานในอนาคต

  • ผู้ใช้จำนวนมากไม่ได้จัดการ transaction โดยตรง แต่พึ่งพา “exactly-once semantics” ของ Kafka Streams API ดังนั้นในอนาคตจึงอาจตรวจสอบความถูกต้องของ Streams application ได้
  • Jepsen พบ unseen write ใน Kafka ระหว่างตรวจสอบ KAFKA-17754 เช่นกัน แต่ไม่สามารถวิเคราะห์ได้เนื่องจากข้อจำกัดด้านเวลา
    • unseen write อาจเป็นสัญญาณของ hanging transaction, stuck consumer หรือ data loss
    • ยังมีข้อสงสัยว่า message Produce ที่ล่าช้าอาจเข้าไปอยู่ใน transaction ในอนาคตและละเมิด transaction guarantee ได้หรือไม่
    • ยังสงสัยด้วยว่าเมื่อเกิด request timeout Kafka Java Client อาจนำ sequence number กลับมาใช้ซ้ำ ทำให้ write ได้รับ acknowledge แล้วแต่ถูก discard อย่างเงียบ ๆ
  • เมื่อเกิด rebalance event ตำแหน่งของ consumer อาจขยับไปข้างหน้าหรือถอยหลังได้ แต่กฎเกณฑ์ยังไม่ชัดเจน
  • หาก Kafka จัดทำเอกสารพฤติกรรมที่ตั้งใจไว้ Jepsen ก็ต้องการตรวจสอบพฤติกรรมนั้น
  • Jepsen อธิบายว่าการค้นหา anomaly ที่พบได้ยากนั้นทำได้ยาก เพราะเป็น random process
    • ปัญหาที่เกิดขึ้นเพียงครั้งเดียวนั้น debugging และ reproduction ทำได้ยากมาก
  • Bufstream ยังใช้ Antithesis ซึ่งรัน distributed system ทั้งหมดบน deterministic hypervisor และ simulated network ด้วย
    • การผสาน workload generation และ history checking ของ Jepsen เข้ากับ environment แบบ deterministic และ replayable ของ Antithesis จะช่วยเพิ่มความสามารถในการทำซ้ำของการทดสอบได้

1 ความคิดเห็น

 
GN⁺ 2024-11-14
ความคิดเห็นจาก Hacker News
  • ถ้าระหว่างตรวจสอบประเด็นอย่าง KAFKA-17754 แล้วยังพบ การเขียนที่มองไม่เห็น ใน Kafka ด้วย ก็ดูเหมือนถึงเวลาที่ Jepsen ควรกลับไปขุด Kafka แบบลึก ๆ อีกครั้งแล้ว
    การตรวจสอบครั้งล่าสุดคือปี 2013 (https://aphyr.com/posts/293-call-me-maybe-kafka, Kafka 0.8 beta) และตอนนี้ดูเหมือน Kafka เองเพิ่งอยู่ในช่วงเริ่มพบปัญหาหลายอย่าง
    เรื่องอย่าง “การเขียนได้รับการยืนยันแล้ว แต่ถูกทิ้งเงียบ ๆ ได้” นี่ค่อนข้างน่ากลัว

    • ผมอยากวิเคราะห์ Kafka จริง ๆ :-)
  • น่าประหลาดใจมากที่ในค่าเริ่มต้น enable.auto.commit=true ผู้บริโภค Kafka สามารถ commit offset ได้โดยไม่เกี่ยวว่าแอปพลิเคชันประมวลผลจริงแล้วหรือไม่
    ผมไม่เคยเข้าใจ auto commit แบบนั้น และถ้าเป็นค่าเริ่มต้นแบบนั้นก็ดูไม่สมเหตุสมผล
    คำอธิบายในเอกสารไม่ได้ชัดเจนมาก แต่โดยรวมผมอ่านแล้วเข้าใจว่า offset จะถูก commit เฉพาะเมื่อประมวลผลเสร็จแล้วเท่านั้น
    ผมเข้าใจว่าการปรับช่วงเวลา auto commit ช่วยลดกรอบเวลาของการประมวลผลซ้ำ ไม่ใช่ทำให้ข้อความหาย ตามที่คาดในแบบ at-least-once

    • น่าประหลาดใจอยู่บ้าง และเห็นด้วยว่าเอกสารอธิบายส่วนนี้ได้ไม่ดี
      ถ้าไม่ commit อย่างชัดเจน Kafka ก็ไม่มีทางรู้ว่าข้อความถูกประมวลผลแล้วหรือยัง
      Kafka ถือว่าข้อความที่ส่งต่อไปถูกประมวลผลทันที
      auto commit คล้ายกับการยื่นโคนไอศกรีมให้แล้วหันกลับทันที พร้อมสมมติว่าอีกฝ่ายกินแล้ว บางคนอาจรับแล้วทำตกทันทีโดยยังไม่ได้กินสักคำ
    • ประเด็นหลักคือ การที่ข้อความถูกส่งถึงไคลเอนต์ Kafka สำเร็จ ไม่ได้หมายความว่าแอปพลิเคชันประมวลผลแล้ว
      ถ้าต้องการการรับประกันนั้น ต้องทำ acknowledgement อย่างชัดเจน
      เช่น ถ้าทำแค่เขียนข้อความลงฐานข้อมูล ทันทีที่ข้อความเข้ามาใน callback handler ของไคลเอนต์ก็จะถูกถือว่ายืนยันแล้ว
      แต่ในทางปฏิบัติ คุณน่าจะอยากให้ยืนยันหลังจาก insert ลง DB สำเร็จมากกว่า
      ถ้า DB เข้าถึงไม่ได้เพราะเครือข่าย, Kubernetes, การตั้งค่าไฟร์วอลล์ ฯลฯ และระหว่างนั้นวิศวกรพยายามรีสตาร์ตจนไคลเอนต์ดับลง ก็เกิดข้อความที่ยังไม่ได้ประมวลผลได้ง่าย
    • ผมเข้าใจว่าฟีเจอร์นี้มีไว้สำหรับ สถานการณ์ประสิทธิภาพสูง
      ระบบอื่นสามารถตัดสินได้ว่าล้มเหลวหรือไม่ และฟีเจอร์นี้ช่วยเลื่อนตำแหน่งขอบบนเพื่อลดการประมวลผลซ้ำ
      แต่ถ้าจังหวะพอดีและเกิดความขัดข้อง หลังรีสตาร์ตต้องถือว่าอาจได้รับบางส่วนที่ประมวลผลไปแล้วกลับมาอีก
      ปัญหาคือกรณีที่ไม่มีการประมวลผลแบบนี้ก่อน auto commit
      อ่านดูแล้วเหมือนตั้งใจให้ commit หลังการประมวลผลผ่านไปสักพัก แต่ก็เหมือนขัดแย้งกันที่เป็น auto commit ทว่าควร commit เฉพาะรายการที่อยู่ก่อนจังหวะ auto commit ไม่กี่มิลลิวินาทีเท่านั้น
    • พอจะให้เหตุผลได้ว่าทำไมฟีเจอร์นี้จึงมีอยู่ มันถูกออกแบบมาสำหรับ consumer แบบ synchronous single-threaded โดยสมมติคร่าว ๆ ว่าเป็นลูปที่เรียก poll แล้วประมวลผลข้อความอย่างทนทาน
      จุดที่ชวนสับสนคือการตรวจ auto commit ไม่ได้เกิดแบบ asynchronous หลัง timeout แต่เกิดที่ จังหวะเรียก poll ครั้งถัดไป
      ดังนั้นกรณีเดียวที่ควรทำให้การเขียนหล่นได้ คือเมื่อไม่ได้ประมวลผลข้อความอย่างทนทานก่อนเรียก poll อีกครั้ง แต่แค่เก็บไว้ เช่น ใช้การประมวลผลแบบ asynchronous, delay, queue ฯลฯ
      นี่อิงตามพฤติกรรมที่มีเอกสารของไลบรารีไคลเอนต์ Java (https://kafka.apache.org/32/javadoc/org/apache/kafka/clients...) ส่วน implementation ปัจจุบันทำจริงแบบนั้นหรือไม่เป็นอีกเรื่องหนึ่ง
      โปรโตคอล Kafka ติดอยู่กึ่งกลางระหว่างระดับสูงกับระดับต่ำ จึงทำได้ไม่ดีนักทั้งสองด้าน
      auto commit เป็นฟีเจอร์ระดับสูงที่ช่วยให้สร้างแอปพลิเคชันง่าย ๆ ได้ง่ายขึ้น แต่ถ้าไม่ใช้ตามรูปแบบที่คาดไว้ ก็ย่อมล้มเหลวได้
      ผมคิดว่าปัจจุบันผู้ใช้ปลายทางควรใช้ implementation ระดับสูงที่จัดการรายละเอียดให้ถูกต้อง มากกว่าจะใช้ไคลเอนต์ Kafka โดยตรง ถ้าเป็นงานข้อมูลก็เช่น stream processing engine ถ้าเป็นงานแอปพลิเคชันก็เช่น persistent execution engine
  • ดูจากหน้าผลิตภัณฑ์ (https://buf.build/product/bufstream) แล้วสงสัยว่าคำอธิบายที่ว่า “รันเฉพาะใน AWS หรือ GCP VPC และไม่ติดต่อออกไปภายนอก” จะอยู่ร่วมกับ การคิดค่าบริการตามการใช้งาน แบบ “$0.002 ต่อ GiB ก่อนบีบอัด” ได้อย่างไร
    คงไม่ได้ดำเนินธุรกิจทั้งหมดด้วยระบบเกียรติยศหรอกมั้ง

    • ในบทนำบอกว่า “ณ เดือนตุลาคม 2024 Bufstream ถูก deploy ให้เฉพาะลูกค้าที่คัดเลือกแล้ว” ดังนั้น ระบบเกียรติยศ ก็อาจเป็นไปได้
      แน่นอนว่ามีความเสี่ยงถูกใช้ในทางที่ผิด แต่ก็อาจเป็นการประนีประนอมที่คุ้มค่าเพื่อดึงดูดลูกค้าเฉพาะกลุ่ม
    • โปรแกรมต้องเป็นอย่างใดอย่างหนึ่ง คือโอเพนซอร์สหรือไม่ใช่
      ถ้าซอร์สไม่ได้เปิดเผย ก็ไม่ควรเชื่อคำกล่าวอ้างว่า “ไม่ติดต่อออกไปภายนอก” เด็ดขาด
  • “โปรโตคอลธุรกรรมของ Kafka พังในเชิงพื้นฐานและควรถูกแก้ไขใหม่” ฟังแล้วเจ็บ
    แต่เช่นเคย การตรวจสอบและบทความ ยอดเยี่ยมมาก

  • สงสัยว่า Kyle เคยตรวจสอบ NATS JetStream หรือยัง อยากรู้ว่าเขาจะคิดอย่างไร

    • ยังไม่เคยตรวจสอบ แต่คุณไม่ใช่คนแรกที่ขอ
      มีบางคนเสนอว่านี่น่าจะ… จะเรียกว่าอะไรดี… สนุกดี :-)
  • หา โปรเจกต์ bufstream บน GitHub ไม่เจอ สงสัยว่าอยู่ที่ไหน

    • อ๊ะ ขอโทษ ตอนนี้น่าจะแก้แล้ว
    • ผมเจอ https://github.com/bufbuild/buf บนเว็บไซต์
    • ตัว bufstream เองดูเหมือนไม่ใช่โอเพนซอร์ส แต่มี https://github.com/bufbuild/bufstream-demo ซึ่งอาจใกล้เคียงกับสิ่งที่ต้องการ
      แต่แปลกที่อันนี้ก็ไม่มี license ด้วย
  • หลังอ่านบล็อกโพสต์และเอกสารที่เกี่ยวข้อง ดูเหมือน “exactly-once delivery” ของ Kafka ถูกนิยามเป็นคุณสมบัติของ งานอ่าน-ประมวลผล-เขียน ที่ worker อ่านจาก topic 1 แล้วเขียนไป topic 2 โดยทั้งสอง topic อยู่ในระบบ Kafka เชิงตรรกะเดียวกัน
    ถ้าถูกต้อง แบบนี้เรียกว่า transaction จะเหมาะกว่าหรือเปล่า

    • Kafka เองก็เรียกสิ่งนี้ว่า transaction จริง ๆ
      เพียงแต่มีสองวิธีในการมอง “exactly-once”
      วิธีหนึ่งคือความหมายแบบ transaction ของฐานข้อมูล คือผลลัพธ์ต้องไม่ซ้ำและไม่หาย
      อีกวิธีหนึ่งใกล้เคียงกับคุณสมบัติของกราฟ data flow เกี่ยวกับความสัมพันธ์ของข้อความข้าม topic-partition และใกล้กับ consistency ใน ACID มากกว่า
      คล้ายกับที่ระบบ transaction แบบ serializable รับประกัน consistency ระดับโดเมนบางอย่าง เราสามารถใช้ transaction เพื่อไปถึงคุณสมบัติ data flow นั้นได้
      เช่น serializability รับประกันว่า invariant ที่คงอยู่เมื่อดูแต่ละ transaction แยกกัน จะยังคงอยู่ในประวัติการทำงานพร้อมกันด้วย
      อาจมองได้ว่า Kafka พยายามไปถึง “exactly-once semantics” ด้วยวิธีนั้น
  • อย่าสับสนกับ https://www.warpstream.com/

    • ใช่แล้ว WarpStream ไม่รองรับ transaction ด้วยซ้ำ
  • Errata: “Transactions may observe none, part, or all” น่าจะเป็น “Consumers may observe none, part, or all”

    • ถูกทั้งคู่ แต่ผมเขียนว่า transaction เพื่อความชัดเจน
      semantics ของ consumer นอก transaction คลุมเครือกว่า
      การอ่านทั้งหมดของ workload นี้เกิดในบริบทของ transaction และผ่านเส้นทาง transaction offset commit
  • สงสัยว่าซอฟต์แวร์นี้ใช้ทำอะไร เครื่องมือวัด? กล่องดำ?

    • Jepsen เป็นเครื่องมือที่ทำให้คุณร้องไห้ได้ ถ้าไม่รู้ว่ามันทดสอบฐานข้อมูลที่คุณพัฒนาอยู่
      แน่นอนว่าเป็นน้ำตาแห่งความดีใจ เพราะการได้รับความสนใจจาก Jepsen เองก็ถือเป็นความสำเร็จอย่างหนึ่ง
    • เป็น Kafka clone โดยทั่วไป Kafka คือ queue ที่ทนทาน