Build Thrift Service on top of RocksDB

Table of contents

Mình đã build Thrift service (a RPC Framework) on top of RocksDB (an embeddable persistent key-value storage) như một database sử dụng cho một số projects khi làm việc tại Zalo. Hiệu năng mà RocksDB cùng với Thrift mang lại kết quả khá tuyệt vời với khả năng đọc ghi dữ liệu cao, phù hợp sử dụng cho các tools mình thực hiện tại Zalo khi không yêu cầu sử dụng DB nội bộ của công ty.

1. Overview

Trước hết, mình xin đề cập đến lý do tại sao mình lại chọn RocksDB để làm embeded database cho con Thrift service của mình. Chuyện là khi mình còn trong giai đoạn Fresher tại Zalo, khi đó mình có task về việc tracking dữ liệu (dữ liệu gì thì mình không tiện đề cập), với lượng dữ liệu khá cao (hơn 3000 req/s). Rõ ràng các DB khác như MySQL hay MongoDB không đáp ứng được nhu cầu này. Đối với Zalo, con DB nội bộ dư sức xử lý mấy loại này, tuy nhiên do mình còn là Fresher cũng như task này không quá quan trọng đối với công ty nên mình không được cấp các DB này. Thế là sau một thời gian tìm kiếm trên Google, mình đã tìm được RocksDB, một con embbeded DB mang lại cho mình khá nhiều tin tưởng, vì là một Open Source từ Facebook, forked ra từ LevelDB của Google, cũng như được sử dụng làm innerDB (engine storage) trong một vài con DB khác.

Còn với lý do mình lại wrap con RocksDB trong một Thrift Service vì

  • Zalo chủ yếu sử dụng Thrift làm RPC framework cho các micro-services
  • Phục vụ khả năng đọc ghi từ nhiều service khác, chứ RocksDB là in-process DB (chỉ 1 process có thể write trên DB này)

1.1. RocksDB

Giới thiệu ngắn gọn qua về RocksDB

  • Một key-value storage
  • Sử dụng Log Structured Database Engine
  • Hiện thực bằng C++
  • Hỗ trợ JNI - Java Wrapper
  • Hỗ trợ Column-Family cho việc partition dữ liệu trong 1 process
  • Key được sort trong lưu trữ, có thể prefix seek key, iterate theo key.

RocksDB cung cấp tài liệu trên Github về cách hiện thực, lưu trữ và sử dụng. Các bạn có thể (nên) đọc về loại DB này nếu thực sự muốn sử dụng nó.

1.2. Thrift Service

Apache Thrift là một RPC framework, cho các mircro-services tương tác với nhau với khả năng cross-language, tức các ngôn ngữ khác nhau có thể gọi mà không thông cần các protocol mức cao hơn (HTTP cũng là một protocol cho phép các ngôn ngữ khác nhau có thể giao tiếp, nhưng sử dụng JSON như cầu nối cho các ngôn ngữ này). Ngoài ra Thrift còn cho phép code generation, tạo nhiều thuận tiện khi làm việc trên nhiều ngôn ngữ.

Tại Zalo, Thrift được sử dụng khá nhiều và phổ biến, và cũng đã được custom, build sẵn rất nhiều lib nội bộconfig tối ưu, từ thrift server cho đến thrift client (hay còn gọi là wrapper, cho phép bất kì service nào cũng có thể gọi đến các thrift server). Trong bài viết này, mình cũng dựa trên Thrift tại Zalo để build 1 Thrift server wrap lại con RocksDB và bộ Thrift client (client wrapper) phục vụ và hỗ trợ nhiều loại cho con server của mình.

2. Implementation

Do mình đã hiện thực build dựa trên Thrift đã custom của Zalo nên mình không thể share cũng như public được cả thrift server và thrift client, tuy nhiên mình có thể chia sẻ về những gì mình đã hiện thực.

Như mình đã đề cập, RocksDB hỗ trợ JNI hay Java Wrapper, do đó cho phép ta dễ dàng build một Thrift service bằng Java (do mình chủ yếu làm việc với Java và sử dụng các tool profiler tại Zalo). Về Java Wrapper mà RocksDB cnug cấp, mình xin để lại link tại đây.

Mô hình sẽ được thể hiện như hình sau, trong đó thrift server sẽ mở 2 port, 1 port config, 1 port read/write, với phân quyền read/write/config cho các clients gọi vào.

Thrift RocksDB

2.1. Partitions

Vấn đề đầu tiên mà chúng ta cần lưu ý ở RocksDB chính là hỗ trợ Column-Family, khả năng phân tách tập DB ra nhiều loại, có thể hình dung như tables trong SQL hay collections trong MongoDB.

Do đó trong Thrift service mình dự định build cũng cần hỗ trợ việc partition này. Các partition mình để giá trị integer hay i32 trong Thrift

typedef i32    TPartition

2.2. Key and Value Types

Trong RocksDB, key và value là dữ liệu binary, do đó trong Thrift tương ứng cũng là binary (hay ByteBuffer trong Java)

typedef binary TKey
typedef binary TValue

Mình định nghĩa thêm các structs dưới đây cho việc định danh dữ liệu dễ dàng, phục vụ cho multiget, map result, … Trong đó master được đính kèm thêm phân vùng mà key-value nằm trên RocksDB

