21 điểm bởi hiddenest 2020-12-24 | 2 bình luận | Chia sẻ qua WhatsApp

Trong một môi trường có số lượng sự kiện trung bình hàng tháng vượt quá 10 tỷ, đã xuất hiện nhu cầu phải phân tích dữ liệu trong thời gian ngắn để thực hiện phân tích tính năng hành vi người dùng (Cohort).

(Ví dụ: phụ nữ trong độ tuổi 30 đã chi hơn 100.000 won mỗi tháng trên ứng dụng của chúng ta trong 6 tháng qua → tỷ lệ quay lại của họ)

Bài viết kể về hành trình tự tay triển khai một datastore mà trước đây lập trình viên chỉ là người sử dụng.

Để triển khai truy vấn phân tích hành vi người dùng thì cần…

  • Phải có khả năng truy vấn các metric chưa được tính toán sẵn từ trước (+ cũng phải hỗ trợ các kiểu phân tích mới mà không cần Re-indexing)

  • Khi Group By dữ liệu sự kiện theo từng người dùng, cần giảm tối đa nút thắt High Cardinality Shuffle

Cân nhắc giữa việc dùng giải pháp sẵn có hay tự xây giải pháp riêng

  • Druid đã được sử dụng ở nơi khác, nhưng do giới hạn của Pre-Aggregation (cách chỉ đọc các giá trị đã được tính sẵn) nên không phù hợp để triển khai tính năng

  • Có thể vận hành các data warehouse như Snowflake hay Redshift ở quy mô lớn, nhưng do tính tổng dụng đặc trưng của chúng, phải vận hành một cụm lớn hơn nhiều so với mục tiêu nên rất tốn kém

  • Để đáp ứng nhiều nhu cầu đa dạng như Funnel, đối sánh ID, v.v. thì DB dựa trên SQL có những giới hạn nhất định

Cuối cùng, tự tạo datastore

  • Luft = datastore được tối ưu để thực hiện nhanh các truy vấn phân tích hành vi người dùng được Group By theo user ID ngay từ đầu

  • Được xây dựng dựa trên Golang

  • Phân tích dữ liệu người dùng ở quy mô hàng chục TB chỉ với dưới 5 node, thời gian trung bình 3 giây ~ tối đa 10 giây

  • Khác với RDBMS thông thường, hệ thống có tính bất biến (nếu cần thì ghi đè dữ liệu của cùng một khoảng thời gian) → thiết kế cụm đơn giản, hiệu năng cao mà không cần triển khai page manager phức tạp, và có thể thiết kế định dạng lưu trữ dữ liệu mong muốn

Mổ xẻ nền tảng kỹ thuật

  • TrailDB (storage engine) - Rowstore lưu trữ sự kiện chuỗi thời gian, tối ưu cho phân vùng theo user ID

    → Giá trị được từ điển hóa và chỉ lưu ID của giá trị đó

    → Sự kiện của người dùng được sắp xếp theo thời gian và chỉ lưu phần thời gian tăng thêm so với sự kiện trước đó, cùng với các cột đã thay đổi (vì phần lớn thuộc tính người dùng không thay đổi)

    → Không có index. Bắt buộc phải full scan.

    → Nhưng lại có tỷ lệ nén cao đến mức gây sốc (CSV 13GB → ~TrailDB 300mb)

    → Vì độ phức tạp thời gian là O(n), nên họ cho rằng chỉ cần giảm độ phức tạp không gian

  • LLVM (query engine)

    → Nhưng TrailDB chỉ hỗ trợ equals theo dạng OR-AND, và truy vấn được parse trong Go phải được truyền sang C, C++

    → Sau đó phát hiện ra PostgreSQL biên dịch truy vấn bằng LLVM JiT

    → Truy vấn thường xuyên được mở rộng tính năng, nên có thể tránh việc tăng chi phí phát triển nếu không phải viết bằng C, C++ (chỉ cần tạo LLVM IR trong Golang rồi chuyển sang C, C++ để JiT compile và thực thi)

  • Tự xây dựng lớp xử lý tính toán

    → MapReduce được dùng nhiều, nhưng vì dùng Golang nên không thể dùng

    → Spark/Hadoop được tối ưu cho Long-running Job nên kể cả khi gắn vào thì hiệu năng cũng không tốt

    → Cái này cũng tự làm luôn → https://github.com/ab180/lrmr

    → Kết hợp gRPC + Protobuf + etcd, vay mượn khá nhiều từ thiết kế Spark quen thuộc

    → Chấp nhận từ bỏ Resiliency → nếu đẩy hiệu năng lên cực hạn, kể cả có sự cố thì chạy lại từ đầu cũng vẫn dưới 10 giây

    → Vấn đề buffer overflow do xử lý dữ liệu quy mô lớn xảy ra thường xuyên (Backpressure) nên đã chuyển sang Pull-based Event Stream (được áp dụng ở Kafka, Armeria, v.v.)

  • Tự triển khai sharding

    → Shard = historical node

    → Nếu dùng phạm vi ngày của partition làm khóa sharding thì sao?

    → Mọi truy vấn đều có thời gian → dễ lọc

    → Cùng một khoảng thời gian sẽ có lượng dữ liệu tương tự nhau → dễ phân tán dữ liệu

    → Môi trường phân tán không hề đẹp đẽ…

    → Nếu node bị down hoặc có node mới được thêm vào thì sao?

    → Nếu đầy dung lượng lưu trữ thì sao?

    → Nếu do sự cố mà dữ liệu dồn về chỉ một node thì sao?

    → Tùy biến Cost Function của Druid để khoảng ngày partition càng gần và càng chồng lấp thì Cost càng cao

    → Để đảm bảo tính sẵn sàng của shard, đã thực hiện các việc sau

    → Đặt TTL cho thông tin shard và cập nhật định kỳ (etcd)

    → Lưu partition trên S3, quản lý danh sách partition bằng DynamoDB

Tình hình production hiện tại

  • Chỉ với 4 instance c5.2xlarge đã có thể scan 500GB dữ liệu trong vòng 15 giây

Mục tiêu sắp tới (hoặc những việc cần làm)

  • Muốn thực hiện phân tích Funnel thời gian thực với cụm dưới 10 máy

  • Muốn hỗ trợ Spark để tích hợp ML, v.v.

  • Đang phát triển column store riêng (Ziegel) để thay thế TrailDB

    → Tối ưu SIMD và đa lõi

    → Lọc trước dựa trên thuộc tính người dùng bằng Bitmap Index

2 bình luận

 
gera1d 2020-12-24

traildb thú vị đấy. https://www.youtube.com/watch?v=-oPFxSwn0lM Hay lắm. Dù là video đã cũ nhưng có lẽ traildb vẫn chưa thay đổi gì nhiều trong suốt thời gian qua.

 
hiddenest 2020-12-24

Giờ mới thấy là còn có cả bài blog của phía nhà phát triển nữa,

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

Mình mới nghe đến TrailDB lần đầu, hình như nó là kiểu như thế này...

https://github.com/traildb/traildb