21 คะแนน โดย hiddenest 2020-12-24 | 2 ความคิดเห็น | แชร์ทาง WhatsApp

ในสภาพแวดล้อมที่มีจำนวนอีเวนต์เฉลี่ยต่อเดือนมากกว่า 10,000 ล้านรายการ มีความจำเป็นต้องวิเคราะห์ข้อมูลให้รวดเร็วเพื่อทำการวิเคราะห์ฟีเจอร์พฤติกรรมผู้ใช้ (Cohort)

(เช่น ผู้หญิงวัย 30+ ที่ใช้จ่ายมากกว่า 100,000 วอนต่อเดือนในแอปของเราในช่วง 6 เดือนที่ผ่านมา → อัตราการกลับมาใช้งานอีกครั้งของคนกลุ่มนี้)

นี่คือเรื่องราวของการลงมือสร้าง datastore ที่เดิมมีแต่ใช้งานมันในฐานะนักพัฒนา

เพื่อทำคิวรีวิเคราะห์พฤติกรรมผู้ใช้ให้ได้…

  • ต้องสามารถคิวรีเมตริกที่ไม่ได้คำนวณเตรียมไว้ล่วงหน้าได้ (+ และต้องรองรับการวิเคราะห์รูปแบบใหม่ได้โดยไม่ต้อง Re-indexing)

  • เมื่อต้อง Group By ข้อมูลอีเวนต์ตามผู้ใช้ ต้องมีคอขวดจาก High Cardinality Shuffle ให้น้อย

ลังเลว่าจะใช้โซลูชันเดิมหรือสร้างเองดี

  • Druid ถูกใช้งานอยู่ที่อื่น แต่มีข้อจำกัดของ Pre-Aggregation (วิธีที่อ่านเฉพาะค่าที่คำนวณไว้แล้ว) จึงไม่เหมาะกับการพัฒนาฟังก์ชันนี้

  • ดาต้าแวร์เฮาส์อย่าง Snowflake หรือ Redshift สามารถรันในสเกลใหญ่ได้ แต่ด้วยความเป็นระบบอเนกประสงค์ ทำให้ต้องใช้คลัสเตอร์ใหญ่เกินเป้าหมายและมีค่าใช้จ่ายสูง

  • หากต้องรองรับความต้องการที่หลากหลาย เช่น Funnel, การจับคู่ ID ฯลฯ ฐานข้อมูลแบบ SQL-based ก็มีข้อจำกัด

สุดท้ายจึงสร้าง datastore ขึ้นมาเอง

  • Luft = ดาต้าสโตร์ที่ออกแบบมาให้เหมาะกับการรันคิวรีวิเคราะห์พฤติกรรมผู้ใช้ที่ Group By ตาม user ID ตั้งแต่ต้นได้อย่างรวดเร็ว

  • สร้างบนพื้นฐานของ Golang

  • วิเคราะห์ข้อมูลผู้ใช้ระดับหลายสิบ TB ได้ด้วยจำนวนโหนดไม่เกิน 5 เครื่อง โดยใช้เวลาเฉลี่ย 3 วินาที ~ สูงสุด 10 วินาที

  • ต่างจาก RDBMS ทั่วไปตรงที่มีความเป็น immutable (หากจำเป็นก็เขียนทับข้อมูลในช่วงเวลาเดียวกัน) → ได้คลัสเตอร์ดีไซน์ที่เรียบง่าย ประสิทธิภาพสูงโดยไม่ต้องทำ page manager ที่ซับซ้อน และสามารถออกแบบฟอร์แมตการจัดเก็บข้อมูลได้ตามต้องการ

เจาะดูพื้นฐานทางเทคนิค

  • TrailDB (storage engine) - Rowstore สำหรับเก็บอีเวนต์แบบ time-series ที่เหมาะกับการพาร์ทิชันตาม user ID

→ ทำ dictionary encoding กับค่าแล้วเก็บแค่ ID ของค่านั้น

→ เรียงอีเวนต์ของผู้ใช้ตามลำดับเวลา แล้วเก็บเฉพาะค่าของเวลาที่เพิ่มขึ้นจากอีเวนต์ก่อนหน้าและคอลัมน์ที่เปลี่ยนไป (เพราะคุณสมบัติของผู้ใช้ส่วนใหญ่มักไม่เปลี่ยน)

→ ไม่มีดัชนี ต้อง full scan เท่านั้น

→ แต่มีอัตราการบีบอัดสูงจนน่าตกใจ (CSV 13GB → ~TrailDB 300mb)

→ เนื่องจาก time complexity คือ O(n) จึงมองว่าหากลด space complexity ลงได้ก็น่าจะพอ

  • LLVM (query engine)

→ แต่ TrailDB รองรับแค่ equals แบบ OR-AND และต้องส่งคิวรีที่ parse จาก Go ไปยัง C, C++

