1. はじめに
画像データを効率的に処理する為、「Cloud Pub/Sub」を活用した、スケーラブルなOCRデータパイプラインを構築しました。
前回の構成では、同期通信に伴う負荷集中や処理遅延が課題となっており、その解決策として、今回はデータパスを 「Pull型の非同期メッセージング」 へと刷新。
「Go言語の受付API」、「EasyOCRを搭載したPythonワーカー」、「Cloud Pub/Subを」に疎結合化し、Kubernetesのイベント駆動自動スケールツールである KEDA(Kubernetes Event-driven Autoscaling) を導入し、柔軟なオートスケーリング(最小1 Podへの縮退)を実現しています。
💡 本記事は、3部作の「総括編」です。
・【改善版】詳細説明編:「Go/Python/Terraform/Helm」の詳細解説。
・【改善版】総括編: Pub/Subを用Push型データパスの要点・効果を解説。
・【改善版】検証編: 負荷試験(k6)による大量リクエスト流入時の挙動を検証・考察。
| ◆構成 |
|---|
| 1. はじめに |
| 2.技術スタックとシステム構成 |
| 3. ポイント①:OCR基盤の「Pull」型データパスへの改良 |
| 4. ポイント②: KEDAによるイベント駆動スケーリング |
| 5. ポイント③: Workload Identityによる認証情報の完全な隠蔽 |
| 6. ポイント④: クラウド財務管理(FinOps)を意識したコスト削減 |
| 7. おわりに |
2.技術スタックとシステム構成
2-1. 技術スタック
本システムでは、以下の技術スタックを採用しています。前回の負荷集中の課題を解決するため、「疎結合」「非同期バッファリング」「イベント駆動型オートスケール」 を軸とした構成へ刷新しました。
| カテゴリ | 技術・ツール | 用途 |
|---|---|---|
| Frontend API | Go (Standard) | HTTPリクエスト受付、GCSアップロード、Pub/Subメッセージ送信、構造化ログ出力 |
| Inference Engine | Python (EasyOCR) | Pub/Subメッセージ受信(Pull)、GCS画像ダウンロード、文字認識処理 |
| Messaging (Buffer) | Cloud Pub/Sub | Go-Python間の通信を仲介するメッセージキュー。スパイク負荷のバッファリング |
| Autoscaling | KEDA | Pub/Subの未処理メッセージ数(Backlog)を検知し、Podを高速にイベント駆動スケール |
| Infrastructure | GKE (Standard) | 完全管理型K8s環境。スポットインスタンスを活用した超低コスト運用 |
| IaC | Terraform | VPC、Cloud NAT、GKE、Pub/Sub、GCS、Log Sinkのコード管理(IaC) |
| Observability | Cloud Logging / BigQuery | 構造化ログの集約、およびSQLによる推論精度・性能分析 |
| Testing | k6 | 大量リクエストを用いた負荷試験。キューの滞留とKEDAのスケーリング挙動の検証 |
2-2. 主要ライブラリ
新アーキテクチャのパフォーマンスとコスト最適化を最大化するために採用した主要ライブラリ・ツールです。
| 対象 | ライブラリ | 採用理由 |
|---|---|---|
| Go | google.com | GCSへの画像アップロードおよびPub/Subへの超高速なメッセージ発行(Publish)のため |
| Go | gopsutil | 1リクエストの処理期間中における純粋なCPU使用率を正確に計測し、ログへ含めるため |
| Python | EasyOCR | PyTorchベースで精度が高く、日本語・英語に標準対応。今回は起動時にプレロードしてコールドスタートを排除。 |
| Python | psutil | OCR推論時のCPU負荷をノンブロッキングで計測し、BigQuery分析用の構造化ログに出力するため。 |
| K8s | KEDA (GCP Pub/Sub Scaler) | Pub/Subのキュー滞留数に応じてPod数を「0台⇄最大5台」までミリ秒単位で高感度スケールさせるため |
| GCP | Workload Identity | サービスアカウントの秘密鍵(JSON)の発行を完全に撤廃し、Pod単位で安全にGCP権限を借用するため |
2-3. ディレクトリ構成
インフラ管理(Terraform)コマンドで、デプロイできる以下の構成となっています。
このデータセットは、GitHubレポジトリ に登録しています。
.
├── terraform/ # GKE, VPC, Artifact Registry, Log Sinkの定義
├── chart/ # Kubernetesデプロイ用リソース (Helm Chart形式)
│ └── templates/ # Deployment, Service, HPA, RBACのマニフェスト
├── go-api/ # フロントエンドAPI (Go / Gin)
├── python-api/ # 推論エンジン (Python / EasyOCR / gRPC Server)
├── pb/ # gRPC定義ファイル (.proto) と自動生成コード
└── k6/ # 負荷試験スクリプト
└── test_images/ # 検証用画像および正解データ(mapping.json)
2-3. システム構成
1. 「データフロー」処理フェーズ(①〜⑪)
2. 「デプロイ・通信」フェーズ(①〜⑤)
3. 「認証・セキュリティ」フェーズ(①〜⑤)
3. ポイント①:OCR基盤の「Pull」型データパスへの改良
今回は、アーキテクチャを従来の「Push型(同期)」から「Pull型(非同期・イベント駆動)」へと変更しました。変更の概要は以下の通りです。
| データパス | 構成・スケーリング方式 | 主な特徴 |
|---|---|---|
| 前回 (Push型) | GoとPythonを同一Pod内に同居(サイドカー構成) HPAを用いたCPUベースのオートスケーリング |
同期的なgRPC通信による密結合な構成 |
| 今回 (Pull型) | GoとPythonを分離 GCP Pub/Subの未処理メッセージ数(キュー滞納数)ベースのオートスケーリング |
Pub/Subを挟んだ疎結合なPull型構成 |
この簡易構成図は以下の通りです。
前回(push型)データパス
今回(pull型)データパス
3-1 : 改良の狙い(なぜPull型にしたのか)
前回の検証では、「gRPC接続の偏りとL4ロードバランシングの限界」 により、負荷分散が適切に行われない現象が発生しました(詳細はこちらを参照)。
具体的には、「Podを5つに増やした状態で 5 VUs(同時接続数5)の負荷を投入」した際、コネクションが特定のPodに固定されてしまい、実際には4つのPodに負荷が集中し、1つのPodが完全に遊んでしまう(あるいはその逆の偏りが発生する)という事象が確認されました。
そこで今回は、適正な負荷分散を最優先課題として 「Pub/Subを用いたメッセージキューイング(Pull型)への刷新」 を行いました。
3-2 : 各コンポーネントの役割定義(適材適所の構成)
前回の課題(gRPCの永続接続による負荷の偏り)を踏まえ、コンポーネント間の同期通信(Push型)を見直し、 Cloud Pub/Subを媒介とした非同期の「Pull型」データパス構成 へと刷新しました。
各コンポーネントの役割を明確化する事で、システム全体の効率性を高めています。
1: 「API受付に特化」(Go)
フロントエンドの役割を 「リクエストの高速受付と、Pub/Subへのメッセージ書き込み」 に限定しました。
並行処理に優れたGo言語の特性を活かし、後述の通り負荷が20VUsまで上昇しても平均処理時間は 159 ms に抑制。
重いOCR処理を非同期に切り離したため、フロントエンドが詰まって(HTTP 5xxエラーなど)エラーなどを返すリスクを排除しています。
2: 「非同期バッファリング」 (Cloud Pub/Sub)
GoとPythonの間にCloud Pub/Subを挟むことで、突発的なスパイクアクセスを一時的に滞留させる 「キュー(バッファ)」 の役割を持たせました。
これにより、バックエンドのOCRワーカーへ直接的な負荷の直撃を防ぎ、メモリ不足(OOM)による OCR処理への悪影響を防ぐ防波堤として機能します。
3. OCR処理に専念(Python / EasyOCR)
バックエンドのPythonワーカーは、API受付やWebサーバーとしての接続維持タスクから解放され、 「自身の処理能力(ペース)に合わせてPub/Subから1件ずつデータをPullし、EasyOCRで文字認識する」 という本来の計算処理に100%専念できる環境を構築しました。
3-3 : 負荷強度に応じた挙動の考察(マトリクス解析)
この適材適所の構成により、負荷強度(同時接続数:VUs)に応じた各コンポーネントの挙動(メトリクス)は以下のようになりました。
※詳細な検証データはこちらを参照。
| 指標 | 1 VU | 5 VUs | 10 VUs | 20 VUs |
|---|---|---|---|---|
| Go処理時間 (平均) | 53 ms | 64 ms | 97 ms | 159 ms |
| Pub/Sub滞納時間 (平均) | 449 ms | 137,524 ms | 310,731 ms | 674,776 ms |
| Python処理時間 (平均) | 775 ms | 1,294 ms | 1,181 ms | 1,299 ms |
📈 メトリクスから読み取れる「Pull型」の挙動と効果
上記の数値は、Pull型アーキテクチャの設計通りの効果を示しています。
-
Python(OCR)処理時間の安定化:
負荷が5VUsから20VUsへと4倍に増加しても、Pythonの処理時間は 約1.2〜1.3秒で完全に頭打ち(平準化) しています。Push型のように処理能力を超えるリクエストがワーカーに殺到せず、ワーカー側が「処理可能なペース」を維持できている証拠です。 -
Pub/Sub滞納時間の増加(意図したバッファリング):
高負荷時にPub/Subの滞納時間が大幅に増加(20VUsで約674秒)しています。これは遅延ではなく、「Pythonワーカーが安全に処理できるまで、Pub/Subが過剰なリクエストを安全に預かってくれている時間」 です。
総括
前回のPush型構成であれば、この過剰なトラフィックによって特定のPodが過負荷でクラッシュしていたはずです。しかし新構成では、「システムをダウンさせずにキューへ安全に貯め、ワーカーが順次確実に消化する」 という、堅牢でスケーラブルな非同期データパイプラインを実現できました。
4. ポイント②: KEDAによるイベント駆動スケーリング
従来のHPA(Horizontal Pod Autoscaler)で用いる「CPU/メモリ使用率」による制御は、計算リソースを大量に消費するOCR処理において適切なスケール閾値の設定が困難でした。
この課題を解決するため、今回は Cloud Pub/Subの「未消化メッセージ数(バックログ数)」 を制御指標に採用しました。
明確なボトルネック要因(=処理待ちの画像数)をKEDA(Kubernetes Event-driven Autoscaling) を介してKubernetesに直接フィードバックし、キューの滞留量に比例してPod数を制御するイベント駆動型のスケーリングへと最適化しました。
この設計により、以下のスケーリング挙動を実現しています。
- 無駄なリソースの削減(最小数への縮退): リクエスト数がゼロの時はPod数を最小の「1」まで自動縮退させ、アイドル時の待機コストを最小化。
- 高速なスパイク対応: 突発的なアクセス増加に対しても、滞留数に応じて必要なPod数を即座に最大数(5 Pod)まで展開。
※詳細な検証データはこちらを参照。
📊 負荷試験時のメトリクス推移
同時接続数(VUs)を一気に引き上げた際の、Pub/Subのメッセージ滞留数とPython Pod数の連動推移です。
◆ Pub/Sub の滞納件数の推移
◆ 各Python Podの増減推移
【挙動の考察】
グラフの通り、トラフィックの急増に伴ってPub/Subの未確認メッセージ数がスパイクすると、KEDAがそれを検知してPython Podを即座に最大値(5つ)までスケールアウトさせています。また、負荷の終了とともにメッセージが消化されると、段階的にPod数が最小値の「1」へと安全にスケールダウンしていく、意図通りの自律的な挙動を確認できました。
5. ポイント③: Workload Identityによる認証情報の完全な隠蔽
本番環境のインフラ運用に、Google Cloud(GCP)の「サービスアカウントキー(JSON形式)」を発行してコンテナ内に配置する手法は、重大なセキュリティリスクを伴います。
そこで今回は、永続的なサービスアカウントキーの利用を完全に廃止し、GKE推奨の安全な認証基盤である Workload Identity を導入しました。
🔐 最小権限の原則(PoLP)に基づくキーレス認証
Kubernetesの「ServiceAccount」とGoogle Cloudの「IAMサービスアカウント」を直接バインドすることで、認証キーを介さない安全なキーレス認証を実現しました。
また、コンポーネントごとの役割に応じて、権限を以下のように明確に分離しています。
-
KEDA(メトリクス監視用):
roles/pubsub.viewer(バックログの滞留数を監視する最小権限) -
OCRワーカー(Python用):
roles/pubsub.subscriber(メッセージのPull)およびroles/storage.objectAdmin(GCSへの結果保存など)
認証情報の管理コストをゼロにしつつ、万が一コンテナが侵害された場合のリスクもインフラレイヤーで最小限に抑える、クラウドネイティブなセキュリティを体現しました。
6. ポイント④: クラウド財務管理(FinOps)を意識したコスト削減
前回の検証はGoogle Cloud(GCP)の無料トライアル枠を利用しておりインフラコストをあまり考慮していませんでしたが、今回は無料期間終了に伴い、「無駄なコストを以下に削減できるか」 にも意識を置いて設計しました。
具体的に実践した5つのコスト削減アプローチを解説します。
① GKE Autopilotの戦略的選択(無駄なノード代のカット)
Standardクラスター では、Podの稼働有無に関わらず 「ノード(VM)の起動時間」 に対して定額課金される一方で、GKE Autopilotは 「Podが要求したCPU/メモリのリソース量」 に対してのみ課金される仕組みです。
前述のKEDAによるオートスケーリングと組合わせ、リクエストが存在しないアイドル時間帯は最小構成(1 Pod)まで自動縮退させて、GKEのインフラ費用を最小限に抑えています。
② 無限ループの空転防止による無駄なCPU消費の抑制
Pub/SubからメッセージをPull(ポーリング)する際、メッセージが空の状態で即座に次の確認(無限ループ)を繰り返すと、コンテナのCPU使用率が100%近くまで跳ね上がります。Autopilot環境では、このリソースの空転がダイレクトに請求額へ反映されます。
そこで、Pythonワーカーのメインループにおいて、メッセージが取得できなかった場合は明示的に time.sleep(0.5)(500ミリ秒の待機) を挟む最適化を実装しました。
# worker.py のメインループ実装例
while not shutdown_requested:
has_message = pull_message()
if has_message:
continue
# ポーリングの空転による無駄なCPU消費(=課金)を抑制
time.sleep(0.5)
この制御により、トラフィックの発生していない時間帯におけるPodのCPU消費量を極限まで引き下げ、不要なコンピュートコストの発生を排除しています。
③ インフラの短寿命化(Terraformによる検証環境の即時破棄)
「検証開始時に terraform apply で環境を立ち上げ、作業終了後は速やかに terraform destroy で全リソースを削除する」というサイクルを徹底しました。これを容易にするため、Terraformコード内のBigQueryデータセット等に delete_contents_on_destroy = true を定義し、ワンコマンドで依存リソースも含めて完全にクリーンアップできる環境を構築しています。
④ SIGTERM捕捉(Graceful Shutdown)によるPod終了タイムラグの最小化
KEDAが負荷減少を検知してPodを減らす(スケールダウン)際、KubernetesはPodへ SIGTERM シグナルを送信します。
アプリケーション側がこのシグナルを正しく処理できない場合、Kubernetesはデフォルトで 30秒間(Termination Grace Period)、Podの強制終了を待ち続けます。Autopilot環境では、この終了を待機している無駄な30秒間も課金対象となります。
これを防ぐため、コード内で signal.signal(signal.SIGTERM, receive_sigterm) を実装し、シグナル検知時に shutdown_requested フラグをTrueへ反転させる制御を追加しました。
処理中のOCRタスクを安全に完了(データの整合性を担保)させた後、 自発的に即座にプロセスを終了(sys.exit(0)) させます。これにより、Podが消滅するまでのタイムラグを最小化し、不要な課金時間の発生を排除しました。
⑤ OCRモデルのイメージ同梱とPyTorch最適化による多重課金の防止
コンテナ起動のたびに数億パラメータのEasyOCRモデルを外部からダウンロードする設計では、コールドスタート時の起動遅延を招くだけでなく、毎回コンテナ外部へのネットワークOutbound通信費が発生します。
そこで、Dockerfileビルド時にモデルデータをイメージ内部に同梱する構成へと変更しました。さらに、ベースイメージに python:3.11-slim を採用し、CPU専用のPyTorchを明示的に指定することで、コンテナサイズを半分以下に軽量化。高速なPod起動とランタイム時の通信費ゼロ化を同時に実現しました。
7. おわりに
当初の目的である「適切な負荷分散」の達成は十分にできた事は確認できました。
ただ、それ以外の面も含めて考えると、まだまだGKEの機能を十分に使いこなせておらず、さらなる改良や検討の余地があると思っています。
特に、単一地域でのAutopilotを選択では全くリソースを使用できない時間もあり、Autopilot自体は非常に有用な機能ではあるけれども、運用面で色々と工夫をしていく必要性も感じました。
この経験を活かし、今後もクラウドを使用した様々なデータパスの構築に挑戦し続けていきます。
最後までお読みいただき、ありがとうございました。
💡 本記事は、3部作の「総括編」です。
・【改善版】詳細説明編:「Go/Python/Terraform/Helm」の詳細解説。
・【改善版】総括編: Pub/Subを用Push型データパスの要点・効果を解説。
・【改善版】検証編: 負荷試験(k6)による大量リクエスト流入時の挙動を検証・考察。







