กลับไปที่บทความ
Data Engineering Real-time Production Kafka

Real-Time Data Pipelines: บทเรียนจากการใช้งานจริง

พลากร วรมงคล
15 สิงหาคม 2567 10 นาที

“Real-time data pipelines ดูเหมือนง่าย: เก็บรวบรวมข้อมูล ประมวลผล และให้บริการผลลัพธ์ แต่ในการใช้งานจริงที่ต้องเก็บข้อมูลกิกะไบต์ต่อวินาที ทุกการตัดสินใจด้านสถาปัตยกรรมจะมีผลกระทบอย่างมหาศาล นี่คือบทเรียนที่เรียนรู้มาแบบยากลำบาก”

ความแตกต่างระหว่าง Demo และการขยายขนาด

Pipeline แบบ real-time ตัวแรกของฉันถูกสร้างขึ้นในอีกหนึ่งสัปดาห์ มันทำงานได้อย่างสมบูรณ์บน laptop ของฉัน: Kafka topics บ้างการประมวลผล stream กับ Spark และผลลัพธ์ที่ถูกเก็บไว้ในฐานข้อมูล Demo ที่นี่ได้จับใจของผู้บริหาร

แล้วเราก็ขยายไปสู่ข้อมูลการใช้งานจริง—100 เท่าของปริมาณที่เราเคยทดสอบมา ระบบพังทลายอย่างสเปกตาคิวลาร์

นี่คือจุดที่ฉันเรียนรู้ว่า data pipelines ไม่ได้ขยายขนาดแบบเชิงเส้น พวกมันแตกออกมาในขั้นตอนที่ไม่คาดหวัง: ครั้งแรกที่สเกล 2 เท่า จากนั้นอีกครั้งที่ 10 เท่า และจากนั้นอีกครั้งที่ 100 เท่า แต่ละขั้นตอนต้องการการคิดใหม่เกี่ยวกับสถาปัตยกรรม

Kafka ไม่ใช่รถบรรทุก

เมื่อคุณพบ Kafka ครั้งแรก มันดูเหมือนเวทมนตร์: เขียนข้อมูลไปยัง topic บ้าง ใช้บริการมันที่ที่อื่น และข้อความจะได้รับการส่งมอบอย่างแน่นอน ในความเป็นจริง Kafka เป็น distributed log ที่มีการรับประกันที่แข็งแกร่งและขอบที่คมชัด

ความเข้าใจผิดที่สำคัญ:

1. การเรียงลำดับเป็นเรื่องที่ยุ่งยาก Kafka รับประกันการเรียงลำดับต่อ partition ไม่ใช่การเรียงลำดับทั่วโลก หากคุณมี 10 partitions เหตุการณ์สามารถเสร็จสิ้นแบบไม่เป็นลำดับ หากแอปพลิเคชันของคุณขึ้นอยู่กับการเรียงลำดับที่เข้มงวด คุณอาจควรใช้ single partition เพียงตัวเดียว (ซึ่งจะทำให้ scalability ตาย) หรือจัดการกับเหตุการณ์ที่ไม่เป็นลำดับในตรรกะการประมวลผลของคุณ

2. Delivery semantics เป็นเรื่องสำคัญ Kafka นำเสนอ “at least once” delivery โดยค่าเริ่มต้น นี่หมายความว่าตรรกะการประมวลผลของคุณต้องเป็น idempotent หากข้อความเดียวกันถูกส่งมอบสองครั้ง มันจะต้องสร้างผลลัพธ์เดียวกันทั้งสองครั้ง ทีมส่วนใหญ่ไม่ได้สร้าง idempotent consumers ในตอนแรกและประสบความเดือดร้อน

3. Consumer lag ไม่มองเห็นจนกว่ามันจะมองเห็น ในการใช้งานจริง คุณจะมีวันที่ทุกอย่างทำงานได้ดี จากนั้นวันหนึ่ง Kafka disk เต็ม หรือ consumer ชน้อนแล้วจู่ๆ คุณก็ล้าหลัง 8 ชั่วโมง คุณต้องการ monitoring ไม่เพียงแค่สำหรับ Kafka เท่านั้น แต่สำหรับ lag นั่นเอง

