この記事は CBcloud Advent Calendar 2019 2日目の記事です。
一人目のエンジニアとしてCBcloudに参画し、CBcloudで技術選定・アーキテクチャ設計をさせていただいている徳盛です。
これまで、プロダクトのコードを扱うチームが一つだったため、モノリスでサービスを展開していましたが、組織改編とともにモノリスでのいろいろな弊害が発生してきたため、弊社でもマイクロサービスを推進してく流れになりました。今はできるところから少しずつ進めています。
なので、今回はマイクロサービス化を推進するに至った経緯と何から手をつけているかを書いていきます!
忙しい人向け要約
- 同一のコードベースを別部門のエンジニアが互いに修正して起こるバグや、データベースを共有している別のサービスが誕生したことによる開発スピードの低下をなんとかしたくて、マイクロサービスの検討を始めました。
- 始めの切り出しの対象選択は以下の基準で選定しました。
- テーブルがメイン機能から完全に分離されている部分(境界づけられたコンテキストにより分割されている可能性が高いのでサービスとして切り出しやすい)
- 他のプロダクトでも利用する可能性が高い部分(サービスとして切り出す意味がある)
- サービスの根幹に影響を与えない部分
- データの整合性をそこまで気にする必要がない部分
- 技術的に実現難易度が高そうなサービス間連携から着手しました。
- 結果AWSのSNS・SQS(スタンダード)を利用すると2日ぐらいでサービス間連携の基礎は実装できました。
マイクロサービス化を検討した理由
弊社では物流業界の根本から変えていくために、日々新しいプロダクトの検討・新規開発が行われています。今まではモノリスのサービスを一つのチームでメンテナンスしていましたが、プロダクトが増えてきたこともあり、同一のコードを別の部署の人が修正したり、プロダクト間で共通化できそうな部分が実感できるようになってきました。また、DBを共有している別のサービスも生まれ、両サービスの開発スピードを担保するのが難しくなってきました。
そのため、これまでの開発スピードを担保するために、個々のサービスがある程度の責務を負って開発スピードを担保できる、マイクロサービス化の検討に踏み切った次第です。
今やっていること
マイクロサービスを運用する上で考えるべき部分は多いかと思いますが、サービスはすでに本番運用されているため、一旦は影響の少ない部分で少しずつやっていくためにどうしたら良いか考えました。
実証実験として切り出しやすい機能を選定する
選定基準は以下で選定しました。
- テーブルがメイン機能から完全に分離されている(境界づけられたコンテキストにより分割されている可能性が高いのでサービスとして切り出しやすい)
- 他のプロダクトでも利用する可能性が高い(サービスとして切り出す意味がある)
- サービスの根幹に影響を与えない部分
- データの整合性をそこまで気にする必要がない部分
せっかく根幹に影響を与えない部分を選んだなら、重要そうで難しそうな部分からトライする
イベントベースのマイクロサービス間の連携部分の実装を優先しました。
もともとサーバレスでの別サービスの運用を一部していましたが、連携部分がリクエスト/レスポンス形式(オーケストレーション)になっていました。そこで、今後の本格的な運用も考え、イベントベース(コレオグラフィ)の手法をAWSのSNS、SQSを利用してやってみました。
オーケストレーション、コレオグラフィの具体的な説明はネット上に転がっているので、今回は説明しません。
SNS、SQSを利用したサービス間連携部分の実装について
今回実装したデータの流れは以下の通りです。
各サービスでそれぞれSQSを所持し、自分に関連するSNSのトピック(イベント)を購読(サブスクリプション登録)しています。下図の場合、サービスB・CがトピックAを購読しているため、サービスAがトピックAにイベントを登録すると、サービスB/サービスCのSQSにデータがインサートされる仕組みになっています。
各々のサービスは自前のSQSを監視し、必要に応じた処理を行っています。
SNSの設定とRoRからのイベントパブリッシュ
イベントのパブリッシュに関してはaws-sdkのgemを利用し、下記コードのみで実現できます。
sdkだとjson形式のデータが送信できなかったため、stringに変換しました。
# SNSへ到着報告イベントのパブリッシュ
def publish_arrival_event()
message = {
~イベントのbody~
}
begin
@@topic.publish(
{
message: JSON.generate(message)
}
)
rescue
raise "SNS Publish Failed: #{JSON.generate(message)}"
end
end
SNS・SQS連携
SQSの種類として、スタンダード(取得順、重複排除が保証されない)かFIFO(取得順番、重複排除が保証される)がありますが、SNSがFIFOをサポートしていない、かつ順番がそこまで重要でなかったため、読み出し側のプログラムで冪等性を担保する前提で、今回はスタンダードで実装しました。もし、処理の内容的に、FIFOの取得順番、重複排除が重要なであれば、SNS→Lambda→SQSの経路で連携が可能です。
SQSの設定とScala(Java)のSQSから情報取得
Actorで定期的にSQSからデータを取得して処理しています。
メッセージの取得に成功し、処理が完了したら、Queueからデータを削除しています。
また、SQSの設定上、数回失敗するとdeadletterqueueに入るようになっています。
class LinkArrivalStatusServiceActor
extends Actor
with MixInFEService {
implicit val materializer = ActorMaterializer()
lazy val accessKey = "AWSアクセスキー"
lazy val secretKey = "AWSシークレットキー"
lazy val credentials = new BasicAWSCredentials(accessKey,secretKey)
lazy val QUEUE_URL = "SQSのURL"
lazy val sqsClient = AmazonSQSClientBuilder.standard().withRegion("ap-northeast-1").withCredentials(new AWSStaticCredentialsProvider(credentials)).build()
def receive = {
case msg: String => {
// queueからデータを取得すると取得したメッセージはinvisible状態になる
val messages = sqsClient.receiveMessage(QUEUE_URL).getMessages().asScala
for (message <- messages) {
val body = message.getBody()
val json = Json.parse(body)
try {
~冪等性を担保した処理をする~
// queueからデータを削除
sqsClient.deleteMessage(QUEUE_URL, message.getReceiptHandle())
} catch {
// queueから削除しなければ自動でvisible状態になる
case ex:Exception => Logger.error(ex.getMessage)
}
}
}
}
}
結論
SNS、SQSを利用したイベントベースのサービス間連携はかなり簡単に実装ができました。2日ぐらい。
とはいえ、細かいところまであまり考えていないので、今のところ大丈夫ですが、おそらく本番運用するにあたり、不都合が発生する箇所は出てくるかと思います。
また、データの整合性どうするとかサービスをどう切り分けるのかとか、まだまだ課題は山積みです。
とはいえ、本番で運用していかないことには知見もたまっていかないため、少しずつ進めていこうと考えています。完璧なマイクロサービス化へ移行するまでの道のりをシリーズものとして、記事にしていければと思っていますのでよろしくお願いいたします!