TECH PLAY

株式会社メルカリ

株式会社メルカリ の技術ブログ

267

こんにちは。メルカリのソフトウェアエンジニアの @sintario_2nd です。 この記事は、連載: メルカリ ハロ 開発の裏側 – Flutterと支える技術 – の4回目と、 Mercari Advent Calendar 2024 の10日目の記事です。 この記事について メルカリ ハロは 2024年3月6日にサービスを開始しました。サービスローンチ後は開発チームの体制が変わり、わたしはGrowth Hackチームに配属されて Customer Relationship Management (CRM) のツールのインテグレーションに関わってきました。いろいろと一筋縄にはいかないこともあり試行錯誤したなかで、今回はプッシュ通知周りで遭遇した課題について、どのような調査を経てどのように解決したか、をご紹介したいと思います。 メルカリ ハロのプッシュ通知の当初構成 メルカリは各サービスへの通知を管理する microservice (Notification Service と呼ぶことにします) をすでに持っており、メルカリ ハロのバックエンド (Hallo Backend) から Notification Service 経由で Firebase Cloud Messaging (FCM) によるプッシュ通知が送られるという構成になっています。Hallo Backend からのメッセージがアプリの載っている端末にプッシュ通知として届くまでの図式としては下図のようになります。 サービスローンチまでは非常に短い時間であったということもあり、まずは最低限の機能を実現するべく、 Flutter で実装されたアプリ側は FlutterFire を使用して FlutterFire のガイドどおりに素直に実装されていました。 Braze を組み込む サービスローンチ後は一般的にどんなサービスでも利用拡大を目指していくものかと思います。そのため、一定のターゲット属性に当てはまる利用者群に向けてキャンペーンのメッセージをお届けする、といった CRM 施策がよく行われます。メルカリ ハロでは、CRMの分野ではよく使われている Braze を採用しています。 Braze は各種のメッセージ手段でキャンペーンを実施する仕組みを持っていて、 In-App Messaging / Content Cards / e-mail / プッシュ通知 などに対応しています。この分野では老舗ということもあり、 Flutter向け Braze SDK の組み込みガイド も提供されていたので書かれた通りに作業すればすんなり動くものと高をくくっていました。実際 IAM などは軽作業のみですんなり使えて拍子抜けしましたが、プッシュ通知については我々がもともと実装済みだった構成との兼ね合いで、想定よりも苦戦することになりました。以下では Android にフォーカスしてご紹介したいと思います。 Braze は Android のプッシュ通知には FCM を使う(※)ので、Braze がサービスに組み込まれると下図のように FCM の送信をトリガーする経路が2系統になります。アプリから見るとプッシュ通知は一律 FCM が送ってくることになるのでただ受信するだけなら単純なわけですが、実際にはそれぞれのプッシュ通知の着信率や開封率を知りたかったり、通知タップ後に何かしら特別なアクションがしたかったりしますよね。そのため受信したプッシュ通知が Hallo Backend 由来か Braze 由来かはアプリ側で通知の中身を見分け、仕分ける必要があります。 ※ iOS については Braze は APNs を直接扱うので少し異なるデータフローになりますが、本題からそれるので今回は説明省略します。 実際、なにも考えずに組み立て終わってレビューと機能検証をしていると、エンジニアの同僚やQAから「 プッシュ通知の2重受信が起きることがあるみたいだ 」と指摘されました。Braze SDK側は Braze 由来のプッシュ通知かどうかを見分けられるものの、もともとあった Hallo Backend 由来のプッシュを扱う実装が Braze からのプッシュ通知を見分けることができておらず、 Braze 由来のプッシュ通知が Hallo Backend 由来のプッシュ通知用のハンドラにも処理される2重ハンドリングが起きていました。 Braze からするとこういった FCM 発行者が複数になるパターンはよくある事案として想定されているようで、 Braze と関係ないメッセージを fallback で扱うための FirebaseMessagingService をAndroid プロジェクト側で指定できる ようになっています。braze.xml というリソースファイルに下記のように fallback の有効化と fallback サービス名を指定することになります。 <bool name="com_braze_fallback_firebase_cloud_messaging_service_enabled">true</bool> <string name="com_braze_fallback_firebase_cloud_messaging_service_classpath">com.company.OurFirebaseMessagingService</string> またこれを前提にしているからか、Dart 向けの braze_plugin.dart では RemoteMessage が Braze 由来のものかを判別するメソッドを提供していないようです。 ということは、FlutterFire が Dart 層までメッセージを流し込む役目をしている FirebaseMessagingService の実装クラスをこの fallback サービスに指定すればいいのかな、、、となるわけですが、 アプリの AndroidManifest.xml をみてもそんなクラスはどこにも見当たらず、 🤔どうしたものか、となったわけです。 FlutterFire を読む 読者の皆さんはもうお気づきでしょうが、私たちは今 Flutter (Dart の世界)の範疇では解決できない領域に足を踏み入れました。ライブラリの Dart の実装部分だけを読んでいてもどうにもなりません。ネイティブアプリの開発知識を駆使して向き合う必要があります。 プッシュ通知のようなネイティブの機能と密接に関わるものはライブラリにもネイティブ実装部分があります。Android の場合は FCM のハンドリングをするサービスを AndroidManifest.xml に宣言する必要があり、アプリ側の manifest ではなくライブラリ側にも AndroidManifest.xml があってビルド時にマージされるので、つまりは FlutterFire のリポジトリにある AndroidManifest.xml を調べてみるのが良いだろう、と当たりをつけたわけです。 ありました。 https://github.com/firebase/flutterfire/blob/_flutterfire_internals-v1.3.35/packages/firebase_messaging/firebase_messaging/android/src/main/AndroidManifest.xml <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.flutter.plugins.firebase.messaging"> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <!-- Permissions options for the `notification` group --> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <application> <service android:name=".FlutterFirebaseMessagingBackgroundService" android:permission="android.permission.BIND_JOB_SERVICE" android:exported="false"/> <service android:name=".FlutterFirebaseMessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT"/> </intent-filter> </service> <receiver android:name=".FlutterFirebaseMessagingReceiver" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND"> <intent-filter> <action android:name="com.google.android.c2dm.intent.RECEIVE" /> </intent-filter> </receiver> <service android:name="com.google.firebase.components.ComponentDiscoveryService"> <meta-data android:name="com.google.firebase.components:io.flutter.plugins.firebase.messaging.FlutterFirebaseAppRegistrar" android:value="com.google.firebase.components.ComponentRegistrar" /> </service> <provider android:name=".FlutterFirebaseMessagingInitProvider" android:authorities="${applicationId}.flutterfirebasemessaginginitprovider" android:exported="false" android:initOrder="99" /> <!-- Firebase = 100, using 99 to run after Firebase initialises (highest first) --> </application> </manifest> FlutterFirebaseMessagingService というのがそれなのかな?と思って開いてみると、これ自身は FCM token のリフレッシュを Dart の世界に引っ張り込む入口程度の薄いクラスなのがわかります。 public class FlutterFirebaseMessagingService extends FirebaseMessagingService { @Override public void onNewToken(@NonNull String token) { FlutterFirebaseTokenLiveData.getInstance().postToken(token); } @Override public void onMessageReceived(@NonNull RemoteMessage remoteMessage) { // Added for commenting purposes; // We don't handle the message here as we already handle it in the receiver and don't want to duplicate. } } We don't handle the message here as we already handle it in the receiver and don't want to duplicate. というコメントの通り、 RemoteMessage としてやってきているはずの通知メッセージの実処理は、実は FlutterFirebaseMessagingReceiver のほうが担っています。 なお、 <receiver android:name=".FlutterFirebaseMessagingReceiver" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND"> <intent-filter> <action android:name="com.google.android.c2dm.intent.RECEIVE" /> </intent-filter> </receiver> という記述を見て Android 開発のキャリアの長い方々はお気づきでしょうが、これは FCM ではなくそれ以前にプッシュ通知に使われていた GCM という仕組みの BroadcastReceiver の実装になります。現在は直接的にアプリや一般のライブラリがこれを使用するのは非サポートとなっているので、ご注意を。 FlutterFire の AndroidManifest に登場していた他のクラスについても少し触れておくと FlutterFirebaseMessagingBackgroundService FlutterFirebaseMessagingReceiver がバックグラウンドでプッシュ通知を受け取った場合に内部的にこのサービスを enqueue して、Dart の世界に通知を取り次ぐ役割をします。 BroadcastReceiver が長時間かかる処理をしないようにこのような構成になっているものと思われます。 FlutterFirebaseMessagingReceiver を使う限りは AndroidManifest に宣言しておかないといけないもの、ということになります。 こうして以下のことがわかったわけです。 FlutterFire は FirebaseMessagingService の実装クラスを持っていたが、困ったことに onMessageReceived(RemoteMessage) の実装が空なので Braze からの fallback にはそのまま使えない。 また FlutterFirebaseMessagingReceiver を単純に組み込むとすべてのメッセージが Dart の世界まで引き込まれてしまう。フィルターアウトしたいメッセージの選別する手段を差し込む口が提供されていない。 さてどうしよう。。。となりました。 Braze SDK も読んでみる 困ったときは実装を読み込むしかないな、ということで Braze SDK の中も見てみます。 Flutter 向けには braze_plugin として提供されていますが、こちらも各OS向けのネイティブライブラリに依存していて、 Android については braze-android-sdk の android-sdk-ui モジュールにプッシュ通知周りの実装が入っているのがわかりました。 このあたり です implementation "com.braze:android-sdk-ui:30.4.0" Braze 自身は標準的な FCM の仕組みに則っていて、 Kotlin で実装された open class の BrazeFirebaseMessagingService により FCM Token と RemoteMessge がハンドリングされるようになっています。このクラスは必ず必要ということになります。うーん。。。ただ onMessageReceived はすごく薄い実装になっていて override fun onMessageReceived(remoteMessage: RemoteMessage) { super.onMessageReceived(remoteMessage) handleBrazeRemoteMessage(this, remoteMessage) } handleBrazeRemoteMessage(this, … という記述から Java や Kotlin に詳しい方にはお察しいただける通り、このクラスは Java の static method (Kotlin の companion object の method)としてメッセージのハンドリング実体を提供する /** * Consumes an incoming [RemoteMessage] if it originated from Braze. If the [RemoteMessage] did * not originate from Braze, then this method does nothing and returns false. * * @param remoteMessage The [RemoteMessage] from Firebase. * @return true iff the [RemoteMessage] originated from Braze and was consumed. Returns false * if the [RemoteMessage] did not originate from Braze or otherwise could not be handled by Braze. */ @JvmStatic fun handleBrazeRemoteMessage(context: Context, remoteMessage: RemoteMessage): Boolean { if (!isBrazePushNotification(remoteMessage)) { およびその冒頭でちらっと見えていますが Braze 由来の RemoteMessage かを判別する static method の /** * Determines if the Firebase [RemoteMessage] originated from Braze and should be * forwarded to [BrazeFirebaseMessagingService.handleBrazeRemoteMessage]. * * @param remoteMessage The [RemoteMessage] from [FirebaseMessagingService.onMessageReceived] * @return true iff this [RemoteMessage] originated from Braze or otherwise * should be passed to [BrazeFirebaseMessagingService.handleBrazeRemoteMessage]. */ @JvmStatic fun isBrazePushNotification(remoteMessage: RemoteMessage): Boolean { を発見できました…!! これでなんとかなりそうです。 解決編 いろいろやり方はあると思いますが、以下のような方法をとりました。 Step1: FlutterFire の FlutterFirebaseMessagingReceiver を tools:node=”remove” を使って除去 FlutterFirebaseMessagingReceiver が直接 RemoteMessage を拾ってしまう限りは Braze 由来のメッセージを誤ってハンドリングするのを防げないので、 Managing manifest files にしたがって以下の記述をアプリ側の AndroidManifest.xml に追加し、 FlutterFire によって宣言されてしまう分を除去させます。 <receiver android:name="io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingReceiver" xmlns:tools="http://schemas.android.com/tools" tools:node="remove" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND"> <intent-filter> <action android:name="com.google.android.c2dm.intent.RECEIVE" /> </intent-filter> </receiver> Step2: BrazeFirebaseMessagingService の派生クラスとして HalloFirebaseMessagingService を実装 BrazeFirebaseMessagingService 相当の機能はすべて残さないといけないし、一方で FirebaseMessagingService の実装クラスが複数いて読解に苦慮するのもなと思ったので、以下のような薄いクラスを書きました。 /** * We are using both FlutterFire and Braze to handle push notifications. * Unfortunately we found that Braze notifications could be handled twice * when integrating both of them following their official ways simply, * therefore we resolved the issue by having our own [FirebaseMessagingService]. * * # About Token refresh * * This class inherits from [BrazeFirebaseMessagingService] intentionally, * since [BrazeFirebaseMessagingService.onNewToken] is needed to register a new FCM token to Braze. * Note that our notification service will also receive a new FCM token in dart layer * via [FlutterFirebaseMessagingService.onNewToken]. */ class HalloFirebaseMessagingService: BrazeFirebaseMessagingService() { /** * FCM from Hallo backend through notification service should be handled by * [FlutterFirebaseMessagingReceiver] which will redirect messages to dart layer. * But braze_plugin provided by Braze for Flutter users doesn't provide any measure * to filter Braze messages, therefore we need to filter out Braze messages in native layer before * [FlutterFirebaseMessagingReceiver] works. * * [BrazeFirebaseMessagingService] can have a fallback service to handle messages from other than * Braze, but we don't use the mechanism to not have multiple [FirebaseMessagingService] * implementations. * * Fortunately [BrazeFirebaseMessagingService.handleBrazeRemoteMessage] is provided publicly * to construct own FCM handling. If it doesn't consume FCM then we should delegate it to * [FlutterFirebaseMessagingReceiver.onReceive]. * * Also we removes [FlutterFirebaseMessagingReceiver] from AndroidManifest * to prevent it from handing FCM directly. */ override fun onMessageReceived(remoteMessage: RemoteMessage) { if (handleBrazeRemoteMessage(this, remoteMessage)) { return } FlutterFirebaseMessagingReceiver().onReceive(this, remoteMessage.toIntent()) } } handleBrazeRemoteMessage が Braze 由来のメッセージでないとみなしてハンドルしなかった場合は FlutterFirebaseMessagingReceiver のインスタンスを直接作って FlutterFire の処理に乗せ直す、ということをやっています。 AndroidManifest には FlutterFirebaseMessagingReceiver がもう宣言されていない状態で、この場で必要なときだけ明示的に仕事を渡す、というかたちにしました。 Step3: アプリの AndroidManifest.xml に、 BrazeFirebaseMessagingService のかわりに HalloFirebaseMessagingService を宣言 <!-- <service android:name="com.braze.push.BrazeFirebaseMessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service> --> <service android:name=".HalloFirebaseMessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service> これでめでたく Braze 由来のメッセージは2重ハンドルされることがなくなり、 Hallo Backend 由来のメッセージも従来通り動作する状態を実現することができました。 結び メルカリ ハロは Flutter アプリです。画面や機能の大部分は1ソースで Android / iOS 両方を実現しており、開発コストの圧縮と十分な性能の両立に Flutter という技術選択が寄与しました。それでも、 Flutter の範疇で解決できない問題に直面するケースはまれにあります。プッシュ通知のようなネイティブの処理に強く影響を受ける個別性の高い機能を扱うなかで、外部提供のSDKやライブラリの組み合わせによって発生した課題を、ライブラリのネイティブ実装部分を解読し工夫することによって乗り切った事例をご紹介させていただきました。 この話は iOS 編も実はありまして、method swizzling 満載の Objective-C を読むもう少し大変な冒険譚があるのですが、今回は Android だけで盛りだくさんになってしまいましたので、またの機会があればお話したいと思います。 明日の記事は danny さんと simon さんです。引き続きお楽しみください。
はじめに こんにちは。メルカリMarketplace SRE Tech Leadの @mshibuya です。 この記事は、 Mercari Advent Calendar 2024 の9日目の記事です。 自身が所属するMarketplace SREチームは、メルカリグループ全体としてのPlatformを提供するPlatform Divisionに所属しています。この記事では、サービスの信頼性を支えるProduction Readiness Checkと呼んでいるプロセスに対して行った改善と、その結果もたらされた開発者体験について取り上げます。 サービスが必要十分な信頼性を持つことの重要性は広く認識されていると考えます。が、そのための取り組みは地道かつ手間がかかるものであり、ともすればそのためのプロセスの存在によって開発スピードを落としてしまうという結果につながりかねません。このたび行われたProduction Readiness Checkプロセスへの改善について、プロセスのどのような側面に着目して改善を行ったか、その結果どのような開発者体験へつながることを目指したかをご紹介します。同種の取り組みを行う皆様の参考となれば幸いです。 Production Readiness Checkとは メルカリには、Production Readiness Check(略してPRC)と呼ばれるプロセスがあります。これは新しく開発されたプロダクトやマイクロサービスが満たすべき一連の基準を集めたチェックリストで、これに合格しないと本番環境において運用開始することはできません。 過去のブログ記事において 概要の紹介 があるほか、チェック項目そのものについても最新のものではありませんが GitHub上にて公開 されています。 メルカリにおいては広くマイクロサービスアーキテクチャを採用しています。フリマアプリ「メルカリ」や、スマホ決済サービス「メルペイ」といったすでに大きな規模となったサービスにおいては多くの機能追加がマイクロサービスの新規開発という形で進みますし、「 メルコイン 」や「 メルカリ ハロ 」といった新たに立ち上がるプロダクトについても「メルカリ」・「メルペイ」同様のマイクロサービス基盤における1マイクロサービスとしての形を取ります。したがって、マイクロサービスの新規立ち上げというのは日常的に発生します。またDevOpsにおけるYou build it, you run itの原則に従い、それらが本番での運用に耐える信頼性を持つよう担保していく責任も個々のマイクロサービス開発チームにあります。 マイクロサービス開発チームは必ずしもこうした新規のサービス立ち上げや、そのために必要な信頼性の担保に精通しているとは限りません。開発チームがマイクロサービスの立ち上げを自律的に行いつつも必要な信頼性を担保することがProduction Readiness Checkプロセスの狙いです。 解決したい課題 Production Readiness Checkは、メルカリにおいて開発されるサービスがお客さまからの実トラフィックを受け稼働していくために十分な信頼性を持っている(i.e. Production Readyである)ことを担保するために欠かせない役割を果たしてきました。その一方で、次第にこのチェックプロセス自体の運用が開発者にとって重荷となりつつあったことは否定できません。 メルカリにおけるProduction Readiness Checkプロセスは、チェックリストを含むissueを作成することで開始され、issueのcloseとともに終了となります。このissueのopen-closeまでの全期間で実作業が発生しているわけではないので参考値とはなりますが、約5年間のデータでは平均35.5日を要していました。 また、Platform Divisionで行っている開発者インタビューにおいても、Production Readiness Checkプロセスへの不満が多く寄せられている状況でした。 社内開発者からのコメントの例: Did PRC as well, lots of “copy this, paste this, take a screenshot of this…” Overall straightforward, just PRC was a pain PRC, takes about 4 weeks Takes a lot of time Personal opinion is that 1-2 sprints could be cut by simplifying the PRC process Too many things to check, some things are hard to understand how to verify 最もやりたくない仕事のひとつ。必要なのはわかる メルカリグループでは、新規プロダクト立ち上げや既存プロダクトへの機能追加においてスピードがこれまで以上に重視されるようになっており、このProduction Readiness Checkプロセスを高速化し、デリバリーにかかる時間を短縮し省力化することは喫緊の課題とみなされるようになりました。 既存のプロセスにおける開発者体験 ここでは新規プロダクトの立ち上げを例に取り、改善前のProduction Readiness Checkプロセスにおける典型的な体験を示します。エピソードはすべて架空のものなので、最悪のケースでは開発者がこんな体験をしていた可能性がある…という程度のものとしてお読みください。 メルカリグループではとある新規のプロダクトを立ち上げることとなりました。このプロダクトはフリマアプリとしてのメルカリとのシステム連携を含むものとなっており、お客さまがスムーズにプロダクトを使っていただくのに十分な信頼性を有している必要があります。 早速開発チームが立ち上げられ、6ヶ月間でのサービス提供開始を目指して怒涛の開発がスタートします。まずチームはプロダクト上の要件を明らかにし、それをシステム上の実装に落とし込むための設計を行い、Design Docの形でまとめます。出来上がった設計を元に実際のアプリケーションコードの実装が順調に進んでいき、リリースも間近の5ヶ月目に大半の機能の実装を終えることができました。 さて、チームは実際にプロダクトのリリース準備に入ります。本番で利用するインフラ環境構築を進めていくのですが、このあたりでチームはProduction Readiness Checkプロセスの存在に気づくのです。このチームは入社してまもないメンバーや既存サービスの定常的な開発に関わってきたメンバーが多かったため、Checkプロセスの必要性を見落としていたのです。これを全部満たすことがプロダクトのリリースに必須と聞いたチームは全力で対応するのですが、対応が必要な項目数自体が多いこともあり、もともと設計上の考慮に含まれていなかった要件なども判明し難航します。 結果、チームはProduction Readiness Checkを完了するのに2ヶ月を要してしまい、その分プロダクトのリリースを延期せざるを得ませんでした。必要な信頼性を満たせていなかった以上仕方ないのですが、その分プロダクトを早く世に出しお客さまからフィードバックをいただくチャンスを失ってしまいました。 解決策 チェックの自動化 プロセスに労力がかかる要因としてまず挙げられるのが、チェックが必要な項目数そのものが多いこと、またそれが増加するトレンドにあることです。 この種のチェックリスト全般に言えることではありますが、時間経過とともにチェック項目は増加する傾向にあります。なにかトラブルが発生した際の原因としてとある設定を持っていなかったことが指摘され、その再発防止策がチェックリストに追加されるという流れが必然的に発生するためです。 こうした対応がその場しのぎの安易な追加とならないよう抑制的に運用されていたとは理解していますが、それでも典型的なサービスにとってのチェック項目数は公開版の時点では62項目であったものが最新の内部版では71項目となり、約3年間で15%近く増加しています。 また、チェックリストに含まれる項目には「どのような状態を満たす必要があるのか」の定義はあっても、「どのような手段が使われていれば満たされていると見なせるのか」についての言及が十分にはされていないものもあります。このこと自体はマイクロサービスアーキテクチャにおける個々のサービスでの技術選定の柔軟性に配慮した結果ではあると考えられますが、「この確認ができればOK」という基準が明確に示されない状態は実際にチェックリストの対応を行う担当者としても、それをレビューする側としても、都度の検討が必要で負担の大きなものとなっていました。 こうした状況を緩和し労力を削減するため、Production Readiness Checkプロセスにおけるチェックの部分的な自動化を推進しました。プロセスにおけるチェック対象はアプリケーションソースコードやインフラ設定など多岐にわたりますが、自動化によるチェック実装が容易に行えるものから実装を始め、現在はチェック項目の半数近く、45%ほどの項目においてなんらかの自動化チェックが行われるようになっています。 この結果、少なくとも自動化済みの部分に対しては開発者が容易にチェックを行うことができ、求められる状態とのギャップを埋めるためのアクションを取れる状態となっています。自動化チェック自体が「どのような手段が使われていれば満たされていると見なせるのか」の基準を内包しているため、レビュワーにとっても迷いなく判断を行えるようになりました。 既存のコンポーネントによるProduction Readiness Check対応の拡充 過去にも様々な機会で発信されているように 、メルカリにおいてはPlatform Engineeringの考え方が、プラクティスとして広く実践されています。セルフサービスを重視したPlatformによって開発者生産性を高めていく思想のもと、Platform Divisionはこれまでも多くのコンポーネントを構築し、開発者に提供しています。 Production Readiness Checkプロセスの負荷が高い原因を探る過程において、我々はそれによって求められている要件と、実際にPlatformとして提供しているコンポーネント群が持つ機能にギャップが存在していることを認識しました。 メルカリのPlatformにおいては、SDLCの全領域において、開発者が効率的に必要な目的を達成できるよう様々なコンポーネントを提供しています。それらのコンポーネントが十分にカバーできていない領域ながら、Production Readiness Checkプロセスにおいて大きな労力を要している部分を特定し、そのギャップを満たすためのコンポーネントの機能拡充を行いました。 またより重要かつ費用対効果の高い改善として、それらのコンポーネントによって満たされるProduction Readiness Check上の要求を明確化するようドキュメントの改善を行っています。 今回の取り組みを通じた気づきとして、開発者がマイクロサービスを構築していく上で避けて通れないProduction Readiness Checkプロセスに対し、こういったコンポーネントをいかに統合し全体としての開発者体験を作れるかが重要という点があります。単にコンポーネントを提供するだけではなく、その先にあるチェックプロセスそのものを改善したことで、相互のフィードバックループが機能する状態を作れたのではないかと考えています。 "Shift-Left"によるアプローチ ここでいう"Shift-Left"はソフトウェアテストやセキュリティの文脈でよく使われる考え方で、例えばテスト実施のような何らかの活動をより早い段階(=タイムライン図における左側)に動かすことを指しています。 先にあげた新規プロダクト立ち上げの失敗例においては、チームはプロダクトをリリースする直前、短期間にProduction Readiness Checkプロセスを行うことを試み、その多大な労力ゆえに困難に直面しています。個人的にはこの手の状況を「夏休みの宿題ギリギリ問題」と呼ぶのですが、これはチームが怠惰だからそうなってしまったのではなく、構造的な問題があると考えます。新たなプロダクトを立ち上げるにあたっては数多のチャレンジ・困難が存在しており、それらを解決していくなかで、重要と知りつつも喫緊の必要がないものは先送りせざるを得ないからです。 この問題に対処するためには、仕組みレベルでの改善が必要と考えました。前項の自動化が達成された今、チームは少なくとも自動化されたチェック項目に対しては最小限の労力で何度もチェックを行い、漸進的に要件を満たしていくことが可能となっています。また、既存のコンポーネントによるProduction Readiness Check対応も拡充され、早い段階よりそうしたコンポーネントの採用を進めておくことによって、労せずに必要な要件をあらかじめ満たしておけるようになりました。仕上げとして、これらの施策の存在をチームが開発初期の段階から認識できる状態をつくることで、リリース直前の短期間に作業が集中することを防げると考えました。 とはいえ、単にそうした新プロセスや解決策の存在を周知したところで、声が届く範囲には限界があります。そこで、既に確立された、開発初期に必ず発生する別のプロセスの中に入れ込むことで、開発初期のチームがその存在を漏れなく認識できるよう工夫しています。 メルカリでは新規システムの設計にあたってはDesign Docを作成し関係するチームのレビューを受ける文化が根付いています。そのDesign Docを作成する元となるテンプレートにおいて、Production Readiness Checkの存在そのものや、その中でも早期からの考慮が必要な項目について記載し注意を促すこととしました。 これら一連の"Shift-Left"によるアプローチの結果として、開発者が実際の開発やインフラ環境の構築に着手するずっと前、設計の段階からそれらの要件を知り、Production Readiness Checkプロセスへ向けより早期に意味のあるアクションが取れるようになりました。 新たなプロセスによる開発者体験 プロセス改善、そして自動化を取り入れたProduction Readiness Checkプロセスによって、先の架空の新規プロダクト立ち上げにおける開発者体験がどのように変わるかを見ていきましょう。 まずは、Shift-Leftの結果として、チームは6ヶ月の開発期間における最初の段階、設計を行いDesign Docを作る際にProduction Readiness Checkプロセスの存在を知ります。そこで注目すべき要件を理解し、設計段階から考慮しておくことで、必要条件を満たすための根本的なアクション(例えばプロダクト上の要件変更についてステークホルダーと相談するなど)に踏み込むことも可能になります。 5ヶ月目、いよいよプロダクトのリリースが近づき、チームはProduction Readiness Checkプロセスの準備を始めます。事前の想定に基づき要求を満たすために適切なPlatformのコンポーネントを選定済みなので、要求を満たすために新たに行う必要のある変更・作業は最小限に抑えられています。 Production Readiness Checkにおけるチェック項目が満たせているかどうかの確認は、自動化によって大幅に労力が削減されました。おかげでチームは1ヶ月間でProduction Readiness Checkプロセスを完了することができ、お客さまに早く価値を届けフィードバックを得ることで更にプロダクトを磨き上げていくことができるようになりました。 今後の展望 このようにProduction Readiness Checkプロセスは一定の改善を受けており、実際のマイクロサービスリリース前におけるチェックにも利用されはじめています。一方、既存のコンポーネントによるProduction Readiness Check要求項目の対応状況や、自動化における対応可能ケースの拡充にはまだまだ改善の余地があり、より良い開発者体験を目指し当面の間注力していくべき領域になると想定しています。 それらの改善を推し進めていった先には何があるでしょう? 筆者個人としては、「チェックを行う」という考え方自体が不要になることが理想的だと考えています。Platformが提供する機能・コンポーネントによって標準でほぼ全ての必要条件が満たされており、開発者はなにも考えずとも、信頼性の高いサービスを構築・運用していける世界観です。 存在を意識する必要がない、当たり前に必要なことが満たされている理想のPlatformを目指して、道のりは遠いながらもいかにそこに近づいていけるかを考えたいです。 まとめ この記事では、メルカリでのProduction Readiness Checkプロセスの概要について説明し、そのプロセスに対してどのような改善を行ったか、その結果としてどのような開発者体験を作ることができたかをご紹介しました。 明日の記事はsintario_2ndさんです。引き続きお楽しみください。
こんにちは。メルペイVPoEの@jorakuです。 この記事は、 Merpay & Mercoin Advent Calendar 2024 の記事です。 メルペイに入社して半年が経ちました。この期間で改めて個人的に実感した「メルカリらしさ」についてお話ししたいと思います。メルカリといえば、Go Bold / All for One / Be a ProといったValueが知られていますが、メルカリグループの魅力はそれだけにとどまりません。多様性を尊重し、挑戦を恐れず、学び続けるこの環境は、成果を最大化させる基礎を築き、自分自身やチームの成長にも大きく貢献しています。 1. Go Global / 多様性を包摂する文化 メルカリには55カ国からプロフェッショナル人材が集まっています。特にエンジニアの半数以上は外国籍の優秀なエンジニアであり、異なる文化や考え方に触れることで、自分の視野が広がるのを実感します。この「多様性」は単なるスローガンではなく、日常の中で感じられるリアルなものです。 特にメルカリはグローバルに展開し、かつ国内外問わず多様なモノや価値の循環を行っているため、異なる視点やアイデアを生み出すことに貢献しています。 参考: メルカリIMPACT REPORT GOTサポートで言語の壁を越える 多様な背景を持つメンバーが活躍する中で、GOT (Global Operations Team) のサポートは非常に心強い存在です。同時通訳者の組織が整備されており、必要に応じていつでも誰でも活用できます。これにより、言語の壁を感じることなくスムーズに仕事を進められます。私も活用することがありますが、GOTの皆さんの技術用語の理解の深さには毎回驚かされます。 参考: GOTの取り組み 言語サポートの仕組み やさしい日本語とやさしい英語 さらに、お互いの理解を深めるために「やさしい日本語」や「やさしい英語」を使う文化も根付いています。「伝わりにくいことは歩み寄る姿勢」が自然と浸透しているのは、メルカリらしいポイントだと感じます。 参考: やさしい日本語・英語の活用 2. Scrap and Buildの精神 メルペイに入って感じたもう一つの素晴らしい文化が、技術的負債の解消やシステムのリアーキテクチャといった「Scrap and Build」の精神です。技術だけではなく組織や人材まで及びます。現状に満足することなく、常に改善を目指して行動する姿勢が、エンジニアとしてのやりがいを大きくしています。過去の話ですがアプリをゼロから作り直した話はとてもGo Boldな取り組みでした。そのような大胆な意思決定が可能であり、それを最後までやり切る力がメルカリの強みです。 現在も、多くのマイグレーションやリアーキテクチャが行われ、常に進化し続ける土台を作っています。12/6の@hibagon-sanのアドベントカレンダーにおいてもMerpay API Rearchitectureについて触れられています。 参考: 大型プロジェクト「GroundUp App」の道程 iOS&Androidのテックリードが振り返る、すべてがGo Boldだった「GroundUp App」 メルカリ新卒1年目のエンジニアが最初の7ヶ月間でやったこと 循環する組織 また、半年ごとに役割が変わるほどの柔軟な組織体制も魅力です。これにより、新しいチャレンジが可能となり常に新しい知識や経験を重ねることができます。もちろん専門性を深掘りしたい人はその意思も反映されます。 一度メルカリを離れたとしても、再びメルカリに戻ってくる人材も少なくありません。このような柔軟な文化が、組織としての強さを支えています。 社外に応募している職種はそのまま社内公募(Bold Choice)にも出されるため、自分のキャリアを後押しすることも可能です。 参考: Talent Development(Bold Choice) 3. ドキュメント文化 メルカリで働く上で特に印象的なのが「ドキュメント文化」です。この文化は、メルカリが持つ透明性と効率性を象徴するものといえます。 会議前の「読む読むタイム」 会議によっては事前に議題に関するドキュメントが作成され、参加者が「読む読むタイム」を持ちます。この時間を通じて、全員が同じ情報を共有した状態で議論を始めることができるため、深い議論や迅速な意思決定が可能になります。さらに、意思決定の背景や議論の経緯もドキュメントに残されるため、後から過去の決定やプロセスを追跡することができます。 図よりも文字で表現 提案資料や説明の場では、Power PointやSlideを使うのではなく、文字で詳細に表現するのが特徴です。これは、図では曖昧になりやすい部分も正確に伝えることができるためです。この文化は、Amazonの「ナラティブ文化」に似ていますが、メルカリ独自の進化を遂げています。 VisionならびにRoadmapの言語化 特に興味深いのは、VisionやRoadmapの作成が全社員に求められる点です。多くの企業では、方向性の策定はDirectorやその上の役職者が主に担当しますが、メルカリではプロジェクトをリードする社員にはVisionやRoadmapを作成することが奨励されています。このプロセスにより自分やチームの進むべき方向性や戦略を明らかにし、ステークホルダの共通認識や期待値の調整が容易になります。長期的な視点をもったアーキテクチャに取り組むこともできます。これが、個人の成長とメルカリ全体の方向性をリンクさせる鍵となっています。 参考: メルカリエンジニアリングのロードマップ作成 4. 成長し続ける環境 最後に、学びのサポートについてです。成長自体は業務における成果によってもたらされますし、成長するかしないかは全ては個人の問題だと捉えています。ただ、メルカリではエンジニアの成長を後押しする制度が整っています。お互いが学び合うOpen Doorは頻繁に開催され、プリンシパルエンジニアによるAsk me anything など。また、書籍購入支援や、O’Reilly Safari Books Onlineの提供、海外カンファレンスへの参加など、個人の成長を強力に後押ししてくれます。このような支援のおかげで、自分のスキルを高めることができ、より高いパフォーマンスを発揮できるようになっています。 参考: Fintech Tech Talk at Office Week を開催したよ! Talent Development(Learning Support) 参考: メルペイエンジニアが参加した/予定の海外カンファレンス(一部) Conference Name Technology Area Droidcon Berlin/Lisbon/London/San Francisco/NYC Android GopherCon UK 2024 Backend QCon 2024 Shanghai Backend SREcon24 Europe/Middle East/Africa , Americas SRE Pragma Conference 2024 iOS SwiftLeeds 2024 iOS KubeCon + CloudNativeCon North America SRE Do iOS 2024 iOS Agile Testing Days 2024 QA/Testing AppDevCon 2025 Mobile KotlinConf 2024 Android Google I/O 2024 Android EuroSTAR Conference 2024 QA/Testing WWDC24 iOS React Advanced London Frontend 終わりに メルペイに入社して半年、メルカリのエンジニアリングカルチャーの強みを日々実感しています。これまで多くの事を学び取り入れてきたつもりですが、この短い期間で、全く異なる変化を生み出せている事を感じています。それは、メルカリの文化がGo Globalな挑戦を後押しし、常に進化し続ける環境を提供してくれるからだと思います。引き続きこれまでの強みを活かした組織を作っていければと思います。 また、メルカリグループのミッションである「あらゆる価値を循環させ、あらゆる人の可能性を広げる」、そしてメルペイのミッションである「信用を創造して、なめらかな社会を創る」は、特に今の時代において、その意義と重要性を増していると感じます。いき過ぎた資本主義や、社会や経済が目まぐるしく変化する中で、これらのミッションは多くの人々にとって新しい価値を提供し、持続可能な未来を形作るための力になると確信しています。このミッションへの思いはまた別途述べる事にします。 この記事を通じて、少しでもメルカリのエンジニアリングカルチャーについてお伝えできていれば幸いです。 次の記事は cyanさんです。引き続きお楽しみください。
こんにちは。メルカリ ハロのモバイルチームのEMの @atsumo です。 この記事は、連載: メルカリ ハロ 開発の裏側 – Flutterと支える技術 – の3回目と、 Mercari Advent Calendar 2024 の7日目の記事です。 はじめに メルカリ ハロは2024年3月にリリースされた、Flutterを用いて開発されたアプリケーションです。本記事では、デザインシステムの導入によって実現した開発効率の向上と、その具体的な運用方法について共有いたします。 目次 デザインシステムの概要 メルカリ ハロのデザインシステム Componentのご紹介 FigmaからFlutterへの実装について Componentを使った画面実装 1年経過して見えてきた課題 今後の展望 まとめ それでは、順を追って説明していきます。 デザインシステムの概要 デザインシステムについて触れたいと思います。 私たちはデザインシステムを『サービスにおけるUXやUIの一貫性を保つための要素と仕組み』と捉えています。デザインシステムを活用することで、個別のコンポーネントや色やタイポグラフィの設定だけではなく、サービス全体におけるそれらの一貫性を担保しながら、開発体験および生産性を向上させることができます。 デザインシステムによってデザイナーの創造性を制限してしまうのではないか?など自由度が下がってしまうのではないかと懸念されることがあるかも知れませんが、私がデザインシステム導入を通じて感じたことは、制限があることで創造性が損なわれるのではなく、むしろ明確なルールと基盤があるおかげで、本来解決すべき課題に集中できるということです。今回メルカリ ハロでデザインシステムを導入したことで、デザイナーとエンジニア間でとても効率的に開発を進めることができたと感じています。 参考: Figma Blog – デザインシステムの基本: デザインシステムとは? メルカリ ハロのデザインシステム Componentのご紹介 Design Token Components Specific Components などのように大きく3つの構成要素で分かれています。 Design Token Design Tokenとして定義しているのは5つあります。 Color Space Radius Shadow Typography Colorに関してはDark Mode / Light Modeを定義しておりvariablesとして管理されています。 Figmaのデザインデータでは、Spacing、Radius、Typographyなども画面サイズに応じて変数で管理されています。ただし、ハロアプリはタブレットなどの大きな画面に対応していないため、これらは反映されていません。 Components Componentsではドメインに依存しないようなUI Componentを定義しています。 Button / AppBar / Checkbox / Divider / Toggle / Badge / Tab / ProgressBar / Indicator / Snackbar / Tooltip など他のデザインシステムにもあるような一般的なものや Input / Section Title / State Message / Callout / Notesなど、少しハロ独自で使っているがドメインに依存しすぎないようなComponentを用意しています。 Specific Components Specific Componentsとしてハロのドメインや仕様に依存していて、複数画面で使用されるようなComponentも定義しています。 Specific Components自体も基本的にはComponentsの組み合わせでできており、それぞれのComponentが持っているプロパティはSpecific Componentでも適用することが可能です。 こちらのSpecific Componentsのケースで言うと内部でCalloutという上記のComponentとして定義したものを使用しているため、CalloutのComponentのプロパティで変更できる部分に関してはこちらでも変更することが可能です。 FigmaからFlutterへの実装について Listのアイテムを例に説明していきます。 ハロ内ではListItemでも複数のパターンのitemを用意しています。 これを全て1つのComponentとしてプロパティを切り替えで定義してしまうと、プロパティがたくさん増え、いろんな組み合わせでいろんな表現ができる一方で、わかりにくいComponentになってしまいます。 ハロでは下記の様にそれぞれ別のComponentとして取り扱っています。 Figma上でコンポーネントのプロパティを✏️で示してもらうことで、変更可能な値がわかりやすくなり、実装時にはそのプロパティを基にしてコンポーネントを作成することで、デザインデータとコードの認識を一致させることができました。 Show Divider: Top / Show Divider: Bottomなど Figmaのデザイン上は便利な一方でだが実際に実装時にComponentには入れない部分などもありますが、おおよそ同じようなプロパティ構成になっています。 実際のComponentのコード class ListItemWithValue extends ConsumerWidget { ListItemWithValue({ required this.label, required this.value, required this.horizontalPadding, super.key, this.note, this.onTap, this.align = ListItemValueAlign.end, ... }) : assert( !((note?.isNotEmpty ?? false) && align == ListItemValueAlign.start), 'Value align can only be start if note is not null', ); final String label; final String value; final String? note; final GestureTapCallback? onTap; final ListItemValueAlign align; ... } Figmaのデータを元に実装する際にはFigmaのDev Modeをよく利用しています。Dev Modeを使用することでCSSやiOSのUIKit / SiwftUI、AndroidのXML / Composeなどのコードに変換された状態で確認することができます。残念ながら、現在Flutterは公式サポートされていないですが、Community pluginでFlutterのコードに変換するようなPluginがいくつかあり、そちらを使うことで実装を容易にすることができます。 Figma to Code (HTML, Tailwind, Flutter, SwiftUI) などのpluginを使って、変換されたコードを見ながら、Widgetの構成を検討することがあります。変換されたコードは高さや幅が固定値になってしまっていたり、無駄なWidgetがあったりするためそのまま使うのは難しいですが、実装時の参考になることは多いです。 Componentを確認する 前回 メルカリ ハロ アプリの技術スタックの紹介 でも Widgetbook についても軽く触れていますが、Componentを確認するためにWidgetbookを使用しています。 ※1: Figma上のプロパティ ※2: コード上のプロパティ 上記のように Figma上の ✏️ 部分(※1)とコードのプロパティ(※2)の認識を合わせることで、Componentで変更できる値がわかりやすくなり、コミュニケーションがとてもスムーズに行えるようになっています。 画面実装前のComponentを量産する時期には、新規画面実装の際のカタログとしてWidgetbookが大いに活躍していました。Webでよく使われている Storybook が持っているようなさまざまな機能などが今後Widgetbookにも追加されていけば、画面実装時のモックUI実装にも大いに役立つ可能性があり、今後もWidgetbookに期待しています。 Componentを使った画面実装 メルカリ ハロのデザインシステムを活用して、具体的な画面実装を行う方法について説明します。ここでは、設定画面を例に、どのようにデザインシステムのComponentを使用して画面を構築するかを紹介します。 設定画面の実装 設定画面では、以下のようにComponentを使用しています。 NavigationBar – 画面のタイトルと戻るボタンを表示 Notes – 設定に関する説明文を表示 ListItem – 現在の設定状態を表示 Button – 設定変更用のボタン class SettingsScreen extends HookConsumerWidget { const SettingsScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final theme = ref.watch(appThemeProvider); return Scaffold( // 1. Navigation Bar - 画面のタイトルと戻るボタンを表示 appBar: const HalloNavigationBar.normal( title: Text('設定画面タイトル'), ), body: SafeArea( child: Padding( padding: EdgeInsets.only( left: theme.spacing.size4, right: theme.spacing.size4, top: theme.spacing.size8, bottom: theme.spacing.size10, ), child: Column( children: [ // 2. Notes - 設定に関する説明文を表示 const HalloNotes( content: '設定に関する説明文をここに記載します', ), Gap(theme.spacing.size8), // 3. List Item - 現在の設定状態を表示 HalloListItem.value( label: '設定項目名A', value: '設定状態', ), const HalloDivider(), HalloListItem.leadingWidget( label: '設定項目名B', leading: const Icon(Icons.info_outline_rounded), note: '設定項目に関するメモなどを記載する', doubleLine: true, ), const HalloDivider(), HalloListItem.trailingWidget( label: '設定項目名C', trailing: HalloSwitchButton( value: true, onChanged: (value) { // Switchのオンオフによって実行される処理 }, ), note: 'オンオフを設定することが可能', ), const HalloDivider(), const Spacer(), // 4. Button - 設定変更用のボタン HalloButton.filled( label: '設定を変更する', onPressed: () { // 設定画面を開く処理 }, ), ], ), ), ), ); } } このように、デザインシステムのコンポーネントを使用することで、統一感のあるUIを簡単に実装することができます。各コンポーネントは再利用可能であり、デザインの一貫性を保ちながら効率的に開発を進めることができます。 リリースから半年後に見えてきた課題 メルカリ ハロは今年の3月にリリースされています。去年の今頃は上記のComponentを用いて各Component自体はWidgetbookで確認することができ、それらのComponentを使用して新規の画面を実装するフェーズでした。Componentの活用により、生産性高く画面実装を行うことができたと感じています。 最近は新しい画面を作ることもありますが、今まであった画面の機能の改善や機能を追加していくことも増えています。その中で既存のコンポーネントでは対応しきれないケースや、当初の設計では考慮されていなかった部分が少しずつ明らかになってきました。 例えばComponentに内包されているPaddingやMarginとは違うものを使った方が、単体の画面構成としては収まりが良いなど、既存のものとは少しだけ異なる亜種のComponentが生まれ始めています。ルールに縛られすぎてUXを阻害するものになってはいけないと思いつつも、デザインシステムは、ユーザー体験だけでなく、開発者やデザイナーの体験も高めることで、その効果を最大限に発揮できると考えています。 現在はデザイナーとコミュニケーションを取り、基本的にはデザインシステムで定義されているものを使用するというルールを改めて確認するようにしています。一方で、デザインシステムで定義されているものだけではユーザー体験を損ねる可能性があるケースにし関しては、デザインシステム自体の改善であったり、Specific Componentsを定義するような流れをとっています。 今後の展望 メルカリ ハロのデザインシステムは、さらなる開発の効率化と品質向上を目指して進化を続けています。この目的のために、いくつかの新しい可能性を模索しています。 まだアイデアの段階ではありますが、Figmaなどのデザインツールにおいて、Componentが正しく使用されていない箇所を自動的に検知できるlintの仕組みを導入することを検討しています。また、新たなチームメンバーが加わる際に、デザインシステムを円滑に理解し活用できるよう、勉強会やドキュメントの整備にも力を入れています。全員が共通の知識とスキルを持てる環境を作り、プロジェクトに貢献できるよう取り組んでいきます。 さらに Figma AI はじめ v0 や bolt.new など生成AIを使ったデザインおよび実装のアプローチに興味を持っており、これらを参考にメルカリ ハロのデザインシステムに基づいたデザインから実装の効率化の方法を模索しています。これによって、デザインを試行錯誤するプロセスが、デザイナーやエンジニアだけでなく、他の職種の方々にも手の届きやすいものになればと考えています。 まとめ この記事では、メルカリ ハロのデザインシステム導入による開発効率向上とその具体的な運用方法についてご紹介しました。デザインシステムは、ビジュアルの一貫性を保つだけでなく、デザイナーとエンジニアが円滑に連携し、生産性を高めるための基盤となっています。リリースから半年以上が経過し、新たな課題も浮上していますが、引き続きデザイナーと協力してデザインシステムの改善を続けています。 今後は生成AIの活用を視野に入れつつ、さらなる効率化とデザインの品質向上を進め、より良いユーザー体験を提供できるよう努めていきます。 明日の記事はmshibuyaさんです。引き続きお楽しみください。
こんにちは。メルコイン エンジニアのpoohです。 この記事は、 Merpay & Mercoin Advent Calendar 2024 の記事です。 目的 LLM(大規模言語モデル)による情報取得能力が向上している今日、以下で紹介する検索テクニックが不要になる時代が目前に迫っているかもしれません。しかし、まだしばらくは役立つ場面が多いと思いますので、ぜひ参考にしてください。 検索技術の中には「知っている人にとっては当たり前」でも、「知らない人にとっては驚き」につながるものがあります。今回ご紹介するのは、Slackで効率的に情報を検索するための基本的なテクニックです。 説明の前に 今回の投稿のためにSlackのドキュメントを読み、知らなかった検索クエリを知ることができ、いい学びとなりました。 新たに on:today は頻繁に使うようになりました。 自分の仕事の振り返りと情報共有を目的として個人的に日報を書いています。その際に、「from:@pooh on:Today」を早速使っています。 ダブルクォーテーションで括る 最初に紹介するのは、「ダブルクォーテーションで括る」方法です。 Slackでは通常、検索クエリに基づいて曖昧検索が行われます。たとえば、以下のような結果が得られます。 「探してね」で検索 → 「探し」「探す」がヒット 「見つかった」で検索 → 「見つか」「見つから」「見つかり」「見つかる」がヒット この曖昧検索は便利な場合もありますが、正確に特定の単語を検索したい場合には不便です。その際には、検索語句をダブルクオーテーション(例: "見つかった")で括ることで、完全一致する結果だけを表示させることができます。 特定キーワードの除外(-) 次に紹介するのは、「-(マイナス)」を使った方法です。特定のキーワードを含むメッセージを検索結果から除外したい場合に便利です。 例: KPI -FY24Q1 → 「FY24Q1」を含むメッセージを除外した結果が表示されます。 特定ユーザーの発言を検索(from:@) 特定のユーザーの発言のみを検索したい場合は、from:@を使用します。 例: from:@example → Slack名が@exampleの人の発言のみが検索されます。 さらに、複数のユーザーを対象とする場合は以下のように記述します。 from:@example1 from:@example2 → @example1と@example2の両方の発言が表示されます。 チャネル内の発言を検索(in:) 特定のチャネル内での発言を検索する場合には、in:を使用します。 例: in:#all-random → #all-randomチャネル内の発言が検索されます。 また、特定のDMを検索する場合には以下のように記述します。 in:@example1 → @example1とのDMを対象に検索。 複数のチャネルやDMを対象にしたい場合は以下のように記述できます。 in:#all-random in:example1,example2 特定ユーザーとのメッセージを検索(with:) with:を使うと、特定のユーザーとのやり取りを簡単に検索できます。 例: with:@example1 → 以下を含む発言が対象になります。 @example1とのDM @example1が参加しているスレッド内のメッセージ 日付を指定して検索(before: / after: / on:) 検索範囲を特定の日付に絞りたい場合は、before: / after: / on:を使用します。 例: on:2024-12-01 → 2024年12月1日の発言だけが表示されます。 before:2024-12-09 → 2024年12月8日以前の発言が表示されます。 after:2024-11-30 → 2024年12月1日以降の発言が表示されます。 日付は年月日のみならず、月単位でも指定可能です。 例: on:2024-10 → 2024年10月の発言 before:2024-10 → 2024年9月以前の発言 まとめ ここで紹介した検索クエリは組み合わせて使用することで、さらに効率的な検索が可能になります。 例: on:2024-12-08 from:@example1 "連絡して" -FY24Q1 → 「2024年12月8日」、「@example1」、「"連絡して"を含む」、「FY24Q1を除外した」発言が検索結果として表示されます。 適切な検索クエリを駆使することで、必要な情報を迅速かつ正確に見つけることが可能です。ぜひ試してみてください!
この記事は、 Merpay & Mercoin Advent Calendar 2024 の記事です。 はじめに こんにちは。メルペイ Frontend の @togami です。 私たちのチームでは Engagement Platform、通称 EGP という内製マーケティングツールの開発をしています。ポイントやクーポンなどのインセンティブの配布、LP の作成と公開、キャンペーンの作成など CRM 関連のことをマーケターや PM がコーディングの知識なしで行えるようにするための社内ツールです。EGP はメルカリ US を除く全てのプロダクトで使われている会社全体の共通基盤となっています。 本記事ではこの中でも LP 作成機能、通称 EGP Pages について紹介します。また、 EGP Pages の拡張版であり Server Driven UI の実装である EGP Cards について紹介します。 EGP Pages とは EGP Pages は WYSIWYG コンポーネントエディタです。LP の作成に特化しています。 Text Image Layout Lottie Entry Button など全部で 28 種類のコンポーネントが用意されており、それらを組み合わせて LP を作成します。このツールを使って公開された LP は月間で 5000 万 PV ほどあり、月によっては 100 を超える LP が新たに公開されています。 EGP Pages の使い方 どのように機能するのか簡単なサンプルの作成を通してみていきます。 このサンプルでは Web ブラウザで閲覧している時は QR コードとテキストを、Mobile で閲覧している時はテキストカスタムボタンを表示するようにします。 ①まずメニューから Layout コンポーネントを選択します(図1.1)。 図1.1 コンポーネントメニュー 左側の Tree View に Layout が追加されました。 ②この Layout は Web 向けのコンテンツのコンテナとして使用するため、右側のタブの設定から”Web Content”と名付けます(図1.2)。 図1.2 Layout へのネーミング ③続いて QR コードのコンポーネント、Text のコンポーネントを挿入します。Text には"Please install app"というテキストを入れます(図1.3)。 図1.3 QRとTextの追加と設定 ④次にアプリで閲覧している時のためのコンテンツを保持するために別の Layout コンポーネントを追加します。”Mobile Content”と名付け、その中にテキストとボタンを追加します(図1.4)。 図1.4 Mobile 用のコンテンツ追加 ⑤全てのコンテンツを作成した後、When コンポーネントを使用して Layout をラップします。このコンポーネントは表示条件を設定でき、その条件が True の時のみ表示されるものです。 ⑥When で作成したコンテンツをラップして表示する条件を追加します。JavaScriptで条件を追加が、多用する条件についてはテンプレートが存在するので利用します。ここでは Mobile でのみ表示したいので、”Is using mobile apps”を選択すると条件が設定されます(図1.5)。 図1.5 Whenによる条件分岐の設定 ⑦同様に Web 向けにもうひとつ When コンポーネントを追加し、⑧条件を設定します(図1.6)。 図1.6 Web用コンテンツの分岐 このエディタにはデバイスの設定(Web/iOS/Android)やログイン状態、エントリー状態の有無など、状態をエミュレートする機能が備わっています。⑨iOS を想定して描画結果を確認してみると、このように Mobile Content の When ブロック内の要素のみが描画されているのがわかります(図1.7)。 図1.7 エディタによるiOS環境のエミュレート ⓾同様に Web を想定したものです。Mobile のブロックは描画されておらず、Web のコンテンツのみが表示されています(図1.8)。 このように形でコンディショナルレンダリングをし、Web と Mobile で要素を出しわけたページを作成ができます。 図1.8 エディタによるWeb環境のエミュレート 簡単なサンプルを通して基本的な使い方を説明しました。今回は触れませんが、これ以外にも LOG の設定や API のモック、開発中のモック機能や Dark モード/Light モードへの対応、日本語以外の英語や台湾華語への設定なども行うことができます。 アークテクチャ EGP Pages は以下のようなアーキテクチャで構成されています(図2.1, 図2.2)。 図2.1 EGP Pages の全体アーキテクチャ 図2.2 EGP pages エディタ部分のアーキテクチャ DB は Cloud Firestore を使用しており、ユーザーが作成したコンポーネントやページの情報を保存しています。ユーザーが作成したコンポーネントやページの情報は全て JSON 形式で保存されており、それを元にページの描画しています。 先ほどの Firestore からロードしたスキーマを元にEGPのエディタの state の初期化 と 編集中の LP を React で描画するための transform を行います。 そして 描画された結果は iframe を通じてエディタに表示されます。 エディタの右側に各種設定項目があり、変更すると Redux の state が更新されて、ます。その結果がリアルタイムで反映されます。 エディタから LP を公開する時には Cloud Run functions 上で LP を SSG することでで HTML と CSS を生成します。生成された静的なページを CDN 経由で配信しています。 こうして配信された HTML が Mobile なら Webview 経由で、Web ならそのまま描画されます。 最終的には静的な HTML を配信しているだけなのでパフォーマンスについても概ね良好です。 エディタの仕組み エディタの仕組みについて、もう少し詳しく説明します。 一見複雑そうに見えますが、実際には各要素は決められたスキーマに沿った単純な JSON データです。先ほどのサンプルを例に挙げて説明します。 まず、Text コンポーネントですがこのエレメントは tagName: "Text" を持ち、 props として value や className を持っています。これらの value や className は、エディタの右側にあるパネルから変更できます。 具体的な JSON データは以下のようになります。 { "tlVV3bzegT-Pi59b3sJgQ": { "id": "tlVV3bzegT-Pi59b3sJgQ", "name": null, "tagName": "Text", "props": { "value": "Please install the app!", "className": "text-center text-[0.8125rem] text-[#000000]" }, "meta": null } } 次に、 QR コード コンポーネントも同様の構造を持っています。ただし、QR コード特有の url や表示に関するプロパティを持っています。 以下がその JSON データです。 { "SsUPPIaLOkuh7zUzgIn-S": { "id": "SsUPPIaLOkuh7zUzgIn-S", "name": null, "tagName": "QrCode", "props": { "url": "https://jp.mercari.com", "margin": "4", "scale": "4", "darkColor": "#000000", "lightColor": "#ffffff", "className": "self-center" }, "meta": null } } これらの各エレメントは、それぞれ親子関係を持って構成されています。具体的には、 Layout コンポーネントがコンテナとなり、その中に Text と QR コード のコンポーネントを子要素として持っています。 以下がその Layout コンポーネントの JSON データです。 { "id": "W682tXEHtC1eWgvLMvyYx", "name": "Web Content", "tagName": "Layout", "props": { "className": "flex flex-col", "children": [ ":=element.SsUPPIaLOkuh7zUzgIn-S", ":=element.tlVV3bzegT-Pi59b3sJgQ" ] }, "meta": null } children には、このレイアウトの子要素として含まれるコンポーネントの参照が記述されています。 ":=element.SsUPPIaLOkuh7zUzgIn-S" は先ほどの QR コードコンポーネントを、 ":=element.tlVV3bzegT-Pi59b3sJgQ" は Text コンポーネントを指しています。 つまり、この Layout コンポーネントは先ほど定義した QR コードと Text のコンポーネントを子要素として持ち、それらを含むコンテナとして機能しています。 これらの JSON データを元に、エディタ内部では React のコンポーネントを再帰的に組み立てていきます。具体的には、 tagName に対応する React のコンポーネントを生成し、props の情報をそれぞれに渡します。 こうして生成されたコンポーネントツリーが、最終的に画面上に描画されます。 図2.2(再掲) EGP pages エディタ部分のアーキテクチャ ユーザーがエディタでコンポーネントの設定を変更すると、その変更は直ちに対応する JSON データに反映されます。そして、その更新された JSON データを元に React コンポーネントが再レンダリングされ、プレビュー画面にリアルタイムで反映されます。 LP のスタイリング LP のスタイリングの裏側では Tailwind CSS を使っています。 CSS をあまり知らなくてもスタイリングが行えるように UIウィジェットを作成し、そこから Tailwind CSS の className を付与することでスタイリングしています(図2.3)。 図2.3 UIウィジェット また最終手段になりますが個々のエレメントに任意の className が付与できるので、細かい調整や少し複雑な表現も Tailwind CSS でできることは全て実現ができるようになっています。 Native Bridge LP の要件によっては Webview と Mobile 間での通信が必要です。例えばボタンをクリックした時にアプリ内の特定の画面を開いたり、特定のアクションを実行するといった動作です。 Webview と Mobile 間で通信するための方法はいくつかあります。その一つとして例えばカスタム URL スキームがあります。しかし、この方法では単方向でしか通信できなかったりセキュリティリスクがあります。 そこでより安全に Webview と Mobile 間で双方向通信を行うために、私たちは Onix という内製の Native Bridge を作成しました。これは Channel Messaging API を利用した双方向通信をサポートしています。 加えて端末の OS やバージョンによる Webview webview の API の差分のハンドリングや Channel Messaging API をサポートしていないバージョンの場合 Deep link へ fallback なども行います。 この Native Bridge は現在 iOS と Android のみ対応しているため、今後は Flutter の Webview でも対応する予定です。 技術的な課題 これまで一通りエディタについて見てきましたが、もちろん課題もあります。その 1 つとして このエディタは複数人によるリアルタイム編集ができません。 そのため、予期せぬ上書きが発生してしまうことがありました。 詳しくは こちらのブログをご覧ください 。 この問題に対しては yjs を使ってリアルタイム編集を実現しようと検証中です。このライブラリは CRDT(Conflict-free replicated data type) と呼ばれるデータ構造の JavaScript による実装で多くの同時編集機能があるアプリケーションで使われています。 運用上の課題 EGP Pages は あくまで LP というドメインに特化したエディタ です。そのため、大量の API コールや条件分岐が存在したり、特別なイベントハンドラが必要な 複雑なものを作ろうとすると限界があります。 LSP や Linter、Formatter、Syntax Highlight、コードジャンプや検索がない VSCode でいわゆる Web Application チックなものを開発しているのを想像してみてください。エンジニアの方には伝わると思いますが、このような制限された環境では複雑な機能を実装したり大規模な物を管理したりすることが非常に困難だということは想像に難くないでしょう。 加えて、 通常のテキストプログラミングと違ってテストを書くことも難しく基本マニュアルで QA しなければなりません。 変更によって予期せぬ問題が発生するリスクが常につきまとっているので、小さな修正でさえ慎重に作業を進める必要があります。 また、これはローコードプラットフォームを作っていると直面することが多い問題ですが、 できることが増えれば増えるほど、より複雑なデザインや機能要件が求められてしまいます。 個々の要件をエンジニアなしで済むような UI や仕組みとして加えていくと、表面上は綺麗でもどんどんコードが複雑化していきます。そのため、どこまでをエディタの機能として提供し、どこからはエンジニアによるカスタマイズが必要かを常に考えていく必要があります。 当初 EGP もエンジニアなしで完結する ノーコード/ローコードプラットフォームのような方向性もありました。しかし今は エンジニアと非エンジニアが効果的に共同作業できるツールを目指すという方向性になっています。 そのため、引き続き 非エンジニア向けに UI ウィジェットを改善していくのはもちろんエンジニア向けにコンポーネントの Encapsulation や KV pair のコンフィグなどの新しいメカニズムを導入してエンジニアと非エンジニアがよりシームレスに協働できる環境を整え複雑な要件にも対応できるようにしていきます。 Server Driven UI 最後に Server Driven UI の実装であり、現在開発中の EGP Cards について解説します。 Server Driven UI とはサーバーからデータを一緒に UI の構造も返却してクライアント(Web / Mobile)でレンダリングする手法です。 2021 年に AirBnB が出した A Deep Dive into Airbnb’s Server-Driven UI System という記事で有名です。 A Deep Dive into Airbnb’s Server-Driven UI System この手法には次のようなメリットがあります。 サーバー側の変更で UI を更新できるため、アプリのアップデートを待たずにリリースできる クライアント側の実装を簡素化できる クロスプラットフォームの一貫性を保ちやすい Native で描画するため Webview と比べてパフォーマンスが良い 採用背景 これまで、マーケターは主に別のツールを用いてパーソナライズされたコンテンツを配信していました。しかし、利用可能なコンテンツカードは 3 種類 のみで、そのカスタマイズ性も限定的でした。また、独自コンポーネントを使用しているため実際に配信されるまでコンテンツのプレビューができず テストが非効率 でした。 これに加えて、 各プラットフォームでの開発工数の増大 が課題となっていました。現在メルカリでは、プロダクトの増加に伴って使用しているプラットフォームが多岐に渡っています。 Mercari アプリ:Swift/Kotlin Mercari Shops、はたらくタブ :アプリ内 WebView Mercari Hallo:Flutter Mercari Web:Web 当初、この課題に対して EGP Pages の活用を検討しました。しかし、WebView を用いた実装では モバイルアプリのパフォーマンスに影響を及ぼす可能性があり 、高パフォーマンスを求める要件には適さないという問題がありました。このような状況下で、EGP Pages の利点を活かしつつ課題を克服するために Server Driven UI の採用を決定しました。 EGP Cards 私たちは、Server Driven UI の実装として EGP Cards と名付けたシステムを開発しました。 まず Mobile と Web 間共用の UI を記述するために Card UI Protocol という JSON ベースのプロトコルを定義しました。 この Protocol を用いてクロスプラットフォーム UI を記述します。 一例として、以下のような UI を考えます。 上下中央揃え、縦横 200px の Box Box 中に"This is test"という Text が中央揃えで配置 これを Card UI Protocol で記述すると以下のようになります。 { "id": "root", "type": "Layout", // UIの構造を定義 "styles": { "direction": "row", "wrap": false, "mainAxis": "center", "crossAxis": "center", "width": { "preferred": { "v": 200, "u": "px" } }, "height": { "preferred": { "v": 200, "u": "px" } }, "background": { "light": "#ffffff", "dark": "#222222" } }, "children": [ { "id": "SCaWRbQdLtwLoiCuEjh8h", "type": "Text", // UIの構造を定義 "styles": { "fontSize": { "v": 13, "u": "px" }, "textAlign": "center" }, "props": { "value": "This is Test" }, "children": [] } ] } この JSON をもとにレンダリングエンジンで描画すると以下のような UI になります(図3.1)。 図3.1 Card UI Protocolで記述したコンポーネントの描画結果 EGP Cards Editor このプロトコルで UI を簡単に作成できるように、EGP Pages のエディタを拡張しました。エディタ上で スタイリングされたコンポーネントを Card UI Protocol に変換して保存できるようにしています(図3.2)。 図3.2 エディタの Cards 向け拡張 EGP Pages では HTML ファイルと CSS ファイルを生成していましたが、EGP Cards ではコンポーネントを Card UI Protocol の形式に変換し最終的には JSON ファイルがデータベースに保存されます。 この JSON ファイルをクライアントに送信し、 各プラットフォームでネイティブの UI として描画します。 レンダリングエンジンは各プラットフォームの言語(Swift/Kotlin/Flutter/JavaScript)で実装されています。以下のように、単一の JSON ファイルから各プラットフォームで共通の UI を描画します(図3.3)。 図3.3 単一のJSONファイルをもとにしたクロスプラットフォームレンダリング さらに、この EGP Cards をセグメンテーションサービスと組み合わせることで、ユーザーごとにパーソナライズされたコンテンツの配信や、A/B テスト の実施などユースケースの拡大を図っています(図3.4)。 図3.4 セグメンテーションサービスと連携した例 まとめ 本記事では内製マーケティングツール Engagement Platform(EGP) と、その中でも特に EGP Pages および Server Driven UI の実装の EGP Cards について紹介しました。 今後も技術的・運用上の課題に取り組みつつ、EGP を進化させていくことでマーケティング活動の効率化と効果向上を目指していきます。 次の記事は @poohさん です。引き続きお楽しみください。
こんにちは。メルペイData Platformチームの@siyuan.liuです。 この記事は、 Merpay & Mercoin Advent Calendar 2024 の記事です。 Merpay Data Platformチームは、社内共通のデータ処理基盤の開発と運用を担当しており、バッチ処理やリアルタイム処理など、さまざまなPipelineを提供しています。その中でも、リアルタイムで大量のログを収集するStream Pipelineがあります。このStream処理は、Kubernetes上で Flink Operator を使用し、Flinkを自動的に管理することで実現されています。 しかし、このStream Pipelineでは、トラフィックピーク時にCPUやメモリ不足によるコンシューマーラグが発生するという課題がありました。これを防ぐため、通常は余剰のCPUやメモリを割り当てますが、その結果、コスト増加を招く新たな課題も生じます。 これらの課題を解決するため、HPA(Horizontal Pod Autoscaler)を導入し、外部のDatadogメトリクスにより、トラフィックに応じてサーバー数を自動でスケールアウトさせることで、ピーク時のリソース不足を防ぎました。また、Tortoise VPAを利用してリソースの使用率に基づいて動的なリソース割り当てを行い、無駄なリソース配置を抑制しコスト削減を実現しました。 本記事では、HPAとTortoise VPA(Vertical Pod Autoscaler)を活用したこれらの解決策とその効果について、参考コードと具体的なデータとともに詳しく解説します。 背景と課題 本節では、Stream Pipelineの構成、およびStream Pipelineにおけるトラフィックピーク時のリソース不足と過剰なリソース割り当ての課題について解説します。 Stream Pipelineの構成 Data Platformチームは、主に社内のPaaS Data Platformの開発と管理を行います。このプラットフォームは、Batch、Stream、CDCなどのデータパイプラインを提供し、各マイクロサービスに分散されたデータへのアクセスと活用を効率化します。その中でStream Pipelineは、各Microserviceからアクセスログとイベントログを収集し、ニアリアルタイムでデータをGoogle Cloud Storageに保存する役割を担っています。また、ログをBigQueryに直接保存することや、他のシステムに転送することも可能です。 本記事で取り上げる課題は、上図の赤枠で示したFlink処理の部分と関わります。この部分は主に、FlinkOperatorが管理するFlinkを使用し、Pub/SubやKafka Topicのデータを消費します。 課題1:トラフィックピーク時にCPUやメモリ不足によるコンシューマーラグ イベント、キャンペーンなどにより、一時的にアクセスログやイベントログのトラフィック量が急増することがあります。このような状況下で、Flink処理に十分なリソースを確保できない場合、Pub/Subのデータを消費しきれず、コンシューマーラグが発生します。これにより、データ同期の遅延や、データの損失につながる可能性があります。 このトラフィックピークによるコンシューマーラグをどのように解決するかは、Data Platformチームにとって重要な課題となっています。 課題2:過剰なリソース割り当てによるコスト増加 Flink処理の安定性を確保するため、Data Platformチームでは通常、Flinkに余裕なリソースを割り当てています。たとえば、通常時においてはCPU 0.5 Core、メモリ2048MでFlinkが正常に稼働しますが、トラフィックピークに考慮してCPU 1 Core、メモリ4096Mを割り当てることが一般的です。1つのFlinkに対してこのような対応を行う場合、大きなリソースの無駄にはなりませんが、Data Platformチームが管理するFlinkは200個以上あり、すべてのFlinkにこの方針を適用した結果、リソースの無駄が生じています。 そのため、Flinkのリソースを動的に最適化することが、重要な課題となっています。 HPAによるトラフィックピーク時の自動スケールアウト 本節では、HPAを使用したFlinkの自動スケールアウト方法と、FlinkのReactive ModeによるTask Managerリソースを最大限に活用する方法について、参考コードとともに解説します。 HPAによるFlink TaskManagerの自動スケールアウト トラフィックピークによるリソース不足のコンシューマーラグを防ぐため、HPAを導入しました。 KubernetesのHorizontal Pod Autoscaler(HPA) は、リソース使用量やメトリクスに基づいてPodのレプリカ数を自動的に調整する仕組みです。トラフィックのピーク時にはスケールアウトし、需要が低いときにはスケールダウンすることが可能です。また、Flink OperatorはHPAをサポートしており、HPAの設定でscaleTargetRefのkindをFlinkDeploymentに指定することで、TaskManagerのスケールアウト・スケールダウンを自動的に制御できます。 HPAで使用する外部のDatadogメトリクス oldest_unacked_message_ageは、サブスクライバーによって確認応答(ACK)されていない、サブスクリプションのバックログ内の最も古いメッセージの経過時間(秒単位)の指標です。トラフィックピーク時にFlinkがPub/Subのデータを消費しきれない場合、このメトリクスの値が増加します。このメトリクスをHPAの閾値として設定すれば、トラフィックに応じてFlinkのTaskManagerの数を自動的に調整できます。 参考コードは以下になります。 metadata: name: event-log-or-access-log-hpa spec: scaleTargetRef: name: event-log-or-access-log apiVersion: flink.apache.org/v1beta1 kind: FlinkDeployment maxReplicas: 12 minReplicas: 6 behavior: scaleUp: stabilizationWindowSeconds: 0 policies: - periodSeconds: 15 type: Pods value: 4 - periodSeconds: 15 type: Percent value: 100 selectPolicy: Max scaleDown: policies: - periodSeconds: 90 type: Percent value: 2 selectPolicy: Max metrics: - external: metric: name: oldest_unacked_message_age target: type: Value value: "420" type: External kind: HorizontalPodAutoscaler apiVersion: autoscaling/v2 この設定により、トラフィックピーク時にPub/Subの消費が7分(420秒)以上の遅延となった場合、FlinkのTaskManagerの数が自動的に増加され、リソース不足を防止します。また、トラフィックピークが収まった際には、TaskManagerの数が元の状態まで自動的にスケールダウンされ、リソースの無駄遣いを防ぎます。 Flink Reactive ModeによるTaskManagerのリソース利用率の最適化 トラフィックピーク時には、TaskManagerのリソースが自動的に増加しますが、それに伴ってJobの並列度(parallelism)を調整しないと、スケールアウトしたTaskManagerのリソースが十分に利用されません。 HPAによって増加したTask Managerのリソースを効率的に利用するため、Flink の Reactive Mode を導入しました。Reactive Modeでは、Jobの並列度が可能な最大値に設定され、Cluster内のTaskManagerのリソースを可能な限り利用します。これにより、トラフィックピーク時に TaskManager のリソースを最大限に活用し、効率的なリソース管理が可能となります。 ただし、Reactive Mode は Standalone Mode でのみ使用可能なため、利用時にはご注意ください。 FlinkのReactive Modeを有効化する参考コードは以下になります。 metadata: name: flink spec: image: flink imagePullPolicy: IfNotPresent mode: standalone flinkVersion: v1_18 flinkConfiguration: scheduler-mode: reactive Tortoise VPAによる過剰なリソース割り当ての最適化 本節では、過剰なリソース割り当ての課題を解決するため、Tortoiseの概念、Tortoise VPAのみを導入する理由、およびFlink Operatorが管理するFlinkへのTortoise VPA導入時の注意点を解説します。最後に、これらの理由と注意点を踏まえ、Tortoise VPA導入の参考コードを示します。 Tortoiseとは Tortoiseは、過去のリソースの使用量や過去のレプリカの数を記録しており、それを元にHPAやVPAを最適化し続ける仕組みです。この中で、Tortoise Vertical Pod Autoscaler (VPA) はCPUやメモリのResource Request/Limitを最適化する役割を担います。これにより、負荷が低下した際に、不要なリソース割り当てを削減することが可能になります。Tortoise の詳細については、以下の公開記事をご参照ください。 「 人間によるKubernetesリソース最適化の”諦め”とそこに見るリクガメの可能性 」 Tortoise VPAのみを導入する理由 TortoiseはHPAとVPAの両方をサポートしていますが、DataplatformチームではTortoise VPAのみを使用しています。その理由は二つあります。 一つ目は、DataplatformチームではすでにKubernetes HPAを利用しているためです。既存のKubernetes HPAは外部のDatadogメトリクスに基づいてリソースを調整していますが、Tortoise HPAは外部のメトリクスによるリソース調整をサポートしていません。 2つ目は、FlinkOperatorを利用しているためです。FlinkOperatorはカスタムリソースを用いてFlinkを管理していますが、Tortoise HPAはカスタムリソースをサポートしていません。一方、FlinkOperatorはKubernetes HPAをサポートしているため、既存のKubernetes HPAは問題なく利用できます。 以上の理由から、Data PlatformチームはTortoise HPAを導入せず、Tortoise VPAのみを使用しています。 FlinkへのTortoise VPA導入時の注意点 Flink Operatorが管理するFlinkにTortoise VPAを導入する際には、FlinkDeploymentをTortoiseに管理させるのではなく、JobManagerとTaskManagerのDeploymentを分け、それぞれのTortoiseに管理させる必要があります。JobManagerとTaskManagerは異なる役割を持ち、それに応じて必要なリソースも異なります。しかし、Tortoiseはカスタムリソースをサポートしていないため、Flink OperatorのFlinkDeploymentをTortoiseに管理させると、JobManagerとTaskManagerが同一のリソースとして扱われてしまい、適切な推奨値が算出されなくなります。 また、JobManagerのリソースがTortoiseによって過度にスケールダウンされると、リソース不足によるダウンタイムが発生する可能性があります。そのため、minAllocatedResource(リソース割り当ての最低値)を設定することが重要です 参考コード 上記の理由と注意点を踏まえ、Tortoise VPAによってリソースを調整する設定は以下になります。 metadata: name: event-log-or-access-log-jobmanager-tortoise spec: targetRefs: scaleTargetRef: kind: Deployment name: event-log-or-access-log-jobmanager updateMode: Auto resourcePolicy: - containerName: flink-main-container minAllocatedResources: memory: 4294967296 autoscalingPolicy: - containerName: flink-main-container policy: cpu: Vertical memory: Vertical kind: Tortoise apiVersion: autoscaling.mercari.com/v1beta3 --- metadata: name: event-log-or-access-log-taskmgr-tortoise namespace: merpay-dataplatform-jp-prod spec: targetRefs: scaleTargetRef: kind: Deployment name: event-log-or-access-log-taskmanager updateMode: Auto autoscalingPolicy: - containerName: flink-main-container policy: cpu: Vertical memory: Vertical kind: Tortoise apiVersion: autoscaling.mercari.com/v1beta3 結果と効果 本節では、具体的なデータをもとに、改善の結果とその効果について説明します。 1. HPAによるトラフィックピーク時の安定性の向上 HPA を導入した結果、トラフィックピークの際に Flink の Pod 数が自動的にスケールアウトされることで、リソース不足によるダウンタイムが発生しなくなりました。 上記の図は、Mercariの検索プロジェクトにおけるoldest_unacked_message_age(最も古い未確認応答メッセージの経過時間)とdeployment_replica(レプリカ数)のメトリクスを対照したものです。10:35 ごろ、 oldest_unacked_message_age メトリクスが 増加する直前に、Flink の Pod 数も増加していることが確認されました。また、同日の 16:55 ごろにピークが収束するとともに、Flink の Pod 数も自動的にスケールダウンされ、安定的なリソース利用が実現されました。 この動的スケーリングの結果、従来はリソース不足で発生していたコンシューマーラグはほぼ 0% にまで削減され、システム全体の安定性が大幅に向上しました。 2. VPAによるコストの削減 2024年5月にTortoise VPA を導入した結果、通常時のリソース使用量に基づいた最適な resource request と limit が自動的に設定され、不要なリソースの割り当てが大幅に削減されました。 その結果、月平均で 100万円以上、年間で 1200万円以上のコスト削減を実現しました。 まとめ Data Platformチームが管理するStream Pipelineにおいて、トラフィックピーク時のリソース不足によるコンシューマーラグと、過剰なリソース割り当てによるコスト増加という2つの課題とその解決策を解説しました。HPAを活用した自動スケーリングにより、Flinkのスケールアウト・スケールダウンを実現し、Stream Pipeline全体の安定性が向上しました。また、Tortoise VPAを用いることで、リソースの効率的な割り当てを行い、無駄なリソース配置を抑制し、コストの削減を実現しました。 今後もData Platformチームでは、Stream Pipelineのリソース最適化をさらに進め、同様の対策をCDC PipelineのKafka Connectにも取り込みたいと考えています。 次の記事は @togamiさん です。引き続きお楽しみください。
こんにちは。メルカリのQA Engineering managerの @____rina____ です。 この記事は、 連載:メルカリ ハロ 開発の裏側 – Flutterと支える技術 – の1回目と、 Mercari Advent Calendar 2024 の3日目の記事です。 先日、11月15日に開催された Tokyo Test Fest というイベントで、"Acceptance criteria: QA’s quality boost"というタイトルで発表を行いました。このセッションでは、Flutterに限らず、開発プロセス全体においてAcceptance criteriaをQAエンジニアが書くことの重要性と、それを開発チーム全員で同期レビューすることの大切さについてお話ししました。 Acceptance criteriaは、開発チームの協業を円滑に進めるための重要な要素です。これを正確に定義し、チーム全体が共有することで、品質を大いに向上させることができます。今回の発表では、私たちのプロジェクトでの具体的な事例をもとに、このプロセスの効果についても言及しました。 また、以前に投稿したAcceptance Criteriaに関する記事も、ぜひあわせてご覧ください(ブログは日本語のみです)。 以前の記事は こちら 今回はこの発表内容について詳細をお届けします。以下に書き起こしを掲載します。 Acceptance criteria: QA’s quality boost 東京テストフェストのみなさん、こんにちは!私はリナです。本日お越しいただきありがとうございます。これから、「Acceptance criteria:QA’s quality boost」というテーマでプレゼンテーションを始めます。 Our QA Team’s initiative さて、今日は私たちが行っている取り組みについてお話しします。具体的には、Acceptance Criteria(以下、AC)にテストケースを記載し、それを開発チーム全員でレビューするという方法です。 ACは、スクラムやアジャイル開発でよく使用されます。ACを導入する前は、ユーザーストーリーとテストケースが分かれていました。これにより、とくにテストの際に誤解が生じることがありました。 この新しいプロセスは、開発チーム全体に役立ちます!プロダクトマネージャー(以下、PM)、デザイナー、エンジニア、そしてQAエンジニアの皆が一緒により良く作業できるようになります。各チームメンバーのメリットを見てみましょう。  例えば、PMは、仕様の確認が容易になり、抜け漏れなく開発を進められるようになりました。以前は、テスト実施の段階で初めて仕様の矛盾に気づくことがありましたが、この活動を通して、早い段階で修正できるようになり、手戻りが減りました。  エンジニアは、フロントエンドとバックエンドの実装方針を事前にすり合わせることができ、スムーズな開発が可能になりました。  また、具体的な文言や表示方法をその場で確認することで、デザイナーからのフィードバックもリアルタイムに反映できるようになり、より質の高いプロダクト開発に繋がっています。  QAエンジニアにとって、テストデータの作成方法を共有し、テストを実行することは、テストフェーズでの作業改善に役立ちました。以前はテスト準備中にテストデータの作成についてエンジニアに相談する必要がありました。この新しいアプローチにより、テストの実行のしやすさに基づいて開発の順序について話し合うことが容易になりました。  チーム全体としては、複雑な開発手順でも、全員が共通認識を持って開発を進められるようになり、コミュニケーションコストが削減されました。また、複数プロジェクトが同時進行する際も、それぞれの進捗状況や依存関係を把握しやすくなるため、混乱することなく、スムーズにプロジェクトを進めることができています。それでは、この取り組みをどのように実践しているのか、詳しく見ていきましょう。 3つの取り組み 実施したことは3つです。 まず、テストケースをACに記載するようにしました。次に、レビュー方法を非同期レビューから同期レビューに変更しました。最後に、レビューする人を開発チーム全員としました。 ところで、みなさんは、普段テストケースをどこに保存していますか? また、誰がどのように活用していますか?  例えば…テスト管理ツールを使ったり、Google スプレッドシートや Excelで作成し、共有しているのではないでしょうか?このように、テストケースは様々な場所に保存され、活用方法も様々だと思いますが、多くの場合、QAチーム内でのみ参照され、他の開発メンバーはあまり活用できていないのではないでしょうか?テストケースの価値はQAエンジニアのみなさんは価値を理解していると思います。これを少数の人にのみ提供しているのはもったいないと思いました。  以前は、ユーザーストーリーとテストケースの関連性が弱く、重要なテストを見落とすリスクが高まり、開発プロセスの後半で手戻りが発生することがありました。テストケースはまるで隠された宝の地図のようでした。皆が利用可能であったにも関わらず、その価値は活用されていませんでした。チームはユーザーストーリー(島)に問題を抱えており、必要な助けがすぐそこにあることに気づいていませんでした。  以前は、適切なテストを見つけることは、多くの島が描かれた宝の地図を使うようなものでした。各島には宝がありましたが、地図で各島に何があるのかを探すのに時間がかかりました。今では、各島に標識があります!その標識がACです。それは、各ユーザーストーリーにどのテストが必要かを正確に教えてくれます。  例えば、標識は製品が何をすべきか、何をしてはいけないか、そしてどのようにテストするかを示してくれます。これにより、誰もが理解しやすく、質の高い製品を作り上げやすくなります。 Acceptance criteriaの例  このスライドは、私たちのACの例を示しています。私たちは、テスト対象、テストの条件、そして期待される結果を明確に定義します。  例えば、1行目では、テスト対象はタイトルとラベルの表示です。タイトルとラベルの両方に「ログイン」と表示されることを期待します。  2行目と3行目では、機能フラグの条件に基づいて、画面の期待される動作を定義します。  最後に、iOSとAndroidの両方で同じように動作することを確認するために、テストを行います。  明確な道標(AC)を設けることで、私たちのチームは開発をより効果的に進めることができます。その結果、チームのコラボレーションが改善され、エラーや手戻りが減り、高品質なプロダクトをより早く提供できるようになりました。チーム全員が目標と、そこに到達する方法を理解し、共に取り組んでいます。 効果的なレビューのシンプルなステップ  次に、私たちが実践しているレビュープロセスについてお話しします。このレビューは、開発チーム全体で品質に対する共通認識を醸成し、高品質なプロダクト開発を実現する上で非常に重要な役割を担っています。ACを中心に据え、テストケースの内容を全員で確認することで、認識のズレや手戻りを防ぎ、開発効率を向上させることができるのです。  次にレビューの方法についてお話します。とてもシンプルです!3つのステップがあります。  1つ目は、声に出して読み上げます。1人がそれぞれのACを声に出して読みます。各ユーザーストーリーに対するACとテストケースを確認できます。読み手はそれぞれの項目について簡単に説明します。  2つ目は、質問をすることです。読み上げの後、全員が質問できます。エンジニア、QAエンジニア、PM、デザイナー、誰でもです。様々な視点を持つことが重要です。例えば、 「このテストではどんなデータを使うの?」や「この部分は本当に必要?」あるいは、「ユーザーはこれを理解できるかな?」といった質問です。私たちは良い議論をすることができるようになります。  最後は、確認です。全員がACを理解していることを確認します。これでレビューは終了です。これらのレビューは、チームが最初から品質について合意するのに役立ちます。シンプルな3つのステップで、誰でも実行することができます。  なぜこの新しいレビュープロセスが効果的なのかについてお話します。私たちは、スペックレビューで要件を確認しています。しかし、その段階では、エンジニアとQAエンジニアは自身の領域に対して詳細まで理解することが困難なこともあります。それはぼやけた写真を見ているようなものです。今回のプロセスでは、スペックレビューの後、エンジニアは設計ドキュメントを作成し、QAエンジニアはACを作成し、テストを設計します。ここで初めて、各機能とユーザーストーリーに対する深い理解が得られました。  私たちの新しいレビュープロセスは、この詳細な検討の後に行われます。全員が要件について高い解像度の理解を持ってレビュー会議に参加します。これにより、より集中した生産的な議論が可能になります。共通の明確なビジョンを持って、潜在的な問題を特定し、詳細を洗練させることができます。プロセスの後半、全員が詳細な理解を深めた後にレビューを行うことで、整合性を確保し、誤解を最小限に抑え、最終的に品質の向上と手戻りの削減につながります。  このレビュー方式は、特別なスキルや経験がなくても、誰でも効果を実感できるのでしょうか? 私たちは、様々なスキルレベルのメンバーでこのレビュー方式を試しました。私自身の経験では、QAエンジニアとしてスクラムチームに所属していた時に初めてこの取り組みを始めました。当時は私一人で実施していましたが、チーム全体で効果を実感できました。  現在、私以外のQAエンジニアたちもこの取り組みを実施しています。もちろん、最初は戸惑うメンバーもいましたが、今ではスムーズにレビューを進めることができています。  なぜ、異なるスキルレベルのメンバーでも効果を実感できているのでしょうか?成功の鍵は、"共通認識" です。ACにテストケースを記載することで、エンジニア、QAエンジニア、PM、デザイナーなど、チーム全員が同じ情報を見て、同じレベルで議論できるようになります。  もちろん、もっと解像度をあげたいという改善すべき点もあります。しかし、このレビュー方式は、チーム全体の品質意識を高め、開発プロセスを改善するための大きな可能性を秘めていると確信しています。  本日は、ACにテストケースを組み込むことで、チーム全体の品質意識を高め、開発プロセスを改善する方法についてお話しました。テストケースをACに集約することで、誰でも簡単にテスト内容を確認できるようになりました。それによって開発チーム全員での同期的なレビューが可能になり、議論も活発化し、プロダクトに対する深い共通理解を得られるようになります。これらの変更によって、開発チーム全体が、開発初期段階から品質に関する共通認識を持ち、認識齟齬や手戻りを防ぐことができるようになりました。 私たちは、この取り組みをさらに発展させ、より効果的な品質保証活動を目指していきます。 みなさんも、ぜひこの方法を試してみて、チームで品質向上に取り組んでみてください! ご清聴ありがとうございました。何かご質問があれば、お気軽にお尋ねください。 この記事の内容が、みなさまのプロジェクトや技術的探求に貢献できたなら幸いです。引き続き メルカリ ハロ 開発の裏側 – Flutterと支える技術 – シリーズを通じて、私たちの技術的知見や経験を共有していきますので、どうぞご期待ください。また、 Mercari Advent Calendar 2024 の他の記事もぜひチェックしてみてください。それでは、次回の記事でお会いしましょう!
こんにちは。メルカリのSite Reliability Engineer (SRE)の @yakenji です。 この記事は、 Mercari Advent Calendar 2024 の2日目の記事です。 私たちメルカリのSREは、コアプロダクトであるフリマアプリ「メルカリ」の信頼性を維持・向上させるために、プロダクトのAvailability(可用性)とLatency(性能)を測定しています。また、それらに対してService Level Objective(SLO)を設定した上で、SLOを満たしているかや、一時的な障害などによりAvailabilityとLatencyが悪化していないかの監視を行っています。 その方法としてCritical User Journey (CUJ)に基づいたSLOを運用しています。今回、このSLOを見直し、以下を実現するSLOの再定義に取り組みました。 CUJの定義の明確化 各CUJに対して1対1となるSLIの定義 各CUJとSLOのメンテナンスの自動化 障害時の各CUJの挙動のダッシュボードによる可視化 本取り組みによりSLOのメンテナンスにかかる時間を 99%削減 するとともに、障害検知後に 影響範囲の特定にかかる時間をゼロ にすることを実現しました。 本記事では、上記の取り組みである User Journey SLO について、見直しするに至った背景と上記4項目の詳細、特にE2E Testを用いた自動化によるSLOの継続的最新化とその活用の取り組みを共有します。 現状の課題意識 本題に入る前に、メルカリにおける2種類のSLOと現状の課題意識を共有します。この章を通じて、なぜ私たちがUser Journey SLOの取り組みを始めたのか、何を目指したのかを知っていただけたら幸いです。 マイクロサービスごとのSLOとその課題 メルカリでは、バックエンドにマイクロサービスアーキテクチャを採用しています。例えば、ユーザー情報はユーザーサービス、商品情報はアイテムサービスというように、あるドメインごとにマイクロサービスとして独立しています(上記は一例であり実際とは異なります)。各サービスには必ずオーナーとなるチームが存在し、独立して開発・運用を行っています。各チームは自らの管理するサービスにSLOを設定したうえで、その目標を下回らないように開発フローを回すことが義務付けられています。また、このSLOをベースとしたモニターを作成することで、開発チームは管理するサービスの障害をアラートとして受け取り障害対応も行います。 開発チームがマイクロサービスごとに独立した開発・運用を行う上で、サービスごとのSLOを定義することは必要なアプローチと言えます。一方でサービスごとのSLOだけではいくつかの課題があります。その一つが、お客さま視点でのメルカリというプロダクトの信頼性を評価することが難しいということです。 各マイクロサービスはあるドメインの機能のみを扱うサービスです。お客さまが自身のアカウントの”ユーザー情報を編集する”ようなドメインに閉じたシナリオを想定した場合、関係するサービスはユーザーサービスだけで済むでしょう。この場合、”ユーザー情報を編集する” SLOの達成度合いはユーザーサービスのそれを用いることができるかもしれません。一方で、”購入された商品を発送する”のようなシナリオの場合、関係するサービスは複数にわたります。この場合には各サービスの達成度合いを単純に用いることはできません。 さらに、あるシナリオを想定した場合に実際に使用されるAPIは各サービスのごく一部でしかありませんし、サービスの開発チームはどのAPIがどのシナリオ・ページで使用されているか完全に把握することも困難です(APIは基本的に汎用的なものを用意し各ページで同じものを使用します)。反対にフロントエンドの開発者も、どのサービスにアクセスするかを厳密に意識することは通常ありません。 以上の背景から、マイクロサービスごとのSLOだけでは”購入された商品を発送できるかどうか”のようなお客さまが実際に感じる信頼性を評価できません。例えば上記の例でサービスA/B/CそれぞれのAvailabilityはSLOを満たしていたとしても、お客さま目線でのAvailabilityは想定よりも低いということが考えられます。また、障害対応の観点から見てみると、あるサービスAのアラートがトリガーされても、実際のお客さまに対してどのような影響が出ているのかを判断することもできません。これは障害の優先度の判断や、影響したお客さまへの対応時に問題になります。 SREにおけるSLO 上記の課題を解決するため、SREでは以前よりマイクロサービスごとのSLO以外の独自のSLOとして、Critical User Journey (CUJ)に基づいた私たちのプロダクトであるフリマサービス全体のモニタリングを行ってきました(CUJとはお客さまがプロダクトを利用する際に頻繁に行われる一連のシナリオのうち特に重要なものを指します)。 一方で、以下のような課題もありました。 定義が不明瞭: CUJの定義・CUJに関連するAPIの根拠などが記録されておらず、新たにCUJを追加することやメンテナンスが難しい CUJに対して1対多のSLOが存在: 関連APIのSLOを直接モニタリングしているため、関連APIが複数の場合に複数のSLOが存在し、お客さまが感じる信頼性の評価が難しい Updateが困難: 機能開発により関連APIは高頻度に変化し続けるが、人力での調査が必要なためメンテナンスコストが高く、最新の状態を維持できない SLOが悪化した場合の挙動が不明: 課題1・2と関連してSLOが未達となった場合に、お客さまにどのような影響が出るのかが明確化されていないため、対応の優先度の意思決定やSRE以外が使用することが難しい 特に課題3から、2021年ごろに設定してから十分なメンテナンスをすることができず、モニタリング対象のAPIの過不足がある可能性がありました。また、SRE以外も有効に使用することでメルカリ全体の信頼性向上やインシデント対応の改善に繋げることも背景として1から再構築することを決めました。 User Journey SLOの概略 まず、課題1の”定義が不明瞭”な点と課題2の”CUJに対して1対多のSLOが存在”する点に関連して、User Journey SLOにおいてどのようにCUJを定義・管理したか、どのようなSevice Level Indicaor (SLI)を定義したかを紹介します。 Critical User Journey (CUJ)の定義 User Journey SLOではこれまでのCUJを踏襲し、粒度を商品出品・商品購入・商品検索のように定めました。具体的な各CUJについても再検討を行い、大小含めて約40のCUJを定義しました。課題1を改善するため、すべてのCUJを画面操作とそれによる画面遷移という形で定義し、以下のような遷移図としてドキュメント化しました。さらに、各画面でAvailableな状態も定義し、それを満たしている場合をそのCUJがAvailable・満たしていない場合はUnavailableとしました(基本的に各CUJのコア機能が提供できればAvailableとし、サジェストなど動作しなくてもコア機能に影響しないサブ機能の動作は無視することにしています)。 SLIの決定 課題2を改善するため、定義したCUJを基に、各CUJのAvailablityとLatencyのSLIがそれぞれ1対1になるようなSLIを定義します。SLIはあくまでObservebilityツールで取得可能なメトリクスなどを使用して測定する必要があります。メルカリではBFFなどを用いたお客さまの1操作 = 単一のAPIコールのような構成ではなく、基本的に1操作で複数のAPIコールが発生します。 CUJの各画面が成功したかどうか直接的に測定できれば良いですが、現在そのような仕組みを持っていません。新たな仕組みを導入し直接的な測定を行うことも考えられますが、約40のCUJを全てカバーしつつ、iOS・Android・Web全てのクライアントでアプリを改修することは、とてもエンジニアリングコストが高く現実的ではありません。また、APMツールのRealtime User Monitoring (RUM)から取得することも検討しましたが、サンプリングレートやコスト、実現性の観点から現時点ではこれも困難と判断しました。 そこで、CUJの間に実行されるAPIを関連するAPIとして、そのAPIの成功率などのメトリクスを使用します。ただし、各操作で発生するAPIコールの中には(1) 失敗したらCUJがUnavailableになるもの、(2)失敗してもCUJがAvailableになるものの2つに大別できます。User Journey SLOではSLIをより正確かつロバストなものにするため、(1)に該当するAPIのみを”クリティカルな API”と定義しSLIの計算に使用することにしました。 クリティカルなAPIのメトリクスを使用して、以下の式でCUJのAvailability・LatencyのSLIをそれぞれ一意に定まるように定義しました。 Availability: 複数のクリティカルなAPIの成功率の乗算をCUJの成功率とする クリティカルなAPI A,Bの成功率を S A , S B とするとCUJの成功率 S CUJ は以下で計算 S CUJ = S A  × S B Latency: 複数のクリティカルなAPIの目標レスポンスタイムの達成率のうち最も低い達成率をCUJの達成率とする クリティカルなAPI A,Bの目標達成率を A A , A B とするとCUJの成功率 A CUJ は以下で計算 A CUJ = min( A A , A B ) クリティカルなAPIの抽出 上記をSLIとして使用するためには、各CUJごとにクリティカルなAPIを抽出する必要があります。コードの静的解析などの方法も考えられますが、現実的に実現できるか等を勘案した結果、実際のアプリケーションを用いる形で以下の手順で抽出を行いました。 開発アプリケーションと開発環境の中間にプロキシを設置した上でCUJに基いてアプリを実際に操作、実行されたAPIを記録 プロキシで各APIのレスポンスが500エラーを返すように障害注入を設定してアプリを再度操作、CUJの基準を用いてAvailableかどうかを検証 ※クライアントとしては、メルカリのクライアントとして最も使用されているiOS版メルカリを使用しました。 通常クライアントアプリ-サーバー間の通信は暗号化されています。今回は暗号化された状態でも通信の確認とレスポンスの書き換えが可能なプロキシを選定しました。Webインターフェースを通じたインタラクティブな操作ができること、アドオンを開発することで必要な機能を追加できる点から、OSSの mitmproxy を採用しました。 これらの取り組みにより障害検知はCUJとともに通知されるため、障害検知後の影響範囲の特定にかかる時間がゼロになり、対応優先度の決定を瞬時に行うことを実現します。 iOS E2E Testを用いた継続的最新化と可視化 次に、課題3である”Updateが困難”なことを改善するE2E Testを用いたクリティカルなAPIを最新に維持する方法と、課題4である”SLOが悪化した場合の挙動が不明”な点を改善する障害時のアプリの挙動を可視化する方法を紹介します。 自動化の必要性 メルカリのiOSアプリは1ヶ月に複数回リリースされています。また、 トランクベース開発 によりフィーチャーフラグを使用してアプリのアップデートなしで新機能のリリースを行うことも可能です。これら全てのアプリの変更をSREが把握することは困難です。また、手動では高頻度のクリティカルなAPIの調査も困難です。結果として知らない間にクリティカルなAPIが変わり、必要なAPIをモニタリングできなかったり、不必要なAPIを過剰にモニタリングしてしまう事態につながります。そのため、アプリの変更に追従してクリティカルなAPIを定期的に更新するためには更新プロセスの自動化が必要です。 iOS E2E Testを用いた自動化 メルカリでは既にXCTestフレームワークを用いて iOSアプリのE2Eテストを自動化 しています。この既存資産を活用してクリティカルなAPIの抽出を自動化することにしました。 具体的にはまず、XCTestでCUJをテストケースとしてシミュレータで実行可能とします。さらにこのテストケースに対して、CUJで定義したAvailableな状態かを検証するアサーションを追加します。これによりXCTestで実行したCUJがAvailableな状態だったかどうかを自動で判別可能な仕組みが整いました。また、テストケースはアプリと同じリポジトリでバージョン管理されます。 XCTestとは別に、前章で用いたプロキシに対して”プロキシで記録したAPIリストの取得”と”任意のAPIに対する障害注入”を操作API経由で実行可能にするアドオンを開発しました。このアドオンにより、XCTestのテストケースやスクリプトからプロキシの操作が可能になりました。 上記のXCTestの実行とアドオン経由でのプロキシの操作をスクリプト化することによって、前章で示したクリティカルなAPIの抽出を自動で実行します。同時に、実行されたAPIがクリティカルなAPIかどうかの結果と障害注入時のアプリのスクリーンショットをそれぞれBigQueryとGoogle Cloud Strage (GCS)に記録します。 BigQueryに記録したテスト結果はIDで管理し前回実行時との差分を比較できるようになっています。また、APMに作成するSLO・Monitor・Dashboardの定義はUser Journey SLO用に開発したTerraform moduleを用いて行います。これによりクリティカルなAPIを定義するだけで、差分の適用や新しいCUJの追加を行うことが可能になりました。 上記の自動化により以下を実現しました。 コードメンテナンス以外の作業をほぼ全て自動化 テストケースとアプリの変更を同一のリポジトリでバージョン管理 テスト結果をID管理し差分を効率的にAPMに反映 最終的なテストケース数は約40のCUJに対して約60となりました。手動実行では困難な数のテストケースを自動化により効率的に運用することに成功しました。また、約60のテストケースを手動で実行してSLOのメンテナンスを行った場合に比べて、自動化によりメンテナンスにかかる時間を99%削減しました。 ダッシュボードによる結果の可視化 User Journey SLOの最終的に目指す姿の一つは、SRE以外も障害対応やお客さま対応で使用できるようにすることです。そのためには、最新のクリティカルAPIや障害発生時のCUJの挙動を誰もがアクセスできる形とすることが必要です。そこで、Looker Studioでこれらの結果を可視化し、各CUJのAPIコール一覧、APIの障害発生時にアプリのどの画面で失敗するかをスクリーンショットで可視化しました。 現状と今後 前章までの活動でUser Jouney SLOに対して以下を実現しました。 CUJの定義の明確化 各CUJに対して1対1となるSLIの定義 各CUJとSLOのメンテナンスの自動化 障害時の各CUJの挙動のダッシュボードによる可視化 その結果として、約40のCUJと約60のテストケースに対してSLOの運用を行っています。現状はSRE内で試験活用中の段階です。現時点でも新しいSLOの導入により、以下の項目の向上を体感しています。 障害発生検知の速度・精度 障害影響範囲把握の精度 障害原因特定の速度 品質可視化の精度 数値的には以下の効果を実現しました。 障害検知後の 影響範囲の特定にかかる時間がゼロ SLOのメンテナンスにかかる時間を 99%削減 この現状を踏まえて今後はSRE以外での活用を進め、以下の取り組みを行っていく予定です。 社内の障害基準としての活用 お客さま対応での活用 おわりに 本記事ではメルカリのCUJに基づいたSLO運用について、SLI・SLOの詳細やiOS E2E Testを使用した継続的な最新化の取り組みについて紹介しました。SLI・SLOの運用に取り組む方々に何かの参考になれば幸いです。 明日の記事は….rina….さんです。引き続きお楽しみください。
この記事は、 Merpay & Mercoin Advent Calendar 2024 の記事です。 はじめに こんにちは。メルペイでBackend Engineerをしている @hibagon です。2024年4月に新卒として入社しました。 この記事では、メルカリ新卒1年目のエンジニアがどのようなことをしているのかについてご紹介できればと思います。特にインターンや新卒としてメルカリで働くことに興味のある方の参考になれば嬉しいです! それでは早速Let’s Go! 4月🌸 4月はDevDojo(※1)をはじめとする新卒研修に大半の時間を費やします。DevDojoでは、Go言語などのメルカリで使用されている技術を中心に、さまざまな分野について横断的に学びます。 研修の合間の時間では、私のチームでの業務に用いるドメイン知識や開発に関する知識のキャッチアップを行いました。ただしこの時点では、オンボーディング資料があまり整備されていないという問題がありました。私のチームは比較的多くのインターン生を受け入れているということもあり、今後のためにも、キャッチアップ内容のアウトプットとしてオンボーディング資料を執筆することにしました。 ※1 技術トレーニングDevDojoで実際に使用されている学習コンテンツを公開しています。 こちら をご参照ください。 Gopherくん(原著者:Renée French) 5月🌿 ゴールデンウィーク明けからは、いよいよ本格的に配属先のチームに合流して業務を開始しました。 最初に取り組んだのは、リリースフローの改善でした。私のチームではgit flowを採用しており、毎週決まった日にリリースブランチとリリース用のPull Requestを作成する運用なのですが、当時はこれを手動で行っていました。これをGitHubActionsを用いて自動化しました。一見大きなインパクトが無いように思えるかもしれませんが、週に30分程度の定期業務を完全に自動化できたことにより、本記事を執筆している現在までの7ヶ月間で約2人日分の工数削減を達成できたことになります。 その他も小さなタスクを拾いつつ、業務に必要な知識のインプットを中心に行いました。そして5月末時点で、4月から継続的に書き進めていたオンボーディング資料のVer.1を書き上げました🍤 6月☔ 6月からは、業務内容が徐々に本格化していきました。 メルペイにはMerpay APIというBFF (Backend for Frontend) があるのですが、リリースから時が経つにつれMerpay APIが巨大化し、責務も曖昧になってきているという問題が発生しています。 これを受け、Merpay APIが持つ全てのAPIに対して、ひとつずつAPIに責任を持つチームを明確にし、Merpay APIを複数のBFFに分割することで管理することを目指したMerpay API Rearchitectureというプロジェクトが進行しています(※2)。 https://engineering.mercari.com/blog/entry/20231023-mmtf2023-day3-9/より引用 私のチームでは、Ownershipを持っているサービス中では比較的小規模な3D Secureに関するAPIから移行することを決め、このプロジェクトの担当として私がアサインされました。これに際し、 昨日のアドベントカレンダー でも解説されているgRPC Federationという技術を用いました。 さらに、7月中旬開始のメルカードのキャンペーンに向け、開発を行うための知識のキャッチアップを行いました。キャッチアップを行う中で、キャンペーンを行うための開発に関して、課題があることを理解しました。具体的には、キャンペーンのたびに一定規模の開発が発生していること、BFFであるMerpay APIにビジネスロジックが流出していることなどの問題がありました。こちらの課題を整理して、社内Tech Talkで発表しました。現在は改善されつつあります。 またこの頃から、オンコールのシフトにも加わりました。オンコールとは「サービスのパフォーマンスが悪化したり、停止が疑われたりする場合に備えて担当者が常時対応できるようにしておく仕組み」です。メルカリグループではPagerDutyを使用してオンコールシフトを管理しており、PagerDutyに紐づけられた特定のDatadog Monitorが閾値を超えた場合、自動で担当者を呼び出す仕組みになっています。 ※2 詳しくは 【書き起こし】gRPC Federation を利用した巨大なBFFに対するリアーキテクチャの試み – goccy【Merpay & Mercoin Tech Fest 2023】 をご参照ください。 7月🌞 7月前半は、7月中旬開始のメルカードのキャンペーンに向けた開発を急ピッチで進めていました。開発自体も(最初に取り組むちゃんとしたプロジェクトとしては)大変ではあったのですが、それ以上に他チームと連携するのが大変でした。キャンペーン開始を遅らせないために、エンジニアというポジションに関わらず積極的に進捗確認したり、課題を整理するなどの立ち回りを意識して行えたのは良かったポイントかなと思っています。QA中にも不具合など様々な問題が発生して、そのサポートも中々大変でしたが、結果としてはキャンペーンは予定通り開始されたのでよかったです。 実際のキャンペーンLPの一部 7月後半からは、インターン生のメンターを初めて担当させていただきました。私自身も入社してから3ヶ月程度しか経っておらず一抹の不安はありましたが、それ以上にワクワクもしていました。 7月中の目標としては、チームないしメルカリという会社に馴染んでもらうということを最優先に考えていました。そのため、積極的にご飯会を企画したり、社内部活に一緒に参加したりしました。また、この時のために温めていたオンボーディング資料を用いてキャッチアップを進めてもらい、必要に応じて加筆修正も依頼しました。オンボーディング資料は、このようにして新メンバーが加わった際に読んでもらいながら、最新の情報にアップデートしていく運用が良いと思っています。 8月🏖️ 8月は、メンター業務を引き続き行いながら、主に2つのタスクを行っていました。 1つ目は、先程述べたメルカードのキャンペーンの次に行うキャンペーンに向けた開発を行いました。7月は初めてということもあり、私のチームのメンバーに伴走してもらいながら進めていましたが、8月のキャンペーンでは、自走できるようになることを意識していました。 2つ目は、Merpay APIにある3D Secureに関するAPIの移行をgRPC Federationを用いて進めました。これは6月に着手していたのですが、7月は別のタスクを優先していたため、後回しになっていました。この頃から、Merpay API Rearchitectureプロジェクトに関しては、チームでのカウンターパートとして、私が情報収集や開発の主導をしていくことになりました。進捗としては、8月中にはおおよそ3D SecureのAPIの移行準備自体は完了しました。ただ他プロジェクトとの兼ね合いもあり、すぐに本番適用はしていません。 またこの頃に、新卒2年目以内の社員とインターン生が主体となって行うアイディアソンである「未現会議」というイベントの運営にも加わり、計画を進めていきました。 9月🍁 9月は、メンターをしていたインターン生のインターン期間が終了するので、それに関連したサポート業務がメインでした。具体的には、インターン生がキリの良いところまで開発を完了できるようにサポートしたり、インターン後に開始することになったQAのサポートを私が行えるようにするために、引き継ぎをしてもらったりしました。 初めてのメンターを終えたので、ここで振り返りをしておきます。 メンターとして心掛けていたのは、フランクに話せる関係を作り、何でも質問しやすい雰囲気を作ることでした。そのためには、同期的なコミュニケーションの機会が重要だと考え、毎日30分から1時間ほど1on1の時間を取りました。そこでは、業務を進めていくためのインプットや質問対応だけでなく、仕事以外の雑談も交えるようにしていました。また、1on1の時間でも、いつでもメンション飛ばしたり、スポットの1on1セッティングしていいからね!という強調も意識的に行いました。結果としてインターン生が高いパフォーマンスを発揮してくださり、新卒内定のオファーをもらうまで至ったのはメンターとしてとても嬉しく思いました。 余談ですが、インターンお疲れ様会でプレート入りデザートをサプライズで頼んでおいたら、インターン生からもチームメンバーからもとても好評で嬉しかったです。 その他には、引き続きキャンペーンのための開発業務も行っていました。この頃には、ほぼ自走できるようになっていました。 ところで、9月中旬のメンター業務が終了したタイミングで少し長めの休暇をもらい、オーストラリア🇦🇺に旅行に行きました。シドニー港は、世界三大美港に数えられるだけあって、とても美しかったです。また、コアラやクジラなどの様々な生き物に触れ合ったり、砂漠やユーカリの森などの広大な自然を体感して、とてもリフレッシュできました。 クジラのジャンプ(ブリーチ) シドニーの夜景 10月🎃 10月は、なんと早くも2人目のインターン生のメンターを行うことになりました。これは他チームの都合もあり急遽決まったのですが、メンター自体は初めてではなかったので、それ程不安はありませんでした。 例によって10月前半は、チームやメルカリという会社に馴染んでもらうということを意識していました。また、オンボーディング資料を用いてキャッチアップを進めてもらいつつ、加筆修正も加えてもらいました。オンボーディング資料の改善サイクルが良い感じに回ってきたので、この流れを止めることのないようにしたいと思います。 メンター業務以外では、10月中に開始するキャンペーンのQAサポートと、3D Secureに続いてメルペイのiDやバーチャルカードに関するAPIの移行を進めて行きました。 さらに、別の小さめのプロジェクトでは、Spec作成から開発までを受け持つことになりました。小規模ではありましたが、それまではちゃんとしたSpecを書いたことがなかったため、とても良い経験になりました。 またこの頃から、「未現会議」に関するタスクも少しずつ増えてきました。 11月🍂 11月は、内容は伏せますが大玉の開発案件があったため、チーム一丸となってAll for Oneで、その開発を進めていました。ありがたいことに、インターン生もとても活躍してくれました。 今まで述べてきた通り、私はチーム内ではgRPC Federationの知見が一番あるため、gRPC Federationを用いたBFF用新規マイクロサービスの構築を担当しました。 新規マイクロサービスを1から作成するという経験はあまり出来ないのでとても面白く、知見も深められました。特にサービスのリソース設定、モニタリング設定、オンコールの設定、本番環境で新規マイクロサービスを動かすために必要なチェックプロセスなどを通して、普段私のチームが持っているマイクロサービスに関する解像度も高まりました。 また、11月は「未現会議」のアイディア募集期間ということもあり、アナウンスやLunch & Learnの開催など、イベント盛り上げのためのタスクにも、運営チームとしてAll for Oneで取り組みました。 https://about.mercari.com/about/about-us/ より引用 12月以降🎄 この記事を書いている12月以降は、次に控えている大玉の開発案件や、Merpay API Rearchitectureの継続進行、「未現会議」のイベント成功などに向けて尽力していきたいと思います! また、年明けから3人目のインターン生のメンターをさせていただくことになったので、引き続きメンター業務の方も頑張っていこうと思います💪 ところで、今年の年末年始は有休を使わなくても9連休あるということで、非常に楽しみですね!特に今年はスノーボードセットを一式揃えたので、たくさん滑りにいけたらと思っています🏂 まとめ 本記事では、メルペイ新卒1年目のエンジニアがどのようなことをしているのかについて、私自身の振り返りの意味も込め、できるだけ詳細に書いてみました。メルカリは、新卒1年目から1人のプロとして扱われ、とてもチャレンジングなことを任せてもらえる環境です。この記事が、そんなメルカリでインターンや新卒として働くことに興味のある方に対して、具体的なイメージを広げる助けになれば嬉しいです。インターンや新卒採用に関しては、 こちら をご参照ください! 次の記事は@timoさんです。引き続きお楽しみください。
Mercari DBRE( D ata B ase R eliability E ngineering Team)のtaka-hです。 本エントリでは、メルカリの開発環境のデータベース移行の事例を紹介します。なお記事は、 MySQL Advent Calendar 2024 、および TiDB Advent Calendar 2024 とのクロスポストになります。 この記事の手法を用いて、開発環境では、MySQLからTiDBへ移行すると同時に、スキーマ統合するということを達成しています。本記事により、読者の方々がProxySQLや、TiDBのエコシステムに興味を持っていただけることを期待しています。 TL;DR TiDBのData Migration(以後、TiDB DMと呼びます)のTable Routingを用いると、TiDBにスキーマ統合したインスタンスを作成できる 2段のProxySQLを構成し、1段目でルーティングを、2段目でinit-connectにスキーマ変更を設定することによって、アプリケーションの滑らかな移行が達成できる 次の構成図を、説明しきることが本エントリのゴールです。 2段ProxySQLによるスムーズなアプリケーション移行の実現 背景 本節では、今回、スキーマ統合に至った経緯を簡単に説明します。 メルカリの本番環境では、サービス提供開始時から必要なデータ保存量やトラフィックの増加に伴い、垂直シャーディングを繰り返してきました。すなわち、データの性質が異なり、結合クエリの発行が必要ない範囲に関しては、ソースおよびレプリカのセット(以後、クラスタと呼びます)を分け、複数のクラスタで運用をしています。 垂直シャーディング 一方で、開発環境ではデータ規模が商用環境ほど大きくないため、運用をシンプルにするためにも単一のクラスタで運用をしています。 ただ、この垂直分割は一定回数繰り返すことで、分割の難易度は次第に高くなります。やがて、大きなクラスタをこれ以上分割するのが難しくなり、データベースのスケーラビリティを解消するために、非常に複雑で骨の折れる、データおよび機能実装の分離が、アプリケーションに求められるようになります。反対に分割を行わないと、より高性能なハードウェアが求められ、マネージドサービスで所望のインスタンスクラスがない、また、ハードウェアの調達時間が長期化しやすい、といった悩みもありました。そこでメルカリでは、MySQLの互換性が非常に高く、スケーラビリティーの優れたTiDBへ移行することを決断しました。 垂直シャーディングは次第に難しくなる 次に、 垂直シャーディングとスキーマの関係について説明します。 クラスタ構成が開発環境と商用環境で異なるため、アプリケーション開発はこれを前提に行う必要があり、開発時にあらかじめテーブルの結合などが必要ない範囲を定義し、開発環境では、そのまとまりごとにスキーマを作成していました。 開発環境と商用環境のクラスタ/スキーマ構成 開発環境と商用環境の差分のイメージ 開発環境でスキーマをまたがなければ、商用環境でクラスタをまたぐ心配はなく、そのような観点から、アプリケーションはスキーマをまたぐようなクエリは発行しないような実装としていました。 他方で、開発環境と商用環境のスキーマの環境差分は、ユーザー定義、他システム連携のシステム構成など様々な環境差分につながり、開発環境の構成が過度に複雑化したり、開発環境での様々なテストの実効性を低下させることにつながっていました。そこで、TiDBの移行の決定とともに、元々抑止しようとしたクラスタまたぎのクエリを事前に抑制することが一時的に達成されなくなる点を加味しても、スキーマ統合を行いこの環境差分を解消することにしました。 切替の作戦 本節では、MySQLで複数のスキーマに分かれていたデータへのクエリを、TiDBにどのように安全かつスムーズに移行していくか、についての考慮事項について記載します。 対象としているデータベースは、利用しているアプリケーションがとても多く、複数箇所のデータベースの接続の設定変更に、時間がかかることが予想されました。各チームは異なる開発サイクル(Sprint)を持ち、DBREで事前に十分にテストを行っているとはいえ、切戻しも考えられました。 そこで、以下の2点を考慮の上、最終的には ProxySQL というプロキシを下図のように多段に構成し移行をすすめました。 切替を各アプリケーションで個別に行うのではなく、DBREで一元的にコントロールする アプリケーションの必要なコンフィグ修正のタイミングに柔軟性をもたせる 2段ProxySQLによるスムーズなアプリケーション移行の実現 なお、本移行においては、ProxySQLの機能としては非常に重要であり、これを目的に利用する人が多いであろう、 Multiplexing と呼ばれる接続を多重化する機能を無効化して利用しています。 移行後のデータの準備 本節では、移行後のデータの準備について説明します。 MySQLからTiDBへの移行には、 TiDB DM を利用します。 TiDB DMはMySQLのbinary logの情報を元に、DDLを含む変更/差分ログをダウンストリームのTiDBに反映することができます。 TiDB DMによる移行先データの準備 TiDB DMでは、初期のデータ同期および、差分の同期がどちらも可能で、大量のデータがある場合は、TiDBに対して、論理データを非常に高速に物理インポートする TiDB lightning と合わせて利用することで、高速にデータが準備できます。 スキーマ統合については、TiDB DMの標準機能である Table Routing を利用することにより、非常に簡易に解決されます。アップストリームのMySQLの、どのスキーマのどのテーブルを、ダウンストリームのどこに移動するか、を routes という設定に RE2 syntaxの正規表現で記載するだけで、統合したデータが準備されます。 下記に、TiDB DMのコンフィグ例 (一部抜粋)を記します。 mysql-instances: - source-id: "dev-database" route-rules: ["route-mercari", "route-mercari-admin"] target-database: host: tidb.internal.mercari.jp. routes: route-mercari-admin: schema-pattern: "mercari_dev_jp_admin" target-schema: "mercari_admin" route-mercari: schema-pattern: "mercari_dev_jp_master|mercari_dev_jp_ads|..." target-schema: "mercari" 2段ProxySQL 本節では、2段ProxySQLにより先程の要件をどのように達成するかを順に説明します。 1段目のProxySQLは、トラフィックシフトを行う役割を持ちます。 ProxySQLでは、 mysql_query_rules という設定で、様々なルーティングなどが可能で、移行割合を weight というパラメータにより、コントロールできます。 これにより、アプリケーションからのレプリカに対する10%のトラフィックをTiDBに、残りの90%をMySQLにルーティングする、といったことなどが実現できます。 また、TiDBへのトラフィックに関しては、1段目のProxySQLが接続するスキーマに応じて適切な2段階目のProxySQLにルーティングします。 これによりアプリケーション開発者が設定を変更することなく、MySQLからTiDBへ切り替わっていく状況が達成されました。 mysql_servers= ( { address = "mercari-admin-proxysql-router-ilb.internal.mercari.jp." # ProxySQL(mercari_admin) hostgroup = 2 weight = 100 }, { address = "mysql.internal.mercari.jp." # 移行元MySQL hostgroup = 2 weight = 0 }, { address = "mercari-proxysql-router-ilb.internal.mercari.jp." # ProxySQL(mercari) hostgroup = 3 weight = 100 }, { address = "mysql.internal.mercari.jp." # 移行元MySQL hostgroup = 3 weight = 0 }, { address = "tidb.internal.mercari.jp." # 移行先TiDB hostgroup = 6 weight = 100 } ) mysql_query_rules= ( { schemaname = "mercari_dev_jp_admin" # 旧スキーマ flagOUT = 1 }, { schemaname = "mercari_admin" # 新スキーマ flagOUT = 2 }, { schemaname = "mercari" # 新スキーマ flagOUT = 2 }, { flagIN = 1 destination_hostgroup = 2 # ProxySQL(mercari_admin) or MySQL }, { destination_hostgroup = 3 # ProxySQL(mercari) or MySQL }, { flagIN = 2 destination_hostgroup = 6 # TiDB } ) 2段目のProxySQLは、アプリケーションが古いスキーマに接続する設定で動作する際に、スキーマ統合後のTiDBに透過的にアプリケーションを実行させる役割を持ちます。 現在、移行元のMySQLと、移行先のTiDBでスキーマ構成が異なりますから、何も考慮せずにMySQLのトラフィックをTiDBへシフトさせると当然失敗する訳です。スキーマが異なったとしても、同じアプリケーションの設定で、重み付きで少しずつトラフィックをシフトできたら、事前のテスト可能性が向上し、大変望ましいと思います。 この問題を、MySQLサーバーのグローバルスコープのパラメータである、 init_connect に関連したProxySQLの init_connect パラメータでスキーマ変更をさせることにより、解決させます。 mysql_variables= { … init_connect = "use mercari_admin" … } mysql_servers= ( { address = "tidb.internal.mercari.jp." hostgroup = 1 weight = 100 ) 実例にて、動作を確認しましょう。 mercari_dev_jp_adminというスキーマがmercari_adminのスキーマに、スキーマ統合(移行)されるケースを考えます。アプリケーションのスキーマの設定は、古いスキーマ mercari_dev_jp_admin だったとしましょう。 このとき、次の処理の順番により、アプリケーションから見て、スキーマ統合された新しいスキーマのテーブルに対して透過的にクエリが発行されます。 接続フェーズでアプリケーションは mercari_dev_jp_admin (旧スキーマ)に接続を試行 ProxySQLのinit_connectにより接続スキーマが mercari_admin (新スキーマ)に変更される クエリはmercari_admin (新スキーマ)上で実行される 透過的なクエリ発行の例 1点付け加える必要があるのは、移行後のダウンストリームのデータとして、スキーマ統合をした実データとともに、移行前の空のスキーマ(テーブルなどのデータは存在しなくて良い)が必要であることです。 移行先のスキーマ構成 また、本構成が利用できる前提条件として、そもそもクエリにスキーマを変更したり、スキーマを指定してクエリを発行するといった実装がない、という前提があります。メルカリのケースでは移行の背景からも、スキーマを指定してスキーマをまたぐクエリが発行される心配は、元からありませんでした。 この設定の追加によりもたらされるメリットは2つあります。 読取りトラフィックを、新・旧スキーマに重み付きで流す、といった 段階的な移行 が可能になることで、より安全にトラフィックが移行できる アプリケーションのスキーマ設定は、新・旧どちらでも動作し、 コンフィグの並行運用期間 が取れるため、各開発チームのタイミングで設定の移行を柔軟に進められる 実際には、トラフィックを全て切り替えた後、 動作が問題ないことが確認されてから 、各アプリケーションにクライアントの 接続先スキーマの設定変更 依頼を行い、これによりメルカリの開発環境のスムーズな移行が達成されました。(※空のスキーマを誤って不要だと錯誤し、削除してしまうというミスは発生しました) なお、本切替のTiDBへのトラフィックシフトの前には、事前に 主要な利用シナリオ用に作成したベンチマークシナリオによるTiDBの性能評価 想定される遅延増加を、擬似的にデータベースレスポンスに付与し、アプリケーションへの影響を評価、アプリケーションの修正が必要な箇所を特定 現行のクエリをキャプチャし、TiDBへリプレイし問題の有無を確認 ということを終えておりました。 終わりに この記事では、メルカリの開発環境で、MySQLからTiDBへ移行すると同時にスキーマ統合する際に用いた手法について説明しました。 多数のアプリケーション接続をかかえるデータベースを、アプリケーション設定変更は実際に移行が成功した後に、順次行える構成を考え、移行を成功させました。ポイントは次の2点です。 TiDBのData MigrationのTable Routingを用いて、TiDBにスキーマ統合したインスタンスを作成 2段のProxySQLを構成し、1段目でルーティングを、2段目でinit_connectにスキーマ変更を設定 メルカリでは、絶え間なく変化するアプリケーションやトラフィックに柔軟に対応するため、クラウドサービスや、ProxySQLなどのOSSソフトウェアを活用したり、必要な機能を独自に実装を行い、問題の解決にチャレンジしています。 様々な課題に意欲的にチャレンジする仲間を募集しています。 https://careers.mercari.com/bloom/
この記事は、 Merpay & Mercoin Advent Calendar 2024 の記事です。 こんにちは、メルペイ BackendエンジニアのSakabeと申します。 私の所属するKYCチームでは、主に本人確認に関するマイクロサービスの開発を行っています。 現在、メルペイの各チームが共通で使用する大規模なBFF(Backend For Frontend)がその巨大さゆえに管理や拡張が難しくなっています。この問題に対応するため、各チームごとにBFFを切り出す取り組みを進めています。KYCチームではこの対応として、 こちらの記事 で紹介されているgRPC Federationの仕組みを活用し、効率的にBFFサービスの立ち上げを行いました(gRPC FederationのOSSリポジトリは こちら )。 今回の記事では、実際のBFFサービスの構築に使われたprotoファイルと実際に使用したgRPC Federationのoptionを紹介することでgRPC Federationを用いた開発を紹介します。また、開発中に感じたことを説明します。 gRPC Federationを採用した背景 KYCチームがBFFを新規作成する際、gRPC Federationを採用した背景について説明します。巨大なBFFをKYCチーム専用のBFFに切り出すにあたり、gRPC Federationを活用する方法と、ゼロからBFFを構築する方法がありました。その中でgRPC Federationを選んだ理由は以下の2点です。 1点目は、少ない手間でBFFが構築できることです。具体的には、Protoファイルにオプションを記述するだけで、BFFのコードを自動生成することが可能です。これにより、シンプルなロジックについては、ゼロからBFFを構築するよりも開発コストが低いと判断しました。 2点目は、社内に基盤が作られていることです。他のチームでも実際に使われているため、デプロイまでのフローが整備されており迅速なリリースが可能と判断しました。 これらの理由から、KYCチームではBFFを新規作成する手段としてgRPC Federationを採用しました。 gRPC Federationを使用した実際のBFF開発 KYCチームではgRPC Federationを利用して新規のBFFを開発しました。今回は、マイクロサービスに新規実装したエンドポイントをそのまま中継するだけのBFFを作成します。 今回はその様子を以下の順番で紹介していきます。 ベースとなる新規サービスのProtocol Buffers定義の作成 gRPC Federationのoption設定 生成されるGoファイル 1. ベースとなる新規サービスのProtocol Buffers定義の作成 今回新たに公開するマイクロサービスのProtocol Buffersの定義(抜粋)です。リクエストは空でレスポンスでは2つの要素を返しています。 service KYCMicroService { rpc GetConfirmation(GetConfirmationRequest) returns (GetConfirmationResponse) { }; } message GetConfirmationRequest {} message GetConfirmationResponse { bool need_confirmation = 1; ConfirmationEvent confirmation_event = 2; } 2. gRPC Federationのoption設定 このマイクロサービスを中継するためにBFFを作成します。gRPC Federationを使用するため、実際のGoのコードを書く必要はなく、protoファイルにgRPC Federationのoptionを記述しBFFのコードの自動生成を行います。 マイクロサービスのprotoファイルをコピーし、以下のような追記を行いました。 service MerpaySharedKYCAPI { option (.grpc.federation.service) = { }; rpc GetConfirmation(GetConfirmationRequest) returns (GetConfirmationResponse) { }; } message GetConfirmationRequest {} message GetConfirmationResponse { option (.grpc.federation.message) = { def{ call {method : "mercari.path.api.v1.KYC/GetConfirmation"} autobind : true } }; bool need_confirmation = 1; ConfirmationEvent confirmation_event = 2; } まず、serviceにgRPC Federationを利用するためのoptionを記述します。 option (.grpc.federation.service) = {}; 次にmessageのResponseを定義するためのoptionを記述します。 option (.grpc.federation.message) = { def[ { call { method : "mercari.path.api.v1.KYC/GetConfirmation" } autobind : true } ] }; 今回使用したのは、 (grpc.federation.message).def.call と (grpc.federation.message).def.autobind です。 (grpc.federation.message).def.call は、gRPCメソッドを呼び出し、レスポンスを変数に代入します。ただし今回は後述する(grpc.federation.message).def.autobindによる自動的な代入を使用しているため、gRPCのレスポンスを代入する変数を省略しています。 (grpc.federation.message).def.autobind は、callのレスポンスのmessageと自身のfieldの名前と型が一致する場合にfieldの値を自動的に代入します。 逆に一致しない場合は、以下の例のようにレスポンスのfieldを明示的に指定する必要があります。call の結果を res に代入し、res の field である foo の結果を need_confirmation 変数に代入しています。 option (.grpc.federation.message) = { def[ { name: "res" call { method : "mercari.path.api.v1.KYC/GetConfirmation" } } ] def { name: "need_confirmation" by: "res.foo"} ... }; つまり、callとautobindのoptionの記述によって、KYCのマイクロサービスのgRPCエンドポイントをcallした結果がfieldに自動的に代入されます。 3. 生成されるGoファイル grpc-federation-generatorコマンドを実行することでprotoファイルからコードを生成することができます。 上記のprotoファイルから、以下のようなGoのコード(疑似コード)が生成されます。 func (s *MerpaySharedKYCAPI) getConfirmationResponse(ctx context.Context, req *GetConfirmationRequest) (*GetConfirmationResponse, error) { // 新しいレスポンスを作成 res := &GetConfirmationResponse{} // APIへリクエストを送信し、結果を取得 apiResponse, err := s.client.GetConfirmation(ctx, &ExternalAPIRequest{}) if err != nil { logError(ctx, err) return nil, err } // 必要な情報を取得しレスポンスへ設定 res.NeedConfirmation = apiResponse.NeedConfirmation // 追加の情報を変換しレスポンスに追加 confirmationEvent, err := s.convertEvent(apiResponse.ConfirmationEvent) if err != nil { logError(ctx, err) return nil, err } res.ConfirmationEvent = confirmationEvent // 処理が成功した場合、最終レスポンスを返す return res, nil } gRPC Federationを使用してBFFの開発した気づき 今回、gRPC Federationを用いてBFFを開発する中で、以下の4つの点でさまざまな発見や気づきを得ました。 短期間でのBFF構築の可能性 高い表現の自由度 技術導入時のコスト評価の重要性 実行順の依存関係の明確化 以下、それぞれのポイントについて詳しく掘り下げていきます。 短期間でのBFF構築の可能性 1点目は、短期間でのBFF構築が可能であることです。 BFFのprotoの実装は1日で終了しました。今回のようなマイクロサービスのAPIをプロキシするだけのBFFであれば、optionを10行程度追加するだけでBFFを生成できるのは大きなメリットだと感じています。gRPC Federationを選ぶ理由として定型作業の簡素化があげられており、今回のようなマイクロサービスのAPIを中継するだけのBFFの実装はまさに定型作業といえます。gRPC Federationではprotocol buffers上でメッセージの対応関係を記述することで、型変換処理を全て自動生成しています。 また、私自身はGo言語に関する専門的な知識を持っていませんが、gRPC Federationによってコードが自動生成されることで、Goにおける最新のベストプラクティスを享受し、ハイパフォーマンスなコードを利用できます。もし同じ品質のものを開発する場合、もっと時間がかかると思います。その点で、gRPC Federationを使えばGoの初心者であっても短期間で高品質の成果を得られます。  さらに、社内ではgRPC Federationを利用したBFFをモノレポで運用しており、そのレポジトリではBFFを迅速にデプロイする環境が整えられています。 高い表現の自由度 2点目は、表現の自由度が高いということです。 現在はCEL(Common Expression Language)のサポートが行われています。proto上で計算を行ったり、計算結果を元にAPIの呼び出しの分岐を書いたりすることも可能です。また、protoからGoのコードを呼び出すことができるため、他のライブラリを使うなどの用途で一部の処理を切り出すこともできます。 このようにgRPC Federationのoptionでできないことを柔軟に実装することが可能です。 技術導入時のコスト評価の重要性 3点目は、新しい技術導入時のコストとメリットを評価する必要があるということです。 新しい技術を導入する際には、そのコストに対してメリットが上回るかを慎重に評価する必要があります。普段はGo言語で開発しており、Goでも十分に開発が可能です。しかし、新しい技術がそれを上回るメリットを提供するかどうかを判断することが求められます。 私自身は、gRPC Federationのオプションに対する生成コードをある程度想像できていますが、チームメンバーは一から学ぶ必要があり、そのためコードレビューが難しくなる場合があります。また、新しいメンバーが加わった際に、BFFに少しの修正を加えるにしても時間がかかることが多いです。そのため、gRPC Federationを導入した際には、チーム内で的確な開発サイクルやリリースフローをしっかりと周知する必要があると感じています。 これはgRPC Federationに限らず、どのような新しい技術を導入する時にも一般的に言える課題であり、gRPC Federation固有の問題ではないことを明記しておきたいです。 実行順の依存関係の明確化 4点目は、実行順の依存関係を明確にする必要があることです。 依存関係を設定しない場合、処理は並列で行われ、最適化された状態で自動的に呼び出されます。これにより、開発者が自らパフォーマンスチューニングを行わなくても済むという利点があります。 しかし、特定の順序での実行が求められる場合は、依存関係を設定する必要があります。例えば、先に実行したいmessageを次のmessageで使用することで、依存関係を明示し、処理を直列に実行させることが可能です。Goのコードであれば、処理を単に上から順に書いていくことで明確な順序を保てます。しかし、これは依存関係のない処理同士が無駄に順番待ちをする可能性もあります。 依存関係を使って実行順序を制御し始めると、並列に実行されるのか、直列に実行されるのかがわかりにくくなる可能性があります。そのため、単純に上から実行されるようなGoのコードであっても、複雑なprotoファイルになることがあります。これを避けるためには、BFF層に複雑なロジックを書かないことが重要です。 optionを駆使すれば複雑なprotoファイルを実装することも可能ですが、その前にロジックを理解し、それをマイクロサービスで処理することが求められます。私個人としては、ロジックを持っているBFFをマイグレーションする際、そのコードを深く理解し、protoに落とし込みやすくする工夫をしています。 まとめ 今回は、gRPC Federationを用いたBFFの開発について紹介しました。KYCチームでは今回のgRPC Federationによる新規BFF開発を足掛かりとして、既存のBFFマイグレーションを行う予定です。もしgRPC Federationに興味のある方は、実際に手を動かして試してみてください! 次の記事はhibagonさんです。引き続きお楽しみください!
この記事は Mercari Advent Calendar 2024 の1日目の記事です。 メルカリでは流出すると多大な影響を及ぼすような有効期限が長い認証情報を減らすために、有効期限が短い認証情報を発行する仕組みを様々な箇所で導入しています。そして、Platform Security Teamでは社内で運用されていたGitHubの認証情報を生成するToken Serverというサービスを拡張することで、Google Cloud上で動作しているGitHubの自動化サービスが保持していたGitHubへのアクセスに使用する認証情報を有効期限が短いものに切り替えました。 このToken Serverの拡張と移行で用いた技術や課題、解決策について紹介します。 概要 メルカリではGitHubを中心とした開発環境を利用しており、その中でGitHubの運用を自動化する様々なサービスを開発・運用しています。 これらのサービスはPAT(Personal Access Token)やGitHub Appの秘密鍵を使いGitHubへアクセスします。しかしこれらの認証情報は有効期限が設定されていない、もしくはとても長く設定されています。これらの認証情報がサプライチェーン攻撃などによって漏洩すると長期に渡り攻撃者から悪用されるリスクがあります。また、これらの認証情報は一度作成されてしまうと多くの場合、どの認証情報がどのサービスで使われているのか不透明になりがちで、かつ付与された権限が見直されることが少ないです。 これらの課題を解決するために、すでにメルカリ社内で運用されていた、有効期限が短いGitHub認証情報を発行するToken Serverというサービスを拡張し、Google Cloud上で動作しているサービスが有効期限が長い認証情報を使うことなくGitHubにアクセスできるようにしました。 これにより、以下のようなメリットを実現することができました。 有効期限が長い認証情報の削減 管理が不透明な部分が多かったPAT及びGitHub Appの削減 認証情報を付与する対象・必要な権限を1つにまとめて管理することで、認証情報の対象の特定と権限の定期的な見直しを簡略化 また、既存のPATや秘密鍵を用いるサービスの実装を大きく変えることなく、Token Serverに移行できるようなGo言語のライブラリも合わせて開発することで移行をすぐに行うことができました。 Token Server メルカリでは様々な形態でGitHubを運用しており、特にGitHub内での自動化においてはあるレポジトリの変更を別のレポジトリに同時に適用するといった複数レポジトリをまたぐ自動化が頻繁に行われています。 特に社内で標準的に使われているCIであるGitHub Actionsにおいて、複数レポジトリをまたぐ自動化を行うための認証情報はデフォルトでは提供されておらず、一般的にはRepository SecretにPATを保存するか、GitHub Appの秘密鍵を保存し create-github-app-token action などで生成する必要があります。しかし、これらはPATおよびGitHub Appの秘密鍵という有効期限の長い認証情報が必要です。 そこで以前からメルカリは、GitHub Actionsのワークフロー内部で取得できる、GitHubが署名し偽造が困難なOIDC Tokenを受信し検証することで、特定の権限を持つ Installation Access Token を提供するToken Serverサービスを運用しています。 Installation Access TokenはGitHub Appの機能で、GitHub Appで事前に設定されている権限(contentsのread権限・Pull Requestのwrite権限など)のサブセットを特定のレポジトリに限定し発行できるトークンです。またInstallation Access Tokenには1時間の有効期限があり、さらにGitHub API経由で有効期限を待たずに失効させることも可能です。これにより最小権限の原則に従った、限定された権限・アクセス範囲・有効期限を持つ認証情報を提供することができます。 Token Server for GitHubのアーキテクチャ Token Serverは事前に設定されたGitHub Appから対象のレポジトリとブランチごとに設定された権限を持つInstallation Access Tokenを作成し各レポジトリ上でのGitHub ActionsのJobに提供します。この際、レポジトリ名及びブランチ名を特定するため、GitHub ActionsのJob上で取得できるOIDC Tokenを利用します。Job上でOIDC Tokenを取得、Token Serverへ送信、Token Server内でOIDC Tokenの検証、レポジトリとブランチごとに設定された権限を検索、Installation Access Tokenを作成・発行を行います。 Token Serverから発行されるInstallation Access Tokenは複数レポジトリ間のGitHub運用の自動化(コミットの追加・Issueの自動作成・Pull Requestの自動作成など)だけでなく、CIでのビルドにおける内部のライブラリのダウンロードなどにも広く使われています。 (注) 2024年4月にChainguard社から Octo STS がリリースされました。基本的な動作原理はToken Serverと同じですが、Token Serverではより統一された権限管理をサポートしている他、後述するGoogle Cloud上へのワークロードへの組み込み・GitHub AppのLoad Balancingなどよりエンタープライズ環境に適したアーキテクチャが採用されています。 Token ServerのGoogle Cloudへの拡張 メルカリでは多くのサービスをGoogle Cloudで運用しています。これはお客さまへのサービス提供を目的としているマイクロサービス郡だけではなく、内部向けの自動化サービスも同様です。これらのサービスはGitHubにアクセスするためにPATやGitHub Appの秘密鍵を使用していました。 Google Cloudの各リソースには他のリソースを操作する権限を付与するためのService Accountが付与されており、更にroles/iam.serviceAccountTokenCreatorが付与されている場合、Service Accountが付与されているGoogle CloudリソースはAPIを通してGoogleが署名したOIDC Tokenを取得することができます。このOIDC TokenをGitHubの場合と同じようにToken Serverで検証し、事前に設定された権限を持つInstallation Access Tokenを発行するよう、Token Serverを拡張することにしました。 Token Server for Google Cloudのアーキテクチャ これにより、ある特定のGoogle Cloudリソース上で動作しているサービスが、Token ServerへOIDC Tokenを送信し、Installation Access Tokenを発行してもらうことで、GitHubへのアクセスができるようになり、今までGoogle Cloud上で使用していたPATやGitHub Appの秘密鍵をなくすことができます。 Google Cloud上のワークロードへのToken Serverの適用 Token Serverの拡張により、Google Cloud上で動作しているサービスがGitHubへのアクセスに使用する認証情報を有効期限が短いものに切り替えることができるようになりました。 これらの新規機能をGoogle Cloud上に新しく作成するサービスに適用することは比較的容易です。しかし今までGitHub PATやGitHub Appの秘密鍵を使っていたサービスは多くの場合、機能が作り込まれており、Token ServerへInstallation Access Tokenをリクエストし、取得したInstallation Access Tokenを使うように変更することが難しい場合があります。 そして、GitHub AppにはAPIの利用数の制限があります。これはGitHub Appごとに設定されており、GitHub Enterprise Cloudを利用している場合、1時間あたり15000回のAPIリクエストが可能です。この制限を超えるとAPIリクエストが失敗するため、APIリクエストの制限を超えないように、Token Serverに対してのリクエストをなるべく減らす必要があります。 このAPIリクエストはInstallation Access Tokenの発行回数のみではなく、発行されたInstallation Access TokenからのAPIリクエストも含まれます。特にToken Serverにおいては複数のGoogle Cloud上のワークロード、GitHubレポジトリの分のAPIリクエストを発行するため、Token Serverに対してのリクエストをなるべく減らすことが重要です。GitHub APIのリクエストごとにInstallation Access Tokenを発行するのではなく、1時間の有効期限を持つInstallation Access Tokenを発行し、それを有効期限内に使い回すことでAPIリクエストを減らすことができます。 GitHubクライアントの初期化におけるPATからToken Serverへの移行 そこで、PATやGitHub Appの秘密鍵を使っていたサービスの実装を大きく変えず、また自動でInstallation Access Tokenを取得し有効期限内で再使用するライブラリを開発しました。メルカリではGo言語を標準的に使用していることから、Go言語でGitHub関連のサービスを作成する代表的なライブラリである google/go-github をベースにGitHub APIを使うためのクライアントを取得できるようにしました。なお、既にgo-githubライブラリを使っているサービスの場合、Service Accountの設定とライブラリの入れ替えのみでToken Serverに移行できるようにしました。 Token Server用ライブラリの構成 go-githubライブラリは初期化の際、任意のhttp Clientを指定することができます。このhttp Clientには、RoundTripperインターフェイスに任意のRoundTripメソッドを実装することでリクエスト送信前にリクエストの内容を変更することができます。そこでこのRoundTripメソッドを使い、リクエストを送信する前にキャッシュされているInstallation Access Tokenもしくは有効期限が切れている場合はToken Serverから新たなInstallation Access Tokenを取得し使用するようにしました。 Token Server用ライブラリの処理 これにより、go-githubライブラリを使用している既存のサービスのコードを1行変更するだけで、Token Serverに移行することができました。 GitHub AppのLoad Balancing 前述の通り、GitHub Appには1時間あたり15000回のAPIリクエスト制限があります。しかし、Token Serverは複数のGoogle Cloud上のワークロード、GitHubレポジトリのAPIリクエストを発行し、また今後の自動化サービスの増加を想定するとこの制限を超えることが予想されます。そこで、GitHub Appを複数作成し、それぞれのGitHub Appに対してInstallation Access Tokenの発行を分散することで、APIリクエストの制限を超えることなく、多くのAPI処理を提供することができます。 しかし、この分散は複数のGitHub AppをそれぞれのToken ServerのPodに一つずつ読み込んでLoad Balancerによってランダムにリクエストの分散を行い、あるInstallation Access Tokenの利用者が受け取るInstallation Access Tokenの発行元が複数のGitHub Appとなってしまうと、コミットステータスの書き込みを行うサービスにおいて、問題が発生します。 GitHubにおいてはCIの動作結果等をあるコミットに対しerror, failure, pending, successのいずれかを書き込むことができます。しかし、この書き込みはそれぞれ書き込んだGitHub Appごとに記録されます。つまり異なるGitHub Appによってコミットステータスが書き込まれると、コミットステータスが混在してしまいます。この場合、以前失敗したステータスが上書きされることなく、別の新しいステータスのみが書き込まれ、コミットステータスがすべて成功しないとマージできないようなBranch Protectionを設定している場合、マージができなくなるという問題があります。 複数のGitHub Appによるステータス書き込み ある自動化処理において最初は失敗ステータスを書き込み、その後成功ステータスを書き込むような処理を行う場合、同じGitHub Appによって行われる必要があります。もしToken Serverが複数のGitHub Appを持つ場合、最初の失敗ステータスをGitHub App 1で書き込んでしまうと、その後の成功ステータスをGitHub App 2で上書きすることができず、GitHub App 1からの失敗ステータスとGitHub App 2からの成功ステータスが混在してしまいます。 そこで、Installation Access Tokenを発行する対象ごとに常に同じGitHub Appを使うようにするために、1つのToken ServerのPodで複数のGitHub Appを読み込み、GitHubにおいてはリポジトリとブランチ名・Google CloudにおいてはサービスアカウントによってGitHub Appを割り当てるようにしました。 GitHub Appの割り当て処理 このように、GitHub Appのインデックスをリポジトリとブランチ名、サービスアカウントによって割り当てることで、同じリポジトリとブランチ名、サービスアカウントに同じGitHub Appが割り当てられるようにしました。 まとめ Token ServerをGoogle Cloudに拡張し、より多くのサービスがGitHubにアクセスする際に有効期限の短い認証情報を使うようにすることで、有効期限が長い認証情報を減らすことができました。そして既存のサービスを最小限の変更でToken Serverに移行できるようなライブラリを開発することで、移行を容易にしました。また、実運用の中で発見した問題も解決し、セキュアかつ快適なGitHubの自動化サービスの運用をサポートしました。 メルカリのSecurity Teamでは、このような認証情報の有効期限の短いものへの切り替えを推進しており、今後もこのような取り組みを進めていく予定です。 Security Teamにおける採用情報については Mercari Career をご覧ください。
はじめに こんにちは。メルペイVPoEの @keigow です。 この記事は、 Merpay & Mercoin Advent Calendar 2024 の記事です。 昨年のAdvent CalendarでCTOの@kimurasからロードマップを作成する取り組みについての記事( メルカリEngineering Roadmapの作成とその必要性 )を出しました。 こちらでも語られているようにエンジニアリング領域への投資を推進するためにロードマップは重要な役割を果たしますが、当時はメルカリのEngineering Roadmapという形で、メルペイについてはまだ作ることができていませんでした。 本日はメルペイでのエンジニアリングへの投資を推進するために、整備してきたEngineering Roadmapや、それを推進するためのEngineering Projectsという取り組みについて書きたいと思います。 ビジネスロードマップとCompany OKR メルカリグループではミッションの達成に向けて中長期で成し遂げたい目標をグループのロードマップとして定義して、その下に各カンパニーでのビジネスロードマップを定義しています( 参考 )。ビジネスロードマップはお客さまへの体験の向上や、新しいサービスの提供などを中心に、どのような価値をいつまでに届けていきたいのかという観点で整理されています。 会社の経営という観点ではロードマップをベースに四半期単位でメルペイ全体のOKR(Company OKRと呼ぶ)を設定し、それに紐づける形で配下の組織のOKRを設定していくという流れになっています。 ビジネス目標とエンジニアリング投資の両立の難しさ Company OKRがビジネス目標を達成するためのものだとすると、どのようにエンジニアリング観点で重要な中長期の投資(アーキテクチャの改善、FinOpsへの投資、Test Automationなど)をスムーズに実現していくか、は非常に重要な課題です。 ここで触れているエンジニアリングへの投資とは、簡単なリファクタリングなどではなく、半年以上の投資を必要とする大きなプロジェクトを想定しています。このような改善への投資は実際にプロダクトを開発するエンジニアの稼働が必要なことも多いため、ビジネス目標を達成するためのエンジニアの稼働とコンフリクトが発生しがちです。 勿論予めCompany OKRの中でエンジニアリングへの投資についても、横に並べて優先順位をつけて実施することができるのが理想だとは思いますが、ビジネス目標の達成とエンジニアリングへの投資に対する優先順位を0/1で判断することは難しいのが現実です。 また、例えばリアーキテクチャなど大きなエンジニアリングへの投資は複数年にまたがるプロジェクトになることも多いため、Company OKRと比較するとそもそもフォーカスするタイムラインが異なることも判断を難しくする要因の一つとなっています。 これまでのメルペイの取り組みとその課題 以前のメルペイではエンジニアリング投資の推進のためのアプローチとして、Company OKRと同様に四半期ごとにEngineering OKRを定義していました。エンジニア組織として重要な課題がフォーカスされて、インシデントの削減など成果を生み出せたものもありましたが、大きく2つの課題がありました。 1つは上述したような優先順位の判断が個々の裁量に委ねられてしまうことが多く、ビジネス上必要な開発を優先することで、Engineering OKRで設定していた目標の進捗を出すことが難しいケースが発生すること。もう1つは前者の課題から、どうしても緊急度の低い(が重要度は高い)エンジニアリング投資の推進は、不確実性が伴うため、OKRという仕組みに求められる厳密なトラッキングに向いていない部分があることです。 投資のバランスとプログラム型組織 メルペイのプロダクト組織は以前記事( メルペイのProgram型組織への移行 )にしたように、2022年の10月からプログラム型組織(大きなドメイン毎にプロダクトマネージャーやエンジニアを含めたクロスファンクショナルなチームが複数集まっている)に移行しました。 このプログラム型組織に移行した理由の1つに前者の課題(ビジネス目標とエンジニアリング投資のバランス)の解決がありました。現実問題として会社単位での優先順位を0/1で決めることが難しいようなエンジニアリングへの投資であっても、ドメインやチーム毎の単位で見ると繁閑の波があり、隙間時間を利用したり、プロジェクトの推進方法を工夫することで、エンジニアリングへの投資を生み出す余地も存在します。そのため、判断自体を会社単位で行うのではなく、ドメインごとに分割したプログラムという単位で行えるようにすることで、よりスムーズなエンジニアリング投資の推進が行えるようにしました。 個別のプログラムにはプロダクトに責任を負うProduct Headと、エンジニアリングに責任を負うEngineering Headを置くことで、各プログラムの単位でビジネスへの目標とエンジニアリング投資のバランスの意思決定が行えるようにしています。 また各プログラムで作成するロードマップに対して四半期毎にストラテジーレビューという会議体でCPO、CTO、VPoEによるレビューを行うことで、プライオリティの認識をすり合わせ、推進するためのリソースの調整を行っています。 エンジニアリング投資の推進とEngineering Projects 後者の課題(エンジニアリング投資の推進に対するOKRという仕組みのアンマッチ)に対しては、Engineering OKRを設定するのではなく、2023年7月からEngineering Projectsという仕組みを作り対処をするようにしました。 Engineering Projectsは中長期でエンジニア組織として推進したいリアーキテクチャやFinOpsなどのプロジェクトを四半期ごとに選定し、隔週の定例ミーティングで進捗をトラッキングします。期によって異なりますが、FY2025 2Q(2024年10-12月期)では6つのプロジェクトを選定しています。トラッキングは仕組み上OKRの運用に似ているところもありますが、違いとしてObjectiveなどの設定はなく、中長期に渡るプロジェクトの推進を前提とした仕組みです。中長期の計画をベースに四半期の目標を設定し、隔週で進捗のブロッカーとなりうるものの排除や、必要に応じた進め方の調整などを行えるようにしています。 Engineering Roamdapの作成 本来、上図の関係で示したようにEngineering Roadmapがあり、そこから各四半期に実施すべきEngineering Projectsがクリアになっていくべきではあるのですが、メルペイのEngineering Roadmapは今年の6月に作成が完了しました。もともとEngineering Projectsとして定義されていたプロジェクトについては、ある程度ロードマップが明確なものも多かったのでそれらのプロジェクトに対しては改めて取りまとめるという形で整理しました。 それらの取り組みに加え、今後のAI活用の取り組みや、新しい決済プラットフォーム基盤などFoundation領域への取り組みも合わせて作っています。 課題と今後の取り組み プログラム組織やEngineering Projectsの実施に比べ、Engineering Roadmapについてはまだ出来たばかりであり、運用方法やアップデートの方法についてもまだ手探りの状態になってしまっている部分があります。それでも、これまで可視化しきれていなかった中長期の取り組みを可視化することができ、エンジニアリング組織として取り組むべき課題を明確にできた部分があると思っています。 今後もロードマップの運用練度を高めていきつつ、ビジネスロードマップの実現に貢献するための、エンジニアリング投資の実現方法を模索していきたいと思います。 おわりに 次の記事は@sakabeさんです。引き続き Merpay & Mercoin Advent Calendar 2024 をお楽しみください。
こんにちは。メルカリのQA エンジニアリングマネージャーの @____rina____ です。 この度、スキマバイトサービス「メルカリ ハロ」が Google Play ベスト オブ 2024「社会貢献部門 大賞」を受賞しました!メルカリ ハロは、お好きな時間に最短1時間から働ける「空き時間おしごとアプリ」として、多くの方にご利用いただいています。スマホ一つで仕事を探し、働き、給与をもらうまでの一連のプロセスを簡単に完結できます。 そこで今回は、私たちがどのようにこのアプリを開発してきたのか、特にFlutterを中心とした技術面に焦点を当てて、連載シリーズをお届けします。開発の舞台裏から見えてくる様々な工夫やチャレンジについても触れていく予定です。 Theme / Title Author 書き起こし:Acceptance criteria: QA’s quality boost @____rina____ A Newcomer’s Experience of Getting Up to Speed in Development @cherry Hallo Design System(仮) @atsumo Push notifications and CRM integration in Mercari Hallo (仮) @sintario FlutterアプリのQAについて @um TBD @howie TBD @naka How to unit-test Mercari Hallo Flutter app @Heejoon 初回の記事は、@____rina____が執筆予定です。技術的な詳細から開発における試行錯誤まで、多面的にご紹介します。 今回の連載シリーズは、メルカリ アドベントカレンダーとのW掲載となります。アドベントカレンダーでは、メルカリの多彩な技術者たちが自社の最新技術や取り組みについて詳しく紹介しています。私たちの「メルカリ ハロ 開発の裏側」シリーズもその一環として、さらなる技術的洞察を提供できればと考えています。 アドベントカレンダーと合わせてお楽しみいただくことで、メルカリが現在どのような技術の潮流と向き合い、どのように成長を遂げているのか、広範囲に理解を深めていただけることと思います。是非、見逃さずにご注目ください! ブログのアップデートに関しては、メルカリ公式DevX(旧Twitter)の @mercaridevjp でも随時お知らせいたします。ハッシュタグ#メルカリハロ開発の裏側 でぜひ検索し、最新情報をチェックしてください。 メルカリの新たな挑戦がどのように形作られているのか、その技術的な旅路をお楽しみいただければ幸いです。どうぞお楽しみに!
こんにちは。メルペイ Engineering Engagement チームの mikichin です。 今年も、メルカリグループは Advent Calendar を実施します! ▶ Mercari Advent Calendar 2024 はこちら Merpay & Mercoin Advent Calendar とは? Advent Calendar の習慣にもとづいて、メルペイ・メルコインのエンジニアがプロダクトや会社で利用している技術、興味のある技術分野やちょっとしたテクニックなど知見をアウトプットしていきます。このAdvent Calendarを通じてクリスマスまでの毎日を楽しく過ごしていただければと思っています。 2023年のMercari / Merpay Advent Calendar はこちら Mercari Advent Calendar 2023 , Merpay Advent Calendar 2023 公開予定表 (こちらは、後日、各記事へのリンク集になります) Theme / Title Author Engineering RoadmapとEngineering Projectsの話 (仮) @keigow grpc-fedeartionを使ったBFF開発 @sakabe 新卒1年目の振り返り @hibagon The Race condition with multiple DB transactions and its solution @timo HPAとVPAによるストリーミング処理のリソース最適化:ピーク時のリソース不足と過剰なリソース割り当ての解決 @siyuan WYSIWYGウェブページビルダーを支える技術とSever Driven UIへの拡張 @togami イーサリアムのバリデーター監視について @pooh TBD @Joraku About Swift Testing @Cyan From Airflow to Argo and DBT Python models @Yani Seamless Shopping Feed Integration: Bridging the Gap Between Systems @hiramekun Argoworkflow @goro spannerにtime.Now()で生成した時刻を渡してしまう問題を検知する @kobaryo メルカリハロと広告事業を支える決済基盤の仕組み @komatsu 効率と品質向上のための MLOps @rio (仮)会計とシステムの連携 @abcdefuji (仮)Spanner external datasetsを用いたデータ品質管理 @iwata メルペイの数百画面をSwiftUI/Jetpack Composeに移行するプロジェクトを推進する @masamichi TBD @kimuras 初日の記事は keigowさんです。引き続きお楽しみください。
こんにちは。メルカリ Engineering Officeのohitoです。 今年もメルカリとメルペイ・メルコインで2本のAdvent Calendarを実施します! ▶ Merpay & Mercoin Advent Calendar 2024 はこちら Mercari Advent Calendar とは? メルカリグループのエンジニアがプロダクトや会社で利用している技術、興味のある技術分野やちょっとしたテクニックなど知見をアウトプットしていきます。このAdvent Calendarを通じてクリスマスまでの毎日を楽しく過ごしていただければと思っています。 2023年のMercari / Merpay Advent Calendar Mercari Advent Calendar 2023 Merpay Advent Calendar 2023 公開予定表 (こちらは、後日、各記事へのリンク集になります) Theme / Title Author Google CloudからGitHub PATと秘密鍵をなくす – Token ServerのGoogle Cloudへの拡張 @Security Engineering TBD @darren Keeping User Journey SLOs Up-to-Date with iOS E2E Testing in a Microservices Architecture @yakenji 書き起こし:"Acceptance criteria: QA’s quality boost" @….rina…. Streamlining Incident Response with Automation and Large Language Models @florencio TBD(somthing related with finops?) @pakuchi Tips on measuring performance of React apps with Chrome Devtools @samlee A Newcomer’s Experience of Getting Up to Speed in Development @cherry FlutterとDesignSystem (仮) @atsumo New Production Readiness Check experience in Mercari @mshibuya Push notifications and CRM integration in Mercari Hallo (仮) @sintario_2nd External service review as code, with the help of LLMs (current draft) @danny, simon メルカリ Tech Radar @motokiee GitHubのBranch Protectionのバイパス方法(仮) @iso メルカリにおけるナレッジマネジメントの取り組み(仮) @raven FlutterアプリのQAについて @um Enhancing macOS configuration management with GitOps @yu TBD @howie TBD @naka Why good internal tooling matters (TBD final title) @klausa スムーズなCDNプロバイダー移行のために行ったアプローチと次のステップ @hatappi How to unit-test Mercari Hallo Flutter app @Heejoon Studying Ph.D. in computer science as a software engineer — culture shock, benefits, opportunities and so on @greg.weng TBD @kimuras 最初の記事は、「Google CloudからGitHub PATと秘密鍵をなくす – Token ServerのGoogle Cloudへの拡張」です。 どうぞお楽しみに!
はじめに こんにちは、mercari.go スタッフの kobaryo と earlgray です。 9月19日にメルカリ主催の Go 勉強会 mercari.go #27 を YouTube のオンライン配信にて開催しました。この記事では、当日の各発表を簡単に紹介します。動画もアップロードされていますので、こちらもぜひご覧ください。 Writing profitable tests in Go 1つ目のセッションは @kinbiko さんによる「Writing profitable tests in Go」です。 発表資料: Writing profitable tests in Go 利益の観点での Go によるテストの考え方というテーマで、テストを書くかどうかを決めるルールや Go でのテスト記述のテクニックについて紹介しました。テストはコードの動作を確認する他、将来での変更で問題がないことを保証する点で役に立ちます。しかし、テストには記述の時間や実行のコストが発生するため、組織の過去のインシデントの影響や給与などを基にして、インシデント対応やデバッグに費やす時間やお金を計算してコストが見合っているか考えることが重要とのことでした。また、Go でのテストにおいては、可読性やコードの品質を高めることによる利点、サブテストの可読性のためにテーブル駆動テストを強要するデメリットなどを Tips として紹介して頂きました。この他にも様々な Tips を紹介して頂いたので興味がある方はぜひご覧になってみてください。 テーブル駆動テストは Go では見かけることが多いですし、サブテストの可読性が高いという観点でついついテーブル駆動で記述してしまう方も多いと思います。私自身もそうでしたが、今回利点と欠点を理解することができたため、今後は適切なユースケースで利用したいと思いました。(earlgray) GC24 Recap: Interface Internals 2つ目のセッションは @task4233 さんによる「GC24 Recap: Interface Internals」でした。 発表資料: GC24 Recap: Interface Internals このセッションでは GopherCon 2024 で発表された Interface Internals の振り返りとして、インターフェースを介した関数呼び出しがどのように実行されているかを、デバッガを用いて実際にメモリ中の値を見ながら説明しました。 Go プログラムをアセンブリにすると、関数の処理が書かれてあるメモリアドレスを call 命令で指定することで関数呼び出しを行っていることが分かります。しかしながら、インターフェースを介するメソッド呼び出しだと動的に呼び出す関数が選ばれるため、この仕組みをそのまま用いることができません。このセッションでは、インターフェースがどのようなデータ構造により実装されているかから始め、呼び出されるメソッドのアドレスを決定する方法、またその高速化手法について説明しました。 密度の高いかつ Go 言語のコアな部分を扱った発表だったため、リファレンスを読みつつ何度も見てきちんと理解したいと個人的に感じました。(kobaryo) GC24 Recap: Who Tests the Tests? 3つ目のセッションは @Ruslan さんによる「GC24 Recap: Who Tests the Tests?」でした。 発表資料: GC24 Recap: Who Tests the Tests? このセッションも2つ目の GC24 Recap: Interface Internals と同様 GopherCon 2024 の振り返りで、 Who Tests the Tests? の内容を扱いました。 我々はソフトウェアの品質が保たれているかの指標としてテストのカバレッジを用いますが、それではテストそのものの品質を担保することはできません。このセッションでは、テストの品質を担保する Mutation Test を紹介しました。この手法を用いることで、プログラム中の演算子や bool 値を変更した場合にテストが失敗するかをチェックし、テストが正しいプログラムのみを通すことを保証することができます。また、そのようなプログラムを AST パッケージを利用して自動で生成する方法についても説明しました。 内容がテストそのものの品質を担保するという興味深いものであった上、実践に移しやすい内容となっており、とても有益なセッションでした。このブログを読んでいる皆様もぜひ導入を検討してはいかがでしょうか。(kobaryo) Cloud Pub/Sub – High Speed In-App Notification Delivery 4つ目のセッションは @akram さんによる「Cloud Pub/Sub – High Speed In-App Notification Delivery」です。 発表資料: Cloud Pub/Sub – High Speed In-App Notification Delivery メルカリでの通知の管理を行う Notification platform における Cloud Pub/Sub の活用事例について紹介しました。メルカリではアプリ内通知や To-Do リストの他、メールや Push 通知などをお客様へ送信しています。2,000万人以上のお客様にリアルタイムかつ非同期的な通知を行えるようなパフォーマンスを実現するため、notification platform では Cloud Pub/Sub を使用しています。具体的には、notification platform 内で Push 通知のリクエストを受けて Pub/Sub に publish するサーバと、Pub/Sub を subscribe して実際に通知を行うサーバの2台の構成で通知処理を行っています。この結果、現在メルカリでは1日あたり1,600万以上(ピーク時 400rps) の Push 通知を実現しているとのことでした。 メルカリという大規模プラットフォームにおける Pub/Sub の活用事例としてとても興味深い内容でした。非同期的なタスクの処理にパフォーマンスの課題を感じている方は Pub/Sub の導入を検討してみてはいかがでしょうか。 (earlgray) おわりに 今回はGo言語のコアな部分から実用的なものまで、幅広い4つの発表をお送りしました。GopherCon 2024 に関する発表もあり、運営メンバーとしてもGoの最先端の内容を知ることができ大変勉強になりました。 ライブで視聴いただいた方も録画を観ていただいた方も本当にありがとうございました! 次回の開催もお楽しみに! イベント開催案内を受け取りたい方は、connpass グループのメンバーになってくださいね! メルカリconnpassグループページ
こんにちは、メルカリのAI/LLMチームで機械学習エンジニアをしている arr0w と sho です! 本テックブログでは、Vision-Language Modelの一つである SigLIP [1]を、メルカリの商品データ(Image-Text Pairs)でファインチューニングし、メルカリの商品画像Embeddingの性能を大幅に改善したプロジェクトについて紹介します。 今回作成したSigLIPの性能を評価するために、 商品詳細ページの「見た目が近い商品」のレコメンド機能でA/Bテストを実施しました。 この「見た目が近い商品」のレコメンド機能は、社内では Similar Looks と呼ばれています。作成したモデルをSimilar Looksの類似画像検索に適用し、既存モデルとの比較のためのA/Bテストを行いました。 そして、その結果として、主要なKPIにおいて以下のような顕著な改善が確認できました。 「見た目が近い商品」のタップ率が 1.5倍に増加 商品詳細ページ経由の購入数が +14%増加 A/Bテストを経て、モデルの有効性が確認できたため、今回作成したモデルによるレコメンドは無事採用され、100%リリースに至りました。以降の章では、商品データを用いたSigLIPのファインチューニングやそのオフライン評価、本番デプロイのためのシステム設計など、本プロジェクトの技術的詳細について説明していきます。 商品データを用いたSigLIPのファインチューニング 画像Embedding 画像Embeddingは、画像内の物体やその色、種類といった特徴を数値ベクトルとして表現する技術の総称です。近年、推薦や検索など、さまざまなアプリケーションで使用されています。 メルカリ内でもその重要性は日々増しており、類似商品レコメンド、商品検索、不正出品の検出といった多様な文脈で画像Embeddingが使用されています。 本プロジェクトにてメルカリのAI/LLMチームでは、 Vision-Language ModelであるSigLIP を用いて、商品画像のEmbeddingを改善する取り組みを行いました。 SigLIP 近年、 CLIP [3] や ALIGN [4] などのように、大規模かつノイズの多い画像およびテキストがペアになったデータセット(e.g. WebLI [5])を用いたContrasive Learningで事前学習されたモデルは、ゼロショット分類や検索といった様々なタスクにおいて高い性能を発揮していることが知られています。 SigLIP は、ICCV 2023で発表された論文で紹介されたVision-Language Modelです。SigLIPは、CLIPで使用されている従来のSoftmax Lossを、 Sigmoid Loss に置き換えて事前学習を実施しています。この変更はLossの計算方法を変えるだけのシンプルなものですが、著者たちは、ImageNet [6]を使用した画像分類タスクを含む複数のベンチマークで、 SigLIPは既存手法と比べてパフォーマンスが大幅に向上した と報告しています。 それでは、ここで、後述するメルカリの商品データを用いたSigLIPのファインチューニングのために実装したLoss関数の実装を見てみましょう。 def sigmoid_loss( image_embeds: torch.Tensor, text_embeds: torch.Tensor, temperature: torch.Tensor, bias: torch.Tensor, device: torch.device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") ): logits = image_embeds @ text_embeds.T * temperature + bias num_logits = logits.shape[1] batch_size = image_embeds.shape[0] labels = -torch.ones( (num_logits, num_logits), device=device, dtype=image_embeds.dtype ) labels = 2 * torch.eye(num_logits, device=device, dtype=image_embeds.dtype) + labels loss = -F.logsigmoid(labels * logits).sum() / batch_size return loss なお、今回のプロジェクトでは、SigLIPのベースモデルとして google/siglip-base-patch16-256-multilingual を使用しました。このモデルは多言語のWebLIデータセットで訓練されており、対応言語にメルカリのサービスで主に使用されている日本語も含まれているためです。 商品データを用いたファインチューニング この章から、メルカリの実データを用いたSigLIPのファインチューニングの実験環境および設定について紹介します。 今回の実験では、過去に出品された商品から、ランダムに抽出した約100万件のメルカリの商品データを使用して、SigLIPのファインチューニングを実施しました。SigLIPへの入力データは、商品タイトル(Text)と商品画像(Image)を用いました。これらはいずれもメルカリ上のお客さまが出品時に作成したものです。 訓練用のコードは PyTorch と Transformers ライブラリを使用して実装しました。さらに、今回は使用したデータセットは、Google Cloud Storage上で管理しており、その規模も大きかったため、データ読み込みプロセスを最適化するために WebDataset を活用し、大量の学習データを効率的に扱えるようにしました。なお、WebDatasetの理解には 公式ドキュメント に加え、 こちらの記事 が大変参考になりました。 モデルの訓練は 1台のL4 GPU を使用して実施しました。訓練パイプラインの構築には、 Vertex AI Custom Training を活用しています。さらに、メルカリは、 Weights & Biases(wandb) のエンタープライズ版を契約しているため、実験のモニタリングには、そちらを活用しました。プロジェクトのタイムライン上、ML実験に割くことができる期間には制約がありましたが、初期にこれらの訓練パイプラインに投資をしたことで、 結果として試行錯誤の回数を増やすことができた ように感じています。 オフライン評価 A/Bテストを実施する前に、既存の「見た目が近い商品」レコメンドのユーザの行動ログを使用してオフライン評価を行いました。Similar Looksは、元々、学習済みのMobileNet [2]( google/mobilenet_v2_1.4_224 )から得られるImage EmbeddingをPCAで128次元に圧縮したEmbeddingを用いていました。オフライン評価では、10000件の行動ログ(セッション)を利用しました。 行動ログの具体例を以下に示します。query_item_idに商品詳細ページに表示されているクエリ画像となる商品のID、similar_item_idに「見た目が近い商品」セクションに表示された商品のID、clickedにその商品を見たかどうかのフラグが格納されています。 session_id | query_item_id | similar_item_id | clicked | ----------------|----------------|-----------------|---------| 0003e191… | m826773… | m634631… | 0 | 0003e191… | m826773… | m659824… | 1 | 0003e191… | m826773… | m742172… | 1 | 0003e191… | m826773… | m839148… | 0 | 0003e191… | m826773… | m758586… | 0 | 0003e191… | m826773… | m808515… | 1 | ... 評価は画像検索タスクとして定式化し、ユーザーのクリックを正例(label:1)として扱いました。パフォーマンスの評価には、nDCG@k(k=5)とprecision@k(k=1,3)を評価指標として使用しました。 これにより、ユーザーの嗜好に合致した形で類似した画像をランク付けするモデルの能力を定量的に評価することができました。 評価にあたっては、比較のために「ランダム推薦」と「現在使われているMobileNetベースの画像検索」2つのベースラインとしました。 以下がオフライン評価の結果になります。 手法 nDCG@5 Precision@1 Precision@3 Random 0.525 0.256 0.501 MobileNet 0.607 0.356 0.601 SigLIP + PCA 0.647 0.406 0.658 SigLIP 0.662 0.412 0.660 評価結果から、 メルカリの商品データでファインチューニングしたSigLIPのImage Encoderから得られた画像Embeddingによる画像検索は、PCAによって768次元から128次元に圧縮された場合でも、MobileNetベースの画像検索を一貫して上回る ということがわかりました。これは、あくまでオフライン評価上は「見た目が近い商品」のレコメンドにおいて、我々が構築したSigLIPモデルが優れたパフォーマンスを示したと言えます。 定量評価だけでなく、目視による定性評価も実施しました。約10万件の商品画像のEmbeddingが格納されたベクターストアを FAISS を用いて作成し、複数の商品で画像検索を行った結果を、以下のようにスプレッドシートにまとめ、目視でチェックしました。 以上のオフライン評価結果から、メルカリの商品データでファインチューニングしたSigLIPのImage Encoderを用いた「見た目が近い商品」レコメンドは、定量的・定性的どちらにも既存のモデルを上回ることが明確に示されました。そのため、作成したモデルを使用してA/Bテストを実施することを決定しました。 次の章では、このモデルを本番環境にデプロイするためのシステム設計について説明します。 システム構成 End-to-End Architecture 個々のコンポーネントの詳細に入る前に、アーキテクチャの全体像を以下に示します: 上図では、メルカリ本体のプラットフォームからSigLIPモデルがホスティングされたマイクロサービスへのデータの流れと、Embeddingがどのように効率的に保存され、取得されるかを示しています。これは初期バージョンですが、このモジュール化された設計により、スケーラビリティと柔軟性を確保しています。 Google Container Registry モデルのデプロイは Google Container Registry(GCR) を通じて管理され、マイクロサービスのDockerイメージがここに保存されています。Dockerイメージは、GitHubリポジトリからGoogle Cloud Buildを使用したCI/CDパイプラインを介して、継続的にビルドされGCRにプッシュされます。 GCRを活用することで、Google Kubernetes Engine(GKE)上のデプロイメントが常に最新のコードバージョンに基づいて行われ、本番環境で稼働しているサービスへのシームレスなアップデートを実現しています。 Google Pub/Sub リアルタイムのデータストリームを処理するために、 Google Pub/Sub を利用しています。メルカリでは、お客さまによって新しく出品が作成されると、新規出品用ののPub/Subのtopicに、Pub/Sub メッセージがpublishされます。推薦や検索をはじめとする関連マイクロサービスがこのtopicをsubscribeすることで、新しい出品に対して、動的に対応できるシステムを実現しています。 今回も同様に、新規出品により発火するEventをSubscribeするWorkerを定義しました( Embedding Worker )。これにより、新しい出品が発生したら、Embeddings Workerが起動します。そこで、新規商品の画像Embeddingを算出し、ベクターデータベースに追加します。この非同期なシステムにより、メルカリの出品量に応じて効果的にスケールすることが可能になっています。 Google Kubernetes Engine システムを構成する主要なマイクロサービスはGoogle Kubernetes Engine(GKE)でデプロイされています。GKEは、本取り組みのアーキテクチャにおける以下の主要サービスをホストしています: Embeddings Worker Embeddings Workerは、Pub/Subの新規出品トピックをSubscribeする重要なサービスです。新規出品の発生ごとに、以下の処理を行います: 出品された商品に対応する画像をストレージから取得 ファインチューニングしたSigLIPモデルを使用して、画像をEmbeddingに変換 類似度検索のレイテンシ改善とストレージコスト削減のため、PCAにより次元を削減(768 dim → 128 dim) EmbeddingをVertex AI Vector Searchに保存 このプロセスにより、効率的な類似画像検索が可能になります。各Embeddingは画像の視覚的内容を表現しているため、メルカリのプラットフォーム全体で視覚的に類似した出品を容易に比較・検索することができます。 Index Cleanup Cron Job メルカリでは多くのお客さまに利用されており、頻繁に新しい商品が出品され、既存の出品が売れたり、削除されたりします。 そのため、現在出品中の商品のみを表示し「見た目が近い商品」レコメンドの体験を向上するために、 削除あるいは売却済みとなった商品を適宜Vector Storeから削除し、最新の状態に保つ 必要がありました。これを実現するために、 Index Cleanup Cron Job を実装しました。 Cronで定期実行されるこのジョブは、Vertex AI Vector Searchから古くなった出品や売却済みの出品に対応するEmbeddingを削除します。 現在はこのBatchで定期実行する方針で体験を大きく損ねず動作していますが、さらなる効率化を図るため、Embedding管理のリアルタイム更新についても、現在検討しています。 Similar Looks Microservice & Caching Similar Looks Microservice は、画像類似性機能の中核となるものです。このサービスは、商品IDを入力として受け取り、対応する画像Embeddingを Vertex AI Vector Search から取得し、最近傍探索を実行してマーケットプレイス内の類似商品を見つけます。 レイテンシを削減するために、このマイクロサービスにも キャッシュ機構 を実装しています。これにより、お客さまが類似商品を閲覧する際に素早いレスポンスを提供し、スムーズなユーザー体験を確保しています。 Vertex AI Vector Search Embeddingの保存と検索には、 Vertex AI Vector Search を使用しています。これはスケーラブルなVector Databaseで、類似したEmbeddingを効率的に検索することができます。メルカリ内の各商品画像は、SigLIPにより、ベクトルに変換され、そのベクトルはVertex AI内で商品IDごとに索引されます。 Vertex AIの最近傍探索アルゴリズムは非常に高速であり、データベース内に数千万件程度の膨大なEmbeddingがある場合でも、視覚的に類似した商品を高速に検索することが可能です。 TensorRTを用いたモデルの最適化 ファインチューニングしたSigLIPモデルのパフォーマンスを最適化し、毎秒生成される大量の出品を高速に処理するため、モデルをPyTorchからNVIDIAの高性能深層学習推論ライブラリであるTensorRTに変換しました。この変換により、推論時間が 約5倍高速化 されました。 TensorRTについて TensorRTはニューラルネットワーク内の操作を、NVIDIA GPU上で効率的に推論できるように、最適化された行列演算のシーケンスに変換します。具体的には、性能を保った上でのモデルの重みのFP16やINT8といった少ない桁数への変換(Precision Calibration)や深層学習モデルを構成する層の融合(Layer Fusion)などの処理を行います。 SigLIPのような大規模モデルをメルカリのような高RPSなサービスにリリースする上で、この改善は非常に重要でした。毎秒大量の商品が出品される中、推論時間を 数百ミリ秒から数十ミリ秒程度に削減できた ことで、 新規出品のほぼすべての商品画像を瞬時にベクトル化 し、Similar Looks Componentsが使用するVertex AI Vector Searchのインデックスに登録できるようになりました。 Next Steps 現在のアーキテクチャは安定しており、スケーラブルですが、私たちは以下のような改善点があると考えており、日々開発に取り組んでいます。 Vector Storeのリアルタイム更新 既に述べたとおり、現在、Index Cleanup Cron Jobが定期的にVertex AI Vector Searchから古くなったEmbeddingを削除しています。しかし、商品が削除されたり売却されたりした時点で即座にEmbeddingを更新するような、よりリアルタイムな方法への移行を計画しています。これにより、Cron Jobによる削除の必要性がなくなり、インデックスが常に最新の状態に保たれることが保証されます。 Triton Inference Server モデル推論をより効率的に処理するため、 Triton Inference Server の使用も検討しています。Tritonは、異なるフレームワーク(例:TensorRT、PyTorch、TensorFlow)の複数のモデルを単一の環境にデプロイすることができます。推論処理をEmbeddings WorkerからTritonに移行することで、モデルの実行をワーカーのロジックから分離し、推論パフォーマンスのスケーリングと最適化においてより大きな柔軟性を得ることができます。 SigLIPモデルを活用した新機能 最後に、ファインチューニングしたSigLIPモデルを活用した新機能の開発に取り組んでいます。「見た目が近い商品」のレコメンドに限らず、開発したSigLIPモデルはたくさんの可能性を秘めていると我々は考えています。このモデルを活用したユーザー体験を向上させる計画は、他にもあるので、今後のメルカリのアップデートにご期待ください 😉 おわりに 今回、メルカリが自社で保有する商品データを用いて、Vision-Language ModelのSigLIPをファインチューニングし、高性能なImage Embedding Modelを構築し、「見た目が近い商品」機能の改善を行いました。 オフライン評価において、ファインチューニングしたSigLIPによる「見た目が近い商品」レコメンドは既存のモデルよりも高い性能を示しました。そのため、 A/Bテストを実施したところ、ビジネスKPIにおいても大幅な改善が確認されました。 本ブログの内容がVision-Language Modelのファインチューニングや評価、深層学習モデルの実サービスへのデプロイ等に興味がある皆さまのお役に立てば幸いです。 メルカリでは、 プロダクト改善を通して大きなインパクトを生み出すことに意欲的な Software Engineer を募集 しています。興味ある方は是非ご応募ください。 参考文献 [1] Sigmoid Loss for Language Image Pre-Training , 2023 [2] MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications , 2017 [3] Learning Transferable Visual Models From Natural Language Supervision , 2021 [4] Scaling Up Visual and Vision-Language Representation Learning With Noisy Text Supervision , 2021 [5] PaLI: A Jointly-Scaled Multilingual Language-Image Model , 2022 [6] ImageNet: A Large-Scale Hierarchical Image Database , 2009
Mercari Search Infra Teamのmrkm4ntrです。 Elasticsearchは1ノードに載り切らない量のデータも複数のshardに分割し、複数のノードに載せることで検索が可能になります。shard数を増やすことで並列にスキャンするドキュメントの数が増えるためlatencyが改善します。ではshard数はいくらでも増やせるのでしょうか?もちろんそのようなことはなく、Elastic社の公式ブログ( https://www.elastic.co/jp/blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster )にもあるようにshard数を増やすことによるオーバーヘッドが存在します。ただしそのオーバーヘッドが具体的に何を指すのかは、先ほどの記事では明らかにされていません。本記事ではそのオーバーヘッドの正体を明らかにするとともに、実際にコストの削減を達成したことについて説明します。 背景 我々の運用するindexはmulti-tierと呼称する構成をとっています。multi-tier構成で直近x日分の商品が入っているindexを1st index、全ての商品が入っているindexを2nd indexと呼びます。新しいもの順で商品を取得する場合はまず1st indexにリクエストし、検索結果が不足していた場合は2nd indexにリクエストします。このような構成により2nd indexに来るリクエスト量を減らすことができます。同時に、2nd indexに来るクエリは1st indexでは十分な数の結果を得ることのできなかった珍しいtermを含むことが多いため、スキャンするposting list(termを含むドキュメントのidのリスト)は短くなることが期待できます。1st indexに比べて2nd indexの1クエリ当たりのコストは10倍程度かかるため、これによりコストの削減を実現しています。 2nd indexは24個のshardに分割されています。元々ファイルシステムキャッシュに載るようにshardサイズの2倍(最悪のmergeの場合を考えたらしい)が空きメモリ量よりも小さくなるようにshard数を決めたらしいですが、実際にmemoryのworking setなどを調査し、1ノードに2つのshard載せることができることが発覚しました。その結果全体24 shard、1ノードあたり2つのshardを載せる構成に変更しました。 2nd indexはmulti-tier構成でかなりコストを削減しているものも、今なお最大のリソースが必要なindexでした。そこでこのコストをさらに削減するために調査を行うことにしました。 CPU proflierでflame graphを取得し眺めていたところ、posting listをスキャンする処理(下図の赤枠)には検索処理において半分以下のCPUしか使われていないことがわかりました。 確かにスキャンすべきposting listが短くなるのでこのことはある程度予期していましたが、検索エンジンの処理のメインはposting listのスキャンのはずです。このスキャンを効率化するために様々な工夫がされています。例えばこの記事( https://engineering.mercari.com/blog/entry/20240424-6f298aa43b/ )もスキャン対象を効率的にスキップする話です。では他は何にCPUを使っているのでしょうか。残りの部分について調べると、ほとんどがスキャン対象のposting listをterm dictionaryから取得する処理(以下term lookupと呼ぶ)であることがわかりました。 term lookup Luceneではposting listはfieldとtermの組み合わせごとに存在します。簡単のため今は一つのfieldについて考えます。termをキーとし、posting listの場所をvalueとするhashmapを用意すればposting listの位置の取得は一瞬で終わるのですが、termの数は非常に大きいためJVMのheapに収めるのは現実的ではありません。 そのためLuceneはFSTというデータ構造を使い省メモリ化を実現しています。FSTとはFinite State Transducerの略称で、有限オートマトンの受理状態に値がつくことで入力をその値に変換することができるというものです。FSTはfield毎に存在し、termを入力、postling listの場所を出力(実際はtermとposting listの対応ではなく、termのprefixとprefixを共有するtermのposting listのアドレスをまとめたブロックのアドレスらしい https://github.com/apache/lucene/issues/12513 )とすることでpostling listの取得を実現しています。 この計算量は入力文字列の長さのオーダーですが、データ自体はファイルに保存する形式でmmapされているため、毎回そこから計算するのはそれなりにCPUを使うようです。 term lookup回数の削減 term lookupにCPUを使っているのなら、term lookupの回数を減らせばコスト削減ができるはずです。今2nd indexは24 shardあるので一つのクエリのterm lookup回数は1 termあたり24回必要になります(実際は違うのですが詳細は後ほど)。2nd indexを12 shardに変更すればterm lookup回数は1クエリあたり12回となるため、その分CPU消費が少なくなるはずです。もちろんshardあたりのスキャンするposting listは長くなるためlatencyが悪化する可能性がありますが、先述のとおり2nd indexにはposting listのスキャンが長いクエリがくる可能性は低いため、latencyの悪化の可能性も低いと考えました。また、term数が増加しますがFSTによるlookupの計算量はterm数には比例しないので問題ないはずです。 パフォーマンスを検証するために、まずは開発環境で本番のデータを使って検証しました。Elasticsearchのshrink APIを使って24 shardのindexから12 shardのindexを作成することにします。shrink APIは書き込み禁止にしたindexのshardをコピーしてまとめることで、shard数が元のindexのshard数の約数となるindexを作成する仕組みです。 リアルタイムのデータ更新がなくても先ほどの想定からパフォーマンスは向上すると見込んでいましたが、実際には全く変化がありませんでした。実はそれは当然で、FSTはshard毎に存在するのではなくsegment毎に存在するため、term lookup回数はshard数に直接比例するのではなくsegment数に比例するからでした。shardは内部的には複数のLuceneのsegmentからなるのですが、shrink API実行直後はあくまでもsegmentのグルーピング単位を変更しただけです。そのため全体のsegment数は変わらないためパフォーマンスに変化が見られませんでした。 そこでリアルタイムの更新を一定期間実施したところ、新しいsegmentの追加、mergeが繰り返され1 shardあたりのsegment数は元よりやや大きいくらいの値で収束しました。segment数はLuceneのTiered Merge Policyやドキュメントのサイズ、追加速度などによって決まり、shardあたりのドキュメント数が倍になってもsegment数は単純に倍にはなりません。再度パフォーマンスを測ったところ無事にパフォーマンスの改善が確認できました。 以下が本番環境に適用した結果です。薄い線が一週間前のもので濃い線が適用後のノード数の遷移を表します。 我々のクラスタはこの記事( https://engineering.mercari.com/blog/entry/20230620-f0782fd75f/ )にあるようにCPU使用量でオートスケールするようになっていますが、明らかに必要なノード数が減少したことが見てとれます。latencyに悪影響も見られませんでした。 こちらが適用後のflame graphです。別要因でクエリのパターンが適用前後で変化したため単純比較はできませんが、term lookupの占める割合が小さくなることでposting listのスキャンが占める割合が相対的に大きくなっています。 さらにsegement数を減らせばよりコストが削減できるはずです。segment数はLuceneのTiered Merge Policyのパラメータを変更することにより調整できます。よりsegment数を減らすために index.merge.policy.segments_per_tier を減らしましたが、segment数は思ったほど減少しなかったと同時に、mergeのためのCPU使用量が上がったのでこちらは期待ほど有効ではありませんでした。 まとめ この記事では、shard数の増加によるオーバーヘッドの一つはsegment数が増えることによるterm lookupの回数が増加であることを示しました。同じノードに複数のshardをおいている場合はshardをまとめた方がCPU負荷が低くなります(もちろんlatencyが上がる場合がありますが)。我々のindexは基本的にリアルタイムのデータ更新がありますが、データ更新がないような静的なindexにおいては、force mergeでsegement数を1にしておくとterm lookup回数が最小となりパフォーマンスが改善することが期待できるでしょう。