Turso ちょっと勘違いしてたこと

Turso を本格的に Deno Deploy で使っていくために確認をしていたら、いくつか思い違い(ドキュメント全部読め)があったので、まとめておきたい。

速度回りの部分は、少し有用な情報だと信じてる。

参考

問題 - 速度がずいぶん遅い -

以下のソースコードを用意し、Deno Deploy から Turso に問い合わせをさせて、速度を確認した。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import "https://deno.land/std@0.185.0/dotenv/load.ts";
import { serve } from "https://deno.land/std@0.185.0/http/server.ts";
import { type Config, createClient } from "https://esm.sh/@libsql/client";

const config = {
url: Deno.env.get("TURSO_DB_URL"),
authToken: Deno.env.get("TURSO_AUTH_TOKEN"),
} as Config;

const db = createClient(config);

await serve(
async (request) => {
console.time('select');
const selectResult = await db.execute("SELECT * FROM items");
console.timeEnd('select');

console.time('insert');
await db.execute({
sql: "INSERT INTO items(name) VALUES(?)",
args: [`USER${selectResult.rows.length}`],
});
console.timeEnd('insert');

return new Response(JSON.stringify({ items: selectResult.rows }), {
headers: { "content-type": "application/json" },
});
},
{ port: 8000 }
);

これを Deno Deploy にて動かす。
select とinsert の速度はそれぞれ Deno Deploy の Logs で確認できる。

手元のブラウザで、アクセスすると次のような結果になった。

1
2
select: 20ms
insert: 11ms

悪くはなさそう。

続けて、アメリカ バージニア州のリージョンでEC2にサーバーを立てて、curlで呼び出してみる。

1
2
select: 658ms
insert: 162ms

ずいぶん遅いと感じたので、調査開始。

フォーラムで質問

質問を投げたらすぐ応答があった。

要約すると「レプリカの作成が不足しているのでは」ということだった。
ここは思い違いをしていて、Turso が各エッジに自動的に展開をしてくれていると認識していたが、それは誤りでレプリカの作成操作が必要だった。

というわけでレプリカを作成する。

1
2
# バージニア州(iad)にレプリカを作成
$ turso db replicate [プライマリのデータベース名] iad

作成できたので、再度バージニアに立てたサーバーから curl でリクエストを送ってみる。
結果は次の通り。

1
2
select: 7ms
insert: 184ms

select が高速化、日本でリクエストしているのと大差ない速度になった。

思い違い

Turso はプライマリが、負荷基準で切り替わるものと思い込んでいたが、実際は異なっていた。
ざっと並べると次のようになっている。

  1. プライマリインスタンスは、CLIで作成した時に一番近いところを設定される。
  2. CLIで作成するとき、任意の場所にプライマリインスタンスを作成できる。
  3. プライマリインスタンスは移動できない。
  4. レプリカは手動で作成する必要がある。
  5. 読み込みは、レプリカに接続することで待ち時間を最小化する。
  6. 書き込みは、プライマリインスタンスにホップ(ドキュメントではホップと書いてある)する。
  7. プライマリインスタンスに書き込まれた内容は、レプリカに反映される。

日本からだけアクセスを試しいたら、ずっと思い違いしていた可能性が高い。

質問してわかったこと

Tursoにアクセスするとき、URLに libsql: を使うと WebSocket 接続が使われ、https: を使うとステートレスなHTTP通信になる。

このため、insert => select の順で操作すると、最初の insert の接続を基準として通信がプライマリに飛んでしまうため、遅くなる可能性があるとのこと。
httpsで接続するとステートレスなので、そういった事象は無いとのこと。

この点を考慮して次の2つが、ベストのパフォーマンスが出そうに感じた。

  • 「書き込みだけ」「読み込みだけ」なら libsql: を優先使用する。
  • いくつかの書き込みの途上で、読み込みを要する場合、そこだけ別のクライアントのインスタンスで https: を使う。

insertを100件呼び出せば、ステートレスな、https: より、接続が流用される libsql: の方が早そうにも思える。

ものは試し、やってみる。
次のソースコードを用意して、動作させてみる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import "https://deno.land/std@0.185.0/dotenv/load.ts";
import { serve } from "https://deno.land/std@0.185.0/http/server.ts";
import { type Config, createClient } from "https://esm.sh/@libsql/client";

const config = {
url: Deno.env.get("TURSO_DB_URL"),
authToken: Deno.env.get("TURSO_AUTH_TOKEN"),
} as Config;

const db = createClient(config);

await serve(
async (request) => {
console.time("insert");
for await (const i of Array(100).keys()) {
await db.execute({
sql: "INSERT INTO items(name) VALUES(?)",
args: [`USER${i}`],
});
}
console.timeEnd("insert");

return new Response(JSON.stringify({ items: [1, 2, 3] }), {
headers: { "content-type": "application/json" },
});
},
{ port: 8000 }
);

環境変数で、https://~libsql://~ を切り替える。

1
2
3
4
5
6
# 6 回試した
# https:
1088ms ~ 1306ms

# libsql:
2487ms ~ 3282ms

結果 https: の方が早かったので、単純な話ではないらしい。
現状積極的に、libsql で接続を試みたいとは感じられない結果が出た。

ただし、libsqlの記述で transaction() が https では効かないという記述があるので、そのケースでは使えるはず。

該当の記述

最後にtransaction を使って試してみる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import "https://deno.land/std@0.185.0/dotenv/load.ts";
import { serve } from "https://deno.land/std@0.185.0/http/server.ts";
import { type Config, createClient } from "https://esm.sh/@libsql/client";

const config = {
url: Deno.env.get("TURSO_DB_URL"),
authToken: Deno.env.get("TURSO_AUTH_TOKEN"),
} as Config;

const db = createClient(config);

await serve(
async (request) => {
console.time("insert");
const tran = await db.transaction();

for await (const i of Array(100).keys()) {
await tran.execute({
sql: "INSERT INTO items(name) VALUES(?)",
args: [`USER${i}`],
});
}

await tran.commit();
console.timeEnd("insert");

return new Response(JSON.stringify({ items: [1, 2, 3] }), {
headers: { "content-type": "application/json" },
});
},
{ port: 8000 }
);

実行結果は次の通り。

1
2
# 6 回試した
297ms ~ 328ms

速度がかなり改善した。

select も同じように複数回投げて速度を確認してみる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import "https://deno.land/std@0.185.0/dotenv/load.ts";
import { serve } from "https://deno.land/std@0.185.0/http/server.ts";
import { type Config, createClient } from "https://esm.sh/@libsql/client";

const config = {
url: Deno.env.get("TURSO_DB_URL"),
authToken: Deno.env.get("TURSO_AUTH_TOKEN"),
} as Config;

const db = createClient(config);

await serve(
async (request) => {
console.time("select");

for await (const _ of Array(100).keys()) {
await db.execute("SELECT * FROM items");
}

console.timeEnd("select");

return new Response(JSON.stringify({ items: [1, 2, 3] }), {
headers: { "content-type": "application/json" },
});
},
{ port: 8000 }
);

結果は次の通り。こちらは、libsql: の方が早かった。

1
2
3
4
5
6
7
# 6 回試した
# データは6000件程
# https:
6532ms ~ 10197ms

# libsql:
4960ms ~ 6836ms

最適化するとき、接続を切り替えるのは1つの手法として検討に値すると考えられる。


Turso の動作検証をする中で勘違いをしていたことをまとめて、追加で調査をした。

Turso の discord は、質問投げた時のレスポンスがとても良かった。ありがたい。
質問投げて10分くらいで、最初のレスポンスがあったくらい。

では。