Vercel PostgresがどうやってEdge RuntimeでORMとコネクションプールを使えるようにしているのか
TLTR
- 実行時にNeon serverless driver(@neondatabase/serverlessモジュール)がnode-postgres(pgモジュール)内のSocketクラスをWebSocket実装に置き換える
- WebSocket接続を受けたneon.techサーバーがTCP接続に変換してPgBouncerに接続し応答する
Neon serverless driverの解説記事が以下にあります。
Edge RuntimeでNode.jsのSocket APIがサポートされていない問題
Node.jsのORMライブラリはPostgreSQLへの接続にnode-postgresからSocket APIを呼び出しますが、Edge Runtimeは互換性の問題からそのままでは動作しません。
これに対して、各マネージドDBのプロバイダーは専用ライブラリを提供してHTTP経由でDBにアクセスする方法が一般的です(Supabaseなど)。
他に例えばPrismaでは中間にAPIサーバーを立ててORMのインターフェイスはそのままに裏側をHTTPベースの通信にするData Proxyというサービスがあります。
しかしVercelのドキュメントを読むとそういった変換レイヤーを挟まないでもEdge RuntimeでKyselyやDrizzle ORMがそのまま動作するようなことが書かれています。
これは一体どういうことなのか気になるので、それぞれのORMライブラリのサポートが詳細がどうなっているのか見ていきます。
Kysely
これがDB作成時に提示されるKyselyのサンプルコードです。@vercel/postgres-kysely
というラッパーがいかにも何かやってそうなので見てみます。
import { createKyselyPool } from "@vercel/postgres-kysely";
interface Database {
person: PersonTable; // see github.com/kysely-org/kysely
pet: PetTable;
movie: MovieTable;
}
const db = createKyselyPool<Database>();
const person = await db
.selectFrom('person')
.innerJoin('pet', 'pet.owner_id', 'person.id')
.select(['first_name', 'pet.name as pet_name'])
.where('person.id', '=', id)
.executeTakeFirst();
@vercel/postgres
を内部で呼び出しているだけというのが分かりました。
@vercel/postgres
createPool()
を起点に読み進めます。
export function createPool(config?: VercelPostgresPoolConfig): VercelPool {
// ...
return new VercelPool({
...config,
connectionString,
maxUses,
});
}
Poolというのは@neondatabase/serverless
を継承したクラスでした。
import { Pool } from '@neondatabase/serverless';
// ...
export class VercelPool extends Pool {
Client = VercelClient;
// ...
}
@neondatabase/serverless
知りたい部分に近付いてきました。
このモジュールはファイル構成が特殊で src/
にインテグレーションテストのようなコード、shims/
以下にNode.js標準ライブラリと同じ名前のモジュール群があり、中身が空のものもいくつかあります。
❯ ls shims/
assert dns net pg-native stream tls util
crypto fs path shims.js string_decoder url
READMEにこう書いてありましたので該当ファイルを見てみます。
Sockerクラスの実装でWebSocketっぽいのを発見。
EventEmitterはEdge RuntimeのNodeコンパチのものが使えそうだけど見たところbrowserify/eventsを読み込んでいた。
socket.connect()を再定義して——
pgモジュールの名前空間を上書きしてる
これでつまりこういうコードの裏側でWebSocket接続の呼び出しになるということです。
const { Client } = require('pg')
const client = new Client()
await client.connect()
const res = await client.query('SELECT $1::text as message', ['Hello world!'])
console.log(res.rows[0].message) // Hello world!
await client.end()
そして
- Edge Runtime環境ではNode.jsの
net.connect()
の中身は動かないけど差し替えたnew WebSocket()
の実装なら呼び出し可能 - neon.techサーバーはWebSocketを待ち受けしてDBに接続できる
ということですね。
Drizzle ORM
Kysely以外にもDrizzle ORMの名前もあったのでどんな感じなのか調べてみます。
Drizzle ORMはドキュメントに記載がまだなかったのですがリポジトリを漁っていたらサンプルが出てきました。
import { sql } from '@vercel/postgres'
import { drizzle } from 'drizzle-orm/vercel-postgres'
// ...
export const db = drizzle(sql)
drizzleの場合はVercelのリポジトリではなくdrizzle側でvercel-postgresアダプタを実装しているみたいです。
内部で@vercel/postgresが使われていました。ということは@vercel/postgres→@neondatabase/serverlessの依存になり、内部接続はWebSocketということで間違いないようです。
Socket APIの謎が解けたので次はコネクションプーリングの問題について確認します。
サーバーレス環境とコネクションプーリング
Vercelのようなサーバーレス環境では起動させっぱなしのインスタンスからDBに持続的にコネクションを張り続けるというプロセスがなく、リクエスト毎にDB接続がopen/closeされ、複数のリクエスト間でコネクションを使い回すことは保証されていません(プラットフォームごとにグローバル変数を使ったキャッシュテクニックなどはある)。
しかし@vercel/postgresのPoolClientを使った場合は、内部でNeon serverless driverの実装のWebSocket接続を通して、neon.techサーバー内でTCP接続に変換され、その背後のPgBouncerによって維持されたPostgreSQLへのコネクションが取得できます。
全体像はこうなります
※WebSocketとTCPの変換レイヤーはNeonが自社で開発するwsproxyを使われています。
コールドスタート
ただしVercelのドキュメントにあるとうりこれはインスタンスがスタンバイ状態である時のシナリオで、5分以上経過して停止している場合はコールドスタートとなり起動時間が最大5秒上乗せされます。
An inactive Vercel Postgres database may experience cold starts. If your database is not accessed within a 5 minute period, your database will be suspended. The next time it is accessed, you will experience a "cold start" of up to 5 seconds. https://vercel.com/docs/storage/vercel-postgres/limits
Q.1: WebSocketプロトコル必要?
リモート接続できて非同期にStreaming形式の応答があればよさそうなのでWebSocketが丁度よいのではないか。
比較対象はSupabseで採用されているPostgRESTなのかも。
Q.2: Neonにロックインされるのでは?
@neondatabase/serverlessが必要なのはWebSocket経由でEdge Runtimeから接続する時だけなので、プラットフォーマー以外はNeonではないPostgreSQLサーバーを使いたくなったら単にNode.jsでpgモジュールをそのまま使うことができます。
Edge Runtimeからどうしても自社のPostgreSQLサーバーに接続したいなら前述のwsproxyを立てるとセルフホストも可能です。
Q.3: PrismaはWebSocketで動くの?
PrismaはこのNeonのアーキテクチャには対応していないようです。
かわりにPrisma Data Proxyを自分で用意して裏側をVercel Postgres(POSTGRES_URL_NON_POOLING)にすると prisma//...
のホスト指定でEdge Runtimeで動かすことができそうです。
Note: Prisma does not currently support connections with Edge Functions except with the Prisma Data Proxy. We recommend connecting using Kysely or Drizzle instead. https://vercel.com/docs/storage/vercel-postgres/limits
との記述があります。
余談: cloudflare/workerdの動向
workeredへTCP接続のAPIが入り、将来的にCloudflare WorkersからNode.js互換のSocket APIが使えるようになったら、WebSocket接続の部分は置き換え可能なので、状況が変わるかもしれません。
まとめ
Vercel PostgresがEdge RuntimeでORMとコネクションプールを使えるようになっているのはだいたいNeonのおかげだった。Neonすごい。
参考
Discussion