この記事は、 Mercari Bold Challenge Month の9日目の記事です。
はじめに
こんにちは。メルカリBackendでTech Leadをしている @kentanです。
私のいるTnSドメインチームでは現在、ACL(Access Control List)と呼ばれる機能のマイクロサービス化を進めております。
この記事ではどのようにACLのマイクロサービス化を進めていったのかを紹介していきたいと思います。
ACLとは
ACLとはAccess Control Listの略であり、一般的にユーザのリソースへのアクセス制御をするための機能です。メルカリでは不正な行為や規約に違反したユーザに対して購入や出品の制限をかけるために使用しています。利用規約に反する出品や行為をなくすことにより、安心・安全なサービスの提供に努めております。
マイグレーションの方針
一般的にメルカリではAPIのエンドポイントごとにマイクロサービス化を進めております。
例えば /mercari/item/get_item のようなendpointがあったときに、これに対応するエンドポイントを呼び出すためのマイクロサービスを作成するようなやり方です。
ACLの場合は、その機能の特徴から、様々なエンドポイントから参照されておりました。そのため、そのまま移植をしようとするとモノリスであるPHP内でのACLに対する参照が多数残ったままで移植を行う必要があり、移植の複雑度が増します。理想的に言えば、呼び出し元となっているすべての他機能のマイクロサービス化が終わったあとに、移植を行うのが好ましいです。しかしながら、ACLでは別途ビジネス要件としてデータベーススキーマの変更が必要となる大幅アップデートをする必要が生じており、それらのマイクロサービス化の完了を待つことがスケジュール的に難しいと言う状況がありました。
そのため、他の移植終了を待たずにマイクロサービス化も行うことを決断しました。
その結果移植元であるモノリスのPHPの内部から、関数呼び出しであった部分を、gRPCの通信に置き換えるという作業を行う必要がありました。
モノリスのリファクタリング
移植のためにソースコードを読み込んでみると、モノリスのアーキテクチャを前提して作られ、また長年の技術的負債の蓄積も重なっており、そもそもACLとよべる機能範囲が設計として明確に切り分けられていないという問題に遭遇しました。そのため、マイクロサービスとしてACLを切り出すまえに、まずはマイグレーションに備えてのリファクタリングを行うことにしました。
依存関係の整理
例えば下記のような依存関係になっているコードが多数ありました。
class X { function doSomething(){ acl->doAcl(); } } class Acl { function doAcl(X x){ .... x->doAnotherThing(); } }
モノリスであるならば、設計上の巧拙はさておき、このような処理も実際として問題ではありませんでしたが、マイクロサービスの移植を考えた場合、これは異なるマイクロサービスを跨ぐ処理が何度も走ることになります。処理が絡み合って理解がしづらいのに加えて、その間の呼び出し処理はgRPCによる通信であるため、不要なオーバヘッドを生じさせる原因になるという問題もあります。
そのため、ロジックの影響を考慮しながら、できる限り下記のような実行順序に置き換える作業を行いました
class X { function doSomething(){ acl->doAcl(); doAnotherThings(); } } class Acl { function doAcl(){ .... } }
SQLのjoinの対応
メルカリのモノリスではすべてのデータがMySQL上に保存されています。そのため、下記のようなSQL文が当然のごとく存在しました。
select * from acl join x on acl.y = x.y where z
マイグレーションが終了したあとは、ACLのテーブルもACLマイクロサービスが動くGoogle Compute Platform上に移動することになるので、このjoin文は変更する必要がありました。非常に簡単に説明すると、aclとxのデータをfetchして、where句や結合条件などで行っていたレコードの絞り込みの処理を、PHP上で行うように処理を書き換えました。joinを使えば、全てMySQLの稼働するサーバのメモリ上で完結していたのが、一度PHPに持ってきて、絞り込みを行わなければ行けないため、処理速度やメモリの使用量にかなり気を使いながら変更を行っていきました。
Facadeの導入
ACLへの呼び出しは、モノリスにおいては単純な関数呼び出しでした。そのため、様々な箇所からACLに関する参照がありました。
これをこのままマイクロサービスに置き換えようとしますと、すべての箇所をgRPCの通信を行うためのコードに書き換える必要があります。
現行のコード
function A(){ Acl->doAcl() } function B(){ ACL->doAcl() } function C(){ ACL->doACL() }
切り替えのためのPull Request
function A(){ [ACL-MSへのgRPCクライアント] -> doAcl() } function B(){ [ACL-MSへのgRPCクライアント] -> doAcl() } function C(){ [ACL-MSへのgRPCクライアント] -> doAcl() }
メルカリでは、マイクロサービスをリリースする場合、基本的に一度に100%開放することはありません。Gatewayなどでマイクロサービスとモノリスへの振り分けを行うことにより、バグの確認やパフォーマンスなどを見ながら、10%,20%と徐々に行っていきます。
ACLでは、この振り分けはGatewayで行わず、機能Aの呼び出しが新しく追加する<gRPC通信コード>か既存の<ACLのxxx function>か、といった形で切り替えることになります。そのため、このまま素直に行うと、開放率を上げるためのPull Requesが複雑になるという問題がありました。開放率を変化は、予期せぬ事態を避けるため、なるべくロジックなどを含んだPRではなく、設定の変更のようなシンプルな形で行うのが望ましいです。それらを実現するために、ACLへのアクセスするためのFacadeを用意することにしました。
Switchable facade
このFacadeに加えて、マイクロサービスにアクセスするためのFacadeを用意しました。開放率を上げてく過程で、マイクロサービスを使用させる場合はマイクロサービスアクセス用のFacadeを、そうでない場合は既存のFacadeを参照させます。これの切り替えはFactoryクラスとA/Bテストを利用して実現しました。
A/Bテストの利用
開放率の変更には、メルカリ内で使用されているA/Bテストの機能を利用することにしました。一般的にA/Bテストは、ユーザの集合をGroup1, Group2のように分けて、Group1にだけ新機能を使わせるようにしたりするのに使用しますが、ここではACLのMSを新機能に見立ててPHPのコードの中で使用するFacadeを切り替えるコードを書きました。
<?php class FacadeFactory { public static function getFacadeOnRead() { if (A/Bテストの結果、MSを使わせる){ return new FacadeForMS(); } else{ return new FacadeForExistingAcl(); } } public static function getFacadeOnWrite( { if (A/Bテストの結果、MSを使わせる){ return new FacadeForMS(); } else{ return new FacadeForExistingAcl(); } } } abstract class Facade{ abstract public function get(); abstract public function write(); abstract public function update(); abstract public function delete(); } class FacadeForMS extends Facade{ ... } class FacadeForExistingAcl extends Facade{ ... }
マイクロサービスを使わせる際には、安全のためにまずは読み込み(Read)の部分だけ使用させたいため、ReadとWriteの切り替えが別々にできるようにしています。また、ここでは詳細は割愛しますが、テスト環境や、呼び出しもとの状況などを考慮してreturnするFacadeを柔軟に設定できるようにしてあります
これの結果、開放率の変更のためのPull RequestはA/Bテストのconfigファイルを作成するだけですむようになりました。コードを直接切り替えるのに比べてずっとシンプルに変更ができるようになりました。
終わりに
今回は「ACLにおけるマイクロサービス開発の話 」について紹介しました。誠にありがとうございました。
明日の記事は、@stersによる「INT 32 障害とその BOLD な対策」です。お楽しみに!