# At least once delivery requires idempotent processing
def process_message(message):
  msg_id = message['id']

  # Check if we've processed this before
  if db.seen_messages.exists(msg_id):
    return

  # Process
  result = expensive_calculation(message)

  # Store result and mark as seen atomically
  with db.transaction():
    db.results.insert(result)
    db.seen_messages.insert(msg_id)

กับดักการประมวลผลแบบ Stateful

เมื่อคุณประมวลผล streaming data คุณต้องการสถานะ: “เราเห็นกี่ events จากผู้ใช้นี้ในชั่วโมงที่แล้ว” หรือ “ราคาเฉลี่ยการทำงานของหุ้นนี้เป็นเท่าไหร่”

Kafka Streams และ Flink ทั้งคู่จัดการสิ่งนี้อย่างสวยงามด้วย state stores—instances RocksDB ในพื้นที่ที่บำรุงรักษา state ของคุณ นี่ใช้งานได้ดีจนกว่าคุณต้องการประมวลผลข้อมูลอีกครั้ง (bug fix ใหม่ต้องการให้ pipeline ทำงานอีกครั้ง) ตอนนี้ state stores ของคุณออกจาก sync กับโค้ดของคุณ

บทเรียนที่ฉันเรียนรู้มาแบบยากลำบาก: state ควรจะสามารถหาได้ ไม่ใช่เก็บไว้ แทนที่จะเป็น “count of user events in the last hour” ให้เก็บ raw events และหา count ตามความต้องการ แทนที่จะเป็น “running average price” ให้เก็บ raw prices และหา average พวกเขาเวลาคิวรี

นี่มีต้นทุน compute เพิ่มเติมในตอนแรก แต่มันคุ้มค่า:

  • Reprocessing เป็นเรื่องธรรมชาติ (ลบผลลัพธ์เก่า รัน ใหม่)
  • Debugging เป็นเรื่องง่าย (คุณมีข้อมูล raw ทั้งหมด)
  • Code changes ไม่ต้องการ state migration
  • คุณสามารถสร้าง derived metric ใหม่ได้โดยไม่สูญเสีย data

Latency vs. Throughput Frontier

Real-time pipelines บังคับให้คุณเลือก: คุณต้องการปรับให้เหมาะสมสำหรับ latency (fast individual events) หรือ throughput (many events per second)?

Latency-optimized pipelines ประมวลผล events ทันทีที่พวกเขามาถึง คุณ buffer น้อยที่สุด แต่ละ event ใช้เวลา milliseconds ในระบบ นี่ต้องการ bespoke tuning และแตกอยู่ที่ scale

Throughput-optimized pipelines ประมวลผล batch events คุณรวบรวม 1,000 events ประมวลผลพวกเขาด้วยกัน จากนั้นย้ายไปยัง batch ถัดไป นี่เสียเวลา latency (คุณอาจรอ 100ms สำหรับ batch ให้เต็ม) แต่ได้ throughput ที่ดีขึ้นแบบเอกโพเนนเชีย

Pareto frontier สำหรับ real-time applications ส่วนใหญ่อยู่ที่นี่:

  • Ingest events ด้วย minimal buffering (คุณต้องการ latency perception ต่ำ)
  • ประมวลผลพวกเขาใน micro-batches of 100-1,000 events (คุณต้องการ good throughput)
  • ผลลัพธ์มีอยู่ภายใน 1-5 วินาที (good latency ดี throughput ยอดเยี่ยม)

สำหรับการทำงาน actual low-latency (stock trading auto-bidding) คุณไปไกลกว่าและปรับให้เหมาะสมที่ microsecond level ทีมส่วนใหญ่ควรจะไม่ infrastructure complexity หลายเท่าแบบเอกโพเนนเชีย

Monitoring ไม่เป็นทางเลือก

โมเมนต์ที่น่ากลัวที่สุดในการทำงาน production data engineering คือเมื่อ pipeline ของคุณทำงานได้ดี แต่ผลลัพธ์ของคุณผิดพลาดโดยเงียบ

