Some abbreviations used in this document:
- TM: Transaction Manager
- TC: Transaction Coordinator
- GLS: GlobalLock Service
- RM: Resource Manager
- TXN: Transaction
- Mini Account System
- Table of Contents
- 1. Distributed systems
- 2. Data structutre
- 3. Transaction
- 4. System designs
- 5. API Authentication
- 6. Deployment
Given the distributed system, where each service has its own Database system, the problem is to make sure the ACID (Atomicity, Consistency, Isolation, Durability) of a transaction between data among the cluster nodes.
This project uses etcd/raft library to implement the Consistency between nodes Link
Raft help to replicate data to other nodes.
Raft is used to achieve CP.
Given the data is sharded between nodes, the problem when a transaction which involving multiple nodes will make sure Atomicity (All-or-nothing).
To achieve Atomicity
, we need to implement a Transaction Coordinator
to control the transactions of multiple database servers.
Use 2PC to achieve CA.
Multi raft
is multiple raft-groups, each raft-group
includes multiple raft-nodes
. Each raft-group will be responsible for manage one shard or one partition of data.
In Mini Account System
, we will call each raft-group is a bucket which will store a shard of the data of the account
table.
Take a example of 3-node cluster. Each node will have 2 buckets work as a bucket for holding a different range or part of data of table account
, each bucket is a raft-node.
Node 1 | Node 2 | Node 3 |
---|---|---|
bucket_1 | bucket_2 | bucket_3' |
bucket_3 | bucket_1' | bucket_2' |
Example:
- Each bucket_* is a Raft-node.
- bucket_1 and bucket_1' belong to the same Raft-group.
When new data incoming, we assign a data to bucket by:
bucket_id := max_id % (num_of_buckets)
We also store the information of the bucket_id and the corresponding peers in Redis:
bucket_id | peers |
---|---|
1 | 127.0.0.1:50041,127.0.0.1:50042 |
2 | 127.0.0.1:50051,127.0.0.1:50052 |
3 | 127.0.0.1:50061,127.0.0.1:50062 |
For example in the picture: |
@startuml
node "Transaction Manager" as node1 #white
node "Resource Manager 1" as node2 {
frame bucket_1 as A1 #red
frame bucket_3 as C1 #yellow
}
node "Resource Manager 2" as node3 {
frame bucket_1 as A2 #red
frame bucket_2 as B2 #green
}
node "Resource Manager 3" as node4 {
frame bucket_2 as B3 #green
frame bucket_3 as C3 #yellow
}
node1 -- node2
node1 -- node3
node1 - node4
A1 .. A2: replicated
B2 .. B3: replicated
C1 .. C3: replicated
@endumls
Noted that for easier we take the example of 3-node, with number of raft nodes per bucket is 2, but for general case we need 5-node cluster because raft nodes needs to be odd number.
We also have a data structure to keep track which data belong to which buckets, this is stored in Redis:
account_id | buckets |
---|---|
1 | 127.0.0.1:50051 |
2 | 127.0.0.1:50041 |
... | ... |
So that when a transaction begins, the transaction client will know which peer bucket to talk with based on the account_id
.
In Transaction Coordinator there are 2 types of object:
Global Transaction
includes multiple Local Transaction
.
@startuml
interface Transaction {
+Prepare()
+Begin()
+Commit()
+Rollback()
}
class GlobalTransaction {
}
class LocalTransaction {
}
Transaction <|-- GlobalTransaction
Transaction <|-- LocalTransaction
@enduml
Each Local Transaction
responsible for a single Transaction on a Bucket
or a Raft-group
.
For example:
- We want to transfer
$100
fromaccountA
(stored in bucket_1) toaccountB
(stored in bucket_2). We will create 2 Local Transaction object, one to send RPC request toResourceManager
ofbucket_1
to update accountA - $100 and one to send RPC request toResourceManager2
ofbucket_2
to update accountB + $100. - The status of the Global Transaction is based on status of its Local Transactions.
@startuml
object GlobalTransaction
object LocalTransaction
GlobalTransaction *-- "1...n" LocalTransaction
@enduml
@startuml component
skinparam monochrome true
actor client
node txn_manager
node resource_manager
client -> txn_manager
txn_manager <-> resource_manager
@enduml
Transaction Manager
includes 2 services with Redis as the storage:
@startuml
node "Transaction Manager" {
[Transaction Coordinator]
[GlobalLock Service]
}
database redis{
frame txn
frame lock
}
[Transaction Coordinator] -> txn
[GlobalLock Service] -> lock
@enduml
Resource Manager
includes 2 services with MySQL as the storage for Account data:
@startuml component
node "Resource Manager"{
[Raft Service] <- [Account Service]: propose
}
database AccountDB {
frame account
frame payment
}
[Account Service] --> payment: write commits after Raft consensus
[Account Service] --> account
@enduml
To achieve Atomicity, we need a Transaction Manager which controls the sub-transactions and also the locking mechanism on the resources.
TC implements gRPC calls for:
CreateAccount(ctx context.Context, in *pb.AccountRequest) (*pb.AccountReply, error)
CreatePayment(ctx context.Context, in *pb.PaymentRequest) (*pb.PaymentReply, error)
This is the entry points of the application, providing APIs to create new account and create new payment between accounts.
TC is responsible for managing GlobalTransaction
, which includes multiple LocalTransaction
.
When TC receive a CreatePayment
request, it will create 2 LocalTransaction
which will send Phase1Request
to ResourceManager
to update the data of Sender
and Receiver
accounts:
@startuml
start
:Create GlobalTXN;
:Create LocalTXNs;
:Each LocalTXN create Phase1Request;
if (all LocalTXN success) then (yes)
:create Phase2CommitRequest;
else (no)
:create Phase2RollbackRequest;
endif
stop
@enduml
GLS implements gRPC calls for:
AcquireLock(ctx context.Context, in *pb.LockRequest) (*pb.LockReply, error)
ReleaseLock(ctx context.Context, in *pb.LockRequest) (*pb.LockReply, error)
@startuml
interface Lock {
+Lock()
+Unlock()
+Status()
+HasHandle()
}
class GlobalLock {
lockId string
properties *LockProperties
lockHandle string
mutex sync.Mutex
}
Lock <|-- GlobalLock: implements
@enduml
To control the lock, follow the code:
var lockProperties = &LockProperties{
LockAttempts: 10,
LockAttemptDelay: time.Duration(1000 * time.Millisecond),
LockTimeout: time.Duration(5000 * time.Millisecond),
}
lockCtl := NewLockController(in.LockId, lockProperties)
var message string
if stt, err := lock.Lock(); !stt || err != nil {
message = "FAILED"
} else {
message = "OK"
}
First we need to create the lockProperties which holds the configuration of the lock.
Second we need to create a lock controller
for the given lockId
.
When calling lockCtl.Lock()
, the lockController will create a unique key in Redis
. If other lockController (for the same lockId) also call the Lock() after that, there will be error, they will retry obtaining the lock with the attempts defined in lockProperties object until one of the following cases happens:
- The current lock controller releases the lock (delete the key in Redis)
- The current lock controller timeouts (defined in LockTimeout parameter)
Resource Manager is a gRPC server with following methods:
ProcessPhase1(ctx context.Context, in *pb.TXRequest) (*pb.TXReply, error)
ProcessPhase2Commit(ctx context.Context, in *pb.TXRequest) (*pb.TXReply, error)
ProcessPhase2Rollback(ctx context.Context, in *pb.TXRequest) (*pb.TXReply, error)
When received request, resource manager will decode the object in the message to the Instruction object with the following fields:
type Instruction struct {
Type string
Data interface{}
}
Depends on the type of instuction, the data is different, for example, if the type of instruction is INS_TYPE_CREATE_ACCOUNT
, then the Data is the AccountInfo
object, if INS_TYPE_SEND_PAYMENT
or INS_TYPE_RECEIVE_PAYMENT
then the Data field is the PaymentInfo
object.
Account Service is responsible for proposing changes to Raft service, when Raft returns the message inform that consensus is done, Account Service will communitcate with GlobalLock Service to acquire the Lock for before committing locally.
Account Service also responsible for Rolling back changes when receives request from TC.
After making changes to DB, Account Service will return status to TC to inform transaction is failed or succeeded.
AccountService interface is defined as below:
type AccountService interface {
Start()
CreateAccount(string, float64) string
ProcessSendPayment(string, string, float64) string
ProcessReceivePayment(string, string, float64) string
ProcessRollbackPayment(accountNum string, amount float64) string
Propose(interface{}) //propose to RaftNode
ReadCommits(<-chan *string, <-chan error) //read commits from RaftNode
}
When starting ResourceManager, we also start Raft service:
clusterInfo := consensus.RaftInit(clusterConfig)
accountDB := db.CreateAccountDB("localhost", "root", "123456", *clusterConfig.DBName)
var accountService rm.AccountService = rm.NewAccountService(accountDB, clusterInfo.CommitC, clusterInfo.ProposeC, <-clusterInfo.SnapshotterReady, clusterInfo.ErrorC)
startServer(accountService, clusterConfig.RPCPort)
When AccountService
wants to make changes in Database, it first send the changes to proposeC
, a go channel, then RaftNode_Leader
takes the proposed changes and make consensus with other RaftNode_Follower*
, when proposed changes are permitted then the change will be sent to commitC
go channel back to AccountService
.
Then AccountService
will call the function ApplyInstructionToStateMachine
to begin commit data changes to database.
@startuml
rectangle AccountService {
}
rectangle RaftService {
node RaftNode_Leader
node RaftNode_Follower1
node RaftNode_Follower2
}
queue proposeC
queue commitC
AccountService --> proposeC: Propose()
AccountService <- commitC: Commit()
proposeC --> RaftNode_Leader
RaftNode_Leader --> commitC
RaftNode_Leader -- RaftNode_Follower1: raftHttp
RaftNode_Leader -- RaftNode_Follower2: raftHttp
RaftNode_Follower1 - RaftNode_Follower2: raftHttp
@enduml
Before committing changes to DB, first AccountService
asks GlobalLock Service
to AcquireLock()
, if success, it will execute the update-command to update the data then return RPC_MESSAGE_OK
to Transaction Coordinator
. If failed, it returns RPC_MESSAGE_FAIL
.
When Transaction Coordinator
receives the RPC_MESSAGE_OK
from all Resoure Managers
, it will execute the Phase2Commit
, update the transaction status to COMMITTED
and then call ReleaseLock()
.
If it receives RPC_MESSAGE_FAIL
, it will execute the Phase2Rollback
, update the transaction status to ABORTED
and then call ReleaseLock()
.
DAO
objects are located in ./dao
directory. Including AccountDAO
, PaymentDAO
and TxnDAO
type AccountDAO interface {
GetAccount(accountNumber string) *model.AccountInfo
CreateAccount(accInfo model.AccountInfo) bool
UpdateAccountBalance(accountNumber string, amount float64) bool
}
type PaymentDAO interface {
CreatePayment(pmInfo PaymentInfo) string
}
@startuml
node AccountService
rectangle AccountDAO
rectangle PaymentDAO
database AccountDB {
frame account
frame payment
}
AccountService *-- AccountDAO
AccountService *-- PaymentDAO
AccountDAO ---> account
PaymentDAO ---> payment
@enduml
TC uses TxnCoordinatorDAO to operate with Redis:
type TxnCoordinatorDAO interface {
GetMaxId() string
IncrMaxId() int64
GetPeersList() map[string]string
CreateTransactionEntry(string, int64, string, string)
CreateSubTransactionEntry(string, string, model.Instruction, string)
CheckLock(lockId string) (int64, error)
CreateLock(lockId string, state string, timeout time.Duration) (bool, error)
RefreshLock(lockId string, timeout time.Duration) (bool, error)
DeleteLock(lockId string) (int64, error)
GetPeerBucket(id string) string
InsertPeerBucket(id string, peer string)
}
@startuml
node "Transaction Coordinator" as TC
rectangle TxnCoordinatorDAO
database "Redis" {
frame lock
frame txn
}
TC *-- TxnCoordinatorDAO
TxnCoordinatorDAO ---> lock
TxnCoordinatorDAO ---> txn
@enduml
Starting Transaction Coordinator & Global Lock Service using goreman, define the Procfile1 with following content:
tc-1: go run main.go --mode tc
Then execute the file with command:
goreman -f Procfile1 start
Start Resource Manager along with Raft Service using goreman, define Procfile2 with the content, this will create 3 processes, each for one single-node raft-group:
# Start 3 Single-node cluster
raftgroup1-1: go run main.go --mode rm --clusterid 1 --id 1 --cluster http://127.0.0.1:12379 --port :50041 --db mas1
raftgroup2-1: go run main.go --mode rm --clusterid 2 --id 1 --cluster http://127.0.0.1:22379 --port :50051 --db mas2
raftgroup3-1: go run main.go --mode rm --clusterid 3 --id 1 --cluster http://127.0.0.1:32379 --port :50061 --db mas3
Then execute the file with command:
goreman -f Procfile2 start
Hiện tại, khi add thêm Resource Manager vào, cần phải thông báo cho TC để TC update lại peer list.
Khi add thêm RM vào cluster, sẽ có 2 trường hợp xảy ra:
-
RM đó chỉ join existing Raft group
- Ta cần thêm tham số
--clusterid
và--id
và--cluster
và--join
để xác định raft group cần join vào. Tham số--db mas3
có nghĩa dữ liệu replica này sẽ được lưu ở db của node 3 (mas3).go run main.go --mode rm --clusterid 1 --id 2 --cluster http://127.0.0.1:12379,http://127.0.0.1:12379 --join --port :50042 --db mas3
- Mọi chuyện trông đơn giản nếu như không có trường hợp thứ 2.
- Ta cần thêm tham số
-
RM introduces 1 raft group hoàn toàn mới, trong trường hợp này có 2 options:
-
Dữ liêu mới sẽ được insert vào đồng đều trên cả RM cũ lẫn mới.
-
Dữ liệu cần được rebalance lại cho phù hợp với số lượng bucket cũng như số lượng replica. Xem Appendix B: TODO: Redistribute (rebalance) Data khi add node
go run main.go --mode rm --clusterid 1 --id 2 --cluster http://127.0.0.1:12379,http://127.0.0.1:12379 --join --port :50042 --db mas3
-
When TC receive a create request from user with AccountInvfo:
type AccountInfo struct {
Id string
Number string
Balance float64
}
When TC receive a payment request from user with PaymentInfo:
type PaymentInfo struct {
Id string
From string
To string
Amount float64
}
TC will create a GlobalTransaction
object with 2 LocalTransaction
objects, one to update balance of Sender
and one to update Receiver
. Workflow as in the following section.
@startumls
control TC
activate TC
activate RM
TC -> Global_TXN: Create GlobalTransaction
activate Global_TXN
entity Send_TXN
entity Recv_TXN
Global_TXN -> Send_TXN: create sub transaction\nfor `Sender` account
activate Send_TXN #red
Global_TXN -> Recv_TXN: create sub transaction\nfor `Receiver` account
activate Recv_TXN #blue
control RM
Send_TXN -> RM: Update acc_balance = acc_balance - 100
Recv_TXN -> RM: Update acc_balance = acc_balance + 100
RM -> Send_TXN: OK
RM -> Recv_TXN: OK
Send_TXN -> TC: OK
Recv_TXN -> TC: OK
deactivate Send_TXN
deactivate Recv_TXN
TC -> RM: Phase2: Commit
@enduml
@startumls
control TC
activate TC
activate RM
TC -> Global_TXN: Create GlobalTransaction\nPaymentInfo{From:A,To:B,Amount:100}
activate Global_TXN
entity Send_TXNv
entity Recv_TXN
Global_TXN -> Send_TXN: create sub transaction\nfor `A` account
activate Send_TXN #red
Global_TXN -> Recv_TXN: create sub transaction\nfor `B` account
activate Recv_TXN #blue
control RM
Send_TXN -> RM: Update A = A - 100
Recv_TXN -> RM: Update B = B + 100
RM -> Send_TXN: OK
RM -> Recv_TXN: FAILED
Send_TXN -> TC: OK
Recv_TXN -> TC: FAILED
deactivate Send_TXN
deactivate Recv_TXN
TC -> RM: Phase2: RollBack
@enduml
Trường hợp 2 local transaction cùng update dữ liệu A = A - 100:
@startumls
entity subTXN_1
activate subTXN_1
entity subTXN_2
activate subTXN_2
subTXN_1 -> subTXN_1: Get global lock
subTXN_1 -> subTXN_1: Update A = A - 100\nLocal commit
subTXN_2 -> subTXN_2: wait for global lock\n...\n...
subTXN_1 -> subTXN_1: Global commit\nRelease global lock
subTXN_2 -> subTXN_2: Got global lock
subTXN_2 -> subTXN_2: Update A = A - 100\nLocal commit
subTXN_2 -> subTXN_2: Global commit\nRelease global lock
@enduml
@startumls
entity subTXN_1
activate subTXN_1
entity subTXN_2
activate subTXN_2
subTXN_1 -> subTXN_1: Get global lock
subTXN_1 -> subTXN_1: Update A = A - 100\nLocal commit
subTXN_2 -> subTXN_2: wait for global lock\n...\n...
subTXN_1 -> subTXN_1: Global rollback\nRelease global lock
subTXN_2 -> subTXN_2: Got global lock
subTXN_2 -> subTXN_2: Update A = A - 100\nLocal commit
subTXN_2 -> subTXN_2: Global commit\nRelease global lock
@enduml
Hiện tại Read không lock record. Nên khi đọc dữ liệu có thể đọc dirty-read dữ liệu chưa được COMMIT:
@startumls
entity subTXN_1
activate subTXN_1
entity subTXN_2
activate subTXN_2
subTXN_1 -> subTXN_1: Get global lock, A = 1000
subTXN_1 -> subTXN_1: Update A = A - 100\nLocal commit
subTXN_1 -> subTXN_1: A = 900
subTXN_2 -> subTXN_2: Select A, A = 900
subTXN_1 -> subTXN_1: Global rollback\nRelease global lock
subTXN_1 -> subTXN_1: Update A = 1000
@enduml
Go requires gRPC to have TLS (Transport Layer Security or SSL) to be enabled first before using JWT credentials.
To enable TLS we first need to create a self-signed SSL certificate.
-
Create private key
$ openssl genrsa -out cert/server.key 2048
-
Create and sign certificate for
localhost
:$ openssl req -new -x509 -sha256 -key cert/server.key -out cert/server.crt -days 3650 -subj '/CN=localhost'
Get the library by running this command:
go get github.com/dgrijalva/jwt-go
To sign and verify token:
echo {\"foo\":\"bar\"} | jwt -key ./secret_key -alg HS256 -sign - | jwt -key ./secret_key -alg HS256 -verify -
For simple, we use HS256
with symetric secret_key
, for both sign and verify, but we can use another asymetric public/private key pair using RSA
or ECDSA
.
First, we will sign the information using the above command, output is stored in the token.jwt
file.
On client side, when client sends a request to server, it will get the token file above:
token, err := credentials.NewTokenFromFile(client.apiToken)
and the server certificate that was created in 5.1:
creds, err := grpc_creds.NewClientTLSFromFile(client.apiCert, "")
and includes in the credentials in the header:
conn, err := grpc.Dial(client.serverAddress,
grpc.WithTransportCredentials(creds),
grpc.WithPerRPCCredentials(token))
On server side, first we need to includ the server certificate and its private key.
creds, err := grpc_creds.NewServerTLSFromFile(credentials.SSL_SERVER_CERT, credentials.SSL_SERVER_PRIVATE_KEY)
if err != nil {
log.Fatalf("Cannot get credentials: %v", err)
}
s := grpc.NewServer(grpc.UnaryInterceptor(credentials.JWTServerInterceptor), grpc.Creds(creds))
We also define a interceptor JWTServerInterceptor
to validate the token.
Right now, for simple we just validate the token at the TC Server where it handles requests from client.
Install the mas_client
cmd:
$ go install mas_client/mas_client.go
Usage of mas_client
:
$ mas_client --help
Usage of mas_client:
-amount float
Amount of money for Payment
-balance float
Account balance
-cert string
Path to Server certificate (default "./credentials/cert/server.crt")
-createAcc
createAcc/createPmt
-createPmt
createAcc/createPmt
-from string
From Account number of Payment
-number string
Account number
-tc string
TC_SERVICE_HOST (default "localhost:9008")
-to string
To Account number of Payment
-token string
Path to JWT token file (default "./credentials/token.jwt")
We can make request to TC using the given token and TSL certificate:
$ mas_client --createAcc --number "abc"\
> --token "/path/to/token.jwt"\
> --cert "/path/to/server.crt"
Each account when registered will be generated with a JWT token which contain the account info, when authenticate, if all info is correct then the transaction will begin otherwise return an authentication error.
Use Docker compose to build image and create containers:
$ docker-compose up --build
After that we will have 3 resource manager and 1 transaction coordinator containers.
- Attach shell to
mas-tc
container and execute below command to start TC services:
goreman -f Procfile2 start
- Attach shell to
mas-rm-*
containers and execute below command to start RM services:
goreman start
If we want to create own containers and services, we can build the image by executing the $ ./build.sh
to build the image then run your cluster by creating containers.
- Hiện tại chỉ hỗ trợ 1
Transaction Manager
, trong tương lai có thể add thêm TM để tạo thành cluster và dùng Raft để sync data trên các TM đó. - Hiện tại chỉ lock resource khi write, khi read dữ liệu có thể bị trường hợp đọc dữ liệu chưa được commit. (
READ UNCOMMITTED
). Có thể nâng cấp lên thànhREAD COMMITTED
. Khi đó cần phải hiện thực shared-lock cho Read statement. - Hiện tại, 1 transaction sẽ get global lock trước rồi mới bắt đầu local lock rồi local commit, nhưng để tăng performance, có thể làm giống Fescar AT đó là local lock -> global lock -> local commit -> global commit. Như vậy nếu trường hợp có 2 transaction cùng update 1 dữ liệu thì sẽ nhanh hơn, nhưng cũng sẽ có trường hợp bị chờ deadlock.
- Account Service cần được thực hiện async, tức là trả về kết quả phase 1 ngay khi propose xong, không đợi đến bước apply change to state machine. Sau đó TC gửi request Commit, thì Account Service mới bắt đầu xin global lock và commit. Tương tự với trường hợp Rollback.
- Hiện tại chưa hỗ trợ việc undo, redo log khi TC mất điện. Có nghĩa là chỉ hỗ trợ undo nếu có 1 giao dịch fail, TC vẫn hoạt động bình thường, nếu như TC chưa kịp commit giao dịch mà bị mất điện thì khi start lên lại, TC phải thực hiện crash recovery, scan log xem có giao dịch nào chưa commit thì sẽ
undo
, còn giao dịch nào đã commit nhưng chưa write xuống DB sẽ đuợcredo
- Multi-raft config change rebalance dvata -> hiện tại chưa hỗ trợ việc redistribute lại data khi add thêm Raft-group vào Multi-raft.
- Thay vì distribute data theo maxId, có thể dùng Nginx để làm reverse proxy và load balancer.
- Benchmark using Locust
- Performance: Số lượng node của raft group ảnh hưởng đến thời gian chạy, càng nhiều node thời gian consensus càng lâu.
- Raft đang gặp performance issue khi test trên 10000 users, khi append log thì hàm này chạy khá chậm. Do có mutex lock lại để đảm bảo thứ tự nên gây ra vấn đề chậm.
- Khi bỏ dòng này ra thì tốc độ cải thiện khoảng x3 ~ 4 lần, tức khoảng 1800 requests/s
- https://en.wikipedia.org/wiki/Isolation_(database_systems)
- https://en.wikipedia.org/wiki/Two-phase_commit_protocol
- https://en.wikipedia.org/wiki/Distributed_algorithm
- https://github.com/fescar-group/fescar-awesome/blob/master/wiki/en-us/Fescar-AT.md
- https://godoc.org/github.com/coreos/etcd/raft
- http://thesecretlivesofdata.com/raft/
- https://ramcloud.stanford.edu/~ongaro/thesis.pdf
- http://blog.thislongrun.com/2015/03/the-confusing-cap-and-acid-wording.html
Theo minh hoạ, Raft cần phải có số node là 2F + 1 (F là số node tối đa có thể fail), để đảm bảo đồng thuận luôn thành công và tránh trường hợp split-brain.
- Install Homebrew sau đó chạy command:
$ brew tap homebrew/versions
$ brew install gcc48 --use-llvm
$ brew install rocksdb
Xem trong stdout có xuất ra đường dẫn đến rocksdb, trong trường hợp này là: "/usr/local/Cellar/rocksdb/5.18.3"
- Tiếp tục chạy command như sau:
$ CGO_CFLAGS="-I/usr/local/Cellar/rocksdb/6.1.2/include"
$ CGO_LDFLAGS="-L/usr/local/Cellar/rocksdb/6.1.2 -lrocksdb -lstdc++ -lm -lz -lbz2 -lsnappy -llz4 -lzstd"
$ go get github.com/tecbot/gorocksdb
To build a docker image including the compiled code and libraries, we need to create a Dockerfile which includes:
- Golang base image
- Install rocksdb and dependencies
- Copy code from current project to image and build