はじめに (Introduction)
最近、Java (Quarkus) アプリケーションで組み込み分析エンジンである DuckDB を利用し、GCS (Google Cloud Storage)にデータをエクスポートする機能を実装しました。
ローカル環境 (macOS) では快適に動作していましたが、いざ Kubernetes (GKE)環境にデプロイすると、エラーログも吐かずに処理が完全に沈黙(ハング)するという不可解な現象に遭遇しました。
本記事では、この問題のトラブルシューティングの過程と、その背後にあった Linux コンテナにおける「libc 互換性」の重要な教訓を共有します。
1. 発生した問題 (The Problem)
環境:
* Framework: Quarkus 3.15 (Kotlin)
* Library: org.duckdb:duckdb_jdbc:1.1.3
* Container Image: eclipse-temurin:21.0.2_13-jre-alpine (Alpine Linux based)
* Infrastructure: GKE (Google Kubernetes Engine)
現象:
アプリケーションが DuckDB への接続を確立しようとした瞬間、ログが途絶え、処理が停止しました。例外 (Exception)もスローされず、タイムアウトもしない、完全な「フリーズ」状態です。
1 2026-01-22 13:48:02.566 JST INFO SQL生成完了...
2 2026-01-22 13:48:02.571 JST INFO DuckDB接続を確立します...
3 (ここで永遠に止まる)
コード上はここです:
1 // ここで止まる
2 DriverManager.getConnection("jdbc:duckdb:").use { conn ->
3 // ...
4 }
2. 迷走と仮説 (Investigation & Hypotheses)
最初は「GCS へのネットワーク接続の問題」や「権限不足」を疑いました。しかし、それなら Timeout やAccessDeniedException が出るはずです。何も起きないのはおかしい。
AI (LLM) の初期提案(浅い解決策)
AI に相談すると、以下のような提案がありました:
1. 「接続タイムアウトを伸ばしてみましょう」 → 効果なし。
2. 「DuckDB のバージョンを下げてみましょう」 →これは危険な誘惑でした。
「なぜ動かないか」を理解せずにバージョンを下げるのは、問題を先送りするだけです。私たちはここで踏みとどまり、「なぜ接続処理自体が開始されないのか?」 を突き止めることにしました。
3. 深度デバッグ (Deep Dive Debugging)
原因を切り分けるため、以下の診断コードを本番コードに埋め込み、デプロイして確認しました。
- ドライバのロード確認: DriverManager.getDriver() でクラスがロードできているか?
- 一時ディレクトリの権限確認: DuckDB は実行時に Native Library (.so ファイル) を /tmpに展開します。コンテナの /tmp が noexec (実行不可) になっていないか?
診断ログの結果
1 2026-01-22 14:05:15.813 JST java.io.tmpdir: /tmp
2 2026-01-22 14:05:15.820 JST 臨時ファイル実行権限テスト: 成功
3 2026-01-22 14:05:15.825 JST DuckDBドライバ登録確認: org.duckdb.DuckDBDriver (v1.0)
4 2026-01-22 14:05:15.910 JST DuckDB接続を確立します...
5 (やはりここで止まる)
権限もクラスロードも問題ありませんでした。つまり、Java コードから Native コード (JNI) に制御が渡った直後、C++の世界で何かが起きている ということです。
ここで、Dockerfile のこの行に目が留まりました。
1 FROM eclipse-temurin:21.0.2_13-jre-alpine
「Alpine... もしかして musl libc か?」
4. 根本原因:Glibc と Musl の非互換性
仕組みの解説
DuckDB の JDBC ドライバの中身は、高速化のために C++ で書かれた Native Library です。公式に配布されている DuckDB の Native Library (libduckdb.so) は、通常 Glibc (GNU C Library)環境でコンパイルされています。
一方、Alpine Linux は軽量化のために Glibc ではなく Musl Libc を採用しています。
なぜハングしたのか?
Glibc 依存のバイナリを Musl 環境で無理やり動かそうとすると(Java の JNI
ローダーはある程度吸収しようとしますが)、以下のような問題が起きることがあります:
- スレッド・ロックの実装差異: ミューテックス (Mutex) やスレッド初期化 (TLS)
の挙動が微妙に異なり、初期化中にデッドロックが発生する。 - シンボル解決の失敗: 特定の関数が見つからず、エラーハンドリングされずにプロセスが迷子になる。
今回は、DuckDB の初期化プロセスが Musl 環境下のスレッド処理と噛み合わず、デッドロック(Deadlock)に陥っていた可能性が極めて高いです。
- 解決策 (The Solution)
解決策はシンプルかつ「Global Optimal (全体最適)」なものでした。
ベースイメージを Alpine から、Glibc ベースのディストリビューションに変更することです。
Before
1 FROM eclipse-temurin:21.0.2_13-jre-alpine
After
1 # Ubuntuベースの軽量イメージ (Glibc採用)
2 FROM eclipse-temurin:21-jre
この変更を適用してデプロイしたところ、DuckDB は一瞬で起動し、GCS へのデータ転送も正常に完了しました。
6. まとめ:エンジニアとしての教訓
今回のバグハントから得られた教訓は、単なる「ライブラリの動かし方」以上のものです。
-
「エラー消し」ではなく「根本解決」を安易にバージョンを下げたりリトライを入れたりせず、「なぜ止まったのか」を追求したことで、「Alpine と Native
ライブラリの相性」 という根本原因に辿り着きました。これにより、将来別の Native ライブラリ(TensorFlow, RocksDBなど)を導入する際のリスクも回避できました。 -
コンテナ選択の重要性
「軽いからとりあえず Alpine」は、Java (特に JNI を使う場合) や Python (pandas/numpy)の世界ではリスクがあります。
* Pure Java/Go/Node: Alpine で OK。
* Native 拡張あり: Debian-slim や Distroless (Glibc 版) が無難。
3.デバッグの基本は「観測」
推測で修正するのではなく、診断コードを差し込んで「どこまで動いているか」「環境はどうなっているか」を事実として観測することが、最短の解決ルートでした。
この情報が、同じようにコンテナ内で謎のハングに悩むエンジニアの助けになれば幸いです。
💬 これまでに経験した失敗談があれば、ぜひコメントで教えてください。