Protocol Buffers RPCにおける破壊的変更の安全な管理手法

Protocol BuffersのRPCサービスにおける破壊的変更を段階的に管理する手法を、具体的なコード例とともに詳しく解説します。

Protocol Buffers RPCにおける破壊的変更の安全な管理手法

マイクロサービスアーキテクチャが普及する中、gRPCとProtocol Buffersを使用したサービス間通信は標準的な選択肢となっています。しかし、サービスの成長とともに避けられないのが破壊的変更への対応です。APIの削除、メッセージ構造の変更、フィールドの廃止といった変更を、いかに安全かつ計画的に実施するかは、システム運用における重要な課題です。

本記事では、Protocol BuffersのRPCサービスにおける破壊的変更を段階的に管理する手法を、具体的なコード例とともに詳しく解説します。

なぜ破壊的変更管理が重要なのか

Protocol Buffersは優れた後方互換性を持ちますが、それでも以下のようなケースでは破壊的変更が避けられません:

  • 非効率なAPIの削除: 初期設計の問題により生まれた非効率なRPCメソッド
  • セキュリティ上の問題: 脆弱性を持つフィールドやメッセージの廃止
  • ビジネス要件の変化: サービス仕様の大幅な変更による既存APIの不適合

こうした変更を無計画に実施すると、依存するクライアントサービスの障害やデータ損失といった深刻な問題を引き起こします。

段階的変更管理のライフサイクル

破壊的変更を安全に実施するため、以下の4段階のアプローチを推奨します:

  1. Phase 0: 計画・準備段階
  2. Phase 1: 非推奨マークと新API追加
  3. Phase 2: 移行促進期間
  4. Phase 3: 削除実行

それぞれの段階を、具体的なユーザーサービスの例で見ていきましょう。

Phase 0: 初期サービス定義

まず、後に破壊的変更が必要になるサービスの初期状態を見てみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// user_service.proto v1.0.0
syntax = "proto3";

package userservice.v1;

service UserService {
  // ユーザー作成
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
  
  // ユーザー情報取得(後で非推奨になる予定)
  rpc GetUserInfo(GetUserInfoRequest) returns (GetUserInfoResponse);
  
  // ユーザー更新
  rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
  int32 age = 3;  // 後で削除予定
}

message CreateUserResponse {
  User user = 1;
}

message GetUserInfoRequest {
  string user_id = 1;
}

message GetUserInfoResponse {
  User user = 1;
  string legacy_format = 2;  // 後で削除予定
}

message User {
  string id = 1;
  string name = 2;
  string email = 3;
  int32 age = 4;         // 後で削除予定
  string department = 5;  // 後で削除予定
}

この初期バージョンには、将来的に問題となる以下の要素が含まれています:

  • GetUserInfo RPC: 機能が限定的で効率が悪い
  • age フィールド: プライバシーの観点から削除が必要
  • legacy_format フィールド: 旧システム向けの形式で不要
  • department フィールド: より詳細なプロフィール情報に統合予定

Phase 1: 非推奨マークと新API追加 (v1.1.0)

破壊的変更の第一段階では、旧APIを維持しつつ新しいAPIを追加します。重要なのは、旧APIに deprecated = true を設定し、代替手段を明示することです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// user_service.proto v1.1.0
syntax = "proto3";

package userservice.v1;

service UserService {
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
  
  // 非推奨マーク: GetUserDetails に移行してください
  rpc GetUserInfo(GetUserInfoRequest) returns (GetUserInfoResponse) {
    option deprecated = true;
  }
  
  // 新しいAPI: 改善された機能
  rpc GetUserDetails(GetUserDetailsRequest) returns (GetUserDetailsResponse);
  
  rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
  
  // 新API: より柔軟なユーザー更新
  rpc UpdateUserProfile(UpdateUserProfileRequest) returns (UpdateUserProfileResponse);
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
  int32 age = 3;  // 非推奨: birth_date を使用してください
  
  // 新フィールド追加
  string birth_date = 4;  // YYYY-MM-DD形式
}

// 非推奨メッセージ
message GetUserInfoRequest {
  option deprecated = true;
  string user_id = 1;
}

message GetUserInfoResponse {
  option deprecated = true;
  User user = 1;
  string legacy_format = 2;  // 非推奨
}

// 新しいメッセージ
message GetUserDetailsRequest {
  string user_id = 1;
  // 追加オプション
  bool include_profile = 2;
  bool include_preferences = 3;
}

message GetUserDetailsResponse {
  UserDetails user = 1;
}

message UserDetails {
  string id = 1;
  string name = 2;
  string email = 3;
  string birth_date = 4;
  UserProfile profile = 5;
  UserPreferences preferences = 6;
}

message UserProfile {
  string department = 1;
  string title = 2;
  string location = 3;
}

message UserPreferences {
  string language = 1;
  string timezone = 2;
  bool email_notifications = 3;
}

Phase 1での運用チェックリスト

