有名テック企業の技術ブログを、ひとつのフィードで。
フィード
32件
こんにちは、Control Plane部認証認可グループの平岩です。私たち認証認可グループではバックエンドAPIにGoを多く採用しています。共通基盤である特性上高RPSに耐えられる必要があり、また安定して低いレイテンシでリクエストを処理することが求められます。本記事ではGoの標準パッケージである sql.DB の内部実装を、ソースコード(Go 1.26時点)を読みながら解説します。 はじめに sql.DB とは何か 内部実装の詳解 DB 構造体の主要フィールド クエリ実行の全体像 BadConn リトライの仕組み コネクションの取得: conn メソッド フェーズ1: idleコネクションがあれば再利用する フェーズ2: 最大接続数に達している場合の待機 フェーズ3: 新規接続の作成 コネクションの返却: putConn メソッド 非同期なコネクション作成: connectionOpener トランザクションとコネクション コネクションの定期削除: connectionCleaner DBStats: プールの状態を観測する コネクションプールの設定 4つの設定パラメータ デフォルト値の問題 各パラメータの解説 SetMaxOpenConns SetMaxIdleConns SetConnMaxLifetime SetConnMaxIdleTime まとめ はじめに GoでRDBを使うとき、何かしらのO/R mapperやライブラリを使うことが多いかと思います。ですが、内部的には標準パッケージの sql.DB が使われています。この sql.DB はコネクションプールの役割を持ち、4つの設定パラメータが存在します。これらの値をデフォルトのまま使うと本番環境でエラーやパフォーマンス問題を引き起こすことがあります。また、内部でリトライが行われるケースがあるなど隠蔽されている挙動も存在します。 本記事では前半で sql.DB の内部実装をソースコードを追いながら解説し、後半でそれを踏まえた設定の考え方を述べます。設定についてだけ知りたい方はコネクションプールの設定まで読み飛ばしてください。 sql.DB とは何か sql.DBの実態はコネクションプールであり、複数のgoroutineから安全に使えます(goroutine safe)。 sql.Openは引数の検証のみを行い、この時点ではデータベースへのコネクションを作成しません。実際にコネクションが作れることを確認するには (*sql.DB).PingContext を使います(詳細はOpening a database handle - The Go Programming Languageを参照してください)。 内部実装の詳解 DB 構造体の主要フィールド ここからは sql.DB の実装を見ていきます。以降のコードを読むにあたって、DB 構造体 の以下の4つのフィールドを把握しておけば十分です。 freeConn []*driverConn: idleコネクションのスライス。プールに戻った時刻 returnedAt の古い順になる connRequests connRequestSet: コネクションを待っているgoroutineの集合 connRequestSet はSetと言っているが実体はindexでの参照を付けたスライス numOpen int: 使用中とidleの両方を含む、現在openなコネクション数 cleanerCh chan struct{}: プールにあるコネクションを閉じるgoroutineへの通知用のchannel すべてのフィールドへのアクセスは mu sync.Mutex で保護されています。 クエリ実行の全体像 まず、 QueryContext を例として全体像を示します。他の QueryRowContext ExecContext PingContext も基本的に同じ流れですが、 QueryContext のみが Rows.Close() を明示的に呼んでコネクションを返却する責任がuser側にあります。 flowchart TD A["(*sql.DB).QueryContext"] subgraph retryScope ["retryの範囲(ErrBadConnの場合、最大3回リトライされる)"] B["(*sql.DB).retry"] Q["(*sql.DB).query"] C["(*sql.DB).conn"] E["(*sql.DB).queryDC"] end A --> B B --> Q Q --> C C -- エラー --> ERR[error を返す] C -- 成功 --> E E -- ErrBadConn --> B E -- エラー --> ERR E -- 成功 --> F[Rows を返す<br>コネクション所有権が Rows に移動] subgraph userCode ["user"] K["(*sql.Rows).Next() で最後まで読むか、(*sql.Rows).Close()を呼ぶ"] end F --> K K --> G["(*sql.DB).putConn"] BadConn リトライの仕組み sql.DB には無効な接続に当たった場合に自動でリトライする仕組みがあります。 このリトライのトリガーとなるのが driver.ErrBadConn です。godocには次のように書かれています。 ErrBadConn should be returned by a driver to signal to the sql package that a driver.Conn is in a bad state (such as the server having earlier closed the connection) and the sql package should retry on a new connection. To prevent duplicate operations, ErrBadConn should NOT be returned if there's a possibility that the database server might have performed the operation. つまり、 driver.ErrBadConn はコネクションがDBサーバ側から切断された場合などに返り、sql package側でリトライすべきエラーであるということです。 実際にリトライを制御する DB.retry を見ていきます。 database/sql/sql.go L1572-1584: const maxBadConnRetries = 2 func (db *DB) retry(fn func(strategy connReuseStrategy) error) error { for i := int64(0); i < maxBadConnRetries; i++ { err := fn(cachedOrNewConn) // 👈 (1) // retry if err is driver.ErrBadConn if err == nil || !errors.Is(err, driver.ErrBadConn) { return err } } return fn(alwaysNewConn) // 👈 (2) } (1) 最大2回、cachedOrNewConn のstrategyで試行します。このstrategyではまずidleコネクションを再利用しようとします。 (2) 2回とも driver.ErrBadConn だった場合、3回目は alwaysNewConn strategyで新規のコネクション作成を強制します。 コネクションの取得: conn メソッド retry から呼ばれる conn メソッドが、プール内のidleコネクションか新規の接続を返します。このメソッドはかなり長いため、全体を3つのフェーズに分けて読んでいきます。 flowchart TD A[connメソッド呼び出し] --> B{freeConnに<br>idleコネクションがある} B -- Yes --> C{コネクションが期限切れ} C -- No --> D[コネクションを再利用] C -- Yes --> E[driver.ErrBadConnを返す → リトライ] B -- No --> F{コネクション数が最大(maxOpen)に<br>達している} F -- Yes --> G[connRequestsで待機] F -- No --> H[新規接続を作成] フェーズ1: idleコネクションがあれば再利用する database/sql/sql.go L1331-1354: // Prefer a free connection, if possible. last := len(db.freeConn) - 1 if strategy == cachedOrNewConn && last >= 0 { // Reuse the lowest idle time connection so we can close // connections which remain idle as soon as possible. conn := db.freeConn[last] // 👈 (1) db.freeConn = db.freeConn[:last] conn.inUse = true if conn.expired(lifetime) { // 👈 (2) db.maxLifetimeClosed++ db.mu.Unlock() conn.Close() return nil, driver.ErrBadConn } db.mu.Unlock() // Reset the session if required. if err := conn.resetSession(ctx); errors.Is(err, driver.ErrBadConn) { conn.Close() return nil, err } return conn, nil } (1) freeConn の末尾を取得しています(LIFO)。コメントにある通り、同時に必要とする数よりも多いコネクションがあった場合、余分なコネクションはidle状態のままcloseされていきます。 (2) 取得した接続が maxLifetime を超えていた場合は driver.ErrBadConn を返し、リトライの対象になります。 フェーズ2: 最大接続数に達している場合の待機 database/sql/sql.go L1358-1426: if db.maxOpen > 0 && db.numOpen >= db.maxOpen { req := make(chan connR
プログラミング言語を選ぶとき、開発効率や学習コスト、エコシステムの充実度など、考慮すべき要素は多岐にわたります。OSS パッケージを標的にしたサプライチェーン攻撃の増加や脆弱性に対するゼロデイ攻撃の発生といった状況を踏まえると、「アプリケーションが依存するライブラリをどう管理するか」も技術選定の重要な軸となっています。 私の所属する Control Plane 部の認証認可グループでは、2025年から、 Go の利用を本格化しました。技術選定時に挙げられていたメリットの一つが、まさに依存管理にあります。 本記事では、私たちが Go を選んだ背景と、1年間実際に開発してみて分かったことを振り返ります。 なぜ Go を選んだか 対象システムの特性 キャディの主流言語: TypeScript Go の特徴 1年間やってみてどうだったか 良かった点 メンテナンスコストの低さ AIコーディングとの相性 苦労した点 ライブラリに頼らないことのトレードオフ Go の慣習に慣れるまでのコスト 期待と現実のギャップ 採用した技術スタック まとめ なぜ Go を選んだか 対象システムの特性 キャディでは、製造業AIデータプラットフォームCADDi の基盤として「Control Plane」と呼ばれるシステム群を開発しています。Control Plane は、テナント管理や認証認可など、アプリケーションの機能から独立した管理層を担うシステムです。詳しくはこちらの記事で紹介しています。 開発言語を選ぶにあたって重要だったのは、Control Plane が持つ特性です。Control Plane は、1つの大きなサービスではなく、認証ゲートウェイ、トークン発行、テナント管理、認可といった、それぞれが明確な責務を持つ小さなサービスの集合体です。各サービスはシンプルな機能を提供する一方で、プラットフォーム基盤としての信頼性が求められます。 つまり、「小さなサービスを多数、手堅く作れること」が言語選定において重要な要件でした。 キャディの主流言語: TypeScript キャディのバックエンドでは TypeScript が主流です。TypeScript の開発エコシステムの充実度は群を抜いています。 また、フロントエンドとバックエンドを同じ言語で統一できるのも大きなメリットです。 一方で、依存管理の観点では、ライブラリの数が増えがちで、依存管理のコストが増大します。 Go の特徴 Go は Web API の構築に向いた言語です。標準ライブラリもサードパーティのライブラリもシンプルなものが多く、必要なパーツを組み合わせて開発するようなスタイルを取りやすいです。 こうした特性が、小さなサービスを数多く開発・運用する Control Plane とよくマッチすると判断しました。 1年間やってみてどうだったか 良かった点 メンテナンスコストの低さ Go を採用して最も良かったと感じているのは、メンテナンスコストの低さです。 Control Plane のアプリケーションは、一度作った後は数ヶ月単位で改修が入らないことも珍しくありません。そのような場合でも、セキュリティアップデートは定期的に適用する必要があります。このような作業は1つ1つは小さくても、積み重なると大きな負担になります。 Control Plane には、Go採用以前から開発・運用されている TypeScript(NestJS)アプリケーションがありますが、これと比較して Go アプリケーションはセキュリティアップデートが必要になる頻度が低いと感じています(およそ1/5程度)。 AIコーディングとの相性 Claude Code や Devin を用いたAIコーディングはキャディ社内でも主流となっていますが、構文がシンプルで書き手による差異が生じづらい Go は、AIが読み書きしやすいという点でもメリットがあります。また、コードフォーマッター(go fmt)やテストランナー(go test)が標準のツールチェインに組み込まれていて、かつ、高速に動作するという点も Go の強みです。 苦労した点 ライブラリに頼らないことのトレードオフ Go では依存ライブラリを少なく保って開発することができます。これは裏を返せば、基本的な処理を自前で実装する必要があるということです。例えば、以下のコードではサーバのグレースフルシャットダウンを行なっています。 ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) defer stop() go func() { // e は Echo インスタンス if err := e.Start(":" + c.Port); err != nil && !errors.Is(err, http.ErrServerClosed) { slog.Error(fmt.Sprintf("error occurred when starting the server: %v", err)) } }() ... <-ctx.Done() ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() if err := e.Shutdown(ctx); err != nil { slog.Error(fmt.Sprintf("error occurred when shutting down the server: %v", err)) } このような、他の言語であればフレームワークが担ってくれるような処理も自分たちで書く必要がありました*1。 依存の少なさと自前実装の手間は、トレードオフの関係にあります。私たちはこのトレードオフを意図的に受け入れました。長期的な運用コストの低さのほうが、初期の実装コストより重要だと判断したからです。 Go の慣習に慣れるまでのコスト チームメンバーの多くは Go での開発経験がそれほど多くありませんでした。Go 特有の慣習やイディオム ── たとえばエラーハンドリングの作法や、パッケージ構成の考え方 ── に慣れるまでには時間がかかりました。言語仕様の学習コストは低くても、「Go らしいコード」がわかるようになるまでには別のハードルがあります。 期待と現実のギャップ Go の採用にあたっては、goroutine による並行処理のしやすさにも期待していました。しかし、現時点では並行処理を積極的に活用する場面はまだありません。現在の Control Plane のサービス群は、典型的なリクエスト/レスポンス型の API が中心であり、goroutine の恩恵を実感する局面がまだ訪れていないのが実情です。 今後、バッチ処理や非同期ワーカーのようなワークロードが増えてきた際に、この特性が活きてくると考えています。 採用した技術スタック Goコミュニティにおいて実績のある技術スタックの中で、薄めのものを選定しました。 通信: Connect フレームワーク: Echo ※アプリケーションによってはフレームワーク無しの場合も ORM: Bun JWT: lestrrat-go/jwx いずれも優れたライブラリですが、特に Connect は素の gRPC に比べてデバッグしやすく、重宝しています。 まとめ Go は万能な言語ではありません。例えば、Go の特徴の一つである明示的なエラー処理は、コードの冗長さと表裏一体です。しかし、小さなサービスを数多く開発・運用する Control Plane においては、Go のシンプルさが強みになりました。 技術選定は常に文脈次第であり、私たちの経験がそのまま他の組織に当てはまるとは限りません。それでも、「Control Planeを Go で作る」という選択は、私たちにとって正しかったと考えています。 今後も Go を活用しながら Control Plane を拡充し、CADDi のマルチテナントプラットフォームをより堅牢なものにしていきます。 Go を使った Control Plane の開発に興味がある方は、ぜひ以下のページもご覧ください。 caddi.tech tech.caddi.com *1:このサンプルコードは Echo v4 です。Echo v5 ではフレームワークにグレースフルシャットダウン機能が搭載されています。