LiteFS入門
LiteFSとは
LiteFSはLitestreamの可用性に関する課題を解決するために同作者によって新しく作られたソフトウェア。
Live Read Replication の実験的な機能ではノード間のHTTP通信でリードレプレカを同期してプライマリで書き込んだデータをrestoreを通さずにレプリカから参照することができるようになる予定だった。
この時書き込みクエリをプライマリに振り分けるのはアプリケーションの責務になる。例:
ただそもそも複数台でLitestreamを利用する用途の為にノード間のLive Replicationを実装したとしても、デプロイやフェイルオーバーでノードの入れ替わりが発生する時に、無停止でプライマリを別のノードに切り替えることも考慮したりと、当初のLitestreamのスコープになかった新しい問題も出てくる。
なので「サーバー内のsqlite3ファイルをS3に同期君」を作ったのに複数のプロセスがサーバー間で動作する複雑なものになってきたから、目的をLiteFSとLitestreamに分けるという決定をBenはしたのだと思われる。
As some of you may know, I've been working on a next generation tool for running SQLite on a distributed platform and, after some discussion, I've decided to keep Litestream as a pure disaster recovery tool and not include live replication. It's going to be difficult to maintain it long term—especially when when the new tooling will work better for this use case.
https://github.com/benbjohnson/litestream/issues/8#issuecomment-1173214316
なので単一のLitestreamのみで使うことや、LiteFSで運用しているSQLiteデータベースにLitestreamを加えるパターンも考慮してLitestreamはこのまま維持するのだと思う。(追記:Litestreamと互換性はないので併用する計画はないみたいだ。LiteFS側にS3同期機能が付くらしい。https://github.com/superfly/litefs/issues/18 )
KUOKAさんがVitessを例に出していて確かに……と思った(https://twitter.com/mumoshu/status/1552064218731790336)。SQLite分散環境ということでrqliteやdqliteの名前を上げている人も居た。
環境
git clone https://github.com/superfly/litefs
チェックアウトするとDockerfileがあるのでコンテナ環境でlitefsが起動できるのかと思いきや、これはたぶんバイナリをGitHub Releasesにアップロードする時のもので、システム内にfuse3が有効になっていないと使えない。
なので現時点ではLinux環境が必要になると思う。ビルドにgoの1.18系が要求されて、自分はUbuntu 22.04の公式パッケージからインストールして使った。
lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04 LTS
Release: 22.04
Codename: jammy
依存ライブラリを入れる
apt install golang-go sqlite fuse3 libfuse-dev consul
go version
go version go1.18.1 linux/amd64
ビルド
go build -ldflags "-s -w -X 'main.Version=latest' -extldflags '-static'" -tags osusergo,netgo,sqlite_omit_load_extension -o ./litefs ./cmd/litefs
consul起動
RaftのLeaderノード選出にconsulを使っているので別途起動しておく。
# consul agent -dev
litefs起動
./cmd/litefs/etc
以下にyamlファイルやホスト名解決などの開発用の設定などあるので利用させてもらう。
mount-dir: "/path/to/mnt"
FUSEのマウント先を mkdir -p "/path/to/mnt"
で作っておく。
consul:
# Required. The base URL of the Consul server.
url: "http://localhost:8500"
Consulのホスト名もここに指定されてる。
# ./litefs -config ./cmd/litefs/etc/litefs.yml
initializing consul: key=http://localhost:8500 url=litefs/primary advertise-url=http://localhost:20202
LiteFS mounted to: /path/to/mnt
http server listening on: http://localhost:20202
primary lease acquired, advertising as http://localhost:20202
起動できた。
レプリケーション実行
複数のマシンを用意するのがめんどうだったのでlitefsを複数起動して、かつsqliteファイルの配置場所を別々にしてみる。
# mkdir /path/to/mnt{2,3}
# cp cmd/litefs/etc/litefs{,2}.yml
# cp cmd/litefs/etc/litefs{,3}.yml
# ls cmd/litefs/etc/
hostname hosts litefs2.yml litefs3.yml litefs.yml resolv.conf
mount-dir: "/path/to/mnt2"
http:
addr: ":20203"
consul:
url: "http://localhost:8500"
advertise-url: "http://localhost:20203"
---
mount-dir: "/path/to/mnt3"
http:
addr: ":20204"
consul:
url: "http://localhost:8500"
advertise-url: "http://localhost:20204"
./litefs -config cmd/litefs/etc/litefs2.yml
./litefs -config cmd/litefs/etc/litefs3.yml
アプリケーションから読み書き
simple.go を参考にこんな感じにREAD or WRITE * mnt,mnt2,mnt3 の振り分けを作ってみる。
package main
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
"log"
)
func main() {
Write("/path/to/mnt/simple.db")
Read("/path/to/mnt/simple.db")
Read("/path/to/mnt2/simple.db")
Read("/path/to/mnt3/simple.db")
}
func Read(path string) {
db, err := sql.Open("sqlite3", path)
if err != nil {
log.Fatal(err)
}
defer db.Close()
rows, err := db.Query("select id, name from foo")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int
var name string
err = rows.Scan(&id, &name)
if err != nil {
log.Fatal(err)
}
fmt.Println(path, id, name)
}
err = rows.Err()
if err != nil {
log.Fatal(err)
}
}
func Write(path string) {
db, err := sql.Open("sqlite3", path)
if err != nil {
log.Fatal(err)
}
defer db.Close()
sqlStmt := `
create table if not exists foo (id integer not null primary key, name text);
delete from foo;
`
_, err = db.Exec(sqlStmt)
if err != nil {
log.Printf("%q: %s\n", err, sqlStmt)
return
}
_, err = db.Exec("insert into foo(id, name) values(1, strftime('%Y-%m-%d %H-%M-%f','now')), (2, strftime('%Y-%m-%d %H-%M-%f','now')), (3, strftime('%Y-%m-%d %H-%M-%f','now'))")
if err != nil {
log.Fatal(err)
}
}
# Write("/path/to/mnt/simple.db")+ Read("/path/to/mnt/simple.db")
# go run simple.go
/path/to/mnt/simple.db 1 2022-07-28 10-13-22.971
/path/to/mnt/simple.db 2 2022-07-28 10-13-22.971
/path/to/mnt/simple.db 3 2022-07-28 10-13-22.971
primaryにINSERTしてSELECTはできたので次はmnt2のREADをしてみる
# Write("/path/to/mnt/simple.db")+ Read("/path/to/mnt/simple.db") + Read("/path/to/mnt2/simple.db")
# go run simple.go
/path/to/mnt/simple.db 1 2022-07-28 10-18-14.342
/path/to/mnt/simple.db 2 2022-07-28 10-18-14.342
/path/to/mnt/simple.db 3 2022-07-28 10-18-14.342
/path/to/mnt2/simple.db 1 2022-07-28 10-18-14.342
/path/to/mnt2/simple.db 2 2022-07-28 10-18-14.342
/path/to/mnt2/simple.db 3 2022-07-28 10-18-14.342
Writeで書き込んだ内容が即時同期されていることが確認できた。
全部いってみる。
# Read All
# go run simple.go
/path/to/mnt/simple.db 1 2022-07-28 10-18-14.342
/path/to/mnt/simple.db 2 2022-07-28 10-18-14.342
/path/to/mnt/simple.db 3 2022-07-28 10-18-14.342
/path/to/mnt2/simple.db 1 2022-07-28 10-18-14.342
/path/to/mnt2/simple.db 2 2022-07-28 10-18-14.342
/path/to/mnt2/simple.db 3 2022-07-28 10-18-14.342
/path/to/mnt3/simple.db 1 2022-07-28 10-18-14.342
/path/to/mnt3/simple.db 2 2022-07-28 10-18-14.342
/path/to/mnt3/simple.db 3 2022-07-28 10-18-14.342
sha1sumが同一なことを確認
# sha1sum /path/to/mnt*/simple.db
8a7a04e69cf75bfc531020452a584a4f83fca9b1 /path/to/mnt2/simple.db
8a7a04e69cf75bfc531020452a584a4f83fca9b1 /path/to/mnt3/simple.db
8a7a04e69cf75bfc531020452a584a4f83fca9b1 /path/to/mnt/simple.db
primaryのmnt(:20202)を殺してみる
# mnt
^Csignal received, litefs shutting down
stream connected
stream disconnected
exiting primary, destroying lease
stream disconnected
# mnt2
cannot acquire lease or find primary, retrying: no primary
existing primary found (http://localhost:20204), connecting as replica
# mnt3
cannot acquire lease or find primary, retrying: no primary
primary lease acquired, advertising as http://localhost:20204
stream connected
mnt3(:20204)
にprimaryが変った。
そして Read("/path/to/mnt/simple.db")
はtableが見付からなくなった。
# Read("/path/to/mnt/simple.db")
# go run simple.go
2022/07/28 18:26:36 no such table: foo
exit status 1
# Read("/path/to/mnt2/simple.db") + Read("/path/to/mnt3/simple.db")
# go run simple.go
/path/to/mnt2/simple.db 1 2022-07-28 10-18-14.342
/path/to/mnt2/simple.db 2 2022-07-28 10-18-14.342
/path/to/mnt2/simple.db 3 2022-07-28 10-18-14.342
/path/to/mnt3/simple.db 1 2022-07-28 10-18-14.342
/path/to/mnt3/simple.db 2 2022-07-28 10-18-14.342
/path/to/mnt3/simple.db 3 2022-07-28 10-18-14.342
mnt2と3は生きてる。
mnt3がprimaryらしいのでmn3に書き込んでみる。
# Write("/path/to/mnt3/simple.db") + Read("/path/to/mnt3/simple.db")
# go run simple.go
/path/to/mnt3/simple.db 1 2022-07-28 10-29-21.040
/path/to/mnt3/simple.db 2 2022-07-28 10-29-21.040
/path/to/mnt3/simple.db 3 2022-07-28 10-29-21.040
書き込めた。
mnt1番氏を復活させてみる。
# Read All
# go run simple.go
/path/to/mnt/simple.db 1 2022-07-28 10-29-21.040
/path/to/mnt/simple.db 2 2022-07-28 10-29-21.040
/path/to/mnt/simple.db 3 2022-07-28 10-29-21.040
/path/to/mnt2/simple.db 1 2022-07-28 10-29-21.040
/path/to/mnt2/simple.db 2 2022-07-28 10-29-21.040
/path/to/mnt2/simple.db 3 2022-07-28 10-29-21.040
/path/to/mnt3/simple.db 1 2022-07-28 10-29-21.040
/path/to/mnt3/simple.db 2 2022-07-28 10-29-21.040
/path/to/mnt3/simple.db 3 2022-07-28 10-29-21.040
不在時に書き込まれた 10-29-21.040 も同期されている。
今度はprimaryではなくなったmnt1に書き込んでみる。
# Write("/path/to/mnt/simple.db") + Read All
# go run simple.go
2022/07/28 18:32:23 "unable to open database file: no such file or directory":
create table if not exists foo (id integer not null primary key, name text);
delete from foo;
/path/to/mnt/simple.db 1 2022-07-28 10-29-21.040
/path/to/mnt/simple.db 2 2022-07-28 10-29-21.040
/path/to/mnt/simple.db 3 2022-07-28 10-29-21.040
/path/to/mnt2/simple.db 1 2022-07-28 10-29-21.040
/path/to/mnt2/simple.db 2 2022-07-28 10-29-21.040
/path/to/mnt2/simple.db 3 2022-07-28 10-29-21.040
/path/to/mnt3/simple.db 1 2022-07-28 10-29-21.040
/path/to/mnt3/simple.db 2 2022-07-28 10-29-21.040
/path/to/mnt3/simple.db 3 2022-07-28 10-29-21.040
クエリ実行時エラーになった。
まとめ
- 今回のような簡単なケースではすべて期待どうりに動いた。素晴しい
- 同一プロセスから別々のsqliteにWriteしてReadはしてみたがトランザクションや並列処理などは試してない
- ただ実際の利用では1つのsqliteを読み書きすることになるんだと思う
- 動的に変わるprimaryを特定して書き込みクエリを振り分けるのはどう実装するのか不明
Discussion