1
2
3
4
5
6
7
## チェックリスト - Phase 1: 非推奨マーク
- [ ] 新APIを追加し、十分にテスト済み
- [ ] 旧APIに `option deprecated = true` を追加
- [ ] 非推奨フィールドにコメントで代替手段を明記
- [ ] クライアント向け移行ガイド文書を作成
- [ ] 非推奨API使用状況の監視を開始
- [ ] 移行期間(3-6ヶ月)をアナウンス

Phase 2: 移行期間中 (v1.2.0 - v1.5.0)

移行期間中は、旧APIを維持しながらクライアントの移行を促進します。この段階では監視とサポートが重要になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// user_service.proto v1.3.0 (移行期間中バージョン)
syntax = "proto3";

package userservice.v1;

service UserService {
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
  
  // 非推奨API - 削除予定日: 2024-12-31
  rpc GetUserInfo(GetUserInfoRequest) returns (GetUserInfoResponse) {
    option deprecated = true;
    // 警告: この API は v2.0.0 で削除されます
  }
  
  rpc GetUserDetails(GetUserDetailsRequest) returns (GetUserDetailsResponse);
  rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
  rpc UpdateUserProfile(UpdateUserProfileRequest) returns (UpdateUserProfileResponse);
  
  // さらなる改善API追加
  rpc BatchGetUsers(BatchGetUsersRequest) returns (BatchGetUsersResponse);
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
  
  // 条件付きサポート: birth_date が提供された場合は age を無視
  int32 age = 3 [deprecated = true];  // 削除予定: 2024-12-31
  string birth_date = 4;
}

// reserved を使用して安全な削除準備
message User {
  // 段階的フィールド削除準備
  reserved 4, 5;  // age, department の番号を予約
  reserved "age", "department";  // 名前も予約
  
  string id = 1;
  string name = 2;
  string email = 3;
  string birth_date = 6;
}

message GetUserInfoResponse {
  option deprecated = true;
  // legacy_format フィールドは削除準備
  reserved 2;
  reserved "legacy_format";
  
  User user = 1;
}

Phase 2での運用チェックリスト

1
2
3
4
5
6
7
## チェックリスト - Phase 2: 移行期間
- [ ] 非推奨API使用量が50%以下に減少
- [ ] 主要クライアントの移行完了確認
- [ ] 削除予定日をAPIドキュメントに明記
- [ ] 警告レベルの監視アラート設定
- [ ] 移行支援ツール提供(必要に応じて)
- [ ] 次期バージョンでの削除をアナウンス

Phase 3: 削除実行後 (v2.0.0)

最終段階では、メジャーバージョンアップとともに旧APIを完全に削除します。ここで重要なのはreserved による番号保護です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// user_service.proto v2.0.0 (メジャーバージョンアップ)
syntax = "proto3";

package userservice.v2;  // パッケージバージョンも更新

service UserService {
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
  
  // GetUserInfo は完全削除
  // rpc GetUserInfo は削除済み
  
  rpc GetUserDetails(GetUserDetailsRequest) returns (GetUserDetailsResponse);
  rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
  rpc UpdateUserProfile(UpdateUserProfileRequest) returns (UpdateUserProfileResponse);
  rpc BatchGetUsers(BatchGetUsersRequest) returns (BatchGetUsersResponse);
  
  // 新機能追加
  rpc SearchUsers(SearchUsersRequest) returns (SearchUsersResponse);
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
  
  // age フィールドは完全削除、番号は予約済み
  reserved 3;
  reserved "age";
  
  string birth_date = 4;  // 必須フィールドに変更
}

// 削除されたメッセージの番号を予約して再利用を防ぐ
reserved 100 to 110;  // 削除されたメッセージ番号範囲
reserved "GetUserInfoRequest", "GetUserInfoResponse";

message User {
  // 削除済みフィールドの番号と名前を予約
  reserved 4, 5;  // 旧 age, department
  reserved "age", "department";
  
  string id = 1;
  string name = 2;
  string email = 3;
  string birth_date = 6;
  
  // 新フィールド追加可能
  string phone_number = 7;
}

message UserDetails {
  string id = 1;
  string name = 2;
  string email = 3;
  string birth_date = 4;
  UserProfile profile = 5;
  UserPreferences preferences = 6;
  
  // v2.0.0 で新機能追加
  repeated string tags = 7;
  int64 created_at = 8;
  int64 updated_at = 9;
}

message SearchUsersRequest {
  string query = 1;
  int32 limit = 2;
  string page_token = 3;
  UserSearchFilter filter = 4;
}

message SearchUsersResponse {
  repeated UserDetails users = 1;
  string next_page_token = 2;
  int32 total_count = 3;
}

message UserSearchFilter {
  string department = 1;
  string location = 2;
  bool active_only = 3;
}

Phase 3での運用チェックリスト

1
2
3
4
5
6
7
## チェックリスト - Phase 3: 削除実行
- [ ] メジャーバージョン (v2.0.0) としてリリース
- [ ] 削除対象API/フィールドを完全除去
- [ ] reserved で番号と名前を予約済み
- [ ] 移行ドキュメントを更新
- [ ] 旧バージョンのサポート終了日を告知
- [ ] 新機能の追加とテスト完了