→ แล้วก็พบว่า PostgreSQL คอมไพล์คิวรีด้วย LLVM JiT

→ คิวรีมีการขยายความสามารถอยู่บ่อย หากเขียนด้วย C, C++ จะเพิ่มต้นทุนการพัฒนา จึงหลีกเลี่ยงปัญหานี้ด้วยการให้ Golang สร้างแค่ LLVM IR แล้วส่งต่อไปให้ฝั่ง C, C++ รันผ่าน JiT compile

  • สร้างเลเยอร์ประมวลผลขึ้นมาเอง

→ ปกติมักใช้ MapReduce แต่ใช้ไม่ได้เพราะเลือกใช้ Golang

→ Spark/Hadoop เหมาะกับ Long-running Job จึงได้ประสิทธิภาพไม่ดีแม้จะพยายามเชื่อมต่อแล้วก็ตาม

→ สุดท้ายก็สร้างเอง → https://github.com/ab180/lrmr

→ ใช้การผสมกันของ gRPC + Protobuf + etcd และยืมดีไซน์ที่คุ้นเคยจาก Spark มาเยอะ

→ ยอมทิ้ง Resiliency → ถ้ารีดประสิทธิภาพให้สุด แม้เกิดปัญหาก็เริ่มใหม่ทั้งหมดได้ในเวลาไม่ถึง 10 วินาที

→ มีปัญหา buffer overflow จากการประมวลผลข้อมูลขนาดใหญ่เกิดขึ้นบ่อย (Backpressure) จึงเปลี่ยนเป็น Pull-based Event Stream (แนวทางที่ Kafka, Armeria ฯลฯ ใช้)

  • ลงมือทำ sharding เอง

→ shard = historical node

→ ถ้าใช้ช่วงวันที่ของพาร์ทิชันเป็นค่า sharding key ล่ะ?

→ ทุกคิวรีมีเรื่องเวลาอยู่แล้ว → กรองได้ง่าย

→ ในช่วงเวลาเดียวกันข้อมูลมักมีขนาดใกล้เคียงกัน → กระจายข้อมูลได้ง่าย

→ แต่ระบบกระจายไม่สวยงามนัก…

→ ถ้าโหนดล่มหรือมีการเพิ่มโหนดใหม่ล่ะ?

→ ถ้าพื้นที่เก็บข้อมูลเต็มล่ะ?

→ ถ้าเกิดเหตุขัดข้องแล้วโหลดไปกระจุกที่โหนดเดียวล่ะ?

→ จึงปรับแต่ง Cost Function ของ Druid ให้ยิ่งช่วงวันที่ของพาร์ทิชันใกล้กันและทับซ้อนกันมากเท่าไร Cost ก็ยิ่งสูงขึ้น

→ เพื่อให้ shard พร้อมใช้งาน ได้ทำสิ่งต่อไปนี้

→ ตั้ง TTL ให้ข้อมูล shard และรีเฟรชเป็นระยะ (etcd)

→ เก็บพาร์ทิชันไว้ใน S3 และจัดการรายการพาร์ทิชันด้วย DynamoDB

สถานะในโปรดักชันตอนนี้

  • สแกนข้อมูล 500GB ได้ภายใน 15 วินาที โดยใช้เพียงอินสแตนซ์ c5.2xlarge 4 เครื่อง

เป้าหมายต่อไป (หรือสิ่งที่ต้องทำ)

  • อยากทำการวิเคราะห์ Funnel แบบเรียลไทม์ด้วยคลัสเตอร์ไม่เกิน 10 เครื่อง

  • วางแผนรองรับ Spark เพื่อเชื่อมต่อกับ ML เป็นต้น

  • กำลังพัฒนา column store ของตัวเอง (Ziegel) เพื่อมาแทน TrailDB

→ ปรับแต่งให้รองรับ SIMD และมัลติคอร์

→ กรองล่วงหน้าตามคุณสมบัติผู้ใช้ด้วย Bitmap Index

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

 
gera1d 2020-12-24

traildb สนุกดีนะครับ https://www.youtube.com/watch?v=-oPFxSwn0lM น่าสนใจมาก แม้จะเป็นวิดีโอเก่าแล้ว แต่ traildb ก็คงไม่ได้เปลี่ยนแปลงอะไรในช่วงนั้นหรอกครับ

 
hiddenest 2020-12-24

ตอนนี้กลับไปดูแล้ว เห็นว่ามีโพสต์ในบล็อกของนักพัฒนาด้วยครับ

https://engineering.ab180.co/stories/introducing-luft

เพิ่งเคยได้ยินชื่อ TrailDB เป็นครั้งแรก แต่เจ้าตัวนี้คือประมาณนี้...

https://github.com/traildb/traildb