คุณมี Kafka rebalance ที่ทำให้ข้อความบางอย่างถูกข้าม หรือ bug ในการประมวลผลที่ drop 0.1% ของ data โดยเงียบ หรือ clock skew บนเซิร์ฟเวอร์ที่ทำให้ timestamps ไม่ถูกต้อง พวกเหล่านี้เกิดขึ้น

คุณต้องการ:

  1. End-to-end tests: สร้าง data ที่รู้จัก push ผ่าน pipeline ตรวจสอบว่าผลลัพธ์ตรงกัน
  2. Sampling and comparisons: นำ 1% ของ data ประมวลผลกับ version เก่าและ version ใหม่ เปรียบเทียบ
  3. Data quality metrics: track ไม่เพียงแค่ว่า pipeline กำลังทำงาน แต่ว่า output สมเหตุสมผล (distribution of values min/max ranges null counts)
  4. Alerting on lag: หากคุณล้าหลัง 5 นาที คุณต้องการรู้ทันที
  5. Audit logs: pipeline run ทุกครั้งควร log สิ่งที่ประมวลผล เมื่อ และ output คืออะไร
# Data quality check (not latency check)
def validate_batch(results):
  # Results should have X rows (within 10% of expected)
  assert len(results) > 9000 and len(results) < 11000

  # All timestamps should be recent
  for row in results:
    age = now() - row.timestamp
    assert age < timedelta(hours=1)

  # No NULLs in required fields
  for row in results:
    assert row.user_id is not None
    assert row.value > 0

Reprocessing เป็นทางออก

การลงทุนที่ดีที่สุดที่คุณสามารถทำได้ใน data pipeline reliability คือความสามารถที่จะประมวลผล historical data อย่างรวดเร็วและปลอดภัย

เมื่อ bug ถูกค้นพบ คุณไม่ต้องการแก้ไข data ด้วยตนเอง คุณต้องการแก้ไขโค้ด ประมวลผลอีกครั้ง และเขียนทับผลลัพธ์เก่า นี่ต้องการ:

  1. Immutable raw event storage (ทั่วไป S3 หรือ similar)
  2. Reprocessing job ที่สามารถอ่านจากช่วงวันที่เฉพาะเจาะจง
  3. Idempotent processing (ดังนั้นการรันครั้งที่สองสร้างผลลัพธ์เดียวกัน)
  4. Version control สำหรับตรรกะการประมวลผลของคุณ (ดังนั้นคุณสามารถจับคู่ผลลัพธ์กับโค้ด)

ด้วยสิ่งนี้ discovery of data corruption กลายเป็น: แก้ไข bug รัน reprocessing job และไป ไม่มี manually patching records ในการใช้งานจริง ซึ่งเป็นหายนะ

Real-Time Promise และ Reality

Real-time data pipelines promise insights ที่มี minimal latency ความเป็นจริงคือพวกมันเป็น distributed systems ที่ซับซ้อนที่มีหลายการทำให้ล้มเหลว

สร้างสิ่งที่คุณต้องการจริงๆ: ถ้าการตัดสินใจทางธุรกิจของคุณทำได้โดย 1-hour latency batch pipelines ง่ายกว่า ถ้าคุณต้อง sub-second latency จริงๆ รู้ว่าคุณกำลังลงนาม significant infrastructure complexity และ ongoing operational work

Sweet spot สำหรับ organization ส่วนใหญ่คือ “near real-time”: ข้อมูลมีอยู่ภายใน few seconds กับ few minutes สร้างสำหรับว่า monitor relentlessly และมี reprocessing strategy ที่คุณเชื่อได้

Comments powered by Giscus are not yet configured. Set PUBLIC_GISCUS_REPO_ID and PUBLIC_GISCUS_CATEGORY_ID in apps/web/.env to enable.

PV

เขียนโดย พลากร วรมงคล

Software Engineer Specialist ประสบการณ์กว่า 20 ปี เขียนเกี่ยวกับ Architecture, Performance และการสร้างระบบ Production

เพิ่มเติมเกี่ยวกับผม

บทความที่เกี่ยวข้อง