TECH PLAY

株式会社メルカリ

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

261

この記事は、 Merpay Tech Openness Month 2023 の14日目の記事です。 はじめに こんにちは。メルペイの機械学習エンジニアの @fukuchan です。私の所属している機械学習チームでは、お客さまの与信枠の決定に関わる機械学習モデル(以下、与信モデル)の開発と運用を行っています。現在、機械学習チーム及び与信管理部では「与信モデル更新マニュアル」を作成し、このマニュアルを元に与信モデルの更新判断を行っています。 本記事では与信モデル更新マニュアルを作成するに至った背景やその内容の一部を紹介します。 背景 メルペイスマート払いは、利用した分を翌月以降に柔軟に支払うことができる与信サービスです。メルカリ・メルペイ上での取引や決済等の利用実績に基づいて、お客さまごとに適切な与信枠を提供しています。 お客さまの与信枠は定期的に更新しています。お客さまへの価値提供・メルペイのビジネス発展において、与信枠及び与信モデルの更新(※)は非常に重要で影響が大きい変更です。そのため、与信モデルの更新においては、モデルのアウトプットをさまざまな観点で分析し、ビジネスチーム、リスクチーム、プライバシーチームやプロダクトマネージャーの方等、多くの方々の目で点検しリリースに至っています。 ※与信モデルの更新とは、モデルの問題設定を大きく変えず最新のデータに適応するための再学習と、モデルそのものをリニューアルする再開発の両方を指します。 近年、ありがたいことにメルペイはますます多くのお客さまにご利用いただいています。与信サービスもメルペイスマート払いだけでなく、 メルカード や メルペイスマートマネー もリリースを迎え、多様化しています。サービスの多様化にともなって、与信枠決定に関わる与信モデルの重要性が以前にも増して高まってきました。与信モデルの更新においては、多様な事業KPI・お客さま体験等、以前よりも多くの観点を考慮・点検しており、リリースの意思決定に時間を要していました。慎重な点検を行い与信の品質を担保することが重要である一方で、与信モデルの改善サイクルを早め、与信の品質改善を迅速に行うことも重要です。 今回の取り組みでは、与信の品質を担保しつつ与信モデルの更新の意思決定をより迅速にすることを目的に、与信モデル更新マニュアルを作成しました。 今回の取り組み 与信モデルの更新基準を明確にし更新の意思決定をより迅速にするため、与信モデル更新マニュアルを作成しました。その中で定めている項目をいくつか紹介します。 リリース判断のための評価指標と収益試算 与信モデル更新時の評価のために、機械学習モデルそのものに関する性能評価に加えて、事業KPIやお客さま体験等の観点まで踏み込んで評価指標を整理しました。 この事業KPIに関する評価指標の1つには「収益試算結果」を含んでいます。この指標は与信モデルのアウトプットを事業の収益性観点での指標に変換したもので、今回の取り組みでその変換ロジックを新たに作成しました。与信モデルの更新が収益性に与える影響の試算ができるようになり、機械学習チームだけでなく、ビジネスチーム、リスクチームやプロダクトマネージャーの方など他チームの方ともコミュニケーションしやすくなりました。結果、与信モデルリリースの意思決定もしやすくなりました。 リリース後のモニタリング指標 リリース判断のための評価指標に加えて、リリース後も継続的にモニタリングする指標を定めました。以前も与信モデルのリリース後に与信管理部全体で多くの指標をモニタリングしていましたが、今回の取り組みで改めて機械学習チームがフォーカスしてモニタリングしていく与信モデルそのものに関する指標と事業KPIに関する評価指標を明確にしました。 現在これらの定めた指標に関して、機械学習チームが定期的にモニタリングを行っています。また与信事業の主要な計数を報告する場にて定期的に報告しています。 与信モデル更新の契機 与信モデルの更新を行う契機となるイベントを定めました。 イベントの例としては以下です。 新商品導入時 与信のフレームワークの変更時 モニタリング指標の定期モニタリング結果に基づき、事前に定めた基準に触れた時 事業戦略に応じて与信モデルの更新の判断をした時 与信モデルの更新を行う契機となるイベントを定めることで、与信モデルの更新の検討タイミングが明確になりました。 モデル更新の手続き・タイムライン 与信モデル更新の手続きとタイムラインを明確にしました。以前も与信モデルの更新を行う際には、さまざまなチームが参加する社内会議にてモデルの更新内容とモデルのアウトプットに関する検証結果を協議し、モデル更新を行うという流れがありました。今回の取り組みで改めてその流れをマニュアルにまとめ、明確にしました。加えてリリースの時期から逆算して、いつ決議する必要があるか、いつモデル開発・改善を行う必要があるかを具体的なタイムラインと共に明確にしました。 マニュアルの所在・管理体制 メルペイでは強固なガバナンス体制とすべく、さまざまな規程やマニュアルが決裁権限者とともに構造的に管理されています。今回作成したマニュアルについても、紐づく上位規程、マニュアルの所管やマニュアル改廃の決裁者を明確にしました。 おわりに 与信モデル更新マニュアルを作成するに至った背景やその内容の一部を紹介しました。与信モデル更新マニュアルでは評価指標や更新フローを整理しており、中でも特に評価指標において収益試算結果を採用することで、他チームとのコミュニケーション、与信モデルの更新の意思決定もしやすくなりました。今後もサービス規模拡大に伴って今回の取り決めた事柄の内容は変わりうるので、適宜アップデートし運用していきます。事業影響の大きい機械学習モデルを取り扱う方にとって、今回の取り組みが参考になれば幸いです。 謝辞 与信モデル更新マニュアルを作成するにあたり多くの方にご協力いただきました。機械学習チームのみなさんをはじめ、ビジネスチーム、リスクチーム、データアナリストの方々にこの場を借りて御礼申し上げます。 明日の記事は@r_yamaokaさんです。引き続きお楽しみください。
アバター
この記事は、 Merpay Tech Openness Month 2023 の13日目の記事です。 こんにちは、メルペイ Solutionsチームのエンジニア @orfeon です。 メルペイ Solutionsチームでは社内向けの技術的な相談対応や研修、部門を跨いだ共通の問題を発見して解決するソリューションの提供など行っています。 自分は主に社内のデータ周りの課題を解決するソリューションを提供しており、一部成果はOSSとして公開しています。 過去の記事 では検索APIサーバを手軽に構築して利用するソリューションを紹介しましたが、今回の記事ではグラフデータベースであるNeo4jを手軽に活用するソリューションを紹介します。 はじめに 社内では日々生成される大量のデータがBigQueryに蓄積され、レコメンドや異常検知などさまざまな用途で活用されています。 活用するデータの形態として不正利用などのユースケースではグラフデータを扱うケースもあります。 しかし一般的なRDBやDWHでは関係性に基づくクエリを実行しようとすると、レイテンシが大きくなったり、SQLで表現するのが難しいといった課題があります。 そのためこうしたグラフデータを活用するのに特化したさまざまなグラフデータベースが選択肢にあがります。 たとえば人気のグラフデータベースの1つである Neo4j では Cypher というグラフクエリを使ってグラフから情報を抽出します。 以下の例ではCypherを使って指定した(この例ではUserID=1を持つ)人物と同じ店舗でよく買い物をする人物を抽出しています。 MATCH (u1:User {UserID: 1})-[:BUY]->(s:Shop)<-[:BUY]-(u2:User) RETURN u2.UserID AS UserID, COUNT(DISTINCT s.ShopID) AS ShopCount ORDER BY ShopCount DESC LIMIT 10 グラフデータベースを活用することでこうした関係性に基づく情報を手軽かつ低レイテンシに抽出することができるようになり、レコメンドや不正検知に活用することができます。 グラフデータの活用にあたっては、グラフデータが実際に業務に本当に有効か検証したり、グラフデータベースが既存システムとの連携でスムーズに運用を行えるか検証する必要があります。 そのためさまざまなデータソースからグラフデータベースを構築し検証するにはまずさまざまなデータの繋ぎこみが必要です。 データ分析やMLで活用するにはデータ加工や特徴量作成などの試行錯誤を高速にまわすことが重要ですので、グラフデータベースの作成で手間取るわけにはいきません。 そこでこうしたデータの繋ぎこみの手間を減らして、さまざまなデータソースからグラフデータベースを構築したり、グラフデータベースと既存データとの付き合わせを手軽にできるようにするソリューションを検討しました。 今回は有力なグラフデータベースのひとつであるNeo4jにフォーカスしました。 Neo4jはフルマネージドなサービスである Neo4j AuraDB などさまざまな形態で提供されています。 こうしたグラフデータベースのシステム採用の検証を容易にすべく、以下の項目を実現するソリューションを紹介します。 手軽にグラフデータベースを構築 BigQuery等の多様なデータソースからグラフデータベースを手軽に作成 コンテナを利用して手軽にAPIサーバを立てたり手元でクエリを試せる 手軽にグラフデータベースを検証 作成したグラフデータベースに対して大量クエリのバッチ処理を手軽に実行 データの生成日時からグラフの発展にあわせたクエリバッチ処理も実現 ニアリアルタイムなグラフデータベースの検証(開発中) なお、今回のソリューションでは検証を主要な目的とすることから以下の制約を想定しました。 1つのマシンに搭載できる大きさのデータしか扱わない 今回紹介するソリューションではグラフデータベースの作成や検証にあたって、大量のデータ処理やバッチとストリーミングで同じ処理を動かすのに便利な Cloud Dataflow をデータ処理基盤として活用しています。 Cloud Dataflowのパイプライン実装はOSSの Mercari Dataflow Template (以下MDT)のモジュール( localNeo4j sink モジュール / localNeo4j transform モジュール )として公開しています。 (Mercari Dataflow Templateについては 過去の紹介ブログ記事 を参照ください) 以下、多様なデータソースからバッチでグラフデータベースを作成するシステムと、作成したグラフデータベースを検証活用するシステムをそれぞれ紹介します。 グラフデータベース作成 まずグラフデータベースに登録したいデータを用意します。 ここではシンプルなケースとしてBigQueryの一つのクエリ結果から構築する例を紹介します。 (MDTがソースとして対応しているものであれば置き換え可能です) グラフデータベースではデータをノード(Node)、関係(Relationship)として登録します。 BigQueryから読み取ったデータは表形式なのでノード、関係として変換する必要があります。 MDTのlocalNeo4j sinkモジュールでは以下のような設定で変換を定義します。 { "sources": [ { "name": "BigQueryInputTransaction", "module": "bigquery", "parameters": { "query": "SELECT UserID, ShopID, Pay FROM `mydataset.Transactions`" } } ], "sinks": [ { "name": "LocalNeo4jSink", "module": "localNeo4j", "inputs": ["BigQueryInputTransaction"], "parameters": { "output": "gs://examble-bucket/neo4j/index/transaction.zip", "setupCyphers": [ "CREATE CONSTRAINT UserUniqueConst FOR (u:User) REQUIRE (u.UserID) IS UNIQUE", "CREATE CONSTRAINT ShopUniqueConst FOR (s:Shop) REQUIRE (s.Shop) IS UNIQUE" ], "nodes": [], "relationships": [ { "input": "BigQueryInputTransaction", "type": "BUY", "source": { "label": "User", "keyFields": ["UserID"] }, "target": { "label": "Shop", "keyFields": ["ShopID"] }, "propertyFields": ["Pay"] } ] } } ] } 上のMDTの設定ファイルではシンプルな例としてBigQueryの購入履歴データから購入グラフを登録しています。 最初のbigquery sourceモジュールではBigQueryから購入者と店舗と支払額を取得しています。 次のlocalNeo4j sinkモジュールではデータから、購入者ノード、店舗ノード、購入関係を作成します。 localNeo4j sinkモジュールの各種パラメータを説明します。 inputs 項目ではグラフデータとして登録した入力元のnameを指定しています。今回は購入履歴として一つの入力を指定します。 parameters 項目の子項目ではより詳細なデータベース情報やグラフ変換内容を指定します。 output では作成したデータベースファイルのアップロード先としてCloud Storageのパスを指定します。 ちなみに今回は指定していませんが、 input という項目でデータベースファイルCloud Storageのパスを指定するとそのファイルを読み込んでデータベースの初期状態とします。 setupCyphers 項目ではデータの登録に先立って実行しておきたいCypherクエリを指定します。 ここではグラフデータ登録の効率化のため、今回登録対象となる2つのノードUser,Shopに対してそれぞれユニークキーによるCONSTRAINTを指定します。 (ユニークキーに対してインデックスが貼られるため更新確認が高速になる) relationships 項目では関係の定義を行っています。 今回は購入者と商品の購入の関係のみ登録しています。 参照する入力名を input で指定して関係の元と宛先のノードのラベル名、ユニークキーをそれぞれ source , target で指定します。 また関係の属性として購入額を登録するように propertyFields でPayを指定しています。 今回は関係登録時に同時にノードも登録しているため利用していませんが、独立したノードを登録するには nodes でノードの登録内容を定義します。 作成したMDTの設定ファイルをCloud Storageにアップロードして以下のようなコマンドでMDTでDataflow Jobを起動します。 gcloud flex-template run create-graphdb \ --project=myproject \ --region=asia-northeast1 \ --template-file-gcs-location=gs://{MDTデプロイファイルパス} \ --staging-location=gs://{stagingパス} \ --parameters=config=gs://{設定ファイルアップロード先パス} Jobが完了すると output で指定したCloud Storageのパスにグラフデータベースファイルがアップロードされます。 このファイルはグラフデータを構築したNeo4jのホーム配下のファイルをzipでまとめたものです。 利用するNeo4jサーバからこのzipファイルを解凍して参照することで作成したグラフデータを活用することができます。 ちなみに今回の検証では1億件強のデータを利用したところ約4時間でJobが完了しました。 zipファイルのサイズは23.8GBで、ノード数はUser,Shopあわせて約560万件、関係数は約1億件でした。 実際のデータ登録に掛かった時間は2時間程度で、残りはグラフデータベースファイルをzipファイルに圧縮してCloud Storageにアップロードするのに掛かった時間でした。 なおCloud DataflowのworkerのmachineTypeには e2-highmem-4 を指定し、SSDのPersistent Diskを256GB指定しました。 作成したグラフデータベースファイルは Cloud Build を利用することで、 Neo4jの公式Dockerイメージ からグラフデータを同梱したコンテナイメージを生成することができますし、 Cloud Run や GKE にデプロイしてAPIサーバとして活用することもできます。 以下、グラフデータが同梱されたイメージを生成するDockerfileの例と、コンテナイメージ生成とCloud Runへのデプロイを定義したcloudbuildファイルの例を紹介します。 (ポートを複数利用するため現状Cloud RunからGUIによるグラフ操作を利用することはできません) Dockerfile_graph FROM neo4j:4.4.21 USER neo4j COPY --chown=neo4j:neo4j data/ /data/ COPY --chown=neo4j:neo4j logs/ /logs/ ENV NEO4J_AUTH=neo4j/password ※ ENV_NEO4J_AUTHではログイン時の初期アカウント名とパスワードを指定します cloudbuild.yaml steps: - name: 'gcr.io/cloud-builders/gsutil' args: ["cp", "gs://examble-bucket/neo4j/index/transaction.zip", "."] - name: 'gcr.io/cloud-builders/gsutil' entrypoint: "unzip" args: ["transaction.zip"] - name: 'gcr.io/cloud-builders/docker' args: ["build", "-f", "Dockerfile_graph", "-t", "$_REGION-docker.pkg.dev/$PROJECT_ID/graph/graph", "."] - name: 'gcr.io/cloud-builders/docker' args: ["push", "$_REGION-docker.pkg.dev/$PROJECT_ID/graph/graph"] - name: 'gcr.io/cloud-builders/gcloud' args: ["run", "deploy", "graph", "--image", "$_REGION-docker.pkg.dev/$PROJECT_ID/graph/graph", "--platform", "managed", "--region", "$_REGION", "--memory", "2Gi", "--port", "7474", "--min-instances", "1", "--no-allow-unauthenticated"] timeout: 600s substitutions: _REGION: asia-northeast1 また、グラフデータベースファイルをPCにダウンロード・解凍して、Neo4jの公式Dockerイメージからコンテナを起動して参照することで、手元で手軽にクエリを試すこともできます。 以下、コンテナ起動コマンド例を紹介します。 docker run \ --name graph \ -p7474:7474 -p7687:7687 \ -d \ -v {graph_db_dir_path}/data:/data \ -v {graph_db_dir_path}/logs:/logs \ -v {graph_db_dir_path}/import:/var/lib/neo4j/import \ --env NEO4J_AUTH=neo4j/password \ neo4j:4.4.21 (windows環境で動かない場合はNEO4j_dbms connector {http|https|bold}_advertised__address環境変数の指定を試してみてください) グラフデータベースの検証活用 次に作成したグラフデータベースファイルをさまざまなデータと付き合わせて手軽に検証活用するソリューションを紹介します。 作成したグラフデータベースの活用方法としてはグラフデータベースAPIサーバを立てて、グラフデータを利用したいサービスからリクエストを送って結果を取得・活用するのが一般的です。 しかしAPIサーバ利用では少し面倒なケースも存在します。 たとえばグラフデータベースに大量のクエリを実行して結果を保存する場合、リクエストを組み立て結果を取得して保存するコードを書く必要があります。 クエリ内容をいろいろなパターンで試したい場合に都度コードを書き換えて実行するのは少し面倒です。 またグラフデータは時間と共に変化していくこともあります。 リアルタイムにグラフデータを活用する場合はグラフデータの発展推移に合わせてクエリを実行する必要があります。 たとえばリアルタイムなグラフデータを活用したMLモデルの活用ではグラフデータを特徴量として活用する際に、特徴量として用いるデータが生成された時の状態のグラフデータへのクエリ結果が必要になります。 APIサーバを使ってこうした特徴量を学習用のデータとしてバッチで生成する場合、APIサーバにデータの生成日時順に更新とクエリを実行して結果を取得する必要があります。 こうした発展推移するグラフデータからのクエリ取得をバッチで手軽に生成できるようになるとデータ分析や特徴量作成での試行錯誤を高速にまわすことができると考えられます。 以下表ではグラフデータの更新の有無に加え、グラフデータの処理形態がバッチかストリーミングかで想定するユースケースをまとめました。 MDTのlocalNeo4j transform モジュールではこれらのユースケースをサポートすることを目指しました。 ここからはMDTによる更新を伴うグラフデータベースへのBatchでのクエリ取得例として、BigQueryにある購入履歴データからグラフデータを更新・クエリ実行結果を取得してBigQueryに保存する例を紹介します。 この例では先ほどと同じ購入履歴を用いて、ユーザが購入を行うごとにその時点での同じ店舗で買い物するユーザの数を数えています。 以下はMDTによる設定例です。 { "sources": [ { "name": "BigQueryInputTransaction", "module": "bigquery", "parameters": { "query": "SELECT UserID, ShopID, Pay, CreatedAt FROM `mydataset.Transactions`" }, "timestampAttribute": "CreatedAt" } ], "transforms": [ { "name": "LocalNeo4j", "module": "localNeo4j", "inputs": ["BigQueryInputTransaction"], "parameters": { "index": { "setupCyphers": [ "CREATE CONSTRAINT UserUniqueConst FOR (u:User) REQUIRE (u.UserID) IS UNIQUE", "CREATE CONSTRAINT ShopUniqueConst FOR (s:Shop) REQUIRE (s.ShopID) IS UNIQUE" ], "nodes": [], "relationships": [ { "input": "BigQueryInputTransaction", "type": "BUY", "source": { "label": "User", "keyFields": ["UserID"] }, "target": { "label": "Shop", "keyFields": ["ShopID"] }, "propertyFields": ["Pay"] } ] }, "queries": [ { "name": "SimilarUserCount", "input": "BigQueryInputTransaction", "cypher": "MATCH (u1:User {UserID: ${UserID}})-[r:BUY]->(s:Shop)<-[:BUY]-(u2:User) WITH u1.UserID AS UserID, u2.UserID AS TUserID, COUNT(DISTINCT s.ShopID) AS ShopCount WHERE ShopCount > 4 RETURN UserID, COUNT(DISTINCT TUserID) AS SimilarUserCount", "schema": { "fields": [ { "name": "UserID", "type": "long" }, { "name": "SimilarUserCount", "type": "long" } ] } } ], } } ], "sinks": [ { "name": "BigQueryOutput", "module": "bigquery", "input": "LocalNeo4j", "parameters": { "table": "myproject:mydataset.results", "createDisposition": "CREATE_IF_NEEDED", "writeDisposition": "WRITE_TRUNCATE" } } ] } 最初のbigquery sourceモジュールではBigQueryから購入者と商品と支払額と購入日時を取得しています。 また先のデータベース作成時には指定していなかったtimestampAttribute項目に購入日時を示すCreatedAtフィールドを指定しています。これは指定したフィールドの値をデータの生成日時として扱うことを宣言するものです。 この指定により次のlocalNeo4jのtransformモジュールでは入力となる購入履歴データをCreatedAtの値の順に処理を実行します。 次のlocalNeo4j transformモジュールでは入力データに基づいてグラフデータを更新・クエリを構築して結果を取得します。 inputs 項目ではグラフデータベースへ登録するデータやクエリの入力元のモジュールのnameを指定しています。今回は購入履歴の取得を定義したBigQueryInputTransactionを指定してグラフデータベース登録かつクエリ生成に利用します。 parameters 項目では詳細なグラフデータの更新設定とクエリ設定を指定します。 index 項目ではグラフデータの更新設定を定義します。 今回の例ではデータベース作成時の設定とほぼ同じ内容を指定しています。 今回は利用していませんが path 項目であらかじめ作成したグラフデータベースファイルのCloud Storageのパスを指定することでデータをロードして処理を開始することができます。 queries 項目では入力データからcypherクエリを生成・実行して結果を取得する定義を行います。 cypher 項目では Apache FreeMarker 形式のTemplate文字列を指定します。 ここに入力データのフィールド値が埋め込まれてCypherクエリが生成・実行されます。 この例では購入履歴レコードのユーザのIDから、5店舗以上同じ店舗で買い物をしたユーザ数を抽出するCypherクエリを生成しています。 schema 項目ではCypherクエリの結果データのスキーマを指定します。 クエリ結果はここで指定したスキーマを持つレコードの配列として保持されます。 こちらのクエリ定義は複数指定することができ、一つの入力から複数種のクエリを実行することもできます。 最後のbigquery sinkモジュールでは生成した結果を指定したBigQueryのテーブルに保存しています。 保存されたデータはデータ分析や特徴量生成などに活用することができます。 おわりに 今回の記事ではグラフデータベースのNeo4jを手軽に試せるソリューションを紹介しました。 グラフデータを活用してみたいけどデータの連携が面倒で試すのに二の足を踏んでいたような場合でしたら今回紹介したソリューションが役立つかもしれません。 今回紹介したソリューションによるグラフデータ活用の展開はまだこれからというフェーズで、紹介したMDTのモジュールも発展途上です。もしご利用いただいた方がおられましたらフィードバックをいただけると幸いです。 過去に紹介した検索APIサーバ構築とも共通するのですが、さまざまなデータソースから各種データベースを構築してコンテナイメージに同梱するなど、1台のマシンに載るサイズの更新不可なデータとして活用できるパターンは他にもまだあるかもしれません。 引き続き社内データ活用を広げるソリューションを見出して提供していきたいと思います。 明日の記事は@fukuchanさんです。引き続きお楽しみください。
アバター
こんにちは。メルカリのEngineering Officeの afroscript です。 2023年4月19日から4月21日までの3日間、メルカリではエンジニアのための技術のお祭り「Mercari Hack Fest (以下、Hack Fest)」が開催されました。 ※参考記事: 社内ハッカソン”Mercari Hack Fest”の作り方 ~ 2023年春ver. ~ 本記事では、Hack Festの最終日に行われた「Showcase Day」の様子や、Award受賞者のプロジェクトを紹介していきます。 ハイブリッドスタイルで開催された「Showcase Day」 Hack Festでは最終日に「Showcase Day」と称して、この3日間で取り組んだ成果を発表する場があります。 メルカリでは“ YOUR CHOICE ”の制度により全国各地でメンバーが働いているため、今回のShowcase Dayもオンライン参加とオフライン参加のハイブリッドスタイルでの開催となりました。 エンジニアやプロダクトマネージャーに限らず様々な部署から約300人がShowcase Dayに参加し、Hack Fest中に生まれた75個のideaのうち24個の成果発表が行われました。 Award Winners 発表されたプロジェクトの中から、審査員を特にうならせたものがHack Fest Awardとして選出されました。 まずはGOLD / SILVER / BRONZE Awardに選ばれた受賞者とそのプロジェクトを紹介していきます。 GOLD Hack Fest Award “Mercari Items Discovery” <メンバー> @chan.jonathan, @Misha.k, @Anandh, @tsubo, @cowana, @anastasia, @alisa <プロジェクト概要> 新着の商品をストーリー形式で閲覧できることで、お客さまが新しく出品されたアイテムをより見つけやすくする機能を開発 SIVER Hack Fest Award “Project-MI” <メンバー> @kiran-k-a, @manoj, @dinesh, @vaibhav, @prajwal, @prasanna <プロジェクト概要> アプリ内の言語表示を、英語と日本語で簡単に切り替えられる機能を開発 BRONZE Hack Fest Award: “Age Group Facet Filter for Fashion Categories” & “Search + ChatGPT” 今回BRONZE Awardには2つのプロジェクトが選ばれました。 Age Group Facet Filter for Fashion Categories Member: @akkie プロジェクト概要: ファッションカテゴリーの検索において、年代で検索結果を絞ることができるフィルターを作成し、選択した年代に人気な商品のみを表示できる機能を開発 Search + ChatGPT Member: @allan.conda プロジェクト概要: ChatGPTを使い、検索バーに言葉を入力すると行きたいページをサジェストしてくれる機能や自分のIDや購買履歴などのデータをチャットで回答を得られる機能を開発 Extra Awards Hack Fest Awardsの他にも、コスト意識の文化の促進や支出に対するオーナーシップを持った個人 or チームを表彰する賞“FinOps Award”や、グループ内においてLLM(=Large Language Model)技術を用いることを促し、より一層LLMの理解を促進するプロジェクトを表彰する”LLM Award”として下記2つのプロジェクトが選出されました。(プロジェクト概要略) Fin Ops Award: “Shell-Shockingly Good Kubernetes Autoscaling” / Member: @sanposhiho LLM Award: “Mercari Comment Assistant By Chat GPT” / Member: @kenmaz また、Hack Fest Awardには惜しくも選出されなかったものの、審査員の印象に強く残った下記3つのプロジェクトが”Judge Special Mention”として紹介されました。(プロジェクト概要略) PJ Name: “Buyer Next” / Members: @erika.takahara, @wills PJ Name: “Improve UI for QAC” / Members: @mohit, @Chin-ming, @romy PJ Name: “Feedback Classification”/ Members: @a-corneu, @meatboy, @aggy, @kazzy After Partyの様子 Showcase Dayのすべての発表を終えたあとは、After Partyです!Hack Festは技術の”お祭り”ということで、今回はお祭りっぽい装飾をしたり射撃や輪投げのゲームを用意して、日本風なお祭り感を演出してみました。 ちなみに射的や輪投げでいい高得点を出した方には、オリジナルHack Fest Tea (ほうじ茶) をプレゼントしました。 まとめ 今回も大盛り上がりなイベントとなり、「これを3日間でつくりあげるなんて…!」と息を呑む成果発表がたくさんありました。 また、オンラインで参加するメンバーも前回よりはるかに増えており、休憩時間やAfter Partyでワイワイとたくさんコミュニケーションをとっていたり、日本風お祭りの装飾やゲームを楽しんでくれていたりしたのも印象的でした。 次回開催は秋の予定です。今後もどんどん内容をアップデートしてよりおもしろい技術の”お祭り”としてブラッシュアップしていくので、ぜひお楽しみに!
アバター
この記事は、 Merpay Tech Openness Month 2023 の12日目の記事です。 こんにちは。メルペイ Engineering Engagement Team の @mikichin です。 私たちのチームは、「メルペイのエンジニアリング組織をスケールさせる」をミッションに、候補者体験(Candidate Experience)と従業員体験(Employee Experience)を業務領域としています。 わたしはTech PRとして、候補者体験(Candidate Experience)の「認知」「興味」の領域を担当しています。 今回ご紹介するのは、わたしが2022年11月から現在まで取り組んできたことで、指標策定と、PDCAサイクルのPlan・Doの部分になります。 Plan ミッション・役割を定義する 以前から社内でTech PRのミッションや役割は暗黙的に認識されていましたが、改めて明確に定義することにしました。定義するにあたって、社内ドキュメント「メルカリ、メルペイエンジニアリング組織の技術広報の方向性(※1)」を参考にしました。 ■ミッション メルペイのエンジニアリング組織に関わる発信(技術、ヒト、組織 etc.)が継続している仕組みをつくる ■役割 ①発信し続ける状態をつくる ②認知されたい印象につながるような発信に取り組む 役割①は発信量、役割②は発信内容を指します。 発信量と発信内容を担保して、「認知」「興味」の領域において候補者が第一想起する企業郡にメルペイが含まれることを期待します。 また、役割①②における具体的な施策を考える際は、メルカリグループおよびメルペイのロードマップ(※2)を参考にします。 現状把握(データ収集・分析) 現状把握のため、2つのアプローチを取りました。 1つ目は、過去実績の整理です。過去の発信数とその変遷、発信内容、発信者数などを調べました。 2つ目は、メルペイのエンジニアに対して個別インタビューも含めアンケート調査をしました。 指標を決める 現在、メルペイ Tech PRは役割①「発信し続ける状態をつくる」に注力しています。 最初は、FY2022の実績を参考にして全体および各技術領域の発信数を指標とした計画をたてました。すると、現実的ではない数字目標になってしまいました….! ▲分析資料の一部 上記の「FYごとの発信数推移」の図を見ると、FY2022(※3)はFY2021と比較すると4倍近くの発信を実施していることがわかりました。 この時期は、全社的に採用を強化していた時期であり、現場からの要望も強くTech PRとしては発信を促進しやすい状況で異常値であることがわかりました。 改めて、Tech PRとして目指したい「発信し続ける状態」とはどんな状態なのかを再考しました。限られたメンバーで発信を行うのではなく、メルペイのエンジニアリング組織に所属する全メンバーがメルペイの技術発信をしている状態をつくりたいと考えました。 PJの状況や緊急対応など時間がない時期もあるかと思いますが、メンバーで順番に「発信し続ける状態」を維持していきたい、そしてそれがわかる指標をつくりたいと考えるようになりました。 そこで、下記3つの指標にたどりつきました。 アンケートの回答率:アンケートデータとして偏りをなくし、組織の正確な状態を確認するための指標 直近半年の発信実施率:発信し続ける状態を維持しているかを確認するための指標 むこう半年の発信意欲:今後も発信し続ける状態を維持することができるかを確認するための指標 ▲各指標の目標数値 課題設定→施策検討 次に、目標達成に向け、課題設定と施策を検討しました。 施策を検討するにあたって、インパクトエフォートマトリクスというフレームワークを使いました。インパクトエフォートマトリクスとは、インパクト(影響度)とエフォート(かかる工数)をマトリクスにして優先順位を決める方法です。 まず、アンケート調査から課題と施策を洗い出しました。続いて、その施策の工数、効果を算出しました。 ▲課題と施策の洗い出し(一部) その後、インパクトエフォートマトリクスを用いて実施する施策の優先順位を決めました。「②すぐに行動する」を中心としつつ、わたし自身の全体工数を考慮しながら、「①パッとやって小さい効果」「④プロジェクト化を検討」の施策を組み合わせながら実施する施策を決めました。 Do:施策実施 大きく分けて3つの施策を行いました。「発信機会・場の提供」「ネタだしの支援」「発信にかかる準備時間の短縮」です。 発信機会・場の提供 アンケート結果によると、発信をした一番の理由は「発信機会や場があったから」ということでした。わたし自身、このブログも「Merpay Tech Openness Month 2023」という企画があったから執筆したと思います(笑)。こういった企画があると発信するきっかけや後押しにつながっていることがわかります。 その他にも、メルカン記事「 Swift愛あふれるメルペイiOSチームに直撃。3年ぶりに開催された「try! Swift Tokyo meetup」はどうだった? #tryswift 」やイベント「 Merpay Tech Talk〜PM、Backendエンジニアによるメルカードの開発舞台裏大公開〜 」などもTech PRが企画をしお声がけしました。 ネタだしの支援 発信をしなかった・できなかった一番の理由は、「ネタがなかった」でした。「ネタがない」という言葉にはいろいろなケースが含まれていると思いますが、まずはネタ出しのヒントになるものを準備したいと考えました。 そこで、メルカリエンジニアリングブログで公開されているブログを技術領域別の記事、複数人で執筆した記事などパターン別にまとめました。 発信にかかる準備時間の短縮 発信をしなかった・できなかった理由で「発信する時間がなかった」も多くいただきました。通常業務もある中、発信する時間がないというのは非常に理解できますし、Tech PRだけではなかなか根本的な解決ができない課題でもあります。 Tech PRとしてできることは、極力発信にかかる時間を短縮するサポートを行うことです。そこで執筆を外部ライターに依頼したり、準備に時間をかけないイベントを企画したりしました。 まとめ 今回、メルペイ Tech PRとしてまわし始めたPDCAサイクルのさわりをご紹介させていただきました。 この6月に初めての振り返りを行います。今、メルペイエンジニアにアンケートをとり、結果を分析している最中です。Check、Actionの取り組みについては今後またブログでご紹介できたらと思います。 これからもエンジニアメンバーとともに、メルペイのエンジニアリング組織の魅力を発信し続けていきたいと思います! 明日の記事は @orfeonさんです。引き続きお楽しみください。 Appendix ※1:技術広報の方向性は、以前外部メディアで紹介しているので、「 メルペイが実践する『技術広報』とは?『採用広報』との違いは何か 」をご参照ください。 ※2:ロードマップについては、メルカン「ロードマップ経営に必要なのは、「 ミッションを本気で達成する」と決める“狂気” #メルカリのイシューを分解する 」をご参照ください。 ※3:FY2022は2021年7月から2022年6月の1年間を指します。
アバター
こんにちは。メルカリのQAの____rina____です。メルカリShopsというサービスのQAをしています。今回は、メルカリShopsのQA活動に欠かせない技術についての紹介と、QAチームがどのような活動をしているかについて紹介します。 私はメルカリShopsのQAエンジニアとして2年超働いていますが、これらの多くの技術解決があることでより広いQAの活動ができました。 現在、QAの活動をもっとよくしたいと思っているQAエンジニアの方や、品質に課題を感じている開発者の方が、このブログを通じて技術面からQA・品質の支援・改善ができることや、QAの可能性を広げられることについて知っていただけると幸いです。 開発環境の概要 Webの開発 メルカリShopsは、機能の多くをWebで提供しています。メルカリアプリでは、同じソースコードで各デバイスへの機能提供が可能で、Webviewで表示しています。iOS、Android、および各PCの対応ブラウザでテストが必要ですが、通常の開発ではiOSの各バージョンやAndroidの各機種によるテストにあまり注意を払う必要はありませんでした。関連記事については、以下のURLをご覧ください。 関連記事: メルカリShops のフロントエンド また、昨年、メルカリアプリをiOS/Androidともに作り直した際にも、メルカリShopsは新機能開発を続けることができました。ただし、Deeplinkなど一部の機能については、アプリ開発が必要でした。 関連記事: ・ メルカリの事業とエコシステムをいかにサステナブルなものにするか?かつてない大型プロジェクト「GroundUp App」の道程 ・ メルカリShopsのためのWebViewの技術 ・ モバイルアプリにおけるディープリンクとメルカリShopsでの実装 モノレポ メルカリShopsは、モノレポ開発を採用しています。モノレポとは、アプリケーションやマイクロサービスなどのコードを1つのリポジトリで管理することを指します。このモノレポ開発のメリットは、QAにとっても非常に有益でした。私たちはUI E2EテストにCypressを採用しており、環境構築に必要な作業をリポジトリに迷わずに済ませることができました。さらに、リポジトリに迷わないため、コードを見るハードルが下がり、テスト実施時にコードを見る機会が増えたと感じています。また、対応チケットとPRが紐付けされておりアクセスしやすい工夫もされています。 関連記事: メルカリShops の技術スタックと、その選定理由 ブランチ戦略 メルカリShopsの開発では、テストを完了した後に、すぐに本番にリリースするためにmasterにマージすることで、本番のコードと開発コードの乖離を防いでいます。 Pull Request(PR)環境 開発コードをmasterにマージする前には、手動でもテストを実施します。開発者がテストを行うこともありますが、開発者以外のQAエンジニアがテストを担当することもあります。そのため、開発中の環境が必要となります。このような状況に対応するために、Pull Request(PR)環境が用意されています。GitHub actionをフックにして、PR環境が自動的に作成されるようになっています。テストを実施したい場合には、PRに「Pull Request env」というラベルを貼るだけで、QA環境が作成されます。この仕組みにより、修正ごとにエンジニアに環境作成を依頼する必要がなくなり、エンジニアも開発に集中しやすくなっているのではないかと思います。 関連記事: メルカリShops の CI/CD と Pull Request 環境 Feature toggle メルカリShopsでは、Feature Flags(Feature toggle)を使用しています。Feature toggleとは、機能の表示や非表示を切り替える機構のことを指します。メルカリShopsでは、Feature toggleを実現するために Unleash というサービスを利用しています。この利点はいくつかありますが、ブランチ戦略にも寄与しています。大きな機能の場合、その機能を構成する開発が全て完了するまで、masterにマージしたり本番環境にリリースする必要があると考えられますが、Feature toggleを利用することで、お客さまに機能を表示させずに、本番環境にリリースすることが可能となりました。また、Unleashを利用することで、本番環境でのホワイトリストによる本番確認や、特定のお客さまへの機能リリースも可能になっています。さらに、UnleashはGUIで操作できるため、PMやQAエンジニアも操作することができます。機能リリースする際は、開発以外にも、CSが用意してくれるお客様向けのガイドページの作成や、PRが用意するShopsマガジンの掲載など、複数のチームと連携する必要があります。これらの連携を待たずにリリースができることも、Feature toggleを採用する利点の1つです。 関連記事: メルカリShops の技術スタック、その後 テストの自動化 QAチームがどのように具体的にテストの自動化に取り組んでいるかについて紹介します。なお、CI環境やエラー動画のキャプチャーとスクリーンショットの保存などエンジニアが設定をしてくれたり、多くの協力があって実現しました。 Cypress によるUI E2E メルカリShopsのリグレッションテストは、Cypressを使用して作成しています。CIでも実行できるようにしており、毎日masterブランチのテストを実施しています。以下の項目はすべて自動的に実行されます。 毎日UI E2Eの実行 結果の表示(Slack通知とURLにより確認可能) エラーの動画キャプチャーとスクリーンショットの保存 Failした場合は、実行結果をビデオやスクリーンショットで確認し、再実行もCIで実行できるためCypressを起動する必要はありません。現在は改修が必要になることが2週間に1回程度となり、安定的に実行できるようになりました。 関連記事: Cypress初心者が短期間でカバレッジを40%あげるまで APIテスト メルカリShopsでは、公開用のAPIを提供しています。そのAPIのE2Eテストは、 Postman を使用して作成しています。PostmanはAPIを使用するためのプラットフォームです。個別のAPIの確認ができるだけでなく、シナリオに沿ったテストも作成できるため、PR環境でもボタンを1つクリックするだけで実行できます。また、 Newman というコマンドラインコネクションランナーを使用することで、Postmanで作成したテストケースを一括で実行し、リグレッションテストが可能になります。Newmanを使用することで、テスト結果をわかりやすく表示することもできます。 これらの技術により、大規模な機能開発でもリリースブロックを防ぎ、まとめてリリースすることによるリリース判定テストなどもほとんど必要なくなっています。 スクラムinQAについて 技術面について紹介しましたが、技術面以外にも取り組んでいることがあります。 メルカリShopsの開発は、PO、PdM、SE、QA、デザイナーがスクラムチームとして活動しています。各スクラムチームに1人ずつQAエンジニアが在籍しており、スクラムセレモニーにも参加しながら、以下のような活動を行っています。これらの活動は、各スクラムチームで最適な活動を採用・改善しています。また、QAエンジニアとスクラムマスターを兼任しているメンバーもいます。 リファインメント リファインメントはユーザーストーリーマッピングの実施をすることがあります。その場合、 プランニングポーカー で見積も実施します。ユーザーストーリーマッピングでは、リスクの洗い出しやQAエンジニアとしての意見を出します。プランニングポーカーはテストを含めた開発からリリースまでをストーリーポイントとして出しています。 リファインメントは、バックログアイテムのリファインメントを実施することもあります。仕様のフィードバックやリスクを出します。 スプリントプランニング 機能の優先順位はPOが決定しますが、スプリントでの開発順序については、QAエンジニアとしてコメントすることがあります。特にiOS/Androidのクライアント開発は、メルカリアプリと一緒に審査をする都合上、メルカリのリリーストレインに乗せなければならないため、先行して開発する必要があります。このため、開発順序を先にしてもらうように要請することもあります。また、できるだけ早くテストできるように、開発の順序について相談やコメントを行うこともあります。 スタンドアップミーティング(朝会) 毎日のスタンドアップミーティングでは、開発状況を把握したり、テストの進捗状況やリリースの確認を行っています。また、開発やテストのブロッカーはもちろん、リリースブロッカーについても確認を行います。例えば、CSへの周知やPRの公開に関する懸念などが考慮すべき一例です。 スプリントレビュー スプリントレビューでは、事前に完成した機能を使ったテストを行うために、QAエンジニアがテストデータの準備を行います。ただし、エンジニアもデータの準備に関わることがあります。また、機能によってはテストデータが複雑で事前準備が必要なときや説明が必要な場合は、QAエンジニアが担当することもあります。 Acceptance Criteria(AC)の追加と読み合わせ会 Acceptance Criteria は通常、POが作成しますが、QAがACを追加することで、開発時により詳細な懸念事項が明確になるようにしました。またACの読み合わせをバックログリファインメントの一環としても実施します。この読み合わせを通じて、より具体的な開発手順や懸念点、機能についての懸念事項を話し合う機会が生まれました。 テスト実施/QAレビュー テスト実施は必ずしもQAエンジニアが行う必要はなく、エンジニア自身がセルフQAとしてテストを実施することもあります。また、PMがテスト実施することもあります。この場合、QAエンジニアはQAレビューを実施することで、QAエンジニア自身の作業負荷を減らしつつ一定の品質に貢献しています。 レトロスペクティブ スクラムチームの一員として参加し、改善提案などの意見を出しています。 不具合報告 不具合が発生した場合、JIRAでチケットを作成します。チケット作成は、QAだけでなく、エンジニアやPMも担当することがあります。対応期限がスプリント内であるかどうかは、適宜Slackや朝会などで確認し、対応時期を決定します。 これらの活動は、可能な限りトレースしやすいように工夫し、JIRAやConfluenceなどで適切に紐付けています。 横断活動 通常、QAエンジニアはスクラムチームに所属しながらも、QAチームとしての活動も行っています。 ミッションとポリシーの作成 QAチームとしてのミッションやポリシーを定めることで、全体的な意識を共有し、トップダウンで何かをやらされるのではなく、主体的に動くことができるようになりました。これはQAチームだけでなく、開発に関わる全ての人にとって、協力して一つの目的を持つことが成功につながると考えられます。このミッションとポリシーは、QAエンジニア全員で議論を行い、後に説明する、「QAの未来を考える会」で決定しました。 関連記事: Souzoh QAのミッション・バリューを作りました 全社おさわりかいの実施 「全社おさわり会」とは、社内の全員がサービスを触って改善点を出し合い、メルカリShopsの品質向上やお客さまの満足度の向上を目指す取り組みです。QAエンジニアがファシリテートを行い、メルカリShopsのローンチに向けて開催されました。おさわり会では多くの機能改善案や不具合が見つかり、サービスのブラッシュアップに貢献できたと思います。また、全社員が参加したことで、より多様な意見が出され、参加者もサービスを自分のものとして捉えられたのではないかと思います。さらに、参加者の意見交換やコミュニケーションの強化にもつながったと思います。 関連記事: All for Oneでたのしいおさわりかいをするよ! UI E2Eテストの自動化 UI E2Eテストの自動化についても、QAチームが取り組んでいます。具体的には、自動化までのテストプロセスやテストケースの整理を行い、JIRAやTestRailを活用することでトレース性を確保した運用をしています。ただし、結果の集計はスプレッドシートに手動入力する必要があるため、今後解決していきたい課題となっています。 QAの未来を考える会 「QAの未来を考える会」では、横断的な活動を実現するために2週間に一度のペースで、QAチームのミーティングを行っています。この会では、前述したQAのミッションについて、どう実現していくかや、私たちがどういう思いで活動したいのかについて話し合います。また、OKRの進捗状況や相談なども行います。さらに、QAチームがより活躍するためのヒントを得るために、シンポジウムへの参加を検討する時間も設けています。 メルカリShopsにおける、QA活動を支えている技術の紹介とQAチームとしての活動について紹介をしました。QAのメンバー一人一人にとって、これらの活動は大きな価値と経験になりました。これらの経験や活動は多くの技術を用意してもらえているからこそできたことだと思います。 技術的解決は、QAの活動もよくします。また、品質に対する課題はQAだけが持つのではなく、メルカリShopsを開発しているみんなで持ち、それを技術的解決をすることで、さらに次の課題解決に取り組めるのだと思います。
アバター
こんにちは。search infraチームのmrkm4ntrです。 我々のチームでは検索基盤としてElasticsearchクラスタをKubernetes上で多数運用しています。これらのElasticsearchクラスタを管理しているnamespaceはマルチテナントな我々のKubernetesクラスタの中で最大のリソースを要求しているnamespaceです。 一方でクラスタのサイズをピークタイムに合わせて固定していたため、そのリソース利用率は非常に低いという問題がありました。Elasticsearch EnterpriseやElastic Cloudにはオートスケーリング機能が存在するのですが、これはスケールイン/アウトのためのものではなく、ディスクサイズに関するスケールアップ/ダウンを提供するもので我々の要求を満たすものではありませんでした。 そこで今回は、HPAを用いたスケールイン/アウトのためのオートスケーリングの仕組みを開発しました。これによってリソース利用率を向上させ、約40%のコスト削減を達成できたので、その詳細について説明します。 ElasticsearchとECK メルカリではElasticsearchをECK( https://github.com/elastic/cloud-on-k8s ) を用いてKubernetes上で管理しています。ECKはElasticsearchというCustom Resourceとそのcontrollerであり、以下のようなリソースを作成すると対応したStatefuleSetやService、ConfigMapおよびSecretなどのリソースが自動で作成されます。 apiVersion: elasticsearch.k8s.elastic.co/v1 kind: Elasticsearch metadata: name: example spec: version: 8.8.1 nodeSets: - name: coordinating count: 2 - name: master count: 3 - name: data count: 6 この定義からcoordinating、master、dataの3つのStatefulSetが作成されます。 Horizontal Pod Autoscaler(HPA)を使ってこれらのStatefulSetをオートスケーリングさせたいのですが、以下のような課題があります。 Elasticsearchリソース自体をHPAの対象とはできない。なぜならscale subresource(後述)が定義されていないため、複数あるnodeSetのどれを増減させれば良いのかわからない。 Elasticsearchをスケーリングする際はPod数の増減だけではなく、そのPodに配置されるElasticsearchのindexもレプリカ数を変更して増減させなければならない。つまりスケーリングの単位は (indexのshard数 / Podあたりのshard数)となる。下図の場合は (3 / 1) = 3。 一方HPAはminReplicasからmaxReplicasまでの間の任意の値を指定する可能性がある。この場合、Elasticsearchのauto_expand_replicasオプションはPodあたりのshard数 = indexのshard数となり、1Podあたり3つのshardが乗ってしまうので我々のユースケースには合わないため、自分でレプリカ数を変更する必要がある。 Elasticsearchリソースの管理下のStatefulSetを直接HPAの対象とした場合、2の問題に加え、親リソースであるElasticsearchを更新した場合にHPAによって調整されていたPod数が親リソースの値にリセットされてしまう。 これらの問題を解決するために新しくKubernetesのCustom Resourceとcontrollerを作成しました。 Custom Resourceとcontroller 以下が新たに導入したCustom Resourceの例です。 apiVersion: search.mercari.in/v1alpha1 kind: ScalableElasticsearchNodeSet metadata: name: example spec: clusterName: example count: 6 index: name: index1 shardsPerNode: 1 nodeSetName: data これは先ほどのElasticsearchリソースのdataという名前のnodeSetに対応します。このリソースは直接Elasticsearchリソースとの親子関係はなく、scale subresourceを提供しており、 kubectl scale コマンドやHPAの対象とすることができます。Custom Resourceの定義はkubebuilderを用いて生成しているのですが、以下のようなコメントを追加することでscale subresourceを提供できるようになります。 //+kubebuilder:subresource:scale:specpath=.spec.count,statuspath=.status.count,selectorpath=.status.selector これは上記のScalableElasticsearchNodeSetの.spec.countがHPAや kubectl scale コマンドの操作対象であることを示し、.status.countに現在のcount数が記録されることを意味します。さらに.status.selectorにこのリソースの管理対象、すなわち対象のStatefulSetの管理対象を選択するためのselectorが記録されます。これらは勿論自動で記録されるわけではなく、そうなるように自分でcontrollerを実装しなければなりません。 また、このCustom Resourceのspec内のcount、shardsPerNodeおよび対象となるindexのshard数から実際のStatefulSetのレプリカ数を以下のように算出します。 ceil(ceil(count * shardsPerNode / shard数) * shard数 / shardsPerNode) Scale subresourceの .spec.count と実際のcountが一致していなくても(少なくとも type: Resource の場合)HPAの挙動に問題がないことは、HPAのソースコードを読んで確認済みです。HPAで設定すべきレプリカ数を計算する際に用いられる現在のレプリカ数は .status.selector で選択されたPodの数となります。 スケールアウト時にはまずElasticsearchリソースの該当のnodeSetのcountを上記の計算式から算出された値に設定し、すべてのPodがReadyになった後、ElasticsearchのAPIを用いてindexのレプリカ数を増やします。スケールインする場合は逆にindexのレプリカ数を減らした後にElasticsearchリソースのcountを変更します。 これで先ほど挙げた課題の1と2については解決できました。3に関してはMutatingWebhookConfigurationを用いて解決します。これはElasticsearchリソースが更新された際に呼び出されるhookを指定する仕組みで、そのhookの中で search.mercari.in/ignore-count-change”: “data,coordinating のようなannotationが指定されていた場合、そのannotationに対応するnodeSetのcount数を現在のcount数に上書きします。これによりHPAの対象となっている状態でElasticsearchリソースの変更をGitOps等で行っても、countがリセットされることがなくなります。 導入に際しての問題と解決 以上の方針で実装したcontrollerを実際に導入してみたところ、いくつかの課題がわかったのでそれらについて紹介します。 スケールアウト直後にlatencyが増加する Force mergeによりHPAのmetricをCPU利用率にできない トラフィックが少ない時間ではボトルネックとなるmetricsが変化する スケールアウト直後にlatencyが増加する この課題は元々rolling updateを行うときなどでも観測できていたのですが、Dataノードが起動し、shardが配置され、検索リクエストを受け付け始めた直後のlatencyが非常に高くなっていました。これはDataノードに限った話ではなくElasticsearchにリクエストを送るmicroserviceにIstioを導入した際に、Coordinatingノード (shardを持たずに最初にリクエストを受け付けてroutingとmerge処理を行うだけのノード)でも発生していました。 原因はおそらくJVMのコールドスタート問題によるもので、Istioの場合sidecarが新しく追加されたPodに即座に均等にリクエストを送ろうとすることが問題でした。この点については、Istio導入以前はHTTPのkeep aliveにより、新しく追加されたPodに緩やかにトラフィックが移行していくため問題となっていませんでした。 この課題を解決するためにpassthrough(Istioのservice discoveryに頼らずそのまま通す)やDestinationRuleのwarmupDurationSecs(指定の秒数をかけて新しいPodに徐々にトラフィックを増やしていく)を使いました。ただDataノードの場合は、routingは完全にElasticsearch依存となり、外部からどうにかできる余地がなかったためElasticsearch自体を修正することにしました。これはupstreamにPull Requestとしてあげています。 https://github.com/elastic/elasticsearch/pull/90897 Force mergeによりHPAのmetricをCPU利用率にできない 我々のindexはドキュメントの削除,更新(Elasticsearchが利用している検索ライブラリであるLuceneにおける更新は、内部的には削除+追加という処理をおこないます)の頻度が高いため毎日トラフィックの少ない時間帯にforce mergeを行って論理的に削除済みのドキュメントを削除していました。このforce mergeを忘れると数日後にトラフィックを捌けなくなるということが過去発生していました。 しかしForce mergeはCPUに負荷のかかる処理であり、またその性質上同じタイミングでスケールアウトを行うべきものでもないため、HPAのmetricをCPU利用率にすることができませんでした。そのため初期は検索リクエスト数をDatadog経由でexternal metricとして利用しようと考えていましたが、新しいmicroserviceから呼び出される際にクエリのパターンが変化し負荷のパターンも変わるため本質的にはCPU利用率をHPAのmetricにすることが望ましいです。 そこでLuceneのソースコードを読んでいると、 deletes_pct_allowed というオプションを見つけました。これは論理的に削除済みのドキュメントの割合を指定するためのもので、デフォルト値は33でした。この値を変更しながらパフォーマンステストを実施すると30%付近から急激にlatencyが悪化することがわかりました。そのためこの値を最小値である20 (最新のElasticsearchではデフォルト20、最小値は5 https://github.com/elastic/elasticsearch/pull/93188 )に設定することでForce merge処理を削除することができました。これによりHPAのmetricにCPU利用率を指定することができています。 トラフィックが少ない時間ではボトルネックとなるmetricsが変化する Elasticsearchではindexの中身をファイルシステムキャッシュに載せることで低latencyを実現します。我々も必要な情報はすべてファイルシステムキャッシュに載せることを目指しているため、巨大なindexでは多くのmemoryを使用します。トラフィックがある程度存在する時間帯ではボトルネックがCPUであり、CPU利用率をHPAのmetricにすることでうまくオートスケールします。 しかしトラフィックが極端に少ない時間帯であっても可用性のために最低限のレプリカは確保しなくてはなりません。そのためその時間帯ではボトルネックはmemoryとなり、必要なCPUに対して無駄に多くのCPUを割り当ててしまうことになります。 元々の構成はmemoryの量がdisk上のindexサイズの2倍となるよう設定されており、 memory.usage も高い値を示していましたが、 memory.working_set を見るとまだまだ余裕がありそうでした。Kubernetesにおいて memory.working_set とは memory.usage からinactive filesを引いた値となります。inactive filesはざっくりいうとほとんど参照されていないファイルシステムキャッシュのサイズとなります。Kubernetesではcontainerのmemory limitに達する前にこれらのファイルシステムキャッシュはevictされるため、割り当てるmemoryはもっと少なくても良いことがわかります。 勿論inactive filesではないファイルシステムキャッシュも必要ならばevictされるのですが、こちらはevictしすぎるとパフォーマンスの劣化につながります。難しいことにinactiveでなくなる条件が意外と緩いのでどこまでevict可能なのかが明示的にはわからないため、memory requestをあまり攻めた値にはできていませんが、これによりmemoryがボトルネックになっている時間帯に合計CPU requestを減らすことができました。 ElasticsearchはstatefulなアプリケーションなのでPodの再起動が必要なVPAを適用するのが難しいですがIn-place Update of Pod Resources ( https://kubernetes.io/blog/2023/05/12/in-place-pod-resize-alpha/ ) が利用可能になるとCPU requestを再起動なしにスケールダウンできるようになるため、この問題が緩和されることを期待しています。 さいごに この記事では、ECKでKubernetes上で動かしているElasticsearchクラスタに対してHPAを用いてCPU利用率を基にオートスケーリングする方法について述べました。これによりElasticsearchの運用に関わるKubernetesのコストが約40%削減できました。おそらく今後Elastic CloudにはServerlessの一環としてこの辺りのオートスケーリング機能が提供されることになると予想しますが、我々の今の状況下においては効果的な手法だと感じています。 search infraチームでは現在ともに働く仲間を募集しています。もし興味がありましたらご気軽にお問合せください。 Software Engineer, Search Platform Development – Mercari
アバター
この記事は、 Merpay Tech Openness Month 2023 の11日目の記事です。 こんにちは。メルペイのデータマネージャー @katsukit です。 本日は、現在メルペイで取り組んでいる非エンジニアのためのデータ集計環境についてご紹介します。 はじめに データ活用には可視化、分析、調査、ML、CRMなど、さまざまな場面があると思います。エンジニアはもとよりデータアナリスト、マーケター、プロジェクトマネージャーなどと利用するユーザーもさまざまです。 これらの利用シーンで使用するデータにはお客さまのデータを取り扱うこともあり、データの管理をしっかりとやる必要があります。 一方で、お客さまへのアプローチまでスピード感が求められるマーケティングやCRM配信など、現場にデータ抽出・作成を委ねているデータ活用では、データガバナンスの維持が難しく、現場全体に統制されたデータ管理体制を構築する必要があると思います。 このような、現場にデータ抽出・作成を委ねるデータ活用に対し、データガバナンスの向上を目的とした取り組みの一つをご紹介したいと思います。 データ管理上の課題 マーケティング、CRM配信など関係者が多く、現場に必要なデータ抽出やデータ作成を委ねているデータ活用では、データの作成手段やルールがさまざまでデータ管理上の統制が難しいという問題があります。 データ管理を統制するために社内のデータ基盤を利用する事も考えられますが、関係者のコミュニケーションやシステムの実装・リリースが伴うので、一定の時間が必要なこともあり、スピード感が求められるデータ作成には適しません。 そこで、データ抽出要件からデータ作成まで、現場の非エンジニアに委ねるべきところは委ね、スピード感を維持する一方で、データ管理を統制するための、簡易的なデータの集計環境とルールを提供し、データガバナンス上の問題を改善する取り組みを行っています。 簡易的なデータ集計環境 非エンジニアがCRM配信などで利用するために提供しているデータ集計環境は、以下のような構成とフローになっています。 データの抽出とデータロードはBigQueryのScheduled Queryで行います。 データ基盤により集計された各マイクロサービスのデータ、もしくは加工された中間データをデータソースとして、Scheduled Queryにより、データ抽出・加工を行います。 実行するクエリや、結果データの保存先やスケジュールなどのデータ作成に関するメタ情報はGitHubで管理し、データ作成情報の履歴管理と承認プロセスを提供します。 クエリやデータ作成情報のGitHubリポジトリへのマージをトリガーに、GitHub Actionsを起動し、Scheduled Queryを登録もしくは更新を行います。 上記により、ユーザーは基本的にGitHubだけを利用し、Scheduled Queryを登録・データ作成までを実現することができます。 Scheduled Queryによる簡易的なデータ集計 Scheduled QueryはBigQueryの1機能で、クエリの定期的な実行をスケジュールすることができる機能です。BigQueryのGUIコンソールでも利用可能で、BigQueryのデータを抽出できるユーザーは簡単に利用することができます。 CRM配信関連のデータ作成では、これまでこのScheduled Queryを多用していたこともあり、当環境でも採用しています。 以下にScheduled Queryの利用の仕方についてご紹介します。 クエリのスケジュール登録/更新 Scheduled Queryの登録・更新はコンソールでの利用の他に、bqコマンド、API、Java、Pythonが利用できますが、Scheduled Queryに利用できる設定内容に差があります。例えば、クエリの実行開始時間や終了時間を設定する場合には、bqコマンドではできず、APIやJava/Pythonを利用する必要があります。当環境はPythonで実装しています。 Pythonで作成する場合は、 google-cloud-bigquery-datatransfer ライブラリを使用します。 実装する際は、BigQueryのガイドラインにあるScheduled Queryの設定内容では、仕様の詳細まではわからないので、Pythonライブラリの ドキュメント で確認したほうがよいと思います。 Scheduled Queryの登録・更新時の主な設定情報は以下の通りです。 パラメータ 型 説明 destination_dataset_id String 結果保存先データセット display_name String スケジュールの名称 params Struct(protobuf) dictionaryも可 実行内容詳細 ├ query String 実行対象のクエリ ├ destination_table_name_template String 作成テーブル名 ├ write_disposition String テーブル書込方法 WRITE_TRUNCATE/WRITE_APPEND ├ partitioning_field String パーティション対象のfield名 schedule String スケジュール schedule_options ScheduleOptions スケジュール詳細 ├ start_time Timestamp 開始時間 ├ end_time Timestamp 終了時間 service_account_name String 実行サービスアカウント またコード例を以下に示します。 * 以下は上位のTransferConfigという抽象クラスで初期化処理を実装している例になります * paramsはjsonで受け取っている例になります 登録: from google.cloud import bigquery_datatransfer from google.protobuf import field_mask_pb2 transfer_client = bigquery_datatransfer.DataTransferServiceClient() class CreateTransferConfig(TransferConfig): def __init__(self, config): super().__init__(config) def execute(self): parent = transfer_client.common_project_path(self.project_id) schedule_options = bigquery_datatransfer.ScheduleOptions( start_time=start_time, end_time=end_time ) transfer_config = bigquery_datatransfer.TransferConfig( destination_dataset_id=self.target_dataset, display_name=self.display_name, data_source_id="scheduled_query", params=json.loads(self.params), schedule=self.schedule, schedule_options=schedule_options ) transfer_config = transfer_client.create_transfer_config( bigquery_datatransfer.CreateTransferConfigRequest( parent=parent, transfer_config=transfer_config, service_account_name=self.service_account_name, ) ) 更新: class UpdateTransferConfig(TransferConfig): def __init__(self, config): super().__init__(config) def execute(self): schedule_options = bigquery_datatransfer.ScheduleOptions( start_time=start_time, end_time=end_time ) transfer_config = bigquery_datatransfer.TransferConfig( name=self.resource_name, destination_dataset_id=self.target_dataset, display_name=self.display_name, params=json.loads(self.params), schedule=self.schedule, schedule_options=schedule_options ) transfer_config = transfer_client.update_transfer_config( { "transfer_config": transfer_config, "update_mask": field_mask_pb2.FieldMask( paths=["params", "destination_dataset_id", "display_name", "schedule", "schedule_options", "service_account_name" ] ), "service_account_name": self.service_account_name, } ) 更新時は、FieldMaskで更新対象を指定します。 テーブルの更新仕様 テーブル更新方法はparams内の write_disposition で設定できます。 設定できるのは WRITE_TRUNCATE (上書き) もしくは WRITE_APPEND (追加)になります。 取り込み時間でのパーティション分割に設定することで実行毎の履歴データとして保存することができます。指定は以下のように設定します。 "destination_table_name_template": "table_name${run_date}" このとき、 partitioning_field には何も設定しないようにしてください。 なお、suffixテーブルとして作成したい場合は、以下のように設定します。 "destination_table_name_template": "table_name_{run_time|"%Y%m%d"}" Scheduled Queryのバックフィル実行時の冪等性を考えて、実行クエリには実行日時にScheduled Queryで利用できるクエリパラメータ@run_time / @run_dateを利用するようにします。 SQL例: -- 実行日以前のユーザー登録を抽出 SELECT user_id , registered_at FROM `<project>.<dataset>.<table>` WHERE date(registered_at) <= @run_date クエリ管理とデータのメタ管理 クエリやデータの作成情報はGitHubで管理します。 しかし、非エンジニアにとってはGitの利用は馴染みがないことが多く、ハードルが高いため、利用を促すために極力簡易化する必要があります。 GitHubを利用するためのツールはいろいろありますが、できるだけWeb上でできるようにGitHub自体の機能を利用しています。 データの作成情報は、Scheduled Queryに必要なパラメータの他に、データオーナーや作成したテーブルの有効期限などを設定します。 カンパニー、プロジェクト/サービス毎にデータの作成情報をまとめ、データが必要な業務やプロジェクトと、データの作成情報が紐づくように管理します。 管理している情報は以下の通りです。 実行クエリ データオーナー 作成データの説明 データ(テーブル)の有効期限 CRM関連データ(配信内容や配信名称) 実行スケジュール(開始日・終了日含む) データ(テーブル)の更新仕様(上書き/追加、パーティションの有無など) 管理する情報は、以下のようにクエリとデータ作成情報に分け、ファイルで管理します。データ作成情報はYAMLで構成しています。 クエリファイル例: SELECT user_id , registered_at FROM `<project>.<dataset>.<table>` WHERE date(registered_at) <= @run_date データ作成情報ファイル例: delivery_name: campaign delivery_schedule: every 24 hours delivery_type: demo_delivery description: "デモ" partition_field: date write_disposition: WRITE_TRUNCATE GitHubのIssue FormとGitHub Actionsの連動 上記情報のGitHubへのアップロードは、GitのcommitやpushなどGit操作の知識が必要になりますが、これをGitHub Issue FormとGitHub Actionsを利用して自動化することで、簡易化を実現しています。 GitHub Issue Form GitHubのIssue Formは、これまでの自由入力なIssueに対してリッチな入力フォームを作成することができる機能になります。テンプレートにより、ユーザーに設定してほしい項目を構造化し、簡単なワークフローを作成することができます。 なお、執筆時点ではbeta版となっており、変更される可能性があるので、ご注意ください。 Issue Formのテンプレートは、マークダウンで記述するIssueテンプレートと同様に .github/ISSUE_TEMPLATE 配下にYAMLで記述します。 以下のような記述式でテキストエリアやドロップダウンなど構成することができます。 構成できる入力タイプは以下のものです。 markdown input textarea dropdown checkboxes 必須チェックといった簡単な入力チェックも可能です。 詳細についてはこちらの ガイドライン をご参照ください。 以下が設定例になります。 name: Request to create deliveries description: Request to create delivery data for CRM title: "[Request]: " labels: ['request delivery'] body: - type: markdown attributes: value: | CRM向け配信対象データの作成クエリの登録 - type: dropdown id: company attributes: label: Company Name description: 配信データを作成するカンパニー options: - mercari - merpay validations: required: true - type: input id: service_name attributes: label: Service Name description: 配信データを作成するサービス名もしくはプロジェクト名 placeholder: e.g. creditdesign validations: required: true - type: input id: delivery_type attributes: label: Delivery Type description: placeholder: e.g. validations: required: true - type: input id: delivery_name attributes: label: Delivery Name description: placeholder: e.g. validations: required: true - type: textarea id: delivery_description attributes: label: Delivery Description description: placeholder: e.g. validations: required: true - type: input id: delivery_schedule attributes: label: Delivery Schedule description: 実行スケジュール(UTC) placeholder: e.g. every 24 hour - type: input id: start_time attributes: label: Start Time description: 開始日時(UTC) placeholder: e.g. YYYY-mm-DD HH:MM:SS - type: input id: end_time attributes: label: End Time description: 終了日時(UTC) placeholder: e.g. YYYY-mm-DD HH:MM:SS - type: textarea id: query attributes: label: Query description: placeholder: e.g. select * from A validations: required: true - type: dropdown id: write_disposition attributes: label: Write Disposition description: options: - WRITE_TRUNCATE - WRITE_APPEND validations: required: true - type: input id: partition_field attributes: label: Partition Field description: - type: dropdown id: ingestion_time_partitioned attributes: label: Ingestion Time Partitioned description: 取り込み時間パーティションの設定 options: - INGETION_TIME_PARTITIONED 上記を表示すると以下のようなフォームになります。 このIssue Formで作成された入力フォームで必要な情報を入力し、submitするだけで、必要なファイル作成とPullRequestまで自動生成する仕組みを提供しています。 作成されたPullRequestを承認者が問題ないか確認し、マージするワークフローを経ることでクエリの一定の品質を担保します。 さらにPullRequestのマージをトリガーに、自動的にScheduled Queryを登録・更新し、Scheduled Queryがデータを作成します。 このようにユーザーはIssue Formの入力と承認ワークフローを経るだけで、定期的なデータ作成を実現できるようになっています。 自動生成は後述するGitHub Actionsで実現しています。 GitHub Actions GitHub ActionsはGitHubが提供するCI/CDです。 GitHubのリソースを直接ビルド、テスト、デプロイが可能で、YAMLにより容易にワークフローを生成することができます。 今回は、このGitHub Actionsの仕組みを活用し、GitHubにpushされたファイルを基にデータ作成までの自動化を実現しています。 今回作成したGitHub Actionsの主なワークフローは以下の通りです。 GitHub Issueの内容をもとにファイルの作成、コミット、PullRequestを作る PullRequestのマージによりScheduled Queryを作成する PullRequestのマージ時のワークフローの大きな流れは以下のようになっています。 GitHub ActionsはワークフローをYAML形式で記述し、 .github/workflows 内に保存することで実行できるようになります。 起動タイミングは以下のように on 要素に記述します。上記のワークフローは以下のように記述しています。 Issue作成: on: issues: types: ['opened'] issue_comment: types: ['created'] Issue_comment も設定しているのは、Issueの内容を修正し、再度PullRequestを作成したいときに、コメントに rebuild please としたときに再度ワークフローを起動するようにしているためです。 関係のないIssueが作成されるケースがあるので、Issueにラベルをつけて、該当ラベルのときだけ起動するよう条件を指定するようにしています。 PullRequestマージ時: on: push: branches: - main paths: - 'deliveries/**' 上記はmainブランチにマージされたときに起動する記述になります。 リポジトリにはデータ作成情報のファイル以外にも保存するファイルがあるので、該当ディレクトリ配下の変更時だけ起動するように paths を指定しています。 ワークフローの各処理は jobs 要素内の steps 要素に処理を記述します。 BQの操作には、BQの操作アカウントでまず認証・認可が必要になります。 以下はWorkload Identity で認証するステップの例です。 - id: auth name: Authenticate uses: google-github-actions/auth@v0 with: workload_identity_provider: ${{ steps.settings.outputs.wip }} service_account: ${{ steps.settings.outputs.sa }} 複数のスケジュールが一度に登録された場合に複数のジョブに分けてそれぞれ実行されるようにするために matrix strategies を利用します。 以下の例では、実行の単位となる親ディレクトリのJSON配列 service_df 分だけジョブが分割され、それぞれのジョブでステップが実行されます。 jobs: check: runs-on: ubuntu-latest outputs: service_df: ${{ steps.diff.outputs.service_df }} steps: ... needs: check if: ${{ needs.check.outputs.service_df != '' }} strategy: matrix: diff: ${{fromJson(needs.check.outputs.service_df)}} steps: ... GitHub Actionsの仕様詳細を知りたい場合は、 こちら をご参照ください。 上記のGitHub Actionsのワークフローにより自動実行されることで、利用者はGitの操作やScheduled Queryの登録を意識しないで済むようになり、Scheduled Queryの登録やデータ作成上のルールを統一し、データ作成を一元管理することが可能になります。 おわりに 今回は非エンジニアのためのデータ集計環境の取り組みについて紹介させていただきました。 当環境で、データ作成の自動化、クエリの管理手段、承認プロセスやワークフローを非エンジニアを含むデータ利用者に提供することで、オペレーションのミス、情報管理上のリスクや思わぬ事故を極力減らし、防ぐことができる、と考えています。 今後は、Scheduled Queryの誤登録を防ぐための入力チェックの強化や、Scheduled Query登録時や実行時の通知機能の実装を検討中です。 今回の記事が読者のみなさんにとって少しでも有益なものになれば幸いです。 明日の記事は @mikichinさんです。引き続きお楽しみください。
アバター
こんにちは、Engineering Officeの yasu_shiwaku です。 2023年6月14日、一般社団法人日本CTO協会様主催の「Developer eXperience AWARD 2023」にて、「開発者体験ブランド力」調査の中で、 メルカリが昨年に引き続き2年連続で1位に選出されました。 今回の調査ではソフトウェアエンジニアをはじめとする技術者にとって各社が「開発者体験※」に関して、どれくらい魅力的な発信をしているかという「テックブランド力」を調査するためのアンケートが実施され、その中で名前の挙がった上位30社のランキングが掲載されています。また選出された各企業にはDeveloper eXperience AWARD 2023の受賞企業として表彰されました。 (※「開発者体験」とはエンジニアとしての生産性を高めるための技術、チーム、企業文化等の環境全般を指します。調査方法等は日本CTO協会様の プレスリリース をご覧ください) また今年はオフラインの会場で授賞式がおこなわれました。当日はGroup CTO 若狭が受賞コメントを述べ、続く受賞企業を交えたトークセッションで私(yasu_shiwaku)がメルカリグループの技術広報戦略や施策、カルチャーなどについて紹介させていただきました。 昨年に引き続き、多くの方から高い評価を得られたことを嬉しく思います!これも日々社内外を問わず、多岐に渡って情報発信に貢献してくれているエンジニアたちのおかげです。 メルカリグループではエンジニアたちが主体的に発信し、コミュニティにその経験や知見を還元していくことで業界全体を活性化・成長させていくカルチャーを育てています。 またメルカリが利用させていただいているオープンソースコミュニティへの還元として、カンファレンスや プロジェクトスポンサー などの支援活動もおこなっています(メルカリの オープンソース に対する考え方はこちら。公開ソフトウェアは こちら ) メルカリグループは今年10周年を迎え、ミッションを 「あらゆる価値を循環させ、あらゆる人の可能性を広げる」 に刷新しました。エンジニアリング組織としても、新しいチャレンジや問題解決に向かい合っていく中でエンジニアリングの価値を循環させ、可能性を広げていくために、今後も社内外の開発コミュニティに向けて貢献できるよう、情報発信を続けていければと思います。 エンジニア向け発信媒体一覧 Mercari Engineering Website (本ポータルサイトです) Twitter( 英語 ・ 日本語 ) イベント関連 Connpass Meetup YouTubeチャンネル Mercari devjp Mercari Gears メルカリグループでどんな開発者体験ができるのか、またどんなカルチャーがあるのか興味がある方は、ぜひキャリアサイトを一度覗いてみてください! Software Engineer/Engineering Manager
アバター
この記事は、 Merpay Tech Openness Month 2023 の9日目の記事です。 はじめに こんにちは。メルペイのバックエンドエンジニアの @tanaka0325 です。 この記事では、私が最近サイドプロジェクトとして取り組んでいる「なめらかなナレッジシェアリング文化を創る」ための活動について紹介したいと思います。 事前に断っておきたいこととして、このプロジェクトはまだ始まったばかりです。プロジェクトメンバー全員がサイドプロジェクトとして参加しているので、これから少しずつ進めていくものになります。 今回は私たちがどのような活動を行っているのか、現状の状況や今後の方針についてお話できればと思います。 ※この記事では表記ゆれを避けるため、資料やコンテンツ、知見などをまとめて「ナレッジ」と表現することとします。 きっかけ まずは、この活動を始めたきっかけについてお話したいと思います。 日々仕事をしていくなかで求められるスキルはたくさんあります。また、求められるスキル以外にも個人的に身につけたいスキルもたくさんあります。 ひとつずつ学んでいく必要があるわけですが勉強は大変です。できるだけ効率よく学びたいものです。 メルペイには優秀な人達がたくさんいます。集合知を活用していくことで効率的に学習できるのではないかと考えました。 みんなの持っているナレッジを何かしらの形にし、それを教材にできるとよさそうです。いわゆるナレッジシェアリングの仕組みが必要でした。 もちろん私がこんなことをいうまでもなく、すでに社内には当然のように学習に使えるナレッジがたくさんあります。しかし現状ではうまく有効活用できている実感がありません。今よりももっとなめらかにできるのではないか?と思いはじめました。 上記の課題感を当時のマネージャーとの1on1で話した際に、一緒にやろう!となったのが、この活動を始めたきっかけです。 求めるもの 自分が求めている「ナレッジシェアリングの仕組み」とはどのようなものなのかを考えたとき、いくつかの条件が見えてきました。 個人のペースに合わせて学べるようになっている ルールが存在し、一定の品質が担保されている 内容が古くならないように、必要に応じて更新される 一部の人だけでなく、みんなが有効活用できる 個人のペースに合わせて学べるようになっていてほしい ナレッジにはいくつか種類があります。 たとえば、新しく参加したメンバー向けのオンボーディング資料や新人研修資料、機能の詳細を知るための仕様書、知識を定着させて使えるものにするためのハンズオン。形式についても、動画、リアルタイムの講義やハンズオン、テキストなどいろいろと考えられます。 今回は、私自身がそのナレッジを使って学習したい、という気持ちがあるので、新人向けやオンボーディング資料は適しません。私は新人ではないのです。 また新人とは異なりすでにプロジェクトにアサインされているためいくつかタスクを抱えており、学習に使える時間が限られているので「4時間の研修です!」といったものは厳しいです。 自分のペースで学べるよう、動画かテキストの形式がよいです。 ただし、動画は作成の負荷が高い上、何度も見返すには早送り/巻き戻しを駆使する必要があります。作業負荷や使い勝手を考慮するとテキストが良さそうです。 ちなみに弊社には新人向けのナレッジとしてDevDojoというものがあります。 いくつか公開されているものもあるので、もし興味があればこちらの記事を参照ください。 メルカリの2023年技術研修DevDojoの資料と動画を公開します! ルールが存在し、一定の品質が担保されていてほしい 前述のとおり、すでにメルペイには学習に使えそうなナレッジが数多く存在しています。 しかし、それらは全体的なナレッジシェアリング目的で作られたわけではなく、各チームのオンボーディング資料であったり新人研修資料であったり各人の学習メモであったり、多種多様な目的で作られてきたものです。 当然フォーマットも情報の粒度もバラバラです。さらにメルペイでは歴史的経緯によりナレッジシェアリングツールが複数存在しており、上記のナレッジが書かれている場所もバラバラです。 学習効率という観点ではフォーマット/情報の粒度/場所は統一されているほうがよいので、特定のルールにそって管理されていてほしいです。 次のような状態になっていると自分は嬉しいです。 分量が必要十分であること フォーマットが決まっていること 何を書いて、何を書かないかが決まっていること 分量が必要十分であること 情報が少なすぎると、それだけを読んでも十分な知識を身につけることはできません。 逆に多すぎると、学習負荷が上がり最後まで読むのが大変です。学習する対象が複雑であれば分量が増えていくのはある程度仕方がありませんが、その場合はちょうどよい量、たとえば初級/中級/上級など、で分割されていたほうがよいです。 また分量が多く学習負荷が高い状態になってしまっている場合、もしかすると公式ドキュメントや書籍で学習したほうが効率がよいかもしれません。 匙加減が難しいところではあります、それを読むことでまぁなんとなく理解でき、ある程度仕事はこなせるくらいの知識が身につく。そしてより深く知りたい場合に公式ドキュメントを読む際の下準備が整う、くらいの分量/情報量になっていると良さそうに思いました。 フォーマットが決まっていること 読み手目線ではフォーマットが決まっているほうが読みやすいです。たとえばすべてのナレッジの一番最初は概要を書く、次に目次を書く、など。 これがあることで、読み手の中にメンタルモデルが形成され、読む際の認知負荷が下がります。 書き手目線ではフォーマット、つまりテンプレートがあることで書きやすくなります。0から書き上げることは大変です。テンプレートが用意されていれば文書構造を考える必要がなくなり書く難易度がぐっと下がります。 何を書いて、何を書かないかが決まっていること 前述のとおり、歴史的経緯によりメルペイには複数のナレッジシェアリングツールが存在しています。ツールが複数存在していること自体は個人的には問題ではありません。それらツールの使い分けにルールがないことがややこしくしているのだと思います。 たとえば、仕様書はツールA、Design DocはツールB、作業メモや個人メモなどはツールCなど使い分けがなされているのであれば、複数ツールが存在することはむしろ好ましいとすら思っています。 しかし使い分けがされていない状態だと目的のドキュメントにたどり着くためには、極論するとすべてのツールで検索し、探しだす必要があります。さらに仕様書のような確定情報が見たい場面で、個人の設計メモのような情報が出てくるかもしれません。 何を書いて、何を書かないかを決めることで、必要な情報にアクセスしやすくなるはずです。 他にも細かいルールについてはいろいろと考えられますが、重要なことは「ルールが存在し、一定の品質が担保されている」ということです。 内容が古くならないように、必要に応じて更新されていってほしい これはいわずもがなでしょう。すでに古くなってしまった情報を参照してつらい思いをする人を減らすために、何かしらの仕組みがあってほしいです。 よくある工夫としては、最終更新日から一定期間が経過した記事には読み手に注意文が表示されたり、書き手に更新を促す通知が飛んだりなどが考えられます。 手段は何でもよいですが、内容が更新されていってほしいです。 一部の人だけでなく、みんなが有効活用できていてほしい メルペイにはたくさんのチームが存在しています。マイクロサービスアーキテクチャを採用しているので、それらのチームが独立して開発・運用しているケースが多く、チームを跨いだコミュニケーションは少なくなりがちです。 チーム内に閉じたナレッジシェアリングに関してはうまく運用できているチームはあると思います。しかし別チームを巻き込んでの共有まではあまりできていない印象です。 各チームが運用しているマイクロサービスは共通の技術・インフラを使用しているので、身につけるべき知識やつまづきポイントなども共通なことが多いです。 自分や自分のチームのみならず、みんなが活用している状態になっていると、全体的なスキルアップ/業務の効率化が測れそうです。 これまで ここまでで、自分がこの活動を始めたきっかけ、そしてどのようなものを求めているのかについて紹介してきました。 次にこれまでに何をやってきたかについてお話します。 仲間集め まずはじめにしたことは仲間集めです。「きっかけ」にあるとおり、最初のメンバーは自分と当時のマネージャーの二人です。この活動をするには単純に人数が少ないですし、チームを跨いだナレッジシェアリングを目指していることを考えると、別チームの人もいたほうがよいです。 しかしプロジェクトの初期段階で多くの人を集めてしまうと、認識のすり合わせをするだけでも大変になってしまいます。最初はある程度絞って声をかけることにしました。 最終的には自分たち含め、同じ課題を感じていた5人のメンバーでやることになりました。 認識のすり合わせ 次にしたことは目指すゴールの認識のすり合わせです。それぞれがどんなものを作りたいかを持ち寄り、議論を重ね、最終的に全員で共通の認識を持ちました。決まった内容はおおむね前述の「求めること」に書いたようなことなので、具体的な内容は割愛します。 ものすごく簡単に説明すると、次の二軸をやっていくぞ!といった内容です。 ナレッジシェアリングをする「場所作り」 特定の誰かが頑張ることなく運用されていく「文化作り」 OKR作成 前述の決めたゴールをもとにプロジェクトのOKRを作成しました。Objectiveはこのブログ記事のタイトルでもある「なめらかなナレッジシェアリング文化を創る」です。 ナレッジシェアリングの場所を作るだけでは意味がありません。社内にはすでにたくさん書く場所があるのです。大事なのはそれが適切に回っていくような文化を創ることです。 プロトタイプ作成 次にナレッジシェアリングをする場所、ようはナレッジシェアリングツールをどうするかを決めました。大前提として、このツールをゼロから自分たちで開発する必要はないと思っています。すでに世の中にはたくさんのよいツールがあります。 重要なことはしっかりとルールを作り、そのルールにそって運用することです。ルールが曖昧なままでは、仮にどれだけよいツールを使っても上手くいかないと思います。 まずはシンプルなツールを選ぶことにしました。使っていくうちにいろいろと希望が出てくるかもしれないので、プロトタイプとして気軽に試せることが大事です。 ちなみに選択したものは MkDocs です。次のような点から選びました。 すでに社内で実績がある Git管理できるので、GitHubのレビュープロセスが使える ドキュメントが単純なmarkdownファイルなので、今後別のツールに移行しやすい plugin機構があるので、カスタマイズできる そしてちょうど今現在、プロトタイプを絶賛作成中です。 次の項目の「ルール決め」と同時並行で進めている最中になります。 ルール決め 前述のとおりこのプロジェクトでもっとも重要なことは、しっかりとしたルール作りです。とはいえ実際に手を動かしてみないとよいルールは浮かんでこないです。プロジェクトメンバーで実際のコンテンツを作りながらルールを考えていっています。 ルールの大枠の方針は、前述の求めるものを満たせるようなものを検討しています。 これから 改めて今現在の進捗状況は次のとおりです。 ナレッジシェアリングツールのプロトタイプ作成中 実際にコンテンツを作成しながらルール策定中 今後はこれらが揃ったタイミングで、改めて実際に本番で想定している運用をプロジェクトメンバーで回しながらブラッシュアップしていくつもりです。 ある程度納得できる状態になったら、トライアルという形で、社内で協力者を募集しようと思っています。 ただしこのあたりは進むにつれ、その都度検討しようと思っているので大いに変わる可能性はあります。 おわりに この記事では、私が取り組んでいる「なめらかなナレッジシェアリング文化を創る」ための活動について紹介してきました。 組織が小さいときはうまくいっていたことでも、大きくなるにつれ自然には回らないことが増えてきました。ナレッジシェアリングもそのひとつです。今後組織がより拡大し、成長を続けるためには必要な活動だと思っています。 この活動はまだまだ初期段階です。これからプロジェクトが進むにつれて今までとは違った新たな気づき、知見が得られると思います。その際は改めて何かしら紹介できたらと思います。 明日の記事は @Amit.Kumarさんです。引き続きお楽しみください。
アバター
この記事は、 Merpay Tech Openness Month 2023 の8日目の記事です。 メルペイのSREチームに所属しておりますt-nakataです。今回はメルペイでのTerraformモジュールを利用したCloud Spannerの設定標準化の取り組みについて紹介します。 Cloud Spannerの設定標準化とは? メルペイのバックエンドではマイクロサービスアーキテクチャを採用しており、各マイクロサービスで利用するデータベースはCloud Spannerを主に利用しております。Cloud Spannerは基本的には各マイクロサービスを担当しているバックエンドエンジニアがTerraformを利用して構築し、運用します。(一部共用のインスタンスもあります。) その際に考慮する必要がある点が多々あります。たとえば、 google_spanner_instance 、 google_spanner_database リソースによるCloud Spannerのインスタンス、 データベース自体の設定はもちろん、運用で必要な監視(Datadog monitor)、アプリケーション側のサービスアカウントに対するパーミッションの付与、データベースのバックアップやインスタンスの負荷に応じてProcessing Unit数をオートスケールをさせる spanner-autoscaler の導入などもあり、これらを構成するためには沢山のTerraformリソースを追加する必要があります。また、これらの実装にはいくつかの選択肢がある一方で、 FinOpsの観点 からコストメリットのある構成にしたいなど、推奨の構成に設定する必要があったりもします。これまで上記の対応はドキュメントを基にバックエンドエンジニアが個々に対応したり、SREへリクエストをしてもらった上でSREが対応したりしていましたが、都度対応する運用コストもかかるようになってきました。このような背景からCloud Spannerに関連するリソースを一通り構成できるようなTerraformモジュールを実装しました。以下を満たすことを目的としています。 マイクロサービスに必要なCloud Spannerに関連するTerraformリソースを一通り作成できるようにする 可能な限り必要な設定を抽象化し、利用者が実装の詳細に立ち入らなくても構成できるようにする 推奨の構成となるようモジュールのinput variableにはdefault値を持ち、カスタマイズしたいマイクロサービスに対してはinput variableで上書きできるようにする モジュールを利用することにより複数選択肢のある構成を統一する 以降では各マイクロサービスが利用するTerraformのリソースが本モジュールを含めてどのように構成されているかについて触れ、そのうえで本モジュールの詳細について簡単に紹介いたします。 Terraformリソースの構成 各マイクロサービスが利用するTerraformのリソースですが、Platform Infraチームが管理しているモノレポ上にあります。(詳しくは 他記事 も参照してください。)本モジュールもこのモノレポ上で利用されることを前提としています。モノレポの構成の概要は以下の図のとおりとなっております。(今回の記事に関連した内容のみを抜粋しております) modulesディレクトリ配下にモノレポ内で利用するTerraformモジュール定義があります。spanner-kitと記載しているものが本モジュールとなります。各マイクロサービスはsourceにバージョンとともにモジュールへのpathを指定して利用します。 microservicesディレクトリ配下に各マイクロサービス向けのTerraformリソースがあります。development/labolatory/productionと環境ごとにstateを持っています。 マイクロサービスには starter-kit を利用します。詳細はリンクの記事を参照していただきたいですが、Google Cloudのプロジェクト等、マイクロサービス作成に必要なものが一式定義されています。加えて、本モジュールを含め、必要なTerraformモジュール、個別のリソース定義を利用して、マイクロサービスに必要なリソースを構成します。 マイクロサービス内の一部のリソースは共有のプロジェクトを利用します。詳細は後述しますが、共有プロジェクトに向けたgoogle provider定義を利用して構成します。 モジュールの詳細 今回実装したモジュールのinput variableは以下のようになっております。(一部社内の具体的な実装に関わる変数については省略、変更しています) default値を利用した通常の構成の場合 module "spanner-with-default" { source = "uri_of_module_with_version" environment = "production" microservice_project_id = "microservice_project_id" instance = { name = "instance-name" processing_units = 1000 } databases = [ { name = "database_name" enable_backup = true } ] providers = { (略) } } input variableを全て指定した場合 module "spanner-with-all-variable" { source = "uri_of_module_with_version" environment = "production" microservice_project_id = "microservice_project_id" instance = { name = "instance-name" config = "regional-asia-northeast1" processing_units = 1000 } databases = [ { name = "database_name" enable_backup = true } ] spanner_autoscaler = { enable = true service_account_id = "service_account_id" } backup = { backup_schedules = ["0 */2 * * *"] interval_hours = 2 retention_days = 7 scheduler_location = "asia-northeast1" scheduler_time_zone = "Asia/Tokyo" workflow_location = "asia-northeast1" } spanner_database_role_on_app_sa { bind = true is_read_only = false } notification = { slack_channel = "slack_channel" } providers = { (略) } } モジュール内ではTerraformリソースごとにtfファイルを持っており、現在は20ファイル程度で構成されています。つまり、モジュールは約20種類程度のTerraformリソースで構成されています。input variableの仕様は terraform-docs を利用してREADME.mdを生成し、利用者に提供しています。 input variableについてはほぼほぼ変数名通りではありますが、以降ではそれぞれについての詳細と構成されるリソースの概要について紹介します。 instance こちらはほぼ google_spanner_instance リソースに向けた変数を指定できます。本モジュールはインスタンスごとの定義となっています。 database こちらはインスタンス内に作成する google_spanner_database リソースに向けた変数を指定できます。また、 enable_backup でデータベースごとにバックアップを構成するかどうかを指定することができます。 spanner_autoscaler こちらはautoscalerを有効にするかどうかを指定できます。default値で有効になっています。有効にした場合はautoscaler用のサービスアカウントや必要なパーミッション等を定義します。マイグレーション向けに service_account_id を指定した場合は、既に存在するサービスアカウントを利用するようにしています。また、autoscaler自体に対する設定についてはautoscalerの設定の実態がKubernetesのCRDであり、既にKubernetesリソースを管理するレポジトリでの資産があるため、そちらを利用してもらうようにしました。 backup こちらはバックアップに関する詳細を指定できます。default値が推奨の値になっています。 backup_schedules でバックアップのscheduleを定義し、Cloud Schedulerによりバックアップをトリガーします。バックアップジョブは Workflows により起動、終了の監視をします。 interval_hours から一定期間内にバックアップが成功しているか、失敗していないか、期間内にバックアップが終了しているかを監視するDatadog monitorを作成します。 retention_days でバックアップの保持期間を指定できます。 spanner_database_role_on_app_sa こちらはアプリケーション側のサービスアカウントに対する権限を指定できます。大きく書き込みもするアプリケーションと読み込みのみをするアプリケーションがあり、 is_read_only で google_spanner_database_iam_member リソースへのroleを roles/spanner.databaseUser か roles/spanner.databaseReader にするかを指定します。 notification 利用者への通知先を指定できます。現状はDatadog monitorの通知先として slack_channel が指定できるようになっています。default値では共用のチャンネルになっています。 providers module blockの仕様 通りのマイクロサービス固有のリソースで使用しているproviderを指定します。 モジュールで工夫した点 以降ではモジュールを実装した際に工夫した点について簡単に紹介します。 processing_unitsをautoscalerが有効の場合にのみignore_changesにする autoscalerを有効にした場合はautoscalerがインスタンスのCPU Utilizationによって processing_units を更新します。この場合Terraform state側との乖離が発生してしまい、 terraform apply をしてしまうと、Terraformで指定した値に processing_units が収束してしまいます。こちらの対応としては lifecycle.ignore_changes を指定する必要があります。一方マイクロサービスによってはautoscalerを利用していないものも存在します。このため、 var.spanner_autoscaler.enable によって動的にlifecycleを設定する必要がありますが、こちらは 現状のTerraformの仕様上 できません。代わりに以下の通り別のリソースを作成することにしました。 resource "google_spanner_instance" "spanner_instance" { count = var.spanner_autoscaler.enable ? 0 : 1 (略) } resource "google_spanner_instance" "spanner_instance_autoscaler" { count = var.spanner_autoscaler.enable ? 1 : 0 (略) lifecycle { ignore_changes = [processing_units, num_nodes] } } locals { spanner_instance = var.spanner_autoscaler.enable ? google_spanner_instance.spanner_instance_autoscaler[0] : google_spanner_instance.spanner_instance[0] } リソースのname、id等のlength制限の回避 作成されるインスタンスやデータベースに紐づくリソースのnameやidにはインスタンス、データベースのnameを持たせたいです。しかしリソースによってはlength制限に該当してしまうケースがあります。例えば、 google_spanner_instance.name には The name must be between 6 and 30 characters in length とあり、 google_service_account.accound_id にも must be 6-30 characters long とあります。account_idに用途ごとのprefixをつけたい場合はインスタンスのnameによってはlengthを超えてしまうケースがあります。今回はこれを回避するために、 Random Provider を使用し、制限を超える場合は一部をより短いlengthの文字列に置き換えることで回避しました。以下のような定義にしました。 resource "google_service_account" "workflow" { account_id = "workflow-${random_string.id_for_spanner_instance_short_name.result}" (略) } resource "random_string" "id_for_spanner_instance_short_name" { (略) } 共有のSecretを複数マイクロサービスで利用したい こちらは本モジュール自体の内容ではありませんが紹介します。本モジュールでプロジェクトごとではないAPI key等のSecretを利用したいケースがありました。共有用のプロジェクトのSecret ManagerにSecretを保存し、Secretを利用する各マイクロサービスのサービスアカウントに roles/secretmanager.secretAccessor roleを付与することで同一のSecretを1箇所に集約して各マイクロサービスからアクセスできるようにするとよさそうです。一方、本モノレポでのCIにおける terraform apply は、権限をマイクロサービスごとに移譲させるため、個々のマイクロサービスに存在する専用のサービスアカウントを利用するようになっています。このサービスアカウントに共有プロジェクトへの権限を直接付与するのは避けたいです。この対応として共有プロジェクトの権限を持つサービスアカウントを impersonate_service_account に設定し、各マイクロサービスの terraform apply をするサービスアカウントが権限を借用できるようなproviderが用意されています。以下のようなリソース定義により、各マイクロサービスから共有のプロジェクトの特定リソースに対して terraform apply ができるようになっています。 # 共有リソース用のprovider定義 provider "google" { alias = "common" impersonate_service_account = "共有プロジェクトへの権限を持つサービスアカウント" } # モジュール定義 module "spanner" { (略) providers = { google = google google.common = google.common } } # モジュール内の共有プロジェクトへのリソース定義 resource "google_secret_manager_secret_iam_member" "some_api_key" { provider = google.common project = "共用のプロジェクト" role = "roles/secretmanager.secretAccessor" member = "serviceAccount:${サービスアカウント}" secret_id = "some_key" } 現状の課題について 最後に本モジュールに関連した現状の課題について紹介します。 既存のマイクロサービスのマイグレーションについて 本モジュールを利用していないメルペイの既存のマイクロサービスに対しても、本モジュールを利用したリソース定義とするべくマイグレーションをしたいと考えております。 github.com/hashicorp/hcl/v2 を利用して、既存の定義をパースし、 cloud.google.com/go 配下の各パッケージを利用し既存のリソースの状態を取得することにより、本モジュールのリソース定義やstateをマイグレーションする定義を出力するスクリプトなどを実装しています。しかし、Terraform管理外の既存のバックアップ等の動作を停止させる必要があったり、Cloud Spannerという極めて重要なリソースに関するマイグレーションであったりすることから、マイクロサービスごと1件づつ対応しており、現在も継続してSREチームで対応中です。 Terraformリソース定義のvalidationについて 本モジュールにより、Cloud Spanner関連のリソース定義を集約できるようになりましたが、依然として各マイクロサービスにて固有にCloud Spanner関連のリソースを定義することができてしまいます。場合によってはベストプラクティスに則っていないものが存在してしまう可能性もあります。こちらの対応として、一通りマイグレーションが終わった後で Conftest によるポリシーを追加し、メルペイのリソースに関してはポリシーによるvalidationをCIですることにより防止したいと考えています。 おわりに 簡単ではありますが、Cloud Spannerの構成を標準化するためのTerraformモジュールについて紹介させていただきました。 明日の記事は @katsukitさんです。引き続きお楽しみください。
アバター
この記事は、 Merpay Tech Openness Month 2023 の7日目の記事です。 はじめに こんにちは。メルコイン Payment Platform チームの @sapuri です。 メルコインではマイクロサービスアーキテクチャを採用しており、お客さまによりアプリの操作が行われると、それぞれのマイクロサービスを横断してリクエストが処理されます。 メルコインの Payment Platform は、お客さまの残高の管理や各種帳簿の作成などの決済事業のための基盤となる仕組みを提供しています。 そのなかで、Payment Service は決済トランザクションを管理するサービスとして、下位層のサービスが提供する各種決済手段を利用して、上位層のサービスが共通して利用できる決済 API を提供しています。 この記事ではマイクロサービスアーキテクチャにおける分散トランザクション管理の課題を説明して、Payment Service で運用されている管理手法を簡単にご紹介します。 分散トランザクションの課題 分散トランザクションとは、複数のノードや複数のデータベースをまたがって実行されるトランザクションのことを指します。 マイクロサービスアーキテクチャでは各サービスが独自のデータベースを持つため、複数のサービスにまたがるトランザクションを行う場合、アプリケーションは単純にローカルトランザクション (ACID) を使用することができません。 そのため、各サービスのデータの整合性をどのようにして保つのかが課題になります。 例えば、メルコイン口座の残高とメルペイのポイントを使ってビットコインを購入する場合を想定してみます。 この場合、決済処理としてざっくり次のような処理を行うことになります。 取引データを作成する メルコイン口座の残高を減らす メルペイのポイントを消費する ビットコイン残高を増やす 取引データを更新する 決済の結果を通知する これは全ての処理が成功するとした場合のシーケンスです。 しかしながら、実際にはネットワークや依存先のサービスの障害などによってエラーが発生することがあるため、それぞれの処理が失敗した場合を想定しなければなりません。 途中で処理が失敗した場合、例えば次のような状態が発生する可能性があります。 決済が失敗したのにメルコイン残高が減っている 決済が失敗したのにメルコイン残高とポイントが消費されている ビットコイン残高が増えない(もしくは増えたかどうかわからない) ビットコインとの交換が行われたが、取引が完了していないことになっている 決済は成功したが結果が通知されず、サブスクライバーの処理が実行されない このように、サービス間のデータ整合性を保つためにどのようなハンドリングをすべきか、どのようにロールバックを実現するかなどについて適切な設計を考える必要があります。 分散トランザクション管理手法: Saga パターン メルコインの Payment Service は、複数のマイクロサービスにまたがる決済トランザクションを処理するために Saga パターンを採用しています。 Saga は複数サービス間のデータの整合性を維持するためのトランザクション管理手法です。 これは、トランザクション処理に数分、数時間、あるいは数日かかるような LLT (Long Lived Transactions) に対する問題解決のために考案されたアプローチです。 操作するリソースごとのサブトランザクションにトランザクションを分解し、それらを独立に処理することでデータを長期間ロックする必要がなくなります。 各サブトランザクションは独立してコミットされるため、単純にロールバックを実行することはできません。 そのため、サブトランザクションによるリソースの変更を取り消すようなトランザクション(補償トランザクション)を実行することによってロールバックを行います。 このようにして、データを長期間ロックすることなく補償トランザクションによってトランザクションの最終的な整合性(結果整合性)を担保します。 前章のユースケースを Saga パターンで実装する場合、次のようにリソースの操作をサブトランザクションとして分割し、それらを取り消す補償トランザクションを設計します。 例えばビットコイン残高を増やすトランザクションが失敗してそれ以上処理を進められなくなった場合、それまでに行ったリソースの操作である「メルコイン残高の減少」と「ポイントの消費」を取り消すトランザクションを順に実行し、最後に取引データの状態を失敗として更新します。 ここでは単純に表現するために直接残高を増減させているかのように書いていますが、実際には TCC パターンのようにリソースの操作ごとに「仮押さえ」と「確定」の二段階の処理に分割しています。 これにより、ロールバック時は仮押さえしたものを解放するだけなので、履歴が汚れるなどの副作用を発生させずに補償処理を実現することができています。 ここで、「では補償トランザクションや確定処理のトランザクションが失敗したときはどうするのか?」と思う方もいるかもしれません。 この問題については、成功するまでリトライし続ける仕組みを用意して最終的に必ず成功させるように実装することで解決できます。 そのためには他のサービスのデータの状態を気にせずにリトライできるように、各サービスは冪等性を持った API を提供する必要があります。 また、Saga パターンには「コレオグラフィ」と「オーケストレーション」の2つのアプローチがありますが、Payment Service ではオーケストレーションのアプローチを採用しています。 オーケストレーションベースの Saga を実現するためには、各サブトランザクションや補償トランザクションを登録して実行するためのインターフェース、そしてそれらを調整するコーディネーターとしてのツールが必要になります。 分散トランザクション管理ツールの選定 オーケストレーションベース Saga を実現するためのツールとして、GCP Workflows があります。 GCP Workflows ではビジネスロジックとフローの定義が分かれており、フローの定義は YAML ファイルに記述します。 一方、Uber が提供する OSS である Cadence は、コードベースのワークフローというコンセプトで、ワークフローを管理するためのイベントソーシングに基づくオーケストレーターを提供します。 ここでのワークフローとはアプリケーションのビジネスロジックの主要な単位であり、状態を持ち長期間実行されるコードの定義を意味します。 提供されている SDK を使用することで、ビジネスロジックを含む関数としてワークフローを記述することができるため、通常のプログラミングと似たような開発体験を実現できます。 しかしながら、Cadence は Spanner をサポートしておらず、OSS の性質上、自社でのデプロイとメンテナンスが必要となります。(Cadence の内部を理解している専門家が必要になる) また、 Temporal は Cadence と同じくコードベースのワークフロー管理を提供します。 Temporal は Cloud Native Computing Foundation に加入しているプロジェクトで、Cadence をベースに開発されました。 このようないくつかの既存のツールを調査した結果、メルコインでは Cadence と Temporal を参考にした独自のワークフローコーディネーターを開発することになりました。 この独自のコーディネーターの詳しい仕組みについてはこの記事では割愛しますが、アプリケーションは提供される SDK を使うことで Cadence と似たインターフェースでワークフローを管理できます。 実装 ワークフローは複数の独立したコミット (アクティビティ) で成り立っており、それらを手続き的にコードで表現します。 Payment Service を例にすると、大まかに次のようなコードでワークフローを記述します。 type PaymentService struct { manager *workflow.Manager } type CreateExchangeRequest struct { IdempotencyKey string // ... } type Exchange struct { ID string Status int64 // ... } func (s *PaymentService) CreateExchange(ctx context.Context, req *CreateExchangeRequest) (ex *Exchange, _ error) { exe, err := s.manager.Workflow(s.createExchangeWorkflow, req).Execute(ctx) if err != nil { return nil, fmt.Errorf("failed to execute workflow: %w", err) } if err := exe.Get(ctx, &ex); err != nil { return nil, fmt.Errorf("failed to get result: %w", err) } return ex, nil } func (s *PaymentService) createExchangeWorkflow(ctx context.Context, params *CreateExchangeRequest) (*Exchange, error) { ex, err := s.createExchangeActivity(ctx, params) if err != nil { return nil, fmt.Errorf("failed to create exchange: %w", err) } saga := workflow.NewSaga(s.manager) if err := s.executeAuthorizeActivities(ctx, saga, ex.ID); err != nil { if !isCompletableError(err) { return nil, fmt.Errorf("returned a non-completable error: %w", err) } if cerr := saga.Execute(ctx, func(e execution.Execution) error { return e.Wait(ctx) }); cerr != nil { return nil, fmt.Errorf("failed to execute compensation activities: %w, orig_err: %v", cerr, err) } if err := s.manager.Activity(s.markExchangeAsFailedActivity, ex.ID).ExecuteGet(ctx, &ex); err != nil { return nil, fmt.Errorf("failed to mark exchange as failed: %w", err) } return nil, fmt.Errorf("failed to authorize exchange: %w", err) } if err := s.manager.Activity(s.markExchangeAsAuthorizedActivity, ex.ID).ExecuteGet(ctx, &ex); err != nil { return nil, fmt.Errorf("failed to mark exchange as authorized: %w", err) } if err := s.manager.ChildWorkflow(s.captureExchangeWorkflow, ex).ExecuteGet(ctx, &ex); err != nil { return nil, fmt.Errorf("failed to execute child workflow: %w", err) } return ex, nil } func (s *PaymentService) captureExchangeWorkflow(ctx context.Context, ex *Exchange) (*Exchange, error) { if err := s.executeCaptureActivities(ctx, ex.ID); err != nil { return nil, fmt.Errorf("failed to capture exchange: %w", err) } if err := s.manager.Activity(s.markExchangeAsCapturedActivity, ex.ID).ExecuteGet(ctx, &ex); err != nil { return nil, fmt.Errorf("failed to mark exchange as captured: %w", err) } return ex, nil } func (s *PaymentService) executeAuthorizeActivities(ctx context.Context, saga *workflow.Saga, id string) error { if err := s.manager.Activity(s.authorizeBalanceExchangeActivity, id, uint64(1000)).ExecuteWait(ctx); err != nil { return fmt.Errorf("failed to authorize balance exchange: %w", err) } saga.AddCompensation(s.cancelBalanceExchangeActivity, id) if err := s.manager.Activity(s.authorizeMerpayPaymentChargeActivity, id, uint64(500)).ExecuteWait(ctx); err != nil { return fmt.Errorf("failed to authorize merpay payment charge: %w", err) } saga.AddCompensation(s.cancelMerpayPaymentChargeActivity, id) return nil } func (s *PaymentService) executeCaptureActivities(ctx context.Context, id string) error { if err := s.manager.Activity(s.captureBalanceExchangeActivity, id).ExecuteWait(ctx); err != nil { return fmt.Errorf("failed to capture balance exchange: %w", err) } if err := s.manager.Activity(s.captureMerpayPaymentChargeActivity, id).ExecuteWait(ctx); err != nil { return fmt.Errorf("failed to capture merpay payment charge: %w", err) } return nil } アクティビティが失敗した場合は、そのエラーが「完了可能エラー」なのかどうかによって二種類のハンドリングに分かれます。 完了可能エラー ワークフローを失敗として完了する。必要に応じて補償トランザクションを実行してワークフローを終了する。 前章で述べたように、補償トランザクションや確定処理のトランザクションはこのエラーを返却することはありません。 例: 予期されたエラー 残高不足エラーや利用制限によるエラーなど その他のエラー Recovery worker によってワークフローがリトライされる。 例: 一時的なエラー 通信の遅延によるタイムアウトなど 予期しないエラー サービスのバグなどによる Internal エラー Recovery worker によって、ワークフローは成功するか、あるいは完了可能エラーが発生するまで一定間隔で無限にリトライされ続けます。 そのため、アプリケーションはそれぞれのアクティビティを冪等にし、ワークフローで実行されるアクティビティの実行順序が決定的になるように実装します。 アクティビティが完了可能エラーによって終了した場合は、補償トランザクションでそれまでに成功したアクティビティによるリソース変更を取り消すためのアクティビティを実行します。 このように、ワークフローによって成功すべきリクエストは最終的には必ず成功し、マイクロサービスをまたいだトランザクションにおいてもデータ整合性を実現することができます。 おわりに この記事では、マイクロサービスアーキテクチャにおける分散トランザクションをワークフローコーディネーターを用いたオーケストレーションベースの Saga によって管理する手法を簡単にご紹介しました。 今回紹介した SDK は、メルコインだけでなくメルペイのいくつかのサービスにも導入される予定で、開発を支援するためにこの SDK に特化した静的解析ツールも開発して運用しています。 この他にも、メルペイ・メルコインでは決済データの不整合によるリスクを回避するために自動的にリコンサイルを行う仕組みを導入しています。 興味のある方はこちらの記事もご覧ください。 マイクロサービスにおけるリコンサイルの話 | メルカリエンジニアリング
アバター
この記事は Merpay Tech Openness Month 2023 の 6 日目の記事です。 はじめに こんにちは。メルペイの Payment Core チームでバックエンドエンジニアをしている komatsu です。 普段はメルカリ・メルペイが提供するさまざまな決済機能のために、決済基盤の開発・運用をしています。 この記事では、我々が開発している決済基盤マイクロサービスである Payment Service における、Source Payment と呼ばれる複数の決済手段を抽象化した概念について紹介します。 決済手段の多様性 メルカリやメルペイはさまざまな決済手段をお客さまに提供しています。 例えば、 メルカリの売上残高 銀行口座からチャージした残高 付与されたポイント メルペイのスマート払い などがあります。 Payment Service はこれらの決済手段を実現するための決済ハブとして、複数のマイクロサービスに決済関連の API を提供しています。 上に示した決済手段は、購入と決済のタイミングが同期的である、つまり、購入のタイミングで利用額が残高や与信枠に反映されるため、1 つのリクエストで決済処理を完了することができます。 一方で、例えばメルカリでは、 キャリア決済 クレジットカード (3DS; 3-Domain Secure 2.0) コンビニ払い ファミペイ といった決済手段も提供しています。 コンビニ払いでは、お客さまがアプリ上で決済方法を選んでから実際にコンビニに行って支払いを行います。 キャリア決済やクレジットカード (3DS)、ファミペイでは、決済のタイミングでメルカリ外のページに遷移して認証情報などを入力し、支払いを行います。 これらの決済手段のフローは、複数のリダイレクトが複数のマイクロサービスや社外システムを横断するため、複雑になりやすいです。 このような購入と決済のタイミングが異なる支払い方法を実現するために、Payment Service では Source Payment という、複雑な決済手段を抽象化した概念を利用しています。 Charge: Payment Service における決済 前提として、Payment Service では 1 つの決済を Charge として表現します。 1 つの Charge は複数の Payment Method (残高決済やスマート払いなど) を持つことができ、さまざまな決済手段を組み合わせることができます。 一般に、決済の流れは authorize (仮売上) と capture (実売上) の 2 つのステップに分かれています。 authorize では消費する金額が利用可能かどうかを確認し、他の決済によって重複して利用されることがないように承認・記録します。 capture では authorize された金額を実際の売上として処理し、支払いを確定します。 例えば残高とスマート払いを利用して決済をする場合、次のような処理の流れになります。 ここで、Client はお客さまの操作に基づいて Payment Service やその他のマイクロサービスにリクエストを送る BFF のようなサービスです。 また、Balance Service は残高を管理するマイクロサービス、Deferred Service はスマート払いの機能を提供するマイクロサービスです。 この図のように、Payment Service は Client が指定した支払い方法ごとに、社内のマイクロサービスや社外のサービスを利用して決済を構築します。 すべての処理が同期的であり、決済の仮売上 (authorize) と実売上 (capture) がそれぞれ Client からの 1 つのリクエストで完了していることがわかります。 Source Payment とは Source Payment とは、非同期的な決済フローを抽象化した概念で、Source はその決済手段を表します。 言い換えれば、Charge にとって残高やスマート払い、Source はすべて Payment Method であり、Source がキャリア決済なのかコンビニ決済なのかは Charge にとって関心事ではありません。 ちなみに、Source については Stripe なども同様の仕組みを提供しています [1]。 Source が Payment Method に指定された場合、その Source の支払いが完了した場合に Charge は Paid (支払い完了) に状態を遷移することができ、クライアントは決済 (Charge) を次のステップに進めることができます。 キャリア決済であればキャリア画面において支払いが完了した状態、コンビニ決済であればコンビニで支払いを完了した状態が Charge を Paid へ遷移できる状態に該当します。 より詳細なフローを見てみましょう。 1 つ目はキャリア決済、2 つ目はコンビニ決済を表しています。 実際にはより多くのマイクロサービスや社外のサービスを跨いでいるのでこれらは簡略した図ですが、 Payment Service が行う処理が似ていることがわかるかと思います。 類似したいくつもの決済手段を Source として共通化して抽象化することで、Charge から見て各決済手段の詳細な処理フローを意識せずに実装することができます。 その結果、複雑な決済手段を汎用的に扱うことが可能となります。 Source Payment のメリット Source という決済手段を用意することで、リダイレクトや社内外含めて関連するサービスが多く、さまざまな状態を持つ複雑な決済手段の差分をそれぞれの Source Payment の決済処理 API で吸収し、メインの決済処理 (Charge) では「Payment Method が Source である」という汎用的な決済手段として扱うことで、拡張性の高い設計ができました。 Source によって実装をシンプルに保つことだけでなく、今後新しい決済手段に対応する際の開発工数も減らすことができるでしょう。 また、メルカリは複数の決済手段を組み合わせた複合決済にも対応しているため、残高やポイントと組み合わせて決済するようなケースにおいても、実装の一貫性を保つことができるようになりました。 このようなメリットから、Source Payment は決済基盤の開発や拡張をする上で有益です。 Source Payment のデメリット 一方で、Source Payment は 1 つの決済を行うために複数の API を利用する必要があるため、処理の流れを開発者が把握することが困難になりやすいです。 このような複雑なドメイン知識は適切にドキュメントを整備しないと知見が属人的になってしまうので、いかにチーム内の共通知識として共有できるかということが 1 つの課題です。 また、現状の実装では同じ Source Payment であるキャリア決済とコンビニ決済を組み合わせることはできないようになっています。 これは 1 つの Charge は各 Payment Method を多くとも 1 つずつ組み合わせることができる、という仕様になっているからです。 仮に将来的に複数の Source Payment を組み合わせた決済手段を提供する場合、内部の実装を見直す必要があるでしょう。 Source Payment の運用術 Source は capture された後、PayCharge によって親となる Charge に紐付けられます。 しかし、ネットワークやクライアントの状態、またはお客さまの操作によっては、Charge に紐付けられる前に意図せず処理が途中で止まることがあります。 例えばクレジットカード (3DS) 決済でパスワード認証を終え、利用枠の authorize が終わった後にネットワークの問題で処理が止まった場合、お客さまのクレジットカードの与信枠をずっと保持してしまうことになります。 実際の支払いが行われているわけではないですが、authorize がクレジットカード会社などによって自動的にキャンセルされるまではお客さまの利用枠は回復しません。 Payment Service は処理の途中でエラーを受け取った場合は自動で rollback を行い、途中で止まっている処理を初期状態、つまり authorize される前の状態に回復する仕組みになっています (このあたりの話は マイクロサービスにおける決済トランザクション管理 で詳しく触れられています)。 しかし、予期せぬエラーを受けて rollback すらできない場合や、rollback 中にもエラーを受け取る場合などもあります。 このような状況にも対応するために、Payment Service ではそのような状態の Source を自動でキャンセルするバッチ処理を運用しています。 これによって例えばクレジットカード会社による自動キャンセルは通常 2 ヶ月程度かかるのに対し、バッチ処理を用いることで最大でも 2 時間程度でキャンセルが可能となります。 このように、バッチ処理を運用することで、予期せぬ問題が発生した場合でもお客さまのメルカリ内外における決済体験を損ねないような仕組みを提供しています。 おわりに この記事では、メルペイの決済基盤において、Source Payment という概念を用いて複雑な決済手段を統一的に扱うための方法を紹介しました。 増え続ける決済手段に柔軟に対応・開発するために Source は有用であり、我々の生産性向上に寄与してくれています。 今後も新しい決済手段への対応や既存の決済基盤の最適化に取り組むことで、お客さまにとってより便利で多様な決済体験を実現したいと思います。 脚注 [1] Stripe の Source は現在は deprecated となっています (ref. https://stripe.com/docs/sources )。
アバター
こんにちは、メルカリのレコメンドチームで ML Engineer をしている ML_Bear です。 以前の記事 [1] では、item2vecと商品メタデータを用いた、メルカリのホーム画面のレコメンド改善のお話をさせていただきました。今回は商品詳細画面でレコメンド改善を行ったお話をさせていただきます。商品詳細画面の例は図1の通りです。ユーザーはアイテムの詳細な説明を見たいときにこの画面に来訪するため、同様の商品を推薦する自然な接点として非常に重要です。 まず、私たちが商品詳細画面で行った改善の概要を示します。各部の詳細については次節以降で詳しく触れます。 日本有数の大規模ECサービスにおいてベクトル検索ベースの商品推薦アルゴリズムを実装し、推薦精度の大幅な改善を実現しました。 協調フィルタリングとニューラルネットワーク (以下、NN) を利用した商品推薦アルゴリズムを構築し、コールドスタート問題を回避しつつ、ユーザーの閲覧履歴を活用することに成功しました。 協調フィルタリングの学習の際にはPython implicitライブラリを活用し、GPUを利用して膨大な行動ログの計算を高速化しました。 NNのモデリングではKaggleコンペティションのsolutionなども一部参考にしつつ、極めて軽量なモデルを作成しました。 モデリングではアクセスログを活用したオフライン評価を行うことで、改善が常に正しい方向へ向かうように工夫しました。 ベクトル検索エンジンにはVertexAI Matching Engineを採用して、少ない工数でベクトル検索を実現しました。 VertexAI Matching Engineは本番運用の高負荷にも十分耐えうるものであり、テスト実行後、迅速に本番適用へ移行することが可能でした。 実際のABテストでモデリング時に見逃していた重要な特徴量も発見することができました。初回のテストの失敗後、それを迅速に修正し、実ビジネスに貢献する強力な推薦モデルの構築に成功しました。 図1. 今回のお話の対象「この商品を見ている人におすすめ」 メルカリにおけるベクトル検索エンジンの活用 昨年、 wakanapo が書いた 記事 [2] でも紹介した通り、メルカリグループではベクトル検索エンジンを活用したレコメンド精度改善にトライしています。以前の記事はメルカリShopsの商品に限定した改善の試みのお話でしたが、今回の私の記事では、メルカリに出品されている全ての商品を対象とした改善の試みをご紹介します。 ベクトル検索エンジンは wakanapo の記事と同様に、Vertex AI Matching Engine [3] (以下、Matching Engine と表記) を採用しました。既に社内での導入事例があるためコードベースや運用ノウハウが流用できること、また、後述の通り高いアクセス負荷にも耐えられることから採用しました。 ベクトル検索エンジンを利用した商品推薦 今回構築した商品レコメンドシステムは以下のような流れで商品を推薦します。(詳細については以降のパートで詳しく説明します) (括弧内の数字はシステムアーキテクチャ概略図に対応) Indexing “何らかの方法” で商品のベクトルを計算する (i, ii, iii) 商品のベクトルを以下2つのGCPサービスに格納する (iv) Bigtable [4]: 全ての商品のベクトルを保存する Matching Engine: 販売中の商品のベクトルを保存する Recommendation ユーザーが商品を閲覧した際 (1) に、以下の流れで推薦を行う。 Bigtable から閲覧中の商品のベクトルを取得する (2, 3) Matching Engine を用いて、そのベクトルと似たベクトルを持つ、販売中の商品を近似近傍探索する。(4, 5) Matching Engine の検索結果を「この商品を見ている人におすすめ」に表示する (6) 図2. システムアーキテクチャ概略図 ちなみに、当初、Matching Engine はベクトルをクエリとして受け付けて、それに対して類似商品のIDを返す、という動作しかできなかったため、Bigtableを必要とする構成になっています。現在は Matching Engine のアップデートにより、(ベクトルの代わりに) 商品IDを投げると類似商品を返してくれるようになったため、Bigtableを不要にすることも可能です。 また、Matching Engineのインデックス作成にはStreaming Update [5]というものを採用しました。詳細は省略しますが、この方式でインデックスを作成しておくと、新たに出品された商品のインデックスへの追加や、売り切れてしまった商品のインデックスからの削除を瞬時にインデックスへ反映することができます。ものすごい勢いで商品の在庫が入れ替わっていくメルカリでは非常に便利な機能でした。 初回のABテストはおもちゃカテゴリを対象 メルカリは販売中の商品だけで数億点、過去全てを累計すると30億点以上[6]の商品が出品されています。「この商品を見ている人におすすめ」 のレコメンドパーツは売り切れた商品にも表示する必要があるため、売り切れた商品にもベクトルの計算が必要です。 仮説の迅速な検証のため、初回のABテストでは一部の商品のみを対象としました。具体的には、まずおもちゃカテゴリを初回の対象カテゴリとして選定し、そのテストが成功した後に、より多くのカテゴリに展開することとしました。参考までに、おもちゃカテゴリを選定した理由は以下の通りです。 商品の流行り廃りがあまりにも早いため、現在のレコメンドのロジックがうまく機能していない。具体的には新商品や新キャラクターに全く対応が追いついておらず、新しく登場した人気キャラクターの商品に対して、全く関係ない商品が表示されたりする。 トレーディングカードをはじめとして売上の大きいカテゴリが複数存在しており、推薦の改善によって、売り上げへの貢献が期待できる。 協調フィルタリングの活用 メルカリShopsの改善では word2vec [7] を活用していたため、今回、私がモデリングを行った際にもword2vecを利用してベースラインモデルを構築しました。しかし非常に多くの商品を扱う際には、word2vecではオフライン評価のメトリクス (MRR: Mean Reciprocal Rank) が伸び悩み、また、目視での推薦結果もあまり満足のいくものではありませんでした。具体的には、商品数が非常に多くなった場合は商品の細かな違いを区別できていないような挙動でした。 一般に配布されている学習済みword2vec以外にも、自社のデータセットで word2vec を学習してみたりもしましたが、思ったほど精度は伸びなかったため、試行錯誤の結果、古典的な協調フィルタリングを利用することにしました。 具体的には、Python の implicit ライブラリ [8] を利用し、ユーザーの閲覧ログから商品の factor を計算しました。 implicit ライブラリはGPUを使って計算を高速化できるため、数億行のデータを突っ込んでも現実的な時間で計算を完了してくれます。また、差分更新にも対応しており、商品の閲覧履歴が溜まるとより精緻なベクトルに更新していくことが可能です。 莫大なユーザーログデータと商品データ数を有するメルカリにとって、このライブラリの存在は非常にありがたいのですが、以下2点の課題がありました。 implicit ライブラリのログの取り回しが非常に煩雑 ライブラリの制約から0始まりのidでデータを扱う必要があり、implicit id と商品idの変換テーブルが必要 (データパイプラインが複雑になって辛い) コールドスタート問題 フリマアプリというサービスの特性上、新しいものに閲覧が集中する。新しい商品では「この商品を見ている人におすすめ」があまり機能しない、というのはユーザー体験の毀損につながってしまう。 (ただこれは協調フィルタリングという手法そのものの問題なのでimplicit単体の問題ではない) よって、ABテストを行う直前に、以下のようなモデルに変更を行ってテストしました。 十分な商品閲覧数をもつ商品に対して協調フィルタリングでベクトル (factor) を計算する タイトル、商品説明文などの商品情報を利用してそのベクトルを再現するNNモデルを学習する おもちゃカテゴリの全ての商品に対してNNモデルでベクトルを計算し、それを商品のベクトルとして利用する。 NNモデルの実装詳細まで書くと長くなってしまうので詳細は省略しますが、以下のような構成のシンプルなモデルを組みました。(数千万商品を処理する必要があるため、初回のテストではBERTなどの重いモデルは利用しませんでした) 図3. NNモデルアーキテクチャ (一部簡略化) テキスト処理において商品タイトルにカテゴリー情報の文字列を足すと言った点は、Kaggleのメルカリコンペ[12] の解法を参照しました。 紆余曲折あって協調フィルタリングのfactorをNNで近似するという結構無理やりな問題設定になってしまったので、別の機会にtwo-tower モデルなどのより効果的と思われるモデルのテストを実施したいと思っています。 なるべく新しい商品を推薦する 実は今回のABテストは一度失敗しました。幸いにも、データ分析の結果すぐに敗因が特定できたので2回目のABテストを実施し、それが成功したのでことなきを得ました。 失敗した原因は「出品から長い時間売れ残って放置されている商品をたくさん推薦してしまっていた」ということでした。 前述の通り、今回の商品ベクトルは主に商品情報 (タイトル・説明文など) を利用してベクトルを生成していましたが、商品がいつごろ出品されたものかということ(新鮮さ)の考慮はしていませんでした。後からわかったことですが、オフライン評価を行う際には、特定の時期のデータのみを利用していたため、新鮮さを考慮しないことがモデリングの問題にならなかったようです。そのため、初回のABテストを行うまで、商品の新鮮さを考慮する必要性に気づけませんでした。 商品の新鮮さを考慮するように推薦ロジックを修正した結果、推薦された商品の購買率が一気に向上し、記事末尾で述べる圧倒的な数値改善に繋げることができました。 その他の苦労した点 これは私たちがMatching Engineを大規模に利用した初めての事例でした。本番環境への適用の際にいくつか問題があったので、以下に箇条書きで列挙しておきます。 Google Cloudのサポートチームがチケットで質問に気軽に対応してくれましたが、Matching Engineのドキュメントにはまだまだ不足している点が多かったです。(SDKの利用方法、Public Endpointの構成方法など。) Tokyo Region の GPU リソースが不足しているためか、GKEのノード自動プロビジョニング(NAP) [13] で全然GPUを掴めないタイミングが稀によくあった。結局、NAPを諦めてインスタンスを1個立ててGPUを常に確保した。(画像生成AIの隆盛の影響だったりするのでしょうか…。) 改善結果 – 商品推薦タップ率が3倍に さて、ここまで書いてきたモデリングの結果、以下のような推薦を行えるようになりました。ユーザーが新しいキャラクターに関連する商品を閲覧している場合、関連する商品をうまく推薦できていなかったのですが、今回の手法を採用することで、その弱点を克服することができました。 図4. うまく推薦を行えるようになりました ([]内の数字は推薦順位) 閲覧中の商品: ちいかわ ワクワクゆうえんち ポーチ 改善前の推薦商品リスト (ちいかわと全然関係ないものも多い) [1] ハイキュー アートコースター まとめ売り [2] 呪術廻戦0 TOHOくじ H賞 ステッカー ジッパー... [3] ちいかわ セリフ付きマスコット ハチワレ プライズ品 [4] 美少女戦士セーラームーンR S カードダス アマダ [5] プロメア ガロ&リオ SGTver. Special Box PROMARE [6] 宇宙戦艦ヤマト 2205 新たなる旅立ち キーホルダー まとめて [7] ドラえもんストラップ付 財布 パスケース [8] 【新品・非売品】日本食研 バンコ ぬいぐるみ [9] ポケットモンスター メイ EP-0137バスタオル サイズ... [10]ちいかわワクワクゆうえんち 限定 タオルセット 改善後の推薦商品リスト (“ちいかわ ワクワクゆうえんち”を認識している) [1] ちいかわ ワクワクゆうえんち 2個セット ポーチ ジェットコ... [2] ちいかわ ワクワクゆうえんち ポーチ [3] ちいかわワクワクゆうえんち 限定 タオルセット [4] 匿名配送 ちいかわワクワクゆうえんち ガチャアクスタ3種 [5] ちいかわ ワクワクゆうえんち マグカップ [6] 匿名配送 新品未開封 ちいかわ ワクワクゆうえんちマコット... [7] ちいかわ ワクワク ゆうえんち 中皿 [8] ちいかわ ワクワクゆうえんち ミニフレームアート ハチワレ [9] ちいかわ ワクワクゆうえんち マスコット セット売り [10] ちいかわ ワクワクゆうえんち 2個セットポーチ (この記事においては著作権に配慮し、推薦される商品の商品名の羅列のみとしております、実際にお手元のアプリで確認してみてください。) ABテストを行った結果、以下のような驚くべき結果を叩き出すことができました。 「この商品を見ている人におすすめ」の商品タップ率が3倍 「この商品を見ている人におすすめ」からの購入が20%増加 上記の結果、メルカリアプリ全体の売り上げが大幅に向上 経営に関するメトリクスが向上したことは当然嬉しいのですが、何よりも、ちゃんとユーザーが閲覧している商品に対して、関係性の深い商品をきちんと提案できるようになったこと、また、それをチームとして誇りに思えたことが何よりも嬉しかったです。 まだまだ改善の余地あり ご紹介したい内容が多く、各部の詳細は非常に簡潔な説明となってしまいましたが、参考になりましたでしょうか? 今回はメルカリ全体でのベクトル検索商品推薦の初回テストということもあり、モデルの設計自体は非常にシンプルなものでした。まだ画像を考慮に入れていなかったり、Matching Engineの高度な機能 (多様性を出すための Crowding Option [14] という機能など) もまだ使っていません。 また、おもちゃカテゴリ以外では今回の施策の適用もしておらず、まだまだ改善の余地があります。今後も改善を繰り返して、より良いサービスへと進化させていきたいと思っています。 ご意見ご感想などあれば Twitter などで聞かせてください。 それではまたお会いしましょう。 References [1] Item2vecを用いた商品レコメンド精度改善の試み | メルカリエンジニアリング [2] Vertex AI Matching Engineをつかった類似商品検索APIの開発 | メルカリエンジニアリング [3] Vertex AI Matching Engine overview | Google Cloud [4] Cloud Bigtable: HBase 対応の NoSQL データベース [5] Update and rebuild an active index | Vertex AI | Google Cloud [6] フリマアプリ「メルカリ」累計出品数が30億品を突破 [7] [1301.3781] Efficient Estimation of Word Representations in Vector Space [8] GitHub – benfred/implicit: Fast Python Collaborative Filtering for Implicit Feedback Datasets [9] [1408.5882] Convolutional Neural Networks for Sentence Classification [10] [1805.09843] Baseline Needs More Love: On Simple Word-Embedding-Based Models and Associated Pooling Mechanisms [11] MeCab: Yet Another Part-of-Speech and Morphological Analyzer [12] Mercari Golf: 0.3875 CV in 75 LOC, 1900 s | Kaggle [13] Use node auto-provisioning | Google Kubernetes Engine(GKE) [14] Update and rebuild an active index | Vertex AI | Google Cloud
アバター
この記事は、 Merpay Tech Openness Month 2023 の5日目の記事です。 はじめに メルカリ技術書典部の knsh14 です。 6月4日まで行われていた技術書典14に参加しました。 技術書典14は5/21にオフライン開催を行いました。 株式会社メルカリは、Goldスポンサーとしてイベントを支援させていただいており、スポンサーブースを割り当てていただきました。そこで、このブースではメルカリ技術書典部から「Unleash Mercari Tech!」という書籍をみんなで執筆し販売しました。 自分は編集長として本の制作に携わったので、当日までどのような作業があったか紹介します。 本を作る Slack を振り返ると、自分が編集長に立候補したのは 3/13 でした。 オフラインイベント当日から逆算すると、2ヶ月ほど活動時間がありました。立候補しプロジェクトを始めるタイミングとしては早めで良かったと思います。 なぜ自分が立候補したのか詳細は覚えていませんが、おそらく雑談している中でそそのかされたんだと思います。 せっかくスポンサーブースに出展できるのなら、物理本(印刷した紙の書籍)を作るぞ!と決めました。 前準備 本の制作に着手する前に事前準備をします。 この時点で必要なものは次の3つです。 本の制作に関わる人がやり取りするためのチャットチャンネル 書籍を作成するリポジトリ 全体のスケジュールをカレンダーに登録する 立候補してからすぐにこの3つの事前準備を済ませると自分の退路を断てるので有効です。 今回の自分は3番目の全体スケジュールをカレンダーに登録する作業をやらなかったので、スケジュール管理が疎かになってしまいました。 チャットチャンネル 本の制作をするためには、編集長とコンテンツを制作する方々とのやりとりをする場所が必要です。 会社の Slack や、Discord など都合が良いツールを使いましょう。 後述のリポジトリで制作状況を管理したり、レビューしたりするので、GitHub と連携できるサービスが好ましいです。 メルカリの標準のコミュニケーションツールはSlackなので、今回もSlackを使っています。 リポジトリ 書籍のデータを管理するためのリポジトリが必要です。 リポジトリ、適切なディレクトリ構成、成果物を確認するためのスクリプトなどを 0 から準備するのは大変なので、TechBooster が公開しているテンプレートリポジトリ TechBooster/ReVIEW-Template を使って作成します。 Slackなどのチャットツールを使っている場合、アプリで連携してコミットが push されたり、 PR が作られたり、レビューがつけられたりするとチャンネルにポストされるようにしておくと、進捗が見えやすくなって良いです。 執筆者を集める 本を制作するための箱が出来上がったので、技術書典部の一員として記事を書く人を集めます。 この仕事が編集長としての最初の大仕事です。 最初に考えたのはどんな人に書いてもらうかです。 今回は会社のプロダクトとして最近の大きなリリースであるメルコインに関連した話はエンジニアの読者なら興味があるかなと思いました。また、メルペイからは昨年メルカードをリリースしています。この2つのプロジェクトからできるだけ書いてもらえる人を集められるように働きかけました。 呼びかける方法にスマートな方法はありません。プロジェクトの開発チャンネルに突撃し、書いてください!とお願いするのみです。 テックリード(TL)やエンジニアリングマネージャ(EM)に相談し、面白そうなネタを持ってる方を推薦してもらったり、社内勉強会の Go Friday などによく顔を出してる方に問い合わせたり、過去自分と一緒のプロジェクトにいた方に聞いてみたりと、とにかく「久しぶりに書いてみるかあ」と思ってくれる人を探します。 また、ちょうどよく大勢のエンジニアの前でライトニングトーク(LT)をする機会があったので、技術書典部に参加する方を募集しています!というLTをしてきました。 LT では次のことを説明し、参加へのハードルを下げられるようにしました。 今まで書いたことがない人にこそ勧めたい。みんなで書きあえば一人の記事は短くても全体でちょっと分厚い本にできる ネタがすぐに思いつかなくてもOK。編集長がめちゃくちゃ協力します! 書く文章量は大体技術ブログ1記事分程度あれば十分。過去の参加者を見てもそれくらいでちょうどよい 今回は十分人も集まったので良かったのですが、全社の開発チャンネルでもっと宣伝したり、各チームの EM に手伝ってもらったりして人を集める必要があったかもしれません。 ここまでで書いてもらう人数の想定は特にしていません。 物理本のページ数、1記事の長さなどから大体の必要な人数は割り出せます。しかし集まった人数ピッタリで打ち切ってしまうと、業務都合で断念する方が出たり、 思ったより記事が短かったりして予定した分量を下回ってしまう懸念がありました。足りなくなるよりはたくさん書いてもらって溢れたら分冊すればいいか…と思ったので人数上限は設けずにできるだけ人を集めようと思いました。 ありがたいことにメルペイ、メルコインのメンバーは技術発信に興味がある方、お願いします!と頼んだら書いてくれる方が多かったので、ギリギリのラインを攻めずにすみました。 人集めはできるなら編集長になった日から着手したほうが良いです。 早く書き始めて貰えれば、それだけ毎日少ない苦労で書き上げることができるので、書いてもらえる方に時間を残せるようにしましょう。 コンテンツを作ってもらう リポジトリもでき、執筆者も集まったのでここからは本格的に制作していきます。 内容を決める 参加してくれる方がどんな記事を書くかを決めていきます。 自分でどういう記事を書くか既に決まっている人はそれが機密情報が含まれていたり、公序良俗に反していなければ特に問題にはなりません。ダメそうならコミュニケーションして別のネタにしてもらう必要があります。 一方、決まらない人がいる場合は編集長の出番です。30分程度1on1で雑談しながら次のことを聞いてみて面白そうなネタを探します。 最近やった仕事 プロジェクトの概要 解決したい問題 難しかったところ 最近気になっている技術 エディタ 気になるライブラリ 便利ツール 1on1 で話してみると、意外と2〜3個くらいのネタがでてくるので、それで書こうと決めました。 もしそれでも見つからなかったらマネージャーに聞いてみて、頑張ってたことなどを聞いてそれを書いてもらうという作戦も考えていました。 締切を決める 編集長の大事な仕事に、締切を決めて守っていくことがあります。 締切の目安は物理本か電子本かでも異なりますし、オンライン参加かオフライン参加かも影響しますが、今回は物理本の締切を考えます。 21日前 – 執筆者候補が全員参加表明をしている 14日前 – 執筆者全員の記事の目次くらいが完成している 10日前 – 技術的なレビューが終わる 8日前 – 執筆者の修正が終わる 7日前 – 編集長が全体をチェックする 6日前 – 最終稿が完成し、印刷所に送る イベント当日 – イベント会場に届いた本を検品し、販売する これくらいのスケジュールで制作を進めていきました。 6日前の印刷所に送る日付だけは絶対に死守しなければいけません。遅れると、特急料金を支払う必要があるので、負担をかけないように頑張る必要があります。 また、その前日には編集長が全体をチェックし修正する時間が必要です。ここで細かく変更が入ってしまうとチェックをやり直しになるので、編集長以外が編集できないようにする必要があります。 今回はGWがあり、執筆者にはそこで追い上げるチャンスがあったので、このようなスケジュールになりましたが、大型連休がない場合はもっと前に執筆表明をしてもらい、時間を確保してもらう必要があります。 この2つの締め切りを守るために本当のやばい締切、目安の締切を執筆者に提示し、うまくコントロールしていける編集長はかなりハイレベルな編集長です。 自分は今回この締切が自分でも曖昧なまま進んでしまい、 vvakame さん、 mhidaka さんに突っつかれながら進めてしまったので、執筆者にも急に締め切りを設定して色々直してもらい迷惑をかけてしまいました。 次回は前準備の段階ですべての予定をカレンダーに入れてきちんと今何をすべきか把握すること、週に一回など定期的に進捗を把握することが必要だなと思います。普段の仕事とまったく同じですね。 リマインドする 前述のスケジュールを守るために、執筆者に定期的にリマインドを送る必要があります。 次のようなリマインドを順に執筆者、あるいは執筆者候補に送ります。 参加表明してくれましたか? ネタは決まりましたか? 記事の Pull Request は作ってくれましたか? 記事はいったんレビューできる状態になりましたか? いったん直さないとやばいものは直しましたか? これらを目安の締切、絶対に守る締切を意識しつつ、心理的安全性を損なわないように送っていきます。ここで如何にいいタイミングで送れるかが編集長の腕の見せ所ですが、自分は結構下手くそでした。プロジェクト管理の手法を勉強すると上達できる気がします。 レビューする 執筆者の方たちが頑張って書いてくれた記事をレビューします。 自分は記事の最初の読者として、読む中で文章が分かりづらいところ、前後関係がわかりにくいところなどを探してレビューしていきました。 技術的な面は自分も使っている技術については多少コメントできたのですが、暗号資産に関連する記事では自分はまったくわからないので、詳しい方にお願いしてレビューしてもらいました。 他人の書いた文章をレビューするのは難しいですが、次の動画や記事から良い文章はどんな文章なのか、読みやすく書くためにはどう書くべきかをある程度把握してから、自分がこのテーマで書くならどうすればよいか考えながらやってみると良さそうです。 merpay Tech Talk \~伝わる技術文書の書き方\~ LINE Technical Writing Meetup 技術的な文章を書くための第0歩 ~読者に伝わる書き方~ 技術的な文章を書くための1歩、2歩、3歩 自分がレビューした後、 vvakame さんも忙しい中レビューしてくれました。 自分だけだと心もとなかったのですが、 vvakame さんは自分より文章を書いたりレビューする経験が豊富なので、よりよい視点からレビューしてくれて自分も勉強になりました。 もし時間があるなら、執筆者どうしで互いの記事をレビューすると、より多くの視点からのレビューができ、全体のクオリティを上げることができそうです。 表紙を描いてもらう 本には表紙が必要です。表紙は文字通り本の顔なので、本職の方にお願いしました。 今回は弊社のデザイナーの tottie さんにお願いしました。 tottie さんは同人誌ノウハウをよくご存じなので、いろんな部分を先回りしてやってくださったのですが、依頼するときには次のことをわかった上で依頼するとスムーズに進みます。 納期 表紙のイメージ 本のタイトル、著者名 表紙 表紙だけなのか、裏表紙も欲しいのか サイズ A4 なのか B5 なのか 形式 印刷所用の形式と、電子版の形式 印刷用ならpsd ファイル 電子版なら、png 形式 どこの印刷所に依頼するか既に決まっているなら共有するとデザイナーさんが気になることを確認できる トンボいる?いらない? 印刷の場合、印刷用の形式(トンボなど)を作成する必要がある また、物理本にする場合は、背表紙の調整を考える必要があります。 (本文のページ数+表紙のページ数)× 0.063mm の厚みを本の背として確保します。0.063mm は今回使用した紙の厚さです。別の種類の紙を使う場合は再度確認し、修正する必要があります。 これを表紙と裏表紙の間にこの厚みを追加してもらうことで、きれいな背表紙になります。 最終的にこのような素敵な表紙ができあがりました! 本にする 本を作るために必要な要素は揃ったので、全体をまとめ上げて印刷所に入稿するためのデータを作ります。 これに着手するのは印刷所に送る直前1日程度です。あまり前すぎると記事を修正したいこともあるので、執筆者に厳しくなりますが、遅すぎても自分の作業時間が取れないので、短すぎないようにします。 編集長の仕事の中で一番大変な仕事です。 ここでの作業の成果物は印刷所に送るためのデータを作成することです。 今回僕らは日光企画さんに印刷してもらったので、必要なデータは表紙用の psd ファイルと、中身の pdf ファイルの2つです。 表紙の psd ファイルは「表紙を描いてもらう」でも書いたとおり、背表紙の幅を調整したものを送ります。 中身については出来上がったものを読んで、おかしい部分を見て修正する作業を繰り返します。 テンプレートリポジトリ https://github.com/TechBooster/ReVIEW-Template から作業リポジトリを作成した場合、pdf はリポジトリの GitHub Actions により生成されてダウンロードすることができます。そこからダウンロードして確認します。 確認する項目は次のとおりです。 書いてもらった記事が全部載っているか 誤字脱字がないか 表示崩れがないか 数式などが崩れていないか コードブロックが崩れていないか URLが長すぎて表示が崩れていないか 確認作業をするときに過去に参加したときの本とリポジトリのセットがあると修正作業がとても楽になります。 このタイミングで、記事の順番も編集長が決めます。 今回は記事の Pull Request を作った順番で並べたのですが、見本誌を手にとって眺めてもらったときに興味を持ってもらえそうな内容を先に持ってくると良かったかもしれません。 また目次の章に記事を書いた人の名前も入れると見本誌でパラパラっと目次を見たときに誰が書いたのか分かりやすくてより興味を持ちやすかったかもしれません。 本を売る 本が出来上がったので、オフラインイベント当日に本を売りまくります。 それに向けてもいくつか準備をします。 売値を決める 当日販売する本の値段を決めます。1,000円刻みで設定しておくと、当日現金で支払われる方にも対応しやすくなります。 事前にお釣り用の現金を準備しておく必要があります。 当日のための準備をする メルカリ技術書典部は株式会社メルカリのスポンサーブースで本を販売しました。 あくまでスポンサーブースなので、会社の広報チームと連携し、本を売るだけでなくノベルティを配るなど採用広報活動にも参加する必要があります。 広報チームと事前に相談することで、会社の備品を使わせてもらうことができ、一気に売れそうなブースになりました。 厳密には編集長の仕事ではないですが、積極的に関わって作った本を1冊でも届くようにしましょう。 当日株式会社メルカリではスポンサー担当、編集長、Talent Acquisition team のメンバー、そして執筆者のうち当日参加できた方で参加しました。 販促グッズの準備 当日本を売るための販促グッズを用意します。 オフラインではとにかく会場にいる方たちの目にとまるように販促グッズを用意しました。 mhidaka さんのアドバイスで、A3 サイズの表紙、販売中をアピールできるグッズを準備しました。 A3 サイズの表紙はたまたまオフィスにラミネート加工できる機械があったので、補強して持ち運びやすくしました。 他にもアピールグッズとして推し活に使われる蛍光色で大きめのうちわを買ってきて会社のシールなどを貼ると遠くからでも目立って良かったです。 販促については同人誌を売るテクニックとして色んなところで紹介されていると思うので、1冊でも多く手に取ってもらうために、事前に調べて試してみる価値はありそうです。 当日 オフラインイベント当日はサークル参加の集合時間に遅れないように会場にいきます。 依頼した物理本の印刷がされて、会場の自分たちのブースに納品されているはずなので、発注した冊数があってるか、いくつかピックアップして乱丁や落丁がないか調べます。 2〜3冊は見本誌として、立ち読みしてもらうために目印を付けておきましょう。 本がちゃんとそろっていて、乱丁や落丁もなければ後は皆さんに届けるのみです。 恥ずかしがらずに勇気を出して、来場者に声をかけて見本誌を読んでもらいながらこの本がいかにすばらしいかを売り込みましょう。 後処理 技術書典14最終日の6/4でのクロージングイベントにも出演しました。 このイベントでは最後に少しでも興味を持ってもらえるように、オフラインイベントで興味を持ってもらえた記事や、執筆に興味を持たれた方の背中を押す方法、株式会社メルカリでの働き方などいろいろお話させていただきました。 後から印刷を行う予定ならここから入稿作業が必要ですが、今回僕らはやらなかったので、僕は作業しませんでした。 この作業はイベントが終わった開放感から忘れそうなので、最後まで気を抜かずに対応しましょう。 次回参加にむけて 今回は初めて編集長として技術書典に参加しました。次回に向けて書籍作成をするときに確認するべきことはこんなことです。 今日からイベント当日までの全体の締め切りの理解 入稿する締め切り 執筆完了の締め切り 最初のバージョンを書き上げる締め切り 製本に必要な要素の理解 執筆者の募集 表紙の依頼 入稿データの作成 当日の確認 当日の宣伝 この他にも YouTube 技術書典チャンネル でも様々な情報を公開しているので、事前に過去の放送分を確認してみると良さそうです。 今回はなんとか人も集まって色々な締め切りにも間に合い素敵な本を出すことができましたが、反省点も多かったので、次に活かせればと思います。
アバター
この記事は、 Merpay Tech Openness Month 2023 の4日目の記事です。 こんにちは。メルコインのバックエンドエンジニアの @goro です。 はじめに このGitHub Actionsのセキュリティガイドラインは、社内でGithub Actionsの利用に先駆け、社内有志によって検討されました。「GitHub Actionsを使うにあたりどういった点に留意すれば最低限の安全性を確保できるか学習してもらいたい」「定期的に本ドキュメントを見返してもらい自分たちのリポジトリーが安全な状態になっているか点検する際に役立ててもらいたい」という思いに基づいて作成されています。 今回はそんなガイドラインの一部を、社外の方々にも役立つと思い公開することにしました。 ガイドラインにおける目標 このガイドラインは事前に2段階の目標を設定して作成されています。まず第1に「常に達成したいこと」として「外部の攻撃者からの攻撃を防ぐ」こと。そして、第2に「可能であれば考慮したいこと」として「内部と同等の権限を持つ攻撃者からの攻撃を防ぐ」ことを目標としています。 ガイドラインの構成 このガイドラインは3部で構成されています。まず1部でGitHub Actionsにおいて起こりうる脅威を紹介しています。2部ではその脅威に対する対策を記載しています。そして最後の3部ではより実践的な対策を講じられるようにセルフチェックリストを用意しました。 それでは実際のガイドラインをお楽しみください。 GitHub Actions Guideline 脅威を知る 権限設定の不備を突く攻撃 Pull Requestを契機に起動するトリガー トリガーの基本的な仕組みについては参考情報の「ワークフローのトリガー」のセクションに記載した。 PRを契機に起動するトリガーは攻撃者がなにかを仕掛ける余地が大きい。不注意にワークフローを構築するとシークレットを外部に送信されて攻撃を受ける可能性がある。 シークレットなどを外部に送信される可能性 ビルドスクリプトに細工をする 依存関係にあるライブラリを悪意のあるものに差し替えられる 自動実行の仕組みに相乗りされる(npmのpreinstall, postinstallなど) 過去、人気のライブラリでローカルファイルをスキャンする事例があった https://ezoeryou.github.io/blog/article/2018-07-13-npm-malware.html 本ドキュメントにおけるシークレットという用語は、GitHub Organization、リポジトリ、またはリポジトリ環境で作成する暗号化された環境変数を意味する。詳しくは GitHubの「Encrypted secrets」 を参照。 上記の攻撃の結果、次のような被害が発生する可能性がある。 攻撃者に、悪意のあるアクションまたは侵害されたアクションによってGitHub Actionsの計算リソースを不正に利用される可能性がある 侵害された、または悪意のあるアクションによって、リポジトリの自動ワークフローが中断される可能性がある Deployment Keyやアクセストークンなどのシークレットへの読み取りアクセスは、攻撃者が他のリソースを侵害するために利用される可能性がある インジェクションによる攻撃 一見安全に見えるワークフローにおいてもコードやコマンドインジェクションを引き起こす可能性がある。 インジェクションによる攻撃例1 例えば、以下のようなコードにはインジェクションの脆弱性がある。 uses: foo/bar@2.0.1 with: comment: | Comment created by {{ event.comment.user.login }} {{ event.comment.body }} コメントに {{ 1 + 1 }} のような二重中括弧が含まれていた場合、Actionは内部で{{ }}の値を補間するためにlodashを使っているため、node.jsコードが実行され出力が2になる。 ワークフローのインラインスクリプトに直接インジェクションを配置するシナリオもある。また、ブランチ名やメールアドレスへのコマンドインジェクションもできる。 インジェクションによる攻撃例2 次のようなコードを例にする。 - name: Check PR title run: | title="${{ github.event.pull_request.title }}" if [[ $title =~ ^octocat ]]; then echo "PR title starts with 'octocat'" exit 0 else echo "PR title did not start with 'octocat'" exit 1 fi 内部の式 ${{ }} が評価され、結果の値に置き換えられるため、コマンドインジェクションに対して脆弱になる可能性がある。 攻撃者は a"; ls $GITHUB_WORKSPACE" といったタイトルのPRを作成する可能性がある 出典: Security hardening for GitHub Action この例では " を使用して title="${{ github.event.pull_request.title }}" ステートメントを中断し、ランナーでコマンドを実行できるようにする。lsコマンドの出力を確認できる。 > Run title="a"; ls $GITHUB_WORKSPACE"" README.md code.yml example.js インジェクションによる攻撃の影響 インジェクションをされると攻撃者は任意のコマンドを実行できるため、単純に攻撃者が管理する 外部のサーバーにシークレットを送信するHTTPリクエストを行うことが可能になる 。 リポジトリへのアクセストークンを取得してもワークフローが完了すると失効するので攻撃は簡単ではない。しかし、攻撃者が自動化し、管理するサーバーにトークンを呼び出して、コンマ数秒で攻撃を実行することは可能となる。その場合GitHub APIを利用してリリースを含むリポジトリのコンテンツを変更することが可能になる。 攻撃者は悪意のあるコンテンツを GitHub Context 経由で追加できる 潜在的に信頼できない入力として扱う必要がある これらのコンテキストは以下の文字列をinjectすることができる body, default_branch, email, head_ref, label, message, name, page_name,ref, title Ex: github.event.issue.title , github.event.pull_request.body たとえば zzz";echo${IFS}"hello";# は有効なブランチ名であり、ターゲットリポジトリの攻撃となる可能性がある。 対策を考える 最小権限の原則に従う 最小権限の原則( Wikipedia: 最小権限の原則 )は、ソフトウェアがタスクを達成するために必要な最小限の権限セットで実行されるべきであるというものになる。これは、ワークフローで利用可能な シークレット の権限と、ワークフロートリガーの種類に基づいて自動的に提供される一時的なリポジトリトークンの両方に当てはまる。 自動的に提供されるリポジトリトークンGITHUB_TOKENの権限は、フォークからのpull_requestイベントの場合には制限されている。 GitHub の推奨するセキュリティ対策 としては、ワークフローでは必要としないGITHUB_TOKEN の権限をすべて削減することとなっている。したがって組織やリポジトリのデフォルト設定を「読み取りと書き込み」権限から「読み取り専用」に変更すべきである。 設定はGitHubの対象リポジトリの Settings > Actions > General から変更できる。 出典: GitHub 必要であれば、特定のワークフローに対して個別に追加権限を付与することができる。権限はワークフロー単位でも設定できるが、Job単位で設定を行うことで権限を最小化できるケースが大半である。参考情報のGITHUB_TOKENの権限に権限の一覧と、Job単位での設定方法へのリンクを記載した。 jobs: job_name: ... permissions: issues: write クロスリポジトリアクセスを考慮した、ワークフローが利用するべき推奨されるアプローチを優先度の高い順に説明する。 GITHUB_TOKEN 可能な限りGITHUB_TOKENを利用する Repository deploy key Managing deploy keys – GitHub Docs GitHub App tokens GitHub Appは、選択したリポジトリにインストールでき、リポジトリ内のリソースに対するきめ細かい権限がある Personal access tokens 使わないこと やむを得ず利用しなくてはならない場合、 Fine-grained personal access token を利用すること SSH keys on a personal account 絶対に使わないこと シークレットの利用について シークレットを利用する場合は以下を考慮すること。シークレットの利用を避けられるのであれば、利用しない。 Long-Lived tokenを利用しない Workload identity federationを用いたSecret Managerの利用を検討する Workload identity federation  |  IAM Documentation  |  Google Cloud Workload Identity – Developer Documentation 構造化データ(JSON, XML, YAMLなど)をシークレットにしない GitHub Actionsは全文をマスクデータとして扱ってくれるが部分文字列はマスクされないため 構造化データ(JSON, XML, YAMLなど)のblobを使用してシークレットを登録しない ひとつずつ個別にシークレットにする ワークフロー内で使用されるすべてのシークレットをマスクするよう登録する シークレットを使用してワークフロー内で別の機密値を生成する場合、その生成された値もシークレットとして登録する シークレットの登録方法は以下を参照 Encrypted secrets – GitHub Docs たとえば、秘密鍵を使用して署名付きJWTを生成してWeb APIにアクセスする場合は、必ずそのJWTもシークレットとして登録する シークレットに保存されたアクセストークンの利用状況を監査する スコープが最小限のクレデンシャルを使用する 登録されたシークレットを監査およびローテーションする シークレットへのアクセスについてレビューを要求することを検討 イベントトリガー PRの処理には pull_request イベントを使えるなら使う リポジトリへのwriteはできないよう制限されている Dependabotなどもシークレットにアクセスできない(社のorganizationの別リポジトリにアクセスできない)ためビルドできない可能性がある ただしDependabotシークレットに登録されていればアクセスできる Configuring access to private registries for Dependabot – GitHub Docs すこし制限を緩めたものとしてpull_request_targetがある GitHub Actionsのワークフロー自体は pull_request_target だと default branch のものが使われる ワークフローのyamlに直接記載する場合は攻撃者によって上書きされない チェックアウトしたコードに含まれるComposite Actionを使う場合注意が必要となる Composite Actionについては参考情報に詳しく記載した pull_request_target – Events that trigger workflows – GitHub Docs に記載されている以下の内容に注意すること 警告: pull_request_target イベントによってトリガーされるワークフローでは、permissions キーが指定され、ワークフローがフォークからトリガーされてもシークレットにアクセスできる場合を除き、読み取り/書き込みリポジトリのアクセス許可が GITHUB_TOKEN に付与されます。 ワークフローはPull Requestのベースのコンテキストで実行されますが、このイベントでPull Requestから信頼できないコードをチェックアウトしたり、ビルドしたり、実行したりしないようにしなければなりません。 さらに、キャッシュではベース ブランチと同じスコープを共有します。 キャッシュ ポイズニングを防ぐために、キャッシュの内容が変更された可能性がある場合は、キャッシュを保存しないでください。 詳細については、GitHub Security Lab の Web サイトの GitHub Actions およびワークフローのセキュリティ保護の維持: pwn 要求の阻止 に関するページを参照してください。 信用できないPRが作成されることを想定する場合は pull_request を使うべき ただし信用できないPRが作成される時点で、大きな問題となるため、これを防ぐべきである Job / Stepの単位 シークレットの内容を露出する単位は可能な限り狭くする Job単位 より Step単位 のほうがよりよい Step間のファイルによるデータやりとりは全ステップから可視であると考える Jobは処理の単位によって分ける テスト & ビルド & デプロイ はそれぞれJobを分けたほうがよい 必要なGitHub Actions上のPermissionやクラウドプロバイダー の権限を細かく制御するため たとえばテストの時にデプロイできる権限は必要ない 上記の場合、id-token: write は不要なはずである id-token: writeについては参考情報に詳しく記載 Dependabot / Renovateを利用したGitHub Actionsの更新 Actionsはバグ修正や新機能によって頻繁に更新される。Dependabot、RenovateでGitHub Actionsの依存関係を最新の状態に保つことができるため、設定を行うこと。 Dependabot: Keeping your actions up to date with Dependabot – GitHub Docs Renovate: Automated Dependency Updates for Github Actions – Renovate Docs | Renovate Docs サードパーティのActionを利用する場合の対応 サードパーティのActionを利用する場合、基本的にFull Changeset Hashに固定する。以下のようにFull Changeset Hashとバージョンコメントを記載することで、どのバージョンを使っているのかわかりやすくなる。 Dependabot Update version comments for SHA-pinned GitHub Actions by jproberts · Pull Request #5951 · dependabot/dependabot-core Dependabot now updates comments in GitHub Actions workflows referencing action versions | GitHub Changelog Renovate Automated Dependency Updates for Github Actions – Renovate Docs | Renovate Docs それぞれの指定の違いは以下の通り。 Full Changeset Hash uses: owner/action-name@26968a09c0ea4f3e233fdddbafd1166051a095f6 # v1.0.0 衝突の成功例はあるが困難 Short Changeset Hash uses: owner/action-name@26968a0 衝突に対して脆弱 Tag / Release uses: owner/action-name@v1 タグを後で変更され、意図しない変更が混入してしまう可能性がある Branch Name uses: owner/action-name@main 将来壊れる可能性がある 意図しない変更が混入してしまう可能性がある Actionのソースコードを監査する サードパーティのホストにシークレットを送信するなどの疑わしいことがないか確認する Managing GitHub Actions settings for a repository を参考に、ワークフロー内で利用している3rd Party ActionsのAction permissionsをセキュリティ観点で見直す。可能であれば、Allow enterprise, and select non-enterprise, actions and and reusable workflowsを設定する。 出典: GitHub 不要なワークフローやJobは削除する 設定はしてあるが必要なくなったものは削除して依存を減らす インジェクションを防ぐ 信頼されない式の入力値を中間環境変数(intermediate environment variable)に設定する。 これによって${{ github.event.issue.title }}式の値はスクリプトの生成に影響するのではなく、メモリに保存されて変数として使用される - name: print title env: TITLE: ${{ github.event.issue.title }} run: echo "$TITLE" シェル変数をダブルクォートして単語の分割を避ける(シェルスクリプトを書く際の一般的な推奨事項) GitHub Security Labの開発する CodeQL queries を利用する script_injections.qlは、記事で紹介されている式注入をカバーしており、精度が高い。しかしワークフローのステップ間のデータフローを追跡することはできない pull_request_target.qlの結果は、pull requestからのコードが実際に安全でない方法で処理されているかどうかを特定するために、より多くの手動レビューが必要。 GitHub のカスタムアクションやワークフローを書くときは、信頼できない入力に対して書き込み権限でコードを実行することがよくあることを考慮する actionlintによるインジェクションの検知 外部Actionとなるが、actionlintを利用することでインジェクション対策ができるので、導入を検討する。 https://github.com/rhysd/actionlint また、 reviewdog/action-actionlint を利用するとGitHub Actionsでactionlintを実行することも可能。 name: Actionlint on: - pull_request_target jobs: actionlint: runs-on: ubuntu-latest permissions: checks: "write" contents: "read" pull-requests: "write" steps: - uses: actions/checkout@v3.1.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} - uses: reviewdog/action-actionlint@1fa528d6a483f3df85059e206eadea033044edd7 with: fail_on_error: true filter_mode: nofilter level: error reporter: github-pr-review その他 完全に攻撃を防ぐことは不可能と考え、問題が発生したときに受ける影響を最小限に抑える Ex. Production環境に影響を及ぼす(サービスを停止させる、不正なImageを送り込む etc)ことがが最悪のケースとなる GitHub ActionがPRを作成またはオーナーとして承認しないようにする OpenSSF Scorecardsを使用したワークフローの保護(ただし利用するにはGitHubのPersonal Access Tokenが必要になる) ossf/scorecard – Security health metrics for Open Source OSSF Scorecard action – GitHub Marketplace actions/starter-workflows: Accelerating new GitHub Actions workflows セルフチェックリスト 本章の内容を定期的にチェックすることでGitHub Actionsの安全な利用につなげる。ガイドラインで学習した内容が本チェックリストでカバーされることを目指す。 CODEOWNERSの設定を見直す CODEOWNERS ファイルで .github ディレクトリ以下に対して適切にCode Ownerが設定されていることを確認する Protected Branch でDefault BranchへのPull Requestのマージには、Code Ownerによる承認が必須になっていることを確認する GITHUB_TOKENのPermissionsを見直す GITHUB_TOKEN に付与される権限を見直す。 デフォルトで付与されるGITHUB_TOKENの権限がReadのみになっているか確認する 「Read and write permissions」になっている場合は「Read repository contents permission」に変更する \ 出典: GitHub Managing GitHub Actions settings for a repository 可能であれば「Allow GitHub Actions to create and approve pull requests」を無効にする 設定方法などは以下を参照 Disabling or limiting GitHub Actions for your organization permissions をJob単位で設定する permissionsはWorkflow全体かあるいはJob単位で設定できるが最小権限にするためにJob単位で設定する Workflow syntax for GitHub Actions – permissions permissions の見直し 以下のリストを元に権限が最小になっているかを確認する Automatic token authentication – permissions-for-the-github_token ビルドやテストなどのジョブを分けることで、強い権限で実行されるステップが少なくなるのであれば分割を検討する GitHub Actions Secretsを見直す GitHub Actions用に設定されているシークレットを見直す。 使っていないシークレットはGitHub上から削除する シークレットの発行元でも無効化しておく 構造化データ(JSON, XML, YAMLなど)をシークレットに設定していないか確認する 個別登録するなどして設定し直す ワークフロー内で使用されるすべてのシークレットやログ出力すべきではない値をマスクするよう登録する シークレットを使用してワークフロー内で別の機密値を生成する場合、その生成された値もシークレットとして登録する 定期的(1年に1回など)にシークレットをローテーションする 新しいシークレットに置換し、それを終えたら古いシークレットは無効化する シークレットにTTL(Time To Live)を設定できる場合は適切な長さのTTLを設定する ローテーションと併せてシークレットに設定されている権限が最小限になっているのか確認する 必要以上に強い権限が付与されている場合は不要な権限を落とす ワークフロートリガーを見直す コードプッシュをトリガとする場合、pull_request か、それが難しければ pull_request_target を使う on: pushをPR用に使っていたら見直す サードパーティのActionsを見直す 不要なWorkflowやJobは削除する 設定はしてあるが必要なくなったものは削除して依存を減らす バージョン指定を確認する 基本的にFull changeset hashに固定し、Full Changeset Hashとバージョンコメントを記載する Dependabot - uses: actions/checkout@01aecc # v2.1.0 Dependabot now updates comments in GitHub Actions workflows referencing action versions | GitHub Changelog Renovate - uses: actions/checkout@af513c7a016048ae468971c52ed77d9562c7c819 # renovate: tag=v1.0.0 Automated Dependency Updates for Github Actions – Renovate Docs | Renovate Docs Managing GitHub Actions settings for a repository を参考に、ワークフロー内で利用している3rd Party ActionsのAction permissionsをセキュリティ観点で見直す。 インジェクション対策を見直す actionlintが導入済みであれば、actionlintで問題がないことを確認する actionlintを導入できない場合、最低限の対応として信頼されない式の入力値を中間環境変数(intermediate environment variable)に設定する おわりに 今回は社内の有志メンバーによって作成された社内用GitHub Actionsのセキュリティガイドラインの一部を紹介しました。 GitHub Actionsは、開発者がよりスムーズで効率的な開発を行うための強力なツールであると言えますが、使用する際にはガイドラインに記載したようなさまざまな観点でセキュリティに十分注意する必要があります。常にセキュリティを考慮し、最適なプラクティスを意識して実践することの重要性をガイドラインを作成する中で強く感じました。GitHub Actionsにおけるセキュリティのベストプラクティスは今後も変化していくと思います。本ガイドラインはこれで完成ではなく、今後も適切に更新していき、よりスムーズで安全な開発をサポートできるよう努めていきたいと思います。 明日の記事は sapuriさんです。引き続きお楽しみください。 Appendix 参考情報 ワークフローのトリガー ワークフローはイベントによってトリガーされる。イベントには、以下のものがある。 ワークフローのリポジトリで発生したイベント GitHubの外部で発生し、GitHub上で repository_dispatch イベントを発生させるイベント 時間指定での実行 手動実行 たとえば、リポジトリのデフォルトブランチにプッシュが行われたときやリリースが作成されたとき、あるいはIssueがオープンされたときなどにワークフローを実行するように設定することができる。 詳しくは About workflows を参照すること。またイベントの一覧は Events that trigger workflows を参照すること。 GITHUB_TOKENの権限 GITHUB_TOKENの権限は以下にまとめている。 Permissions for the GITHUB_TOKEN Jobごとに権限を変更する方法は以下に記載されている。 Assigning permissions to jobs – GitHub Docs 権限をジョブに割り当てる – GitHub Docs Composite Action Composite ActionはカスタムActionの一つであり、使用することでワークフローの複数のステップを組み合わせて 1 つのアクションにすることができる。 たとえば、複数の run コマンドを 1 つのアクションにまとめて、そのアクションを 1 つのステップとしてワークフローから呼び出して実行することが可能。 作成方法に関しては以下に記載されている Creating a composite action – GitHub Docs シークレットのマスク ログ中での値のマスク – GitHub Actions のワークフロー コマンド Workflow commands for GitHub Actions 以下のような記述を行うことで値をマスキングすることが可能。マスキングされた単語は「*」 に置き換えられ、ログに出力されなくなる。マスク可能な値は環境変数または文字列である。 ::add-mask::{value} 例:Stringをマスクする 以下のような設定を行った上でログに「Mona The Octocat」を出力すると「***」が表示される。 echo "::add-mask::Mona The Octocat" 例:環境変数をマスクする 以下のような設定を行った上でログに環境変数 MY_NAMEと”Mona The Octocat"を出力すると *** が表示される。 jobs: bash-example: runs-on: ubuntu-latest env: MY_NAME: "Masking on GitHub Action" steps: - name: bash-version run: echo "::add-mask::$MY_NAME" - run: run: | echo "Mona The Octocat" echo "::add-mask::Mona The Octocat" echo "Mona The Octocat" echo "$TITLE" echo "::add-mask::$TITLE" echo "$TITLE" 以下のように表示される。 Mona The Octocat *** Masking on GitHub Action *** actions/toolkitを利用する場合 actions/toolkit はGithub Actionsの作成を容易にする一連のパッケージを提供している。 toolkitの@actions/coreパッケージを利用することで、以下のような記述でシークレットのマスクを設定することも可能。 core.setSecret('Mona The Octocat') Setting a secret – toolkit/packages/core ログ中での値のマスク – GitHub Actions のワークフロー コマンド / Workflow commands for GitHub Actions id-token:writeで実現できること id-token: write はGitHubによる署名が行われたOpenID ConnectのID Tokenが取得できるようになる権限。これを使うとどういうことができるかは 公式ドキュメント を参照する。 例えばGCPのWorkload identity federationの機能を通じて、GitHub ActionsのID Tokenがあれば設定されたService AccountのAccess Tokenを手に入れることができる。つまり、id-token: write をGitHub Actions中で利用するということは短時間(デフォルトでは1時間)ながら、GCPプロジェクトへのアクセス権限を渡すのと同義となる。secretsに固定のcredentialをもたせるのに比べれば圧倒的にセキュリティが高いが、それでもID Tokenにアクセス可能な範囲を適切にコントロールすることは重要となる。 References GitHub Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests Keeping your GitHub Actions and workflows secure Part 2: Untrusted input Keeping your GitHub Actions and workflows secure Part 3: How to trust your building blocks Security hardening for GitHub Actions About code owners – GitHub Docs About protected branches – GitHub Docs Automatic token authentication – GitHub Docs Contexts – GitHub Docs Managing GitHub Actions settings for a repository – GitHub Docs Setting the permissions of the GITHUB_TOKEN for your repository – Managing GitHub Actions settings for a repository Automatic token authentication – GitHub Docs Modifying the permissions for the GITHUB_TOKEN – Automatic token authentication – GitHub Docs Managing deploy keys – GitHub Docs Encrypted secrets – GitHub Docs Creating encrypted secrets for a repository – Encrypted secrets – GitHub Docs Configuring code scanning – GitHub Docs Preventing GitHub Actions from creating or approving pull requests – Disabling or limiting GitHub Actions for your organization Verified Creator – GitHub Marketplace · Actions to improve your workflow Others rhysd/actionlint: Static checker for GitHub Actions workflow files
アバター
この記事は、 Merpay Tech Openness Month 2023 の3日目の記事です。 こんにちは。メルペイBackendエンジニアの@yushi0010です。 私が所属するPartner Platformチームでは社内向け管理ツールを開発しています。この記事では、そのツール内でのページネーションで起きたバグを解消した話を紹介します。 概要 今回のページネーションを利用していた管理ツールの検索ページでは、あるテーブルが持つカラムに対して条件を指定し、その条件に合うレコードを取得して一覧表示する機能がありました。しかし、ある特定の条件下でどれだけ次ページに遷移するボタンをクリックしてもページ遷移が行われないというバグが発生しました。 バグが起きた状況 どのようにしてページ遷移が行われなくなったのかを説明するために、その時の状況を共有します。 まず、検索の対象とするテーブルは以下のようなスキーマです。 table ( id INT64 NOT NULL, month DATE NOT NULL, status1 INT64 NOT NULL, status2 INT64 NOT NULL, (中略) created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL, ) それぞれのカラムに入る値について、 month はDate型で表現されていますが年月だけの情報を保持しており、何日なのかという情報は必要がないため全て1日で固定されています。また、 status1 や status2 はカテゴリカルな値が入り、とりうる値の範囲はせいぜい0から9までの一桁に収まるくらいです。 このスキーマに対して条件を指定して一覧表示をさせていました。実際の条件は以下のような内容です。 month が2023年5月より以前になっている status1 が (0, 1, 3) のどれかである status2 が (1, 2, 4, 5) のどれかである ページネーションを実現するアルゴリズムとしては、典型的なものとしてOFFSET句を利用するパターンと、前のページの最終行の情報をカーソルとして保持し次のページでそのカーソル以降のレコードを表示させるパターンが主に考えられます。今回のコードでは後者を使用していました。 また、カーソルとして使用したカラムは month 、 status1 、 status2 、 created_at の4つです。その4つのカラムでOrdey Byさせた後、ページで表示させる件数+1つのレコードを取得してその+1つめのレコードの値をカーソルとし、ページ遷移するときにはそのカーソルを含むそれ以降のレコードを取得するという実装になっていました。 例えば一つのページに50件を表示させたいとき、 1ページ目を取得する場合は、 SELECT * FROM table ORDER BY month, status1, status2, created_at LIMIT 51; で51件取得し、50件をページに表示させ、51件目をカーソルの値に使用していました。 次に2ページ目を取得する場合は、先ほどの51件目のカーソル以降(51件目を含む)となるレコードを取得すれば良いので、 SELECT * FROM table WHERE (month > @cursor_month) OR (month = @cursor_month AND status1 > @cursor_status1) OR (month = @cursor_month AND status1 = @cursor_status1 AND status2 > @cursor_status2) OR (month = @cursor_month AND status1 = @cursor_status1 AND status2 = @cursor_status2 AND created_at >= @cursor_created_at) ORDER BY month, status1, status2, created_at LIMIT 51; で取得をします。 バグが起きた原因 以上のようなコードによってページネーションロジックが実装されていましたが、どのようなことが原因で前述のバグが発生していたでしょうか? 自分で考えてみたい人はスクロールをここで一旦ストップしてください。ここまでに共有した情報の中にそのバグを発生させていた原因が含まれています。以下でその原因を示します。 予想はつきましたでしょうか? 今回のバグの原因となっていたのは、カーソルとして使用していたカラムの組み合わせがユニークではないことでした。 実装当初の想定では、 created_at を含む4つのカラムを組み合わせてカーソルを作成することで、カーソルは各レコードにわたってユニークになるだろうと考えていました。しかし、実際にはデータマイグレーションの際に一括Insertをしたことで created_at を含む4つのカラムが全て同じになっているレコードがページの表示件数以上に存在していました。 ではユニークでないレコードが大量に存在することで、どのようにページ遷移が出来なくなるのでしょうか。 例えばページの表示件数が50件で、カーソル (month, status1, status2, created_at) が (2023年5月, 4, 2, 2023年4月10日) となるユニークでないレコードが51件より多く存在する場合を考えてみます。 ページ遷移を行っていたところ51件目がユニークでないレコードとなり、カーソルが (2023年5月, 4, 2, 2023年4月10日) となってしまいました。このとき、次ページに遷移するときに取得するレコードは (2023年5月, 4, 2, 2023年4月10日) が含まれるので、先ほど取得したはずのユニークでないレコードが再度取得され、このユニークでないレコードは51件以上存在するのでカーソルも再度 (2023年5月, 4, 2, 2023年4月10日) に設定されます。これ以降はどれだけページを次に遷移をさせても同じ情報が取得され続けます。このようにしてページネーションロジックはエラーなく動作しているもののページが遷移できないバグに陥りました。 このバグを発生させないためにはカーソルの値が常にユニークでなければならないので、今回このバグの解決策としてとった対応は、カーソル (month, status1, status2, created_at) に レコードごとにユニークな値である id カラムを created_at の代わりに含めて (month, status1, status2, id) とすることで、カーソルが重複してしまうレコードが存在しないようにしました。 バグからの学び 今回の実装でよくなかったところは、ページネーションで利用されるカーソルにユニークなカラムが含まれていなかったことはもちろんなのですが、 created_at にレコード作成日より大きな意味を持たせてしまったことにあると考えています。 ページネーションロジック実装時には created_at に対してカーソルで利用するユニークなカラムという役割を持たせましたが、データマイグレーションを行う人は created_at にそのような役割があるということが認識することができず、 created_at が同じ値となるようにレコードの一括挿入を行いました。 一括挿入時以外においては created_at が重複することはないと仮定したとして(実際には重複することが十分考えられます)カーソルとして利用はできそうです。しかし、一般的に作成日として認識されているカラムに対してそれ以上の意味を持たせることで、そのカラムの使い方に齟齬が生じ、それが原因となって今回のようにバグが発生することが考えられます。 カラムの使い方に限らず、一般的な利用方法について共通の認識があるものに対してどうしても特別な意味を持たせたいときには、ドキュメントやコメントによってその意図を伝える方法が考えられます。しかし、利用者がそのドキュメントを確認して実装者が想定する意図を汲み取ってくれるとは限りませんし、そもそもそれを認識しなければならないという利用者への不要な負担を強いる状況を発生させています。よって特別な意味を持たせることは避けるべきであり、意味を持たせる用の項目を別で新たに定義するべきだと学びました。 まとめ この記事では、メルペイの管理ツールのページネーションに発生したバグの概要、原因、そこからの学びを紹介しました。 明日の記事はBackendエンジニアの@komatsuさんです。引き続きお楽しみください。
アバター
この記事は、 Merpay Tech Openness Month 2023 の2日目の記事です。 メルペイFrontendエンジニアの @togami2864 です。普段はPartner Platformというチームで加盟店申込みフォームや審査・管理を行うためのMerchant Supportツールの開発・運用を担当しています。 本記事ではRust製TypeScriptコンパイラであるstcについて筆者の観測範囲での概要、開発状況、課題等を紹介します。なお、内容は全て2023年5月時点のものです。また、本記事の一部は Node学園 41時限目 書籍について で発表したものと重複していることをご了承ください。 概要 stcは2022年10月にオープンソース化されたRust製のTypeScriptコンパイラです。 https://github.com/dudykr/stc 製作者はRust製のトランスパイラ swc の作者である kdy1氏 で、Rustとparallelな解析によってTypeScriptのビルドとイテレーションを短縮して DX を改善することを目的としています。 1 また、tscの動作に準拠したコンパイラ(drop in replacement)を目指すという立場をとっており、tscの挙動を仕様として追従していく予定です。 一時はRustの採用を諦め、Goで開発していた時期もあったようですが、最終的にはRustで作ることを決定しました。 2022/01/26 元々Rustで作っていたが、tscが多くの共有可変性やGCに依存していることを理由にRustの採用を見送り。ZigとGoで実験した結果Goを採用 2 2022/10/10 tscを実直にGoで行ごとに移植していたものの、量が膨大すぎるためTypeScriptコンパイラのコードを行ごとにGoに変換し、コンパイラを生成するコンパイラを考案 3 2022/10/27 Goを使っているとはいえ、コンパイラを通して生成したGoのコードには非効率なものが多く含まれること、不要な部分の移植も必要なため結局Rustに戻すことを決定 4 移植難易度の高さ 言うまでもなく、tscの他言語への移植は非常に困難で挑戦的なプロジェクトです。その主な理由の一つは、他のプログラミング言語と異なり、TypeScriptには明確な仕様書が存在しない点です。 5 そのため、stcはtscの挙動を仕様とみなしています。また、開発に際しては以下の3つのリソースを参考にしています。 1. 機能が追加された時のPRを見る TypeScriptには、基本的な型に加えて、conditional types、mapped types、template literal typesといった独自の型が存在します。これらの機能に関する詳細な説明やエッジケースはPRに書いてあります。 ちなみに大きな機能追加のほとんどはTypeScriptの共同創案者である Anders Hejlsberg氏 のものです。彼のPRは詳細な説明を書いている上に、テストケースも豊富に書いているため非常に重要です。 例: conditional types unknown type variadic tuple 2. テストケースを参考にする TypeScriptのリポジトリには、 tests/casesディレクトリ に多数のテストケースがあります。これらのテストケースの入力・出力とコメントを参考にして開発が進められます。また、stcではcompilerとconformanceディレクトリ内のテストケースを流用してテストが実施されています。 3. tscのソースコードとコメントを読み解く これが最も確実な方法でありながら、非常に高難易度です。TypeScriptのコンパイラのコードベースは10年以上にわたる開発が続けられているため巨大かつ複雑です。 https://twitter.com/kdy1dev/status/1652531146138464259 結構有名な話ですが型検査のコードがあるchecker.tsのみでファイルサイズは約2.7MBあり、GitHub上で表示できません。 GitHub上のchecker.ts 引用: microsoft/TypeScript 仕組み 次のようなシンプルなTypeScriptコードに対し、型チェックを行うとしましょう。 const foo: number = 1 + 1 定数fooを宣言し、型注釈としてnumber型を指定しています。値として1 + 1を代入しています。 型チェックを行うためにまずソースコードを字句解析、構文解析を通してASTにする必要があります。stcではTypeScriptのコードをASTにするためのlexerとparserは swc を使っています。あくまでstcが担当するのは型チェックのみです。 そこで生成されたswcのASTを使って型検査を行ないます。簡略化したASTは次のようになります。 stcでは Visitor pattern を実装しています。 Visitorとして Analyzerという構造体 が用意されており、 各ASTのタイプに対応するvisitメソッドが実装 されています。ASTをたどりながらAnalyzerに実装されている操作を呼び出し、そのタイプごとに独自の処理を行います。 このサンプルコードでは、単純に型注釈に対して右の式の結果の型が代入可能であるか(つまり部分型であるかどうか)をチェックします。 明示的な型注釈により、変数の型がnumberと判断されます。次に式1 + 1ですが、BinaryExpressionに到達したときに演算子が+であることがわかります。その後、leftとrightの式の型が分かれば、結果がどのようになるかチェックできます。もし+演算が適用できない型同士であれば、ここでエラーが出されます。 今回は両方ともnumber型の値なので、式の結果もnumber型になることがわかります。 number型に対してnumber型は代入可能ですから無事に型チェック完了です。 現在の開発状況 stcは現在TypeScript5.0のブランチのconformance testをもとに開発されています。 基本的な型、構文、演算子、builtin typesのサポート 基本的な型に加え、Generics、オーバーロード、mapped types、conditional typesといった高度な型もサポートしており、2023年4月現在TypeScript4.9のsatisfies operatorまでサポートしています。また、ES2022までのbuiltin typesの解析が可能です。 tscとの互換性 stcはTypeScriptとの互換性を重視して開発されています。そのため、TypeScriptとの動作の違いを把握することが重要です。そこで、stcのリポジトリには tsc-stats.rust-debug というファイルが用意されています。 Stats { required_error: 3538, matched_error: 6497, extra_error: 771, panic: 74, } このファイルでは、本家tscが出力した結果とstcが出力した結果を比較して、エラーの一致数やパニックの発生数などを集計しています。tscのリポジトリからコピーした/conformanceディレクトリ内のテストケースを使って集計されています(stcにはconformanceテスト以外にもテストケースがありますが、このファイルの数値には含まれていません)。 required_error (false-negative) これは、tscがエラーを出しているのに対して、stcがエラーを出していないケースの数を示しています。現在、3538箇所存在しています。この値はできるだけ減らしたいものです。 matched_error (true-positive) これは、tscとstcの両方が正しくエラーを表示できている箇所の数を示しています。現在、6497箇所存在しています。この値はできるだけ増やしたいものです。 extra_error (false-positive) これは、tscではエラーを出していないのに、stcだけが誤ってエラーを表示している箇所の数を示しています。 この値は最優先で減らすべきです。 現在、771箇所存在しています。 理想的には0になってほしい値ではありますが、現状では多くのエッジケースが含まれており、どこまでサポートするかは今後の課題となります。 panic この項目は、panicによってプログラムが終了するケースの数を示しています。これらのケースは主にparser (swc) の問題や、解析中のオーバーフローが原因となっています。 これらの数値は、4月まで https://stc.dudy.dev/ で週に1回進捗が共有されていました。しかし、最近の大きなタスクや容易に修正できる部分がほぼ解決されたため、更新頻度が月1回に変更されています。 @typesパッケージの解析 @types/node や @types/react といった有名なツールの型定義ファイルは、普段の開発でほぼ必須となります。stcも実用段階に達するためには、これらのパッケージを解析できることが必須でしょう。 ただし、namespaceを使用している部分が並列解析できなかったり(特に@types/nodeはnamespaceを多用している)、単純なプロパティの多さからくるデバッグの難しさなどの理由で、まだ十分な進捗がありません。 未対応のケース 現状では、基本的な型の多くは解析できますが、対応できていないケースも存在しています。 例): https://github.com/dudykr/stc/blob/7c76ed2314a82040efba2f82db951eee6c2c88bb/crates/stc_ts_type_checker/tests/conformance/controlFlow/controlFlowAliasing.ts#L6-L14 // @strict: true // @declaration: true // Narrowing by aliased conditional expressions function f10(x: string | number) { const isString = typeof x === "string"; if (isString) { let t: string = x; // 本当はエラーにならないのにTS2322が表示 } else { let t: number = x; // TS2322 } } 変数xがif句、else句内でそれぞれnarrowingされることが期待されますが、現在のstcではif-else句内でもxを(string | number)と判断しています。そのため、 TS2322: Type '(string | number)' is not assignable to type 'number'. というエラーが誤って表示されます。 おそらく、式typeof x === "string"の結果の型を判定する際に、その式がifステートメントの条件として使用されていない場合、変数xをnarrowingする処理が行われていないものと思われます。 このようなfalse-positiveケースが多数存在しており、特にclass構文周りではfalse-positiveが多いようです(メンバーやメソッドなどの解析の順番を決めるのが難しいため)。 false-positiveをどこまで妥協するか false-positiveは極力減らすべきです。しかしながら、false-positiveの中には現実的なユースケースとして本当に現れるのかというケースも大量にあります。例えば次のコードは現在のfalse-positiveのケースの一つです。 https://github.com/dudykr/stc/blob/main/crates/stc_ts_type_checker/tests/conformance/classes/members/privateNames/privateNameComputedPropertyName3.ts // @target: esnext, es2022, es2015 class Foo { #name; constructor(name) { this.#name = name; } getValue(x) { const obj = this; class Bar { #y = 100; [obj.#name]() { // <----------- Umimplemented return x + this.#y; } } return new Bar()[obj.#name](); } } console.log(new Foo("NAME").getValue(100)); TypeScriptのコードとしては不正ではないものの、重箱の隅をつついてくるようなfalse-positiveのテストケースが大量に存在しておりそれらをどう扱うかははっきりしていません。 今後の動き まだまだ開発途中であり、決まっていることは少ないですが、アルファ版へのロードマップは https://stc.dudy.dev/docs/roadmap で公開されています。 assign ruleの改善 tscの挙動を仕様とし、互換性や@typesパッケージの解析のために改善が続けられるでしょう。またGenerics推論の改善や解析順序の改善が予定されています。 VSCode拡張 2023年4月に開発が始まったようです。現在は開発者向けのVSCode拡張機能の開発が進行しています。 開発中のVSCode拡張機能 引用: This week in stc, 23 独自の構文拡張はあり得るか (筆者の意見ですが)非常に可能性は低いと考えられます。なぜなら、作者のkdy1氏は標準遵守の意識が強く、 swcでもその姿勢を維持しているからです。 また、 bun がJSXの独自構文を導入した際にもかなり難色を示していました。 https://twitter.com/kdy1dev/status/1609013152590725120?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1609013152590725120%7Ctwgr%5Ed9b51ff7ef2db59201ba768191816a2788474fff%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fkdy1.github.io%2Fpost%2F2023%2F2%2Fstc-ethics%2F 独自の破壊的変更によるコミュニティの混乱を避けるため、stcがTypeScriptに独自の拡張を導入する可能性は非常に低いと考えられます。 6 まとめ stcの概要について紹介しました。高速なtscと聞くと非常に魅力的に聞こえますが、まだ鋭意開発中であり、うまくいくかどうかはtscの複雑さも相まって全くもって未知数です。また、実際に高速に動作するのか正確なベンチマークが用意されていないため、その点も確認できません。 7 しかし、swcのASTを中心としたエコシステムの一員として、個人的には非常に期待しています。 脚注 [1] Rewriting TypeScript in Rust? You’d have to be… https://www.totaltypescript.com/rewriting-typescript-in-rust [2] I’m porting tsc to Go https://kdy1.dev/posts/2022/1/tsc-go [3] Status update of my tsc port https://kdy1.dev/posts/2022/10/tsc-port-status [4] Open-sourcing the new TypeScript type checker https://kdy1.dev/posts/2022/10/open-sourcing-stc [5] 正確にいうと、TypeScript v1.8までの仕様書は存在しています。しかしながら、それ以降の更新はなく このissue を見る限りほぼ放置されていると思って良いでしょう。 [6] stc의 윤리적 문제 https://kdy1.github.io/post/2023/2/stc-ethics [7] csstypesの解析がtscの57倍高速であったという 報告 や、@types/reactの解析にわずか0.174秒しかかからなかったという 報告 があります。これらから期待はできますが、実行環境や条件が明確でないため、参考程度に留めておくのが良いでしょう。
アバター
この記事は、 Merpay Tech Openness Month 2023 の1日目の記事です。 背景 メルペイのバックエンドエンジニアのa-r-g-vとsminamotです。私達はメルペイ加盟店の管理システムを開発しているチームに所属しています。私達のチームには、複雑な条件を持つBigQueryのSQLクエリがいくつか存在しています。例えば、加盟店管理に関する費用計算などの計算クエリのように、外部環境の変化によって要件が定期的に変更され、マイクロサービス化などのシステム化が難しいクエリがあります。このようなクエリは複雑であるだけでなく、テスタビリティにも問題がありました。そのため、開発者がテストを実施することが困難になっており、クエリの変更を安心して行うことができない状態にありました。 クエリの複雑性 抽出条件の複雑さと複数のマイクロサービスへの依存により、クエリが複雑になっていました。 抽出条件の複雑さ 契約条項に基づく複雑なビジネス要件が、クエリの複雑さを増す要因となっていました。例えば加盟店管理費用を計算するビジネス要件においては、正しく費用を計算するために審査通過日 、加盟店獲得後の決済情報、決済用QRコードの要否のような情報を組み合わせてクエリを行う必要があります。このような条件がクエリを複雑にしているのです。 複数のデータベースへの依存 クエリが複数のマイクロサービスのデータベースを横断して参照することが、複雑さを増していました。メルペイではマイクロサービスアーキテクチャを採用しており、業務ドメイン単位でサービスが分割されています。例えば加盟店の申込み、審査、事業者の情報、決済、QRコード配送などは、それぞれ別のマイクロサービスとして分割されています。一方で前述した管理費用を計算するためには、これらのデータベースやテーブルを横断的に参照する必要があります。また、依存しているマイクロサービスの中には、別のチームが管理しているものもあります。このような、依存するテーブル数の多さがクエリを複雑にさせていました。 課題 クエリに対する開発者テストの煩雑さ 開発者テストを煩雑にしていたのは主に以下2つの点でした。 一つ目はテストデータの投入が煩雑であったことです。複数のマイクロサービスのテーブルに依存しているために、投入する対象のテーブルの数や投入データ行数が多くなってしまっていました。また、クエリの抽出条件が複雑であるため、必要なテストパターン数が多く、そのためデータとして投入しないといけない量も多くなっている課題がありました。 二つ目の課題は、手作業が多いことです。実際のテスト環境のテーブルに対して、マイクロサービスが生成していないデータを投入することは問題です。そのため、クエリをテストするために新しくテーブルを作り、そこにテストデータを投入した後に、そのテーブルを使用するようにクエリを書き換え、クエリ実行と結果検証・クリーンアップという手順を行う必要があります。これを毎回のクエリ改修や、テストパターン毎に行うのが大変であるという課題がありました。 クエリに対する自動テストの不存在 クエリに対する自動テストの欠如も課題でした。デグレード(機能低下)を検知できる自動テストスイートが存在しませんでした。そのため、クエリの変更を安心して行うことができない状況でした。 解決策 この問題を解決するために、Go言語を用いてクエリに対するユニットテストを実装する仕組みを作りました。主に、以下の2点を実施しました。 Goのテストコードの中でテストデータを投入し、BigQuery上でのSQL実行を簡単に行えるように、専用のヘルパー関数を作成した テストデータ作成を支援するために、クエリからGo構造体を自動生成するツールを作成した これらにより、クエリの実行結果が意図通りなっていることをGo言語のtesting packageを使って可読性・メンテナンス性が高い形でテストできるようになりました。 動作イメージ 全体の動作イメージを説明します。クエリのテストはGoのテストとしてテストケースを実装するようにしました。テストケースごとに、以下を実行します。 テスト対象のクエリが利用しているテーブルを抽出し、テスト用データセット配下にテーブルを1件ずつ作成します。 テストケースで指定されているテストデータを、テスト用テーブルに挿入します。 テスト対象のクエリのFROM句に書かれているテーブル名を、上記で作成したテスト用テーブルを利用するように書き換えます。 書き換えたクエリを実行し、期待している結果と同じか確かめます。 テストケースのクリーンアップ動作で、作成したテスト用テーブルをすべて削除します。 また、テストケースからのデータ投入を支援するために、クエリが利用しているテーブルを Goの構造体として自動生成する仕組み https://github.com/ginokent/bqschema-gen-go をベースに作成しました。具体的には、同一リポジトリに存在する全てのSQLファイルを読み、コード生成を行うコマンドを作りました。コマンドは2つの構造体を生成します。 クエリが利用しているテーブル一覧を表すGo構造体 クエリが利用しているテーブルを全列挙し、対応関係をGoの構造体として生成します。利用テーブルの列挙は正規表現を利用し、FROM句をパーズして行います。 クエリが利用しているテーブル定義に対応するGo構造体 クエリが利用しているテーブルのスキーマ定義を実際のステージング環境のBigQueryテーブル定義を参照し生成します。 例えば、以下のようなクエリがあったとします。 SELECT SUM(Charge.Amount) As TotalAmount FROM `querytest-demo.user_service.Users` Users INNER JOIN `querytest-demo.payment_service.Charge` Charge ON Users.UserID = Charge.UserID WHERE Users.ReferralType = "ORGANIC" ここから、コード生成コマンドを実行すると、以下のような2つの構造体が生成されます。 // Code generated by bigqueryschema; DO NOT EDIT. package bigqueryschema import "cloud.google.com/go/bigquery" // Charge is BigQuery Table `querytest-demo:payment_service.Charge` schema struct. type Charge struct { ChargeID bigquery.NullString `bigquery:"ChargeID"` UserID bigquery.NullString `bigquery:"UserID"` Amount bigquery.NullInt64 `bigquery:"Amount"` Status bigquery.NullString `bigquery:"Status"` CreatedAt bigquery.NullTimestamp `bigquery:"CreatedAt"` UpdatedAt bigquery.NullTimestamp `bigquery:"UpdatedAt"` } // Users is BigQuery Table `querytest-demo:user_service.Users` schema struct. type Users struct { UserID bigquery.NullString `bigquery:"UserID"` Name bigquery.NullString `bigquery:"Name"` ReferralType bigquery.NullString `bigquery:"ReferralType"` CreatedAt bigquery.NullTimestamp `bigquery:"CreatedAt"` UpdatedAt bigquery.NullTimestamp `bigquery:"UpdatedAt"` } // Code generated by gentestqueries; DO NOT EDIT. package testqueries import ( "context" "testing" "github.com/stretchr/testify/require" "github.com/a-r-g-v/querytest-demo/src/bigquery" "github.com/a-r-g-v/querytest-demo/src/bigqueryschema" "github.com/a-r-g-v/querytest-demo/src/querytest" ) var QueryQueriesTotalUserAmount = querytest.NewQuery("queries/total_user_amount.sql") type QueriesTotalUserAmountParams struct { Charge []bigqueryschema.Charge Users []bigqueryschema.Users } func (i *QueriesTotalUserAmountParams) ToMap() map[string]interface{} { return map[string]interface{}{ "querytest-demo.payment_service.Charge": i.Charge, "querytest-demo.user_service.Users": i.Users, } } func QueriesTotalUserAmount(t *testing.T, bq *bigquery.Client, i *QueriesTotalUserAmountParams, options ...querytest.Option) *querytest.QueryTest { t.Helper() qt, err := querytest.NewQueryTest(t, context.Background(), bq, QueryQueriesTotalUserAmount, i.ToMap(), options...) require.NoError(t, err) return qt } この 2つのファイルを利用して、コーダーは以下のようなテストコードを書くことができます。 package test import ( "context" "fmt" "testing" "cloud.google.com/go/bigquery" "github.com/a-r-g-v/querytest-demo/src/bigqueryschema" "github.com/a-r-g-v/querytest-demo/test/testqueries" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestAmount(t *testing.T) { userID := uuid.NewString() qt := testqueries.QueriesTotalUserAmount(t, bigQueryClient, &testqueries.QueriesTotalUserAmountParams{ Charge: []bigqueryschema.Charge{ { ChargeID: bigquery.NullString{}, UserID: ValidString(userID), Amount: ValidInt64(1000), Status: bigquery.NullString{}, CreatedAt: bigquery.NullTimestamp{}, UpdatedAt: bigquery.NullTimestamp{}, }, }, Users: []bigqueryschema.Users{ { UserID: ValidString(userID), Name: bigquery.NullString{}, ReferralType: ValidString("ORGANIC"), CreatedAt: bigquery.NullTimestamp{}, UpdatedAt: bigquery.NullTimestamp{}, }, }, }) result, err := bigQueryClient.RunQuery(context.Background(), qt.Query()) require.NoError(t, err) assert.Equal(t, 1000, result[0]["TotalAmount"]) } クエリテストの動作デモ 効果 この仕組みを導入したことにより、以下の効果がありました。 開発エンジニアによるテストへの効果 開発者テストの実施が容易になり安心してクエリを変更できるようになった テストデータのコーディングに型の支援を得られるようになった 列名やデータ種別の誤指定の防止 IDEによるコード補完の恩恵 テストデータの共通化やテーブルテストの活用が可能になり差分テストケースの追加が簡単になった 境界値のテストケースのような 1つの値だけを変更してテストを行うというようなケースの追加が簡単になった 共通化によりクエリに対するテストを網羅的に実施するコストが低下した 開発者テストケースの蓄積によりデグレート検知できるようになった 自動化されたテストケースが蓄積されたことによりクエリ変更に際するデグレートの検出が簡単に行えるようになった より安心感を持ってクエリ変更を行うことが可能になった QA エンジニアによるテストへの効果 テストデータの作成が効率化された 関係するマイクロサービスが多いこともあり、テストデータを作成するためにかなりコストがかかっていました。例えばテストしたいパターンが100通りある場合、手動でテストデータを100通り作成する必要があったのですが、この仕組みによりQA エンジニアはテストデータのパターンを考え、テストデータの投入をお願いするという形になりテストデータ作成にかかっていた工数はかなり削減されました。 ※今後はQAエンジニアでテストデータ投入まで行えるようになる予定です。 より精度の高いテストが行えるようになった 今まではテストデータの作成が困難で諦めていたテストパターンについてもテストが行えるようになりました。例えば時間の条件として2023年4月1日 0:00:00という条件があった場合、2023年3月31日 23:59:59と2023年4月1日 0:00:00のテストデータを作成する必要があります。ただ、こういったテストデータを手動で作成することは不可能に近く、厳密な境界値でのテストは諦めていました。 この仕組みを活用することでこのようなテストデータの作成も容易になり、今まで諦めていたテストパターンについてもテストが行えるようになったため、より精度の高いテストが行えるようになりました。 導入後の課題 上記のクエリテストの仕組みを導入することで複雑なクエリに対してもテストを行うことができ、クエリの修正時も安心感を持って修正作業を行うことができるようになりました。 一方でテストコードが拡充していく中で次のような問題に直面しました テストケースが増えることによるテスト実行時間の増加 テスト実行時間を抑えるためにテストの並列化を行ったことでBigQueryの最大同時実行クエリ数を超える割り当てエラーの発生 エミュレータの導入 上記の課題を解決するために、BigQueryのエミュレータを導入することにしました。エミュレータを利用することで、テストケースやテスト内で実行するクエリ数が増えても、BigQuery自体にリクエストが行われないため、安定したパフォーマンスが期待できます。 BigQueryでは公式のエミュレータが提供されていません。そこでメルペイ Architect の@goccy により作成されOSSとして公開されている bigquery-emulator を利用しました。 bigquery-emulator はGoで実装されたBigQueryのエミュレータサーバです。betaプロジェクトではありますが、すでに多くの機能が実装されています。 テストと同一のプロセスでエミュレータを起動することができるため、テストの前処理としてエミュレータサーバを起動し、BigQueryクライアントのリクエスト先に起動したエミュレータサーバを指定するように変更しました。 package test import ( "context" "testing" "cloud.google.com/go/bigquery" "github.com/goccy/bigquery-emulator/server" "github.com/goccy/bigquery-emulator/types" "google.golang.org/api/option" ) func NewClient(t *testing.T, useBQEmulator bool, projectID, datasetID string) (*bigquery.Client, error) { t.Helper() var opts []option.ClientOption if useBQEmulator { bqServer, err := server.New(server.TempStorage) if err != nil { return nil, err } if err := bqServer.Load( server.StructSource( types.NewProject( projectID, types.NewDataset( datasetID, ), ), ), ); err != nil { return nil, err } ts := bqServer.TestServer() t.Cleanup(ts.Close) opts = append(opts, option.WithEndpoint(ts.URL), option.WithoutAuthentication()) } ctx := context.Background() return bigquery.NewClient(ctx, projectID, opts...) } エミュレータの導入により、BigQueryの最大同時実行クエリ数を超える割り当てエラーを起こすことなくテストを実行できるようになり、テストの実行速度も改善され導入前後で約55%のテスト実行時間の削減が実現できました。 今後の展望 今後、さらに追加したい機能や応用の方法については以下の3つを考えています。 QAテストケースの置き換えの検討 クエリテストをQAフローにも導入することによりQAテストにおけるテストデータ投入の効率化ができましたが、現状はQAチームが作ったテストケースをもとにエンジニアがデータ投入用のテストロジックを作成・実行し、再度QAチーム側でそのデータを利用した確認を行っています。 QAテストにおいてもテストケースに応じて柔軟かつより容易なテストデータ投入からQAテストの実施、テストケースのメンテナンスをQAチーム側で完結できる仕組みを作成したいと考えています。 クエリテストのケース網羅性可視化 クエリのテストケースの網羅性を可視化するためのメトリクスを導入したいとチームメンバーで議論しています。 Goの通常のテストでは、コードカバレッジ等のテストケース網羅性を計算・可視化するためのメトリクスを簡単に利用できます。 SQLクエリに対してMC/DCカバレッジを使用する研究 があり、類似の仕組みを本手法にも導入していきたいです。 投入テストデータの正しさの検討 投入テストデータとマイクロサービスが実際に生成するデータに不一致がある場合、テストの意味がなくなってしまいます。現状はクエリテストに利用するテストデータを作成する際、依存マイクロサービスの振る舞いを理解してデータを作成しています。この不一致のリスクを最小限にするために、データインターフェースの明文化を検討したいと考えています。
アバター
こんにちは。メルペイ Engineering Engagement チームの mikichin です。 メルペイは単なる決済サービスではなく、新しい「信用」を基盤として、それに基づく循環型社会、なめらかな社会を創ることを目指しています。そのためには、お客さま・企業・金融機関など、さまざまなステークホルダーに対して「OPENNESS」な姿勢で向き合うことで、あらゆる世の中のお金の流れを、もっと身近なものに変えていきたいと考えています。 「Merpay Tech Openness Month」は、技術も「OPENNESS」にしていこうという考えのもと、2019年にスタートした企画です。 メルペイのエンジニア組織がテクノロジーでお客さまの課題解決を実現することを大切にし、その挑戦の中で得た知見を6月6日から約1ヶ月間に渡り毎日公開していきます!技術、開発設計や思想、組織ストラクチャー、Tips、その他最近の取り組みなど、幅広くお伝えします。 2019年は こちら 2020年は こちら 2021年は こちら 2022年は こちら ▼公開予定表 (こちらは、後日、各記事へのリンク集になります) Theme / Title Author Go言語によるSQLクエリテストの取り組み @a-r-g-v, @gen(sminamot) Rust製TypeScriptコンパイラstcの現状と今後 @togami2864 ページネーションのバグを解消した話 @yushi0010 社内用GitHub Actionsのセキュリティガイドラインを公開します @goro 新人編集長の技術書典14参戦記 @knsh14 メルペイ決済基盤における Source Payment による決済手段の抽象化 @komatsu メルコイン決済基盤における分散トランザクション管理 @sapuri Terraformモジュールを使ったCloud Spannerの設定標準化の取り組み @t-nakata なめらかなナレッジシェアリング文化を創る @tanaka0325 Resilient Retry and Recovery Mechanism: Enhancing Fault Tolerance and System Reliability @Amit.Kumar 非エンジニアのためのデータ集計環境について @katsukit メルペイ Tech PR が実際にまわしている PDCA サイクル @mikichin お手軽なグラフデータベース活用 @orfeon 与信モデル更新マニュアルを作成した話 @fukuchan テストコードの改革を進めている話 @r_yamaoka Cloud Tasksで外部APIへの流量制御をするときに考えたこと @panorama Designing iOS Screen Navigation for Best UX @kris Cloud ComposerとSecret ManagerでAirflowをセキュアにSlack連携する @champon Goでテスト用のフィクスチャを生成する @youxkei , @fivestar New Member として見たMerpay Tech Asset First Impression @nu2 TBD @kimuras どんな知見が得られるのか、毎日が楽しみです。 Merpay Tech Openness Month 2023 の1日目は、BackendEngineer @a-r-g-v と @gen(sminamot) が執筆予定です。 ひとつでも気になる記事がある方は、この記事をブックマークしておくか、 エンジニア向け公式Twitter をフォロー&チェックしてくださいね!
アバター
こんにちは!メルカリ Engineering Office チームの@aisakaです。 メルカリのエンジニア組織は、メンバーが相互に学び合い、メンバー自身が自走し、成長できる組織を目指し、「互いに学び合い、成長し合う文化」の醸成を行っています。 こうしたメルカリの「互いに学び合い、成長し合う文化」を体現する仕組みの一つが、社内技術研修「DevDojo」シリーズです。 昨年から、一部のDevDojoシリーズを外部公開( 参考 )していますが、今回さらに新しいコンテンツを公開することになりました! 今日のブログでは公開するセッションとその内容をご紹介します! Learning materials Website 技術研修DevDojoとは DevDojoは、技術開発を学ぶ場として「Development」と「Dojo(道場)」をかけ合わせて名付けられた完全In-houseの社内研修シリーズです。 シリーズを構成するコンテンツは多岐にわたり、メルカリ、メルペイのエンジニアの知見やアイディアが詰め込まれたものとなっております。(研修の全体像や概要は こちらのブログ で紹介しています。) 毎年4月と10月に実施しており、今年も4月に新卒社員が多く入社したタイミングで研修を行いました! また、研修は社内のメンバーであれば誰でも受講できるようにオープンにしており、今回も様々な組織に所属するメンバー50名ほどが参加しました。 公開コンテンツはこちら! メルカリのエンジニア組織は、半数以上が海外籍社員です。こうした背景からDevDojoの講義は、半分は英語、半分は日本語で行われるように調整しています。 すべての研修に同時通訳チーム(GOT)が入り語学のサポートをしています。 それでは、新たに公開したメルカリ、メルペイの8コンテンツをご紹介します! Introduction to Machine Learning (メルカリのMachine Learning入門) メルカリのユニークな機能の一つである写真検索機能は、膨大なデータをAIに機械学習させることで実現しています。このコンテンツでは、一般的な機械学習の考え方や、AI・MLの基礎知識について解説しています。また、メルカリでは実際にMLをどう実装しているのか、実際のプロジェクトについても紹介しています。 Slide英語 Design System for Mobile (メルカリのDesign System Mobile) 持続的に一貫したサービス体験をお客さまに提供できるよう、メルカリではDesign Systemにとても力を入れています。このコンテンツでは、モバイルにおけるDesign Systemの基礎知識から、メルカリで実際に行っているデザインの作り方、運用方法について解説します。 Slide英語 Introduction to Mobile Development (メルカリのモバイル開発入門) より使いやすいサービスを迅速に提供していくため、メルカリのモバイル開発はリリースサイクルや運用プロセスのルール化を行っています。このコンテンツでは、メルカリのモバイルアプリ開発において実際に運用している開発サイクルとプロセスについて解説しています。 Slide英語 Successful Scrum Team at Mercari (成功するスクラムチームとは) メルカリのプロダクト開発に取り入れられているスクラム開発 (Scrum) とはアジャイル手法のひとつで、少人数のチームに分かれ短期間の開発サイクルをくり返し行うフレームワークです。このコンテンツでは、基本的なスクラムの考え方と、メルカリにおける開発プロセス、そしてその目的を解説しています。 Slide日本語 / Slide英語 Introduction to Design Doc (メルカリのDesign Doc入門) プロダクト開発に必要なDesign Docの基礎知識を解説し、メルカリが今実際に使っているテンプレートを紹介します。また、良いDesign Docの書き方やメルカリでDesign Docをどのように使っているかについても説明しています。 Slide英語 Introduction to Authentification Platform (メルペイの認証基盤入門) 決済プラットフォームであるメルペイは、安全に外部通信を行うために認証と認可が必要です。このコンテンツでは、アカウントと認証、AuthN/AuthZに関する基本的な知識を解説し、メルカリグループの認証基盤について紹介しています。 Slide英語 KYC in Action (メルペイにおけるKYCの活用) メルペイは決済サービスを提供しているため、メルペイを利用して取引を行うお客さまには本人確認を実施しています。このコンテンツでは、KYCの基本的な知識やKYCの種類、メルペイでの活用について解説しています。 Slide英語 Quality Assuarance Policy (メルペイ品質保証ポリシー) 安心安全に早い開発サイクルでサービスを持続的に提供していくためには、Quality Assuaranceは非常に大切です。このコンテンツでは、どのようなQAのプロセス、ツール、テクニックで問題を迅速に特定し、解決しているのかを解説しています。 Slide日本語 / Slide英語 最後に 研修資料を社内だけでなくコミュニティに還元し、日本、海外のエンジニア業界全体の活性化に貢献できるよう、引き続きDevDojoシリーズのアップデートを行っていきます。 今回は講義の箇所をメインに公開しましたが、将来的にはHands-onのRepositoryなど実際に研修でHands-on練習用につかっているコードなども公開していきたいと思っております。 最後になりますが、社内で研修を実施し、そしてコンテンツを一般公開するには、公開箇所の選定、編集、ブランディング、レビュー等、様々な方々の協力が不可欠です。今回のコンテンツ公開にも、多くのエンジニアの方々、チームメンバー、セキュリティチーム、知財チーム、そしてデザインチームの協力があって実現できました。 また、メルカリグループでは、積極的にエンジニアを採用しています。ご興味ある方、ぜひご連絡お待ちしております! Open position – Engineering at Mercari
アバター