🕌

マイクロサービスアーキテクチャの密結合を疎結合にしてみた

2021/08/30に公開約4,200字

Agenda

  • はじめに
    • 想定読者
  • 3つの「密」
  • 密結合により生じていた課題
    1. liveness, readness を用いたヘルスチェックを REST のサーバーのみでしか行っていない
    2. gRPC サーバーにしか Graceful Shutdown の実装が入っていなかった
    3. DB schema migration はサーバーと同居している Pod にコマンドを実行する仕様になっていた
  • 問題解決へのアプローチ
    1. command から同一のドメインモデル、ユースケースを使用するディレクトリ構成でサーバーを分ける作業
    2. デプロイメントパイプラインを分ける作業
    3. Dockerfile のマルチステージビルド & スリム化
  • 最終的な構成
  • まとめ

はじめに

こんにちは。 株式会社 Magic Moment ではマイクロサービスアーキテクチャを採用しており、GCP のマネージド Kubernetes である GKE 上で運用しております。

今回は、これまでマイクロサービスアーキテクチャで構築・運用してきた中で、1 つの Pod に複数のプロセスが密結合していたことによる課題と、それら課題への対応についてご説明したいと思います。

想定読者

  • Kubernetes について多少の理解がある人
  • GKE 等マネージド Kubernetes の運用経験や興味がある人

3つの「密」

マイクロサービス間の通信は主に gRPC を使用していますが、一部のサービスでは外部から REST を用いて通信を受け付ける要件や、同一サービス内で Database の Schema Migration を管理したい、といった要件がありました。
結果、1 つの Pod に gRPC サーバー, REST サーバー、Schema Migration を実行するプロセスが同居する構成となっていました。

密結合により生じていた課題

上記の構成により生じていた課題として、以下の 3 つを上げました。

  1. liveness, readiness を用いたヘルスチェックを REST のサーバーのみでしか行っていない
    • gRPC サーバーに対しては、gRPC probeによって、ヘルスチェックを実施する必要がありますが、liveness, readiness は 1Pod 内で1つしか設定できないため、コンテナ内のプロセス群にヘルスチェックが網羅されておらず可用性が低い構成になっていました
  2. gRPC サーバーにしか Graceful Shutdown の実装が入っていなかった
    • これは単なる実装漏れなのですが、kubelet から受け付けた SIGTERM を、 gRPC サーバしか正常に処理せず、REST サーバーは Pod の terminate に recreate 引きずられて、処理中であってもシャットダウンされていました
    • 2 つのサーバ ーが1つの Pod に同居していたため、Graceful Shutdown が正しく実行されていないことにしばらく気づきませんでした
  3. Database への Schema Migration は同居している Pod にコマンドを実行する実装になっていた
    • これにより、Job として実行できず、リリース時の手作業になっていました

このように、複数のプロセスを1つのコンテナイメージにまとめたことで密結合な構成になっており、いずれかのプロセスに深刻な問題が起きた際に芋づる式で影響が広がってしまう状態を脱する必要がありました。

解決へのアプローチ

解決していくには、3つのプロセスをそれぞれのコンテナイメージとして分離していく必要があります。

  1. command から同一のドメインモデル、ユースケースを使用するディレクトリ構成でサーバーを分ける作業
  2. デプロイメントパイプラインを分ける作業
  3. Dockerfile のマルチステージビルド & スリム化

1. command から同一のドメインモデル、ユースケースを使用するディレクトリ構成でサーバーを分ける作業

元々の構成として単一プロセスに特化したディレクトリ構成となっていました。

.
├── Dockerfile
├── domain
│   └── service
│       ├── delivery
│       ├── repository
│       └── usecase
├── go.mod
└── main.go

上記のディレクトリ構成を変更し、 REST, gRPC, migration のプロセスが一つになっているコンテナイメージの分離を行いました。

.
├── build
│   ├── Dockerfile.Rest
│   ├── Dockerfile.gRPC
│   ├── Dockerfile.Migrate
│   └── ci_cd.yaml
├── cmd
│   ├── grpc
│   ├── rest
│   └── migrate
├── domain
│   └── service
├── grpc
├── rest
└── migrations

2. デプロイメントパイプラインを分ける作業

上記のコンテナイメージの分離に付随したデリバリー、デプロイメントパイプラインを分離するため、 k8s のマニフェストをそれぞれ分ける作業を行いました。

Before

└── grpc-and-rest-and-migrate-service
    ├── base
    └── overlays
        ├── staging
        │   ├── service-deployment.yaml
        │   ├── service.yaml
        │   ├── kustomization.yaml
        │   └── service-migrate-job.yaml
        └── production

After

├── grpc
│   ├── base
│   └── overlays
│       ├── staging
│       │   ├── service-deployment.yaml
│       │   ├── service.yaml
│       │   ├── kustomization.yaml
│       │   └── service-migrate-job.yaml
│       └── production
└── rest
    ├── base
    └── overlays
        ├── staging
        └── production

また、gRPC, REST のマニフェストを分けたことで、それぞれに対して liveness, readiness probe の設定ができるようになりました。

3. Dockerfile のマルチステージビルド & スリム化

1 つの Dockerfile の責務が 1 つのプロセスになったことで、不要なものをイメージから除外することができるようになったため、マルチステージビルドを行い、イメージのスリム化をしました。

結果として、250MB → 10MB, 10MB, 5MB およそ 1/10 にスリム化ができました。

最終的な構成

1 つの Pod に gRPC, REST サーバー、Schema Migration を実行するプロセスが同居している構成から、kubelet から単一のプロセスに対してそれぞれ操作・管理が可能な構成となりました。

まとめ

密結合にしてしまったのは、マイクロサービスアーキテクチャに対して理解が浅かったことが原因でした。プロダクトをリリースし、運用が進むにつれて今回の課題に気づき、対応することができました。

理想的には1サービス1責務ですが、それが避けられない場合は、われわれのとったアプローチを参考にしてみてください。

Magic Moment では、日々成長するプロダクトのアーキテクチャを一緒に考えて開発してくださる方を募集しています。

https://www.wantedly.com/companies/magicmoment

Discussion

ログインするとコメントできます