マイクロサービスアーキテクチャは、現代のソフトウェア開発において標準的な選択肢になっている。
しかし、実際に導入してみると「思ったよりメリットがない、運用が大変、デバックがつらい」という意見が多いように感じる。
なぜマイクロサービスが辛く感じるのか?その理由を技術的に考察し、具体的な対策について書きながら整理する。
マイクロサービスが辛い理由
1. 複雑性の爆発
モノリシックアプリケーションでは、複雑性は1箇所に集中する。一方、マイクロサービスでは、複雑性が分散する。
モノリシック:
アプリケーション(1) ──→ データベース(1)
複雑性: O(n)
マイクロサービス:
サービスA(1) ──→ DB-A(1)
サービスB(1) ──→ DB-B(1)
サービスC(1) ──→ DB-C(1)
複雑性: O(n²) # サービス間通信の組み合わせサービス数が増えると、複雑性は指数関数的に増加する。
2. デバッグの困難さ
エラーが発生した場合、どのサービスで発生したか特定するのが困難である。
ユーザーリクエスト
→ API Gateway
→ 認証サービス
→ ユーザーサービス
→ 注文サービス
→ 決済サービス ← ここでエラー
→ 在庫サービス
エラーログ:
API Gateway: 500 Internal Server Error
認証サービス: 正常
ユーザーサービス: 正常
注文サービス: 500 Internal Server Error
決済サービス: Connection Timeout ← 原因
在庫サービス: 正常エラーの根本原因を特定するまでに、複数のサービスのログを追跡する必要がある。
X-Rayのような追跡ツールもあるが、バルクAPIを使っていると全部が記録されしまい、結局追いにくいと感じる。
3. トランザクション管理の複雑さ
分散トランザクションは、ACIDを保証するのが非常に困難である。
# モノリシック(簡単)
Repo.transaction(fn ->
Orders.create_order(order)
Inventory.reserve_item(item)
Payments.charge(payment)
end)
# マイクロサービス(困難)
# 1. 注文サービス: 注文作成
# 2. 在庫サービス: 在庫確保
# 3. 決済サービス: 決済処理
# → どこかで失敗したらロールバック必要
# → Sagaパターンなどの複雑な仕組みが必要理想はマイクロサービスは純粋関数のように、単に計算機として機能することである。しかし、実際のビジネスでは上記のようにトランザクションが必要なシーンが多く存在し、なるべく問題が起きないことを願うという神頼みになってしまっている場合がある。
4. デプロイの複雑さ
単一のアプリケーションではなく、複数のサービスをデプロイする必要がある。
# モノリシック
git push origin main
# CI/CDが自動デプロイ
# マイクロサービス
# サービスごとにデプロイ
kubectl apply -f user-service/
kubectl apply -f order-service/
kubectl apply -f payment-service/
kubectl apply -f inventory-service/
# 依存関係を考慮した順序が必要
# バージョン互換性の管理が必要このへんについては、Kubernetes/Argo/Helmセットが流行っており解決しつつあるが、
- タグをつけ忘れ
- 関連するマイクロサービスをリリースノートに記載して、レビューアがすべてを点検する作業する
などが別の問題が発生し、結局負担が大きいことには変わりない。
5. データの整合性
各サービスが独自のデータベースを持つため、データの整合性を保つのが困難である。
ユーザーサービス:
User(id, name, email)
注文サービス:
Order(id, user_id, items)
問題:
- ユーザーが削除されたら、注文データは?
- ユーザー名が変更されたら、注文の表示は?
- 結合クエリは不可能DBをひとつにしてしまうという荒技もあるが、それはマイクロサービスではないと個人的には感じる。
データ整合性バッチを作り始めたりするとさらに負担が増えるのが辛い。
6. ローカル開発の困難さ
すべてのサービスをローカルで起動するのは、リソース的に困難である。
マイクロサービス構成:
- API Gateway
- 認証サービス
- ユーザーサービス
- 注文サービス
- 決済サービス
- 在庫サービス
- 通知サービス
- レポートサービス
必要なリソース:
- CPU: 8コア以上
- メモリ: 16GB以上
- Docker Containers: 20個以上
→ ノートPCでは動かないちなみにkindというツールを私は利用していて一括で起動できる。が、メモリが足りないことが多く解決にならなってない。
7. Kubernetesを使っているから
マイクロサービス×Kubernetesはよくある設計パターンである。
ただ、Kubernetesは複雑である。便利なのは間違いない。しかし学習コストが高く、一般のWebアプリ開発者がインフラ構築の集大成とも言えるKubernetesを理解して使いこなすのには年単位の教育が必要である。
少なくとも最初の3ヶ月はベテランと一緒に作業して慣れてもらう必要があり、誤った操作をするとネットワークが破壊され本番障害につながってしまうこともある。
【余談】
隣のプロジェクトで実際に上記のネットワークを吹っ飛ばす事故が起きたことがあった。かなりの損金が出たらしく、私はKubernetesの作業をするときは時々あの事故を思い出す。
Kubernetesには思いもよらないトラップも多く存在する。最初に作るのはことはClaudeでも可能だが、トラブルシューティングは経験が必要になる。執筆時点でClaudeでも解決しない問題もまだまだ多い。
マイクロサービスを理解していて、Kubernetesを理解していて、Helm/Argo/Terraformの経験があって、というエンジニアはなかなか少ないのも問題である。経験ある開発者に頼りきってしまい疲弊してしまうケースも過去にあった。
なぜマイクロサービスを選ぶのか
それでも、マイクロサービスを選ぶ理由は存在する。
1. スケーラビリティ
特定のサービスのみをスケールできる。
注文サービス:
通常: 2インスタンス
セール時: 20インスタンス
決済サービス:
通常: 2インスタンス(負荷が低い)モノリシックでは、全体をスケールする必要がある。
2. 独立したデプロイ
他のサービスに影響を与えずにデプロイできる。
注文サービスの修正:
→ 注文サービスのみデプロイ
→ ユーザーサービス、決済サービスは影響なし3. 技術スタックの柔軟性
サービスごとに最適な技術を選択できる。
ユーザーサービス: Elixir(リアルタイム通信)
注文サービス: Go(高速処理)
レポートサービス: Python(データ分析)
決済サービス: Java(エンタープライズ連携)4. チームの独立性
チームごとにサービスを担当し、独立して開発できる。
チームA: ユーザーサービス
チームB: 注文サービス
チームC: 決済サービス
各チーム:
- 独自のリポジトリ
- 独自のCI/CD
- 独自のリリースサイクルマイクロサービスを成功させる対策
1. 分散トレーシング
OpenTelemetryなどを使い、リクエストの流れを追跡する。
# Elixir with OpenTelemetry
defmodule MyApp.Tracer do
require OpenTelemetry.Tracer
def trace_request(request) do
OpenTelemetry.Tracer.with_span "process_request" do
# リクエスト処理
# → トレースIDが自動的に伝播
end
end
endこれにより、エラーがどのサービスで発生したか即座に特定できる。
2. Sagaパターン
分散トランザクションをSagaパターンで管理する。
defmodule OrderSaga do
def execute(order) do
with {:ok, _} <- create_order(order),
{:ok, _} <- reserve_inventory(order),
{:ok, _} <- process_payment(order) do
{:ok, :completed}
else
{:error, :payment_failed} ->
# 補償トランザクション
release_inventory(order)
cancel_order(order)
{:error, :saga_failed}
end
end
end3. API Gateway
すべてのリクエストをAPI Gatewayで集約する。
クライアント
↓
API Gateway
├→ 認証サービス
├→ ユーザーサービス
├→ 注文サービス
└→ 決済サービス
メリット:
- 認証/認可の一元管理
- レート制限の一元管理
- ルーティングの簡素化4. サービスメッシュ
Istioなどのサービスメッシュを導入する。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order-service
http:
- retries:
attempts: 3
perTryTimeout: 2s
timeout: 10sサービス間通信の信頼性が向上する。
5. Contract Testing
サービス間のインターフェースをContract Testで保証する。
# Pact を使った Contract Test
defmodule OrderServiceContractTest do
use ExUnit.Case
import Pact.Consumer
test "注文作成のコントラクト" do
pact do
service_consumer "OrderService"
has_pact_with "PaymentService"
upon_receiving "決済リクエスト"
with_request(
method: :post,
path: "/payments",
body: %{amount: 1000, currency: "JPY"}
)
will_respond_with(
status: 200,
body: %{id: "payment_123", status: "success"}
)
end
end
end6. ローカル開発の簡素化
Docker ComposeでローカルLenvironmentを構築する。
version: '3.8'
services:
api-gateway:
image: api-gateway:latest
ports:
- "8080:8080"
user-service:
image: user-service:latest
environment:
DATABASE_URL: postgres://db/users
order-service:
image: order-service:latest
environment:
DATABASE_URL: postgres://db/orders
# 一括起動
$ docker-compose upまた、不要なサービスはモックで代替する。
# テスト用モック
defmodule PaymentServiceMock do
def process_payment(_order) do
{:ok, %{id: "mock_payment", status: "success"}}
end
end7. gRPCで統一的なインターフェースを組み合わせて開発すると開発体験が良くなる
Protobufで定義したRPCとメッセージをサービス全体で共有することでインターフェースの変更など検知しやすくなる。わざわざカタログを作らなくても「Protobufを見て」で解決できたり、便利なシーンも存在する。
特にマイクロサービス×gRPCはかなり完成度が高いシステムを目指しやすい(その上gRPC通信は速い)。
マイクロサービスを選ぶべきか
選ぶべきケース
- 大規模チーム: 10人以上の開発者
- 高トラフィック: 100万リクエスト/日以上
- 部分的なスケール: 特定機能のみ高負荷
- 長期運用: 5年以上の運用予定
選ぶべきでないケース
- 小規模チーム: 5人以下の開発者
- 低トラフィック: 10万リクエスト/日以下
- プロトタイピング: 素早い機能検証が必要
- 短期プロジェクト: 1年以内の運用予定
まとめ
マイクロサービスは、銀の弾丸ではない。
(正直なところ資金力がないとマイクロサービスは続かないです)
メリット:
- スケーラビリティ
- 独立したデプロイ
- 技術スタックの柔軟性
- チームの独立性
デメリット:
- 複雑性の爆発
- デバッグの困難さ
- トランザクション管理の複雑さ
- データの整合性
成功の鍵:
- 適切なツールの選択(分散トレーシング、サービスメッシュ)
- 設計パターンの適用(Saga、API Gateway)
- チーム体制の整備
- 段階的な移行(モノリシックから徐々に分割)
マイクロサービスは「悪」でも「正義」でもない。適切なコンテキストで使えば強力だが、不適切なコンテキストでは複雑性を増すだけである。
自分のプロジェクトの規模、チームの能力、将来の展望を考慮して、慎重に選択する必要がある。
導入時にビジネスサイドとよく相談が必要と感じます。将来的に開発チームは大きく縮小する予定である場合はマイクロサービスは運用不可になるため、避けた方がいいです。