サービス指向アーキテクチャ — SOAの教訓
「マイクロサービスとどう違うの?」
カオリの元に、別チームのエンジニア・ダイキが相談に来た。
「SOAって古い概念ですよね?マイクロサービスと同じじゃないんですか?」
「似ているけど、重要な違いがある。SOAが失敗した理由を知っておくと、マイクロサービスの設計で同じ過ちを繰り返さなくて済む」
カオリはホワイトボードに向かった。
INFO
SOA(Service Oriented Architecture)は2000年代に普及した設計思想で、企業システムを再利用可能なサービスとして構成します。ESB(Enterprise Service Bus)を中心とした実装が多く、複雑な統合レイヤーがボトルネックになりやすい問題がありました。
SOAとは何だったのか
SOAは「サービス」という単位でシステムを構成する。顧客サービス、注文サービス、在庫サービスがあり、それらがESBを通じて通信する。
ESBの問題
ESBは「全ての通信はここを通る」という中央集権的な設計だ。これが問題を引き起こした。
<!-- SOA時代のESB設定例(WSDLより単純化)-->
<service name="OrderService">
<interface name="IOrderService">
<operation name="createOrder">
<input message="CreateOrderRequest"/>
<output message="CreateOrderResponse"/>
<fault message="OrderFault"/>
</operation>
</interface>
<binding name="OrderServiceSOAP">
<!-- SOAP/XML over HTTP -->
<soap:binding style="document"/>
</binding>
<orchestration>
<!-- ESBがビジネスロジックを持ち始める... これが問題 -->
<step name="validateOrder" service="ValidationService"/>
<step name="checkInventory" service="InventoryService"/>
<step name="processPayment" service="PaymentService"/>
<step name="sendConfirmation" service="NotificationService"/>
</orchestration>
</service>ESBが「賢い配管工」になり始める。ルーティング、変換、オーケストレーション、ロジックまで持つようになる。
結果:ESBがスパゲッティの温床になり、デプロイのボトルネックになった。
マイクロサービスとの本質的な違い
| 観点 | SOA | マイクロサービス |
|---|---|---|
| 通信基盤 | ESB(中央集権) | 分散(P2P) |
| サービスサイズ | 大きい(機能の塊) | 小さい(1つの責務) |
| データ共有 | 共有DBが多い | DB per Service |
| 技術スタック | 統一(SOAP/XML) | 多様性を許容 |
| デプロイ単位 | 統一デプロイ | 独立デプロイ |
| チーム構造 | 機能横断チーム | プロダクトチーム |
Martine Fowlerの名言:
マイクロサービスはSOAが正しく実装された場合に辿り着くものだ
SOAから学ぶべき教訓
教訓1: 通信のボトルネックを作るな
# NG: 全ての通信が単一のゲートウェイを通る(SOAのESBと同じ過ち)
class ApiGateway
def process(request)
case request.service
when 'orders'
# ここで変換・ルーティング・ログ・認証・全部やる
validate(request)
transform(request)
log(request)
forward_to_order_service(request)
transform_response(response)
when 'inventory'
# ...
end
end
end
# OK: ゲートウェイは薄く保ち、各サービスが独立して賢い
# サービス間は直接通信か、軽量なメッセージングを使う教訓2: 共有データベースは避ける
SOAの失敗パターンの一つが「共有DB」だ。
# NG: 複数サービスが同じテーブルに直接アクセス
# 注文サービス
OrderRecord.where(user_id: user_id)
# 分析サービスも同じテーブルに直接アクセス
OrderRecord.where(created_at: last_month).group(:status).count
# → どちらかがスキーマ変更すると両方が壊れる
# OK: サービスは自分のDBを持ち、他サービスへはAPIでアクセス
# 分析サービスはAPIを通じてデータを取得し、自前のDWHに保存
AnalyticsService.fetch_orders_from_api(period: last_month)教訓3: ロジックをバスに載せるな
# NG: メッセージングシステムにビジネスロジックが混入
class EventBusProcessor
def process(event)
if event.type == 'order.placed'
# ここでビジネスロジックを書き始めると...
if event.data[:total] > 10000
trigger_fraud_check(event)
end
if event.data[:user_new?]
send_welcome_discount(event)
end
# どんどん膨らんでいく = ESBの再現
end
end
end
# OK: バス/キューはシンプルな配達係
# ロジックは各サービスが持つ
class FraudDetectionService
def on_order_placed(event)
return unless event.total > 10000
check_fraud_risk(event.order_id)
end
endRailsでのサービス分割パターン
実際にRailsモノリスをサービスに分割するとき、どうアプローチするか。
ストラングラー無花果パターン
# API Gatewayのルーティング設定(nginx example)
# /etc/nginx/conf.d/api_gateway.conf
upstream monolith {
server rails-app:3000;
}
upstream payment_service {
server payment-service:8080;
}
upstream notification_service {
server notification-service:8081;
}
server {
# 新しいサービスへのルーティング
location /api/v1/payments {
proxy_pass http://payment_service;
}
location /api/v1/notifications {
proxy_pass http://notification_service;
}
# まだモノリスに残っているもの
location / {
proxy_pass http://monolith;
}
}サービス間通信のRails実装
# lib/services/inventory_client.rb
class InventoryClient
include HTTParty
base_uri ENV.fetch('INVENTORY_SERVICE_URL')
def check_availability(product_id:, quantity:)
response = self.class.get(
"/api/v1/products/#{product_id}/availability",
query: { quantity: quantity },
headers: {
'Authorization' => "Bearer #{service_token}",
'Content-Type' => 'application/json',
'X-Request-ID' => RequestStore.store[:request_id]
},
timeout: 5 # タイムアウトは必須
)
raise ServiceUnavailableError unless response.success?
response.parsed_response.deep_symbolize_keys
rescue HTTParty::Error, Timeout::Error => e
# サーキットブレーカーパターン
circuit_breaker.handle_failure(e)
raise ServiceUnavailableError, "在庫サービスが利用できません"
end
private
def service_token
# サービス間認証トークン(短命なJWT)
JWT.encode(
{ iss: 'order-service', exp: 5.minutes.from_now.to_i },
Rails.application.credentials.service_key
)
end
def circuit_breaker
@circuit_breaker ||= CircuitBreaker.new(
failure_threshold: 5,
recovery_timeout: 30.seconds
)
end
endサーキットブレーカーパターン
分散システムの必須パターン。依存サービスが落ちたとき、自分も道連れにならないための防護壁。
# lib/circuit_breaker.rb
class CircuitBreaker
STATES = [:closed, :open, :half_open].freeze
def initialize(failure_threshold: 5, recovery_timeout: 30)
@failure_threshold = failure_threshold
@recovery_timeout = recovery_timeout
@failure_count = 0
@state = :closed
@last_failure_time = nil
end
def call(&block)
case @state
when :open
if recovery_timeout_elapsed?
@state = :half_open
try_call(&block)
else
raise CircuitOpenError, "サービスが利用できません(回路開放中)"
end
when :half_open
try_call(&block)
when :closed
try_call(&block)
end
end
private
def try_call(&block)
result = block.call
on_success
result
rescue => e
on_failure
raise
end
def on_success
@failure_count = 0
@state = :closed
end
def on_failure
@failure_count += 1
@last_failure_time = Time.current
if @failure_count >= @failure_threshold
@state = :open
end
end
def recovery_timeout_elapsed?
@last_failure_time && (Time.current - @last_failure_time) >= @recovery_timeout
end
endAWSでのSOA/マイクロサービス構成
# AWS SAM / CloudFormation でサービスメッシュ構成
# App Mesh を使ってサービス間通信を管理する例
Resources:
AppMesh:
Type: AWS::AppMesh::Mesh
Properties:
MeshName: myapp-mesh
OrderServiceVirtualService:
Type: AWS::AppMesh::VirtualService
Properties:
MeshName: !GetAtt AppMesh.MeshName
VirtualServiceName: order-service.myapp.local
Spec:
Provider:
VirtualRouter:
VirtualRouterName: order-router
# ヘルスチェックとサーキットブレーカー
InventoryServiceVirtualNode:
Type: AWS::AppMesh::VirtualNode
Properties:
NodeName: inventory-node
Spec:
Listeners:
- PortMapping:
Port: 8080
Protocol: http
HealthCheck:
HealthyThreshold: 2
IntervalMillis: 5000
TimeoutMillis: 2000
UnhealthyThreshold: 3分散システムの現実
SOAとマイクロサービス、どちらを選んでも分散システムの問題は避けられない。
分散システムの8つの誤った仮定(Peter Deutsch):
1. ネットワークは信頼できる
2. レイテンシはゼロ
3. 帯域幅は無限
4. ネットワークはセキュア
5. ネットワークトポロジーは変わらない
6. 管理者は一人
7. 転送コストはゼロ
8. ネットワークは均一
→ これらは全て誤りだ
WARNING
分散システムの複雑さを過小評価しないでください。ネットワーク障害、タイムアウト、部分的な失敗、データの不整合—これらは全て必ず起きます。「起きたとき」ではなく「起きることを前提に」設計することが重要です。
「SOAから学べることは多い」とカオリはまとめた。「ESBという失敗パターンと、分散システムの現実。これを知った上でマイクロサービスに進むのと、知らないで進むのでは大きく違う」
次章ではサーバーレスアーキテクチャへ進む。「関数」という単位でシステムを考える新しいパラダイムを、AWS Lambda と Step Functions で実装する。