struct TMasterKey {
    1:required TPartition partition,
    2:required TKey key,
}

struct TData {
	1:required TKey key,
	2:required TValue value,
}

struct TMasterData {
	1:required TPartition partition,
	2:required TKey key,
	3:required TValue value,
}

2.3. Range Types

Như mình đã đề cập, RocksDB hỗ trợ iterate theo tập key, do đó một kiểu dữ liệu cần hỗ trợ là list theo key và list theo data (key-value) như sau

typedef list<TKey> TListKey
typedef list<TValue> TListValue
typedef list<TData> TListData

struct TKeyRangeResult {
	1:required i32 error,
	2:optional i32 offset, // offset from seek key
	3:optional i32 total, // total keys in result
	4:optional TListKey keys, // list keys are iterated
}

struct TDataRangeResult {
	1:required i32 error,
	2:optional i32 offset, // offset from seek key
	3:optional i32 total, // total keys in result
	4:optional TListData data, // list key-value are iterated
}

2.4. Map Types

RocksDB cũng hộ trợ multiget theo key, do đó ta cần kiểu map theo key

typedef map<TMasterKey, TValue> TMapData
typedef map<TMasterKey, i32> TMapError

struct TMapDataResult {
	1:required i32 error, // last error, useful to check if all data are retrieved successfully or not
	2:optional map<TKey, TValue> dataMap,
	3:optional map<TKey, i32> errorMap, // for check error of any fail data
}

2.5. Supported APIs

Các APIs thrift mình hỗ trợ dưa trên RocksDB hỗ trợ gồm (mình chỉ ghi tượng trưng, không đúng syntax trong thrift)

service ZRocksDBService {

	exist(TPartition, TKey)
	get(TPartition, TKey)

	// multi get by key
	multiGet(TPartition, TListKey)

	// iterate by key
	scanKey(TPartition, TKey seekKey, offset, limit)
	scan(TPartition, TKey seekKey, offset, limit)
	
	// RocksDB: flush to disk
	syn_put(TPartition, TKey, TValue, PutPolicy add_or_update)
	syn_remove(TPartition, TKey)
	syn_removeRange(TPartition, TKey beginKey, TKey endKey)
	
	// RocksDB: async
	put(TPartition, TKey, TValue, PutPolicy add_or_update)
	remove(TPartition, TKey)
	removeRange(TPartition, TKey beginKey, TKey endKey)

	// one-way in Thrift
	ow_put(TPartition, TKey, TValue, PutPolicy add_or_update)
	ow_remove(TPartition, TKey)
	ow_removeRange(TPartition, TKey beginKey, TKey endKey)

	// config service
	backup(TBackupOptionParam)
}

Việc hiện thực chi tiết con server mình sẽ không public ở đây, tuy nhiên căn bản là gọi các API từ RocksDB đã build sẵn.

2.6. Thrift Client Wrapper

Đối với Thrift client cho việc gọi sang con thrift server, mình sẽ hỗ trợ tận răng các APIs build sẵn, cho phép define key-value types, phân vùng partition để dễ dàng sử dụng các client này, không cần trải qua các bước serialize/deserialize cục binary từ key/value. Việc này cũng khá dễ dàng.

3. Benchmarking

Mình đã thử benchmark cho khả năng đọc/ghi dữ liệu của con Thrift server

  • Server: 24 CPUs, RAM 128GB
  • Clients: 3 nodes, mỗi node 8 CPUs, RAM 8G

Mỗi client nodes mình thực hiện 3 java process, mỗi process là một client gọi sang server:

  • Put 4000 req/s
  • Get 4000 req/s

Server mình profiler được thì hoàn toàn có thể xử lý đồng thời put 36K req/sget 36K req/s.

Mình nhận ra giới hạn này là do phía client không thể xử lý hơn được nữa, do cấu hình tương đối, nếu có thêm client node, mình có thể dễ dàng scale ra để tăng req/s này lên mức cao hơn. Tuy nhiên khả năng như trên cũng đã cho mình kết quả khá đẹp :D.

4. Conclusion

Túm lại mình đã build thành công một con Thrift service trên RocksDB, có thể sử dụng rộng ra cho nhiều dự án nhỏ khác, dễ dàng cho phép nhiều bên truy cập và xử lý dữ liệu thông qua con thrift này.

Mình cũng đã benchmark về hiệu năng trên một con server tương đối mạnh, cho kết quả put 36K req/sget 36K req/s rất chi là khả quan.

Các vấn đề khác mình còn vướng phải và cần phát triển hơn

  • Làm thế nào để scale 1 node ra nhiều nodes như một cluster DB?
  • Time-to-live cho DB, làm thế nào để roll dữ liệu không dùng nữa.
  • Cải thiện performance của DB khi thực hiện delete
  • Tách rời Thrift và tool profiler của Zalo để có thể public source cho mọi người phát triển và sử dụng.

Cảm ơn các bạn đã đọc đến dòng này của bài viết này. Nếu các bạn có thắc mắc gì thì có thể liên hệ mình hoặc feedback trong khung chat dưới đây để có thể dễ dàng trao đổi và phát triển nhiều hơn.