Cài đặt Key-Value Storage bằng Go và C++ — Phần 2

Quincey Pham
ZaloPay Engineering
3 min readDec 30, 2019

1. Giới thiệu

Ở bài viết trước (các bạn có thể xem tại đây), chúng ta đã nói về đặc tả cấu trúc file của B+Tree storage cũng như cách xử lý đồng bộ các tương tác với file trong môi trường đa luồng. Bài viết lần này, mình sẽ nói về cách kết nối GodBee service Golang với storage C++.

2. Kiến trúc tổng quan

GodBee architecture

Bên client sẽ tương tác với GodBee service qua các lời gọi gRPC. Để thực hiện các tương tác với storage, GodBee service layer sẽ thực hiện lời gọi tới Data Access Layer, là tầng quản lý các repository ứng với từng loại storage. Các repository này sẽ gọi các method từ dưới storage để phục vụ cho việc xử lý dữ liệu.

Cả hai hai tầng Service Layer và Data Access Layer được viết bằng Golang, còn B-Tree và B+Tree được viết bằng C++. Công cụ CGO được sử dụng gọi được các hàm bên C từ Golang. Tuy nhiên, CGO không hỗ trợ trực tiếp cú pháp và các tính năng về class trong C++ do đó ta không thể dùng CGO để gọi trực tiếp các method từ B-Tree và B+Tree storage. Phần tiếp theo sẽ nói cách ta thực hiện lời gọi C++ method từ Golang.

3. Gọi C++ method từ Golang

Ý tưởng ở đây là chúng ta phải wrap C++ class bởi một interface C thuần, sau đó các hàm trong interface C sẽ map với các hàm của Go bằng CGO.

Bước 1: Định nghĩa C++ class

Trước tiên, ta có class BPlusTreeStore bên C++ là class chính thực hiện các method của Key-Value storage:

Các phương thức của C++ class

Bước 2: Đóng gói C++ class với Interface C

Bước tiếp theo là bước đóng gói class BPlusTreeStore ở trên với interface của C. Ở đây ta có một file GBplusTreeStore.h sẽ định nghĩa interface C mà ta sẽ dùng để wrap:

C++ class wrap bởi C interface

Sau đó chúng ta có thể định nghĩa các hàm wrapper của interface C dựa trên class C++ là BPlusTreeStore. Ta có file GBPlusTreeStore.cpp tương ứng như sau:

Định nghĩa các hàm wrapper của interface C

Nên nhớ hàm GBPlusInit() phải được chạy để ta có được một đối tượng BPlusTreeStore được “wrap” dưới dạng GBPlusTreeStore, phục vụ cho các hàm service. Ta có thể thấy các hàm wrapper đều được truyền tham số là biến interface GBPlusTreeStore, chính là đối tượng trả về của hàm GBPlusInit(). Trong các hàm này, biến GBPlusTreeStore đó có thể ép kiểu lại thành BPlusTreeStore để có thể chạy các phương thức C++ bên storage.

Bước 3: Chuyển đổi hàm trong Interface C sang Go

Bước cuối cùng là ta sẽ wrap các phương thức của interface C thành phương thức trong Go. Đến đây, ta đã có thể gọi các hàm bên C từ Go bằng CGO:

Các phương thức bên Go gọi phương thức của interface C sử dụng CGO

Ở đây, biến global gS kiểu struct GBPlusStore chứa đối tượng tree chính là đối tượng GBPlusTreeStore để truyền vào các hàm service của interface C như đã nói ở trên. gS là một singleton vì ta chỉ cần 1 đối tượng BPlusTreeStore thực hiện các service bên trong storage. Khi gọi hàm GetInstance(), đối tượng tree bên trong gS được khởi tạo thông qua phương thức GBPlusInit() của interface C.

4. Lời kết

Việc hiện thực một Key-Value Store sử dụng cấu trúc dữ liệu cây B-Tree và B+Tree giúp mình củng cố được những kiến thức về hệ điều hành, xử lý đồng bộ khi đọc/ghi file cũng như cách phòng tránh memory leak hiệu quả.

Mình xin gửi lời cảm ơn chân thành đến anh AJ Pham đã hướng dẫn trong suốt quá trình làm project này. Cảm ơn bạn Trân. đã chia sẻ những ý tưởng hay và đồng hành viết bài cùng mình.

Các bạn có thể xem thêm tại:

Repo: https://github.com/zalopay-oss/godbee

Slide: B+Tree, KV-Store Service

--

--