運用におけるベストプラクティス

1. 監視とメトリクス収集

各段階で適切な監視を行うことが成功の鍵です:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// サーバー側実装例(Go)
func (s *UserService) GetUserInfo(ctx context.Context, req *pb.GetUserInfoRequest) (*pb.GetUserInfoResponse, error) {
    // 非推奨API使用時の警告ログ
    log.Warn("GetUserInfo API is deprecated, use GetUserDetails instead", 
             "client", getClientFromContext(ctx))
    
    // メトリクス収集
    deprecatedAPICounter.WithLabelValues("GetUserInfo").Inc()
    
    // 内部的に新APIにフォワード
    detailsReq := &pb.GetUserDetailsRequest{
        UserId: req.UserId,
        IncludeProfile: true,
    }
    return s.getUserDetailsInternal(ctx, detailsReq)
}

2. クライアント側の対応戦略

クライアント側では、バージョン対応と段階的移行を実装します:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class UserServiceClient:
    def __init__(self, stub):
        self.stub = stub
        self._use_new_api = self._check_server_version()
    
    def get_user(self, user_id: str) -> User:
        if self._use_new_api:
            # 新API使用
            request = GetUserDetailsRequest(
                user_id=user_id,
                include_profile=True
            )
            response = self.stub.GetUserDetails(request)
            return self._convert_user_details(response.user)
        else:
            # 旧APIをフォールバック
            request = GetUserInfoRequest(user_id=user_id)
            response = self.stub.GetUserInfo(request)
            return response.user
    
    def _check_server_version(self) -> bool:
        # サーバーのバージョンを確認して新APIサポートを判定
        try:
            # ヘルスチェックやバージョンAPIで確認
            return True
        except:
            return False

3. 段階別管理フレームワーク

破壊的変更管理のための体系的なアプローチ:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# Protocol Buffers 破壊的変更管理ガイド

## Phase 0: 計画・準備段階
### チェックリスト
- [ ] 変更影響範囲の調査完了
- [ ] 既存クライアントリストの作成
- [ ] 移行計画の策定(3-6ヶ月スケジュール)
- [ ] 監視システムの準備
- [ ] バックアップ計画の確認

## Phase 1: 非推奨マーク・新API追加 (v1.1.0)
### 実施内容
- 新APIの追加と十分なテスト
- 旧APIに `deprecated = true` マーク
- 移行ガイドドキュメント作成

### 監視項目
- 旧API使用率の定期測定
- エラーログの監視
- クライアント別使用状況の追跡

## Phase 2: 移行促進期間 (v1.2.0 - v1.5.0)
### 実施内容
- 削除予定日の明確化
- 積極的な移行支援
- 警告レベルの監視設定

### 成功指標
- 旧API使用率 < 10%
- 主要クライアント移行完了率 > 90%
- 新API安定性確認

## Phase 3: 削除実行 (v2.0.0)
### 実施内容
- メジャーバージョンアップ
- 完全削除と reserved 設定
- リリースノート詳細記載

### 事後確認
- 削除後の動作確認
- 残存クライアントへの対応
- パフォーマンス改善確認

緊急時の対応戦略

予期しない問題が発生した場合の対応手順も準備しておくことが重要です:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
## 緊急ロールバック手順
1. **即座の対応**: 旧バージョンへのトラフィック切り戻し
2. **影響調査**: 破綻したクライアントの特定
3. **修正版リリース**: hotfix バージョンの準備
4. **再移行計画**: より慎重なスケジュール再設定

## フィールド番号の管理
- 削除済み番号の reserved 設定
- 番号衝突の防止
- 将来の拡張性確保

## 文書化要件
- API変更履歴の維持
- 移行ガイドの更新
- FAQ の継続メンテナンス

まとめ

Protocol BuffersのRPCにおける破壊的変更は、適切な段階的アプローチにより安全に管理できます。重要なポイントは:

  1. 計画性: 事前の影響調査と移行計画の策定
  2. 段階性: 即座の削除ではなく、非推奨→移行→削除の段階的実施
  3. 監視: 各段階での使用状況とエラーの継続監視
  4. コミュニケーション: クライアント開発者への適切な情報提供
  5. 安全性: reserved による番号保護と緊急時対応の準備

この手法により、サービスの進化を続けながらも安定性を保つことができ、マイクロサービスアーキテクチャの長期的な成功につながります。破壊的変更は避けられませんが、適切に管理することで、システム全体の品質向上を実現できるのです。


著者について: この記事は、大規模なマイクロサービス環境でのProtocol Buffers運用経験を基に執筆されています。実際のプロダクション環境での破壊的変更管理の知見を共有することで、同様の課題を抱える開発者の助けになれば幸いです。

技術ネタ、趣味や備忘録などを書いているブログです
Hugo で構築されています。
テーマ StackJimmy によって設計されています。