取り組みの背景——なぜ Kafka × Gemini API なのか
現代のプロダクションシステムでは、数千から数百万のイベントが毎秒流れ続けます。ECサイトのユーザー行動ログ、IoTセンサーデータ、SNSへの投稿ストリーム——これらを AI で分析し、リアルタイムにインサイトを返すアーキテクチャへの需要は急速に高まっています。
Apache Kafka は、高スループット・低レイテンシの分散イベントストリーミングプラットフォームとして業界標準の地位を確立しています。一方 Gemini API は、テキスト・画像・構造化データを横断した高度な推論を提供します。この2つを組み合わせることで、「大量データをリアルタイムで AI 処理する」という課題に対して、スケーラブルかつコスト効率の高い解答が得られます。
Kafka + Gemini API の基本アーキテクチャ設計
Python による Consumer 実装とエラー処理
バックプレッシャー・サーキットブレーカーによる障害耐性
レート制限とコンテキストキャッシュを活用したコスト最適化
Kubernetes 上での本番デプロイパターン
前提知識として、Kafka の基礎概念(Topic、Partition、Consumer Group)と Python の非同期処理(asyncio)の基本的な理解があると読み進めやすいです。
アーキテクチャ全体像
まず、全体アーキテクチャを整理します。
[イベントソース群]
ECサイト / IoT / SNS
│
▼
[Kafka Cluster]
Topic: raw-events
Partitions: 12
│
├──► [Consumer Group A: AI 分析ワーカー]
│ │ Gemini API 呼び出し
│ ▼
│ Topic: ai-results
│
└──► [Consumer Group B: ログ / 監査ワーカー]
│ そのまま保存
▼
Object Storage
重要なポイントは Consumer Group の分離 です。AI 処理(Gemini API 呼び出し)と非 AI 処理を別の Consumer Group に分けることで、Gemini API の一時的な遅延やレート制限が全体のパイプラインに波及するのを防ぎます。
環境構築
必要なパッケージ
pip install confluent-kafka google-generativeai tenacity prometheus-client pydantic
Kafka の起動(ローカル開発用)
# docker-compose.yml
version : "3.8"
services :
zookeeper :
image : confluentinc/cp-zookeeper:7.6.0
environment :
ZOOKEEPER_CLIENT_PORT : 2181
kafka :
image : confluentinc/cp-kafka:7.6.0
depends_on : [ zookeeper ]
ports :
- "9092:9092"
environment :
KAFKA_BROKER_ID : 1
KAFKA_ZOOKEEPER_CONNECT : zookeeper:2181
KAFKA_ADVERTISED_LISTENERS : PLAINTEXT://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR : 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE : "true"
docker-compose up -d
基本的な Consumer 実装
イベントスキーマの定義
Pydantic を使ってイベントのスキーマを厳密に定義します。型安全性を確保することで、後続の AI 処理での意図しないエラーを防ぎます。
# schemas.py
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
class RawEvent ( BaseModel ):
event_id: str
event_type: str # "user_review" | "sensor_alert" | "support_ticket"
payload: str # AI に渡すテキストコンテンツ
metadata: dict = Field( default_factory = dict )
timestamp: datetime = Field( default_factory = datetime.utcnow)
class AIResult ( BaseModel ):
event_id: str
sentiment: Optional[ str ] = None # "positive" | "neutral" | "negative"
summary: str
tags: list[ str ] = Field( default_factory = list )
confidence: float = 0.0
processed_at: datetime = Field( default_factory = datetime.utcnow)
model_version: str = "gemini-2.5-flash"
Gemini API クライアントの設定
# gemini_client.py
import google.generativeai as genai
import os
import json
from schemas import RawEvent, AIResult
genai.configure( api_key = os.environ[ "GEMINI_API_KEY" ])
# モデルの初期化(起動時に一度だけ実施)
model = genai.GenerativeModel(
model_name = "gemini-2.5-flash" ,
generation_config = {
"temperature" : 0.1 , # 分析タスクは低温度で安定性を確保
"max_output_tokens" : 512 ,
"response_mime_type" : "application/json" ,
}
)
SYSTEM_PROMPT = """
あなたはイベント分析AIです。受け取ったテキストを分析し、以下のJSON形式で応答してください。
{
"sentiment": "positive | neutral | negative",
"summary": "100文字以内の要約",
"tags": ["関連タグの配列(最大5つ)"],
"confidence": 0.0〜1.0の信頼度スコア
}
"""
async def analyze_event (event: RawEvent) -> AIResult:
"""単一イベントを Gemini API で分析する"""
prompt = f " { SYSTEM_PROMPT }\n\n 分析対象: \n{ event.payload } "
response = await model.generate_content_async(prompt)
data = json.loads(response.text)
return AIResult(
event_id = event.event_id,
sentiment = data.get( "sentiment" ),
summary = data.get( "summary" , "" ),
tags = data.get( "tags" , []),
confidence = data.get( "confidence" , 0.0 ),
)
本番対応 Consumer の実装
レート制限とリトライロジック
Gemini API には レート制限 があります。429 エラーへの適切な対応は本番運用の必須条件です。tenacity ライブラリを使って指数バックオフ付きリトライを実装します。
# worker.py
import asyncio
import json
import logging
from confluent_kafka import Consumer, Producer, KafkaException
from tenacity import (
retry,
stop_after_attempt,
wait_exponential,
retry_if_exception_type,
)
import google.api_core.exceptions as google_exc
from schemas import RawEvent, AIResult
from gemini_client import analyze_event
from prometheus_client import Counter, Histogram, start_http_server
logger = logging.getLogger( __name__ )
# Prometheus メトリクス
events_processed = Counter( "events_processed_total" , "処理イベント総数" , [ "status" ])
api_latency = Histogram(
"gemini_api_latency_seconds" ,
"Gemini API レイテンシ" ,
buckets = [ 0.5 , 1.0 , 2.0 , 5.0 , 10.0 , 30.0 ],
)
dlq_events = Counter( "dlq_events_total" , "Dead Letter Queue 送信数" )
# リトライデコレーター
@retry (
retry = retry_if_exception_type(
(google_exc.ResourceExhausted, google_exc.ServiceUnavailable)
),
wait = wait_exponential( multiplier = 1 , min = 2 , max = 60 ),
stop = stop_after_attempt( 5 ),
)
async def analyze_with_retry (event: RawEvent) -> AIResult:
with api_latency.time():
return await analyze_event(event)
class AIStreamWorker :
def __init__ (self, kafka_config: dict , topic_in: str , topic_out: str , topic_dlq: str ):
self .consumer = Consumer({
** kafka_config,
"group.id" : "ai-analysis-group" ,
"auto.offset.reset" : "latest" ,
"enable.auto.commit" : False , # 手動コミットで安全性確保
"max.poll.interval.ms" : 300_000 , # 5分(Gemini API の遅延を考慮)
})
self .producer = Producer(kafka_config)
self .topic_in = topic_in
self .topic_out = topic_out
self .topic_dlq = topic_dlq
self ._running = False
async def _process_message (self, msg):
"""メッセージを受け取り、AI 分析を実行してから結果を publish する"""
try :
raw = json.loads(msg.value().decode( "utf-8" ))
event = RawEvent( ** raw)
result = await analyze_with_retry(event)
# 分析結果を output topic へ送信
self .producer.produce(
self .topic_out,
key = event.event_id,
value = result.model_dump_json(),
)
self .producer.poll( 0 )
events_processed.labels( status = "success" ).inc()
logger.info( f "✅ Processed: { event.event_id } | sentiment= { result.sentiment } " )
except Exception as e:
# リトライ上限超過 → Dead Letter Queue へ
logger.error( f "❌ Failed: { msg.key() } | { e } " )
self .producer.produce(
self .topic_dlq,
key = msg.key(),
value = json.dumps({
"original_message" : msg.value().decode( "utf-8" ),
"error" : str (e),
}),
)
dlq_events.inc()
events_processed.labels( status = "failed" ).inc()
async def run (self, concurrency: int = 5 ):
"""メインのイベントループ"""
self .consumer.subscribe([ self .topic_in])
self ._running = True
semaphore = asyncio.Semaphore(concurrency) # 同時実行数を制限
logger.info( f "🚀 Worker started | concurrency= { concurrency } " )
try :
while self ._running:
messages = self .consumer.consume( num_messages = concurrency, timeout = 1.0 )
if not messages:
continue
tasks = []
for msg in messages:
if msg.error():
raise KafkaException(msg.error())
async def bounded_process (m):
async with semaphore:
await self ._process_message(m)
tasks.append(bounded_process(msg))
await asyncio.gather( * tasks, return_exceptions = True )
# 全タスク完了後に一括コミット(at-least-once 保証)
self .consumer.commit( asynchronous = False )
finally :
self .consumer.close()
self .producer.flush()
logger.info( "Worker stopped gracefully." )
バッチ処理による API コール削減
個別メッセージを1件ずつ Gemini API に送ると、API コールのオーバーヘッドが積み重なります。複数イベントを1回のリクエストにまとめる「バッチ分析」で、コストとレイテンシを大幅に削減できます。
# batch_analyzer.py
import json
from gemini_client import model
from schemas import RawEvent, AIResult
async def analyze_batch (events: list[RawEvent]) -> list[AIResult]:
"""
複数イベントを1回の Gemini API 呼び出しで処理する。
最大20件程度が最適(コンテキストウィンドウと応答速度のバランス)
"""
if not events:
return []
# バッチプロンプトの構築
items = " \n " .join([
f '[ { i } ] event_id= { e.event_id }\n 内容: { e.payload[: 500 ] } '
for i, e in enumerate (events)
])
prompt = f """
以下の { len (events) } 件のイベントを分析し、JSONの配列で応答してください。
配列のインデックスは入力の番号と対応させてください。
各要素の形式:
{{ "sentiment": "positive|neutral|negative", "summary": "100文字以内", "tags": [], "confidence": 0.0〜1.0 }}
分析対象:
{ items }
"""
response = await model.generate_content_async(prompt)
try :
results_data = json.loads(response.text)
return [
AIResult(
event_id = events[i].event_id,
** data,
)
for i, data in enumerate (results_data)
]
except (json.JSONDecodeError, IndexError ) as e:
# バッチ失敗時は個別処理にフォールバック
from gemini_client import analyze_event
return [ await analyze_event(e) for e in events]
バッチサイズは 10〜20 件が実用的な上限です。大きすぎると Gemini API のコンテキストウィンドウを圧迫し、応答品質が低下するリスクがあります。
コンテキストキャッシュによるコスト最適化
同じシステムプロンプトを毎回送信するのは非効率です。Gemini API の コンテキストキャッシュ機能 を活用することで、固定プロンプトのトークンコストを最大 75% 削減できます。
# cached_analyzer.py
import google.generativeai as genai
import os
genai.configure( api_key = os.environ[ "GEMINI_API_KEY" ])
LARGE_SYSTEM_CONTEXT = """
あなたはリアルタイムイベント分析の専門AIです。
[ここに長大な分析ガイドライン・用語定義・業界固有のルールを記述]
...(数千トークン分のコンテキスト)
"""
# キャッシュの作成(TTL: 1時間)
cache = genai.caching.CachedContent.create(
model = "models/gemini-2.5-flash" ,
system_instruction = LARGE_SYSTEM_CONTEXT ,
ttl_seconds = 3600 ,
)
# キャッシュを利用するモデルの初期化
cached_model = genai.GenerativeModel.from_cached_content(
cached_content = cache
)
async def analyze_with_cache (payload: str ) -> dict :
"""キャッシュ済みコンテキストを使って高速・低コスト分析"""
response = await cached_model.generate_content_async(
f "次のイベントを分析してください: \n{ payload } " ,
generation_config = { "response_mime_type" : "application/json" },
)
return response.text
コンテキストキャッシュはキャッシュ保持料金が別途発生しますが、同一キャッシュを多数のリクエストで再利用するパイプラインでは圧倒的にコスト優位になります。
サーキットブレーカーパターン
Gemini API が高負荷・障害状態に陥ったとき、Consumer がリトライを続けると Kafka のラグが積み上がり、結果的に復旧後の爆発的なバーストが発生します。サーキットブレーカーを実装して「適切に諦める」設計が重要です。
# circuit_breaker.py
import time
from enum import Enum
class State ( Enum ):
CLOSED = "CLOSED" # 正常
OPEN = "OPEN" # 遮断中
HALF_OPEN = "HALF_OPEN" # 試行中
class CircuitBreaker :
def __init__ (
self,
failure_threshold: int = 5 ,
recovery_timeout: float = 60.0 ,
success_threshold: int = 2 ,
):
self .failure_threshold = failure_threshold
self .recovery_timeout = recovery_timeout
self .success_threshold = success_threshold
self .state = State. CLOSED
self .failure_count = 0
self .success_count = 0
self .last_failure_time = 0.0
def call (self, func):
"""コンテキストマネージャーとして使用"""
import functools
@functools.wraps (func)
async def wrapper ( * args, ** kwargs):
if self .state == State. OPEN :
elapsed = time.time() - self .last_failure_time
if elapsed < self .recovery_timeout:
raise RuntimeError (
f "Circuit OPEN: { self .recovery_timeout - elapsed :.0f } s 後に再試行"
)
self .state = State. HALF_OPEN
self .success_count = 0
try :
result = await func( * args, ** kwargs)
self ._on_success()
return result
except Exception as e:
self ._on_failure()
raise
return wrapper
def _on_success (self):
if self .state == State. HALF_OPEN :
self .success_count += 1
if self .success_count >= self .success_threshold:
self .state = State. CLOSED
self .failure_count = 0
else :
self .failure_count = max ( 0 , self .failure_count - 1 )
def _on_failure (self):
self .failure_count += 1
self .last_failure_time = time.time()
if self .failure_count >= self .failure_threshold:
self .state = State. OPEN
# 使用例
breaker = CircuitBreaker( failure_threshold = 5 , recovery_timeout = 60 )
@breaker.call
async def safe_analyze (event):
return await analyze_with_retry(event)
監視とオブザーバビリティ
本番パイプラインでは、プロダクションモニタリング が不可欠です。Prometheus + Grafana による指標収集の構成例を示します。
# metrics.py
from prometheus_client import Counter, Histogram, Gauge, start_http_server
# 主要指標の定義
KAFKA_LAG = Gauge( "kafka_consumer_lag" , "Consumer Group のラグ" , [ "topic" , "partition" ])
EVENTS_TOTAL = Counter( "events_total" , "総イベント数" , [ "status" , "event_type" ])
API_LATENCY = Histogram(
"gemini_api_request_duration_seconds" ,
"Gemini API 処理時間" ,
buckets = [ 0.1 , 0.5 , 1 , 2 , 5 , 10 , 30 ],
)
CIRCUIT_STATE = Gauge( "circuit_breaker_state" , "サーキットブレーカー状態(0=CLOSED, 1=OPEN)" )
DLQ_SIZE = Gauge( "dlq_events_current" , "現在の DLQ イベント数" )
def start_metrics_server (port: int = 9090 ):
"""Prometheus スクレイプ用 HTTP サーバーを起動"""
start_http_server(port)
print ( f "📊 Metrics server started on : { port } " )
監視すべき主要アラート条件は以下の通りです。
Kafka Consumer Lag > 10,000 : ワーカーの処理速度が追いついていない
Gemini API p95 レイテンシ > 10 秒 : モデル側の問題または負荷増加
DLQ 増加率 > 1% : 構造的なエラーが発生している可能性
サーキットブレーカー OPEN : 即時アラートが必要
Kubernetes デプロイ設定
# deployment.yaml
apiVersion : apps/v1
kind : Deployment
metadata :
name : gemini-kafka-worker
spec :
replicas : 3 # Kafka Partition 数に合わせてスケール
selector :
matchLabels :
app : gemini-kafka-worker
template :
metadata :
labels :
app : gemini-kafka-worker
annotations :
prometheus.io/scrape : "true"
prometheus.io/port : "9090"
spec :
containers :
- name : worker
image : your-registry/gemini-kafka-worker:latest
env :
- name : GEMINI_API_KEY
valueFrom :
secretKeyRef :
name : gemini-secrets
key : api-key
- name : KAFKA_BOOTSTRAP_SERVERS
value : "kafka-service:9092"
- name : WORKER_CONCURRENCY
value : "8"
resources :
requests :
memory : "256Mi"
cpu : "250m"
limits :
memory : "512Mi"
cpu : "500m"
livenessProbe :
httpGet :
path : /health
port : 8080
initialDelaySeconds : 30
periodSeconds : 10
Kafka の Partition 数と Worker の replicas を合わせる点が肝心です。Consumer Group では1つの Partition を同時に複数の Consumer が読むことができないため、replicas > partitions の設定は無駄なリソース消費になります。
個人開発者の視点から(実体験メモ)
ここまでの要点
Consumer Group の分離 : AI 処理と非 AI 処理を分け、障害の局所化を徹底する
リトライ + サーキットブレーカー : 指数バックオフリトライと OPEN/CLOSED 制御を組み合わせて堅牢性を確保する
バッチ処理 + コンテキストキャッシュ : API コール数とトークン使用量を最小化してコストを最適化する
Prometheus でオブザーバビリティ : Consumer Lag・API レイテンシ・DLQ 増加率をリアルタイム監視する
このアーキテクチャは、ECサイトのレビュー分析、ログ異常検知、カスタマーサポート自動分類など、幅広いユースケースに応用できます。まずはローカルの Docker 環境で小規模に動かし、処理要件に合わせて Partition 数とワーカー数を調整していくことをお勧めします。
Gemini API と組み合わせた実装の理解を深める土台として、ぜひご活用ください。