TECH PLAY

株式会社メルカリ

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

267

この記事は、 Merpay Tech Openness Month 2023 の20日目の記事です。 はじめに こんにちは。メルペイ VP of Platform Engineering の @nu2 です。 私は2023年5月に入社したばかりのNew Memberです。 入社後すぐに本企画への参加を @mikichin さんから打診され、お伝えするテーマに困りましたが「OPENNESS」マインドで今まで外から媒体を通して感じていたメルペイの技術アセット(Culture, Technology Stack, People)に対し、実際に肌で感じ取ったことを今回お伝えします。 Culture 「Go Bold, All for One, Be a Pro」 メルカリのバリュー(特にGo Boldについて)は、一度は聞いたことがある言葉ではないでしょうか。 これらのバリューを体現するための取り組みが本当に浸透しているとオンボーディングで感じますし、その成果として実際にメンバーの皆さんから無意識的にバリューを発揮している場面も見受けられました。 またメルペイのミッションである「「信用を創造してなめらかな社会を創る』を実現するために「なめらかな社会」の一部である企業内活動でもなめらかさを意識した行動を実践している方々がとても多いと感じました。 記事にあるやさしいトレーニング は日本語話者、英語話者間の分断を抑制する主たる行動だと思います。 日本語話者、英語話者が共に参加する会議体ではプレゼン資料もしくはドキュメンテーション内に「やさしい日本語」や「やさしい英語」と文面で注意喚起を促しています。 更にその会議体は書籍「amazonのすごい会議」で語られるようなテキスト中心の内容であり、MECE フレームワークに近い漏れのない議論が展開されます。 (「amazonのすごい会議」の内容が気になる方は是非メルカリをご利用ください :-)) 私も入社時に自分のUser Manual を英文で準備し、メルペイのミッションを達成するための自分の内なるミッションを公開しています。 Technology Stack 基本的には公開されている 情報 の通りです。 これらを使いこなすメンバーは「全員 Be a Pro」です。また、プロフェッショナルであろうと日々業務に向き合っている姿は尊敬します。 特筆すべきはScalability の元で各要素技術による品質が担保されていることです。 そのScalability を支えるArchitect チームが独立した組織として存在し、非機能要求グレード、非機能要件をなめらかに設計していくチームの存在はとても頼もしいと感じています。 後述するTech Fest でもArchitect チームからメンバーが登壇予定ですので是非視聴してくだされば幸いです。 また公開されている要素技術を使い倒すTechnique 指向だけではなく、要素技術を生み出すTechnology 指向も持っているメンバーも多いです。 またメルペイに限った話ではありませんが、グループ全体の社内業務におけるChatOps の活用がかなり進んでいると感じました。 COVID-19 の流行以降バーチャルオフィスと化しているSlack 上のオペレーションで業務が成立することは効率面で非常に有用です。 入社以前は会社全体の生産性を向上する為に技術選定や導入を推進する役割を担っていたので、追い求めていたChatOps がここにありました! People 行動をバリューに照合し評価する文化があるので、特にその人にとっての挑戦点が共有されていれば周囲が本当にサポートしてくれます。 英語を話す場合に私のCEFR(英語をはじめとした外国語の習熟度や運用能力を同一の基準で評価する国際標準)レベルがまだ低い状態なので、日々挑戦の連続なのですが特に英語話者の方々がサポートしてくださり最初の1on1 でもきちんとコミュニケーションが成立する成功体験を手助けしてくださいました。 また課題を設定し、それに対し自律的な行動や実践を行なっているメンバーが多いのは、連日更新される連載記事の内容からも感じ取れるのではないでしょうか。 改めて、「Merpay Tech Openness Month 2023」で公開されている記事を確認してみてください。 https://engineering.mercari.com/blog/entry/20230531-notice-merpay-tech-openness-month-2023/ まとめ 個人の感想をまとめており大変恐縮していますが、私は今後 VP of Platform Engineering として事業成長につながる取り組みをエンジニアリングから支えていきたいと考えています。メルペイでは事業を的確なタイムラインで成長させるモメンタムを描いており、エンジニアリングへの要求は成長に応じるScalability、それに伴うコストを最小限にとどめること、インシデントを抑制することなど総じて高いです。継続的かつ安心安全なサービスデリバリーを実現するためにブロッカーとなる要素を除きメルペイのミッションを達成するためにひとつひとつ取り組んでいきます。 最後になりますが、8月22日(火)から8月24日(木)までの3日間にわたり、「Merpay & Mercoin Tech Fest 2023」をオンライン開催します!本イベントにてより解像度の高い内容の話を公開予定となっています! 是非とも参加登録の上、ご視聴ください。 https://events.merpay.com/techfest-2023/ 明日の記事は CTOの@kimuras さんです。引き続きお楽しみください。
アバター
この記事は、 Merpay Tech Openness Month 2023 の19日目の記事です。 こんにちは。メルペイのバックエンドエンジニアの @youxkei と@fivestarです。 前回の記事 「Goでテスト用のフィクスチャをいい感じに書く」 では、fixtureパッケージを導入することで、テスト用のデータベースのフィクスチャを以下のような点で「いい感じに」記述できるようになりました。 モデルのIDのセットなどの自明な処理が暗黙的に行われる 記述した際のコードのネストがモデルのリレーションを表す その際、マッピング用のモデルが必要な場合は暗黙的に用意される fixtureパッケージを使用することで、テストケースに必要な値をモデルにセットしつつ、モデル間のリレーションがわかりやすい形でフィクスチャを記述することができます。 各モデルに対応するマッピング用の関数はほぼ定形なので、これを自動生成することで汎用的に使うことができそうです。 そこで、モデルとなる構造体一覧からfixtureパッケージを生成するツールyofixtureを作成しました。 yo のジェネレータシステムをベースに実装したので、yoにあやかってツールの名前をyofixtureとしました。ただ、yofixtureはyoで生成したモデル以外でも使用することができます。 yofixtureによるfixtureパッケージの生成 前回の記事と同様に、具体例として以下のような図書館蔵書モデルを考えます。 package models type Library struct { LibraryID string Name string } type Book struct { BookID string Name string LibraryID string } type Author struct { AuthorID string Name string } type BookAuthorMapping struct { BookID string AuthorID string } yofixtureでは、CLIで以下のようなyamlの設定ファイルからfixtureパッケージのソースコードを生成することができます。 models: - name: Library relations: - Book: { LibraryID: LibraryID } - name: Book - Author: {} - name: Author - name: BookAuthorMapping 設定ファイルでは、モデルとそのリレーションを設定することができます。 ここでは、前回の記事の具体例で使用した図書館蔵書モデルと、LibraryとBook、BookとAuthorのリレーションを定義しています。 LibraryとBookのリレーションについては「Book.LibraryIDにLibrary.LibraryIDをセットする」という形で定義しています。フィールドの値をセットする形であれば、設定ファイルでリレーションを定義できます。 BookとAuthorのリレーションについては、BookAuthorMappingを介したリレーションのため、設定ファイルでは定義できません。 このような複雑なリレーションを実現するために、yofixtureはプロトタイプパターンを用いて既存のリレーションの挙動を変更できるようなコードを生成します。 BookとAuthorのリレーションは、以下のように生成したfixtureパッケージ内でリレーションを定義できます。 package fixture import ( "testing" "path/to/models" ) func init() { prototype.ConnectToBook = func(tb testing.TB, fixt *Fixture, book *models.Book, connectingModel any) { tb.Helper() switch connectingModel := connectingModel.(type) { case *models.Author: // BookとAuthorのリレーションの場合、BookAuthorMappingを追加する fixt.AddBookAuthorMapping(tb, fixt, prototype.CreateBookAuthorMapping(func(m *models.AddBookAuthorMapping) { m.BookID = book.BookID m.AuthorID = connectingModel.AuthorID }), ) } // デフォルトの処理 connectToBook(tb, fixt, book, connectingModel) } } このように、fixtureパッケージに生成されるデフォルトのprototypeを拡張することで、BookからAuthorへのリレーションを張る際の独自の処理を定義することができます。 さらに、prototypeの拡張によって、以下のようにモデルを作成した際のフィールドのデフォルト値を定義することができます。 package fixture import ( "testing" "github.com/google/uuid" "path/to/models" ) func init() { prototype.CreateLibrary = func(setters ...func(l *models.Library)) *models.Library { l := &models.Library{ // デフォルト値をセット LibraryID: uuid.New().String(), } for _, setter := range setters { setter(l) } return l } } yofixtureで生成したfixtureパッケージを使う 生成したfixtureパッケージは、前回の記事と同様に使うことができます。 import ( "testing" "path/to/fixture" "path/to/models" ) func TestListBooksByAuthor(t *testing.T) { author := fixture.Author(func(a *models.Author) { a.Name = "夏目漱石" }) f := fixture.Build(t, fixture.Library(func(l *models.Library) { l.Name = "ほげ図書館" }).Connect( fixture.Book(func(b *models.Book) { b.Name = "吾輩は猫である" }).Connect(author), fixture.Book(func(b *models.Book) { b.Name = "こころ" }).Connect(author), // 同じauthor ), ) setupDB(t, f.Collect()) // 以下テストコードが続く } まとめ fixtureパッケージを生成するyofixtureを作成しました。 yofixtureは現在社内ツールとして使われていて、オープンソース化も検討しています。ご期待ください! 明日の記事は @nu2 さんです。引き続きお楽しみください。
アバター
この記事は、 Merpay Tech Openness Month 2023 の18日目の記事です。 はじめに メルペイでBackend Engineerをしている @champon です。 普段はApache Airflow(以下、Airflowと呼ぶ)を用いた与信枠計算パイプラインの運用をしています。 この記事では、Cloud Composer(以下、Composerと呼ぶ)を用いたAirflowからSlack通知を行う実装例について紹介します。 AirflowにおけるSlack連携 メルペイの与信枠計算では、データパイプラインとしてComposerを用いたマネージドなAirflowを採用しています。Airflowでは、有向グラフ上に定義したタスクを順次実行していくワークフロー(DAG)を構築することができます。自分のチームでは、Airflowを運用する上で主にアラートの用途として、DAGが失敗したときにSlackに通知が送られるようにしています。これにより、DAGの失敗にチーム全員が気付けるようになり、またより早く修正対応に取り組むことができるため、総合的に運用コストを下げることができます。 以降では、AirflowのSlack連携において、Secret Managerを導入したよりセキュリティの高い実装例について記載します。 Secret Managerについて Secret Managerは、APIキーなどの機密性の高いシークレットデータを暗号化して保存することができるGoogle Cloud Platform(以下、GCPと呼ぶ)上のサービスです。シークレットデータのバージョニングも行えるため、汎用性高く使用することができます。 さて、Slack連携をするためにはSlackで発行したトークンが必要になります。このトークンは機密性の高い情報であるため、露出した場所に保管するのはリスクがあります。例えば、Composerでは環境変数を設定することができますが、設定した値はGCPコンソール上で直接確認できるようになっています。該当するGCP Projectのコンソールにアクセスできるユーザーは限られてはいますが、暗号化されてない上に露出した形で保管されているのは良くないです。 また、環境変数への設定はシークレットデータのローテーションにおいても不都合が生じます。万が一シークレットデータが漏洩してしまった場合、以前まで使っていたシークレットデータを失効し、新しいものに設定する必要があります。しかし、Composerでは環境変数の更新に際してComposer環境が再起動されます。Composer環境の再起動には数十分の時間を要するため、開発の遅延やスケジュール実行との競合などが発生する恐れがあります。 そこで、SlackのAPIトークンをSecret Managerで管理することで、Composerと切り離した形で運用することができ、より安全かつ柔軟性を高めることができます。 Cloud ComposerからSecret Managerにアクセスする ComposerからSecret Managerにアクセスするには、terraformにおいて以下のように定義し、Composerのサービスアカウントに対してRoleを付与します。 resource "google_service_account" "composer_service_account" { project = “my-project” account_id = "composer-service-account" display_name = "A service account for composer" } resource "google_secret_manager_secret" "slack_api_token" { project = “my-project” secret_id = "slack-api-token" } resource "google_secret_manager_secret_iam_member" "composer_service_account_is_secret_accessor_to_slack_api_token" { project = “my-project” secret_id = google_secret_manager_secret.slack_api_token.secret_id role = "roles/secretmanager.secretAccessor" member = "serviceAccount:${google_service_account.composer_service_account.email}" } Secret Managerからシークレットデータを取得するためには、GCPのClient Libraryを用いることで実現可能です。Pythonでは、SecretManagerServiceClientのaccess_secret_versionメソッドを用いてSecretのIDおよびVersionを指定することで、シークレットデータを取得できます。 client = secretmanager_v1beta1.SecretManagerServiceClient() name = client.secret_version_path(“my-project”,”slack-api-token”,”latest”) response = client.access_secret_version(name=name) secret_data = response.payload.data.decode(“utf-8”) AirflowではPythonOperatorを使用することでPythonでDAGのタスクを実装できるため、タスク単位でAPIトークンを取得してSlack通知を行うことも可能です。 Airflow Connectionを使う方法 先の節では、GCP Client Libraryを用いてSecret Managerからシークレットデータを取得しました。しかし、Airflow Connectionという機能を用いることで、Secret ManagerとAirflowを直接連携できます。 まず、Airflow ConnectionでSecret Managerをbackendにするために、terraformにおいてgoogle_composer_environmentのairflow_config_overridesを設定する必要があります。また、Slack通知との連携を行うためのパッケージを別途インストールするようにします。 resource “google_composer_environment” “my_composer” { config { software_config { airflow_config_overrides = { secrets-backend = "airflow.providers.google.cloud.secrets.secret_manager.CloudSecretManagerBackend" } pypi_packages = { apache-airflow-providers-slack = "" } } } } ※基本的な設定項目は省略しています また、Airflow Connectionを使用するためには、Secret ManagerのSecret IDのprefixを’airflow-connections’に指定する必要があります。 resource "google_secret_manager_secret" "slack_api_token" { project = “my-project” secret_id = "airflow-connections-slack_api_token" } 次に、Secret ManagerにJSONフォーマットでデータをアップロードします。 { “conn_id”: “slack_api_token”, “conn_type”: “slack”, “password”: “<YOUR SLACK API TOKEN>” } ※JSONフォーマットを使用する場合はapache-airflow>=2.3.0である必要があります。代わりにURIフォーマットも使用可能です ※conn_idはSecret IDからprefixを取ったものとなります あとは、SlackAPIPostOperatorを用いて、Slack通知を行うOperatorを簡単に実装することができます。 from airflow.providers.slack.operators.slack slack = SlackAPIPostOperator( task_id=”slack-notification”, channel=”#test-channel”, conn_id=”slack_api_token”, text=”Hello World”, ) まとめ 本記事では、Composerで構築したAirflowにおいて、Secret Managerを用いたSlack連携の実装例について紹介しました。GCPのドキュメントを読むとAirflow Connectionを用いる方法が推奨されていますが、同じSecretを別のサービスにも用いておりSecret IDを変更したくない(後からprefixを付けたくない)等の理由があれば、より柔軟に対応できるClient Libraryを用いる方法が良いかもしれません。 明日は @youxkeiさんと@fivestarさんの記事です。引き続きお楽しみください。
アバター
この記事は、 Merpay Tech Openness Month 2023 の16日目の記事です。 こんにちは。メルペイのバックエンドエンジニアの @panorama です。 今回はメルカードのバックエンドにおいて「外部APIへのリクエストの流量制御を実現するためにCloud Tasksを導入した話」をご紹介します。 背景 メルカードのバックエンドでは提携している企業さまのAPIをさまざまな処理で呼び出しています。(以降このAPIを外部APIと呼びます。) メルカードをご利用いただいているお客さまが増えるにつれ、通常のご利用時やカスタマーサポートでこの外部APIを呼び出す処理も増え、急激に負荷がかかることも発生するようになりました。 もし、時間当たりの処理件数が外部APIの処理速度を上回ってしまうと処理が失敗してしまいます。 しかし外部APIは自社内のマイクロサービスとは異なり、自分たちで自由にスケールすることはできません。 対策 外部APIの呼び出しは同期的でなくても良いケースがあります。 また今回の高負荷時の調査で「同期的でなくても良い呼び出し」が同時に複数起こっているケースを観測していました。 これらを非同期の呼び出しに変更し、処理レートを設定することで、非同期化できた部分の負荷を一定以下に制御することができます。 今回の課題は瞬間的な高負荷(スパイク)への対処です。 負荷が上がっている場合の最もシンプルな対策は外部APIのスケールを依頼することですが、平常時は現在の処理速度で問題なく、一瞬の高負荷な状態さえなだらかなものに変えることができれば解決できます。 実現手段 上記の実現方法としては、例えば以下のような案が考えられます。 アプリケーションレイヤーで非同期化する 定期実行バッチで非同期化する 流量制御が可能なキューイングのマネージドサービスで非同期化する それぞれ 言語の並行処理・並列処理の機能を用いて非同期化し、非同期化された処理全体のレートが一定以下になるように連携する 処理対象をデータベースに一時的に記録し、その記録をもとにバッチ処理で回収する マネージドサービスがサポートする設定項目で自サービスの求める流量制御を実現する ことによってその処理レートを設定します。 メルペイではGo/k8s/GCPを使用しているので、1はgoroutine、2はCronJob、3はCloud Tasksが該当します。 1に関して「複数のpodで複数のgoroutineが動く環境全体の外部API呼び出しのレートを一定以下にする」ということをアプリケーションレベルで実現するのはかなり実装コストが掛かります。また「非同期化の対象が増えたときに容易に追加できる」「対象毎に処理レートの設定が可能な」汎用的な仕組みである必要があります。 2の方法はシンプルですが、高負荷時以外の多くの場合でバッチに拾われるまでに無駄な遅延が発生します。(CronJobのスケジュールの間隔は最短でも1分) 一時的なアクセス増においてのみ外部APIの呼び出しをなだらかにしたいという目的だったので、それ以外ではほとんど即時に処理されてほしいです。 3のCloud Tasksを選択した場合はその特徴から上記1、2の問題点は解消されます。 フルマネージドサービスなのでキュー管理を意識する必要がない メルペイではGoogle CloudのリソースをTerraformで管理しているため、キューの追加やレートの変更はtfファイルの変更で容易にできる 一定のレートを超えないように流量制御するが、それ以下のときはほとんど即時実行される(つまりスパイクのみなだらかにしてくれる) また標準でリトライ機構があるため、一時的に外部APIが不安定になり失敗した場合でも、自前でリトライ処理を実装する必要がありません。 今回は使用していませんが、タスクを実行する時間を指定するスケジューリングの機能や重複排除などもサポートしています。 一方で、今回は非同期化対象が外部APIなので、 外部APIの認証情報をCloud Tasksに持たせたくない リクエスト/レスポンス時のログを自社サービス内で落としたい(ロジックに通したい) という事情がありました。 よってこれらを解決しつつ、Cloud Tasksの利点を活用することにしました。 前提知識 先程の「実現手段」でCloud Tasksの特徴を挙げましたが、ここでCloud Tasksについて説明しておきます。 Cloud Tasks Cloud TasksはGoogle Cloudが提供する非同期タスク実行を行うためのフルマネージドサービスです。 タスクと呼ばれる単位をキューに送信すると、非同期に取り出されてワーカーに送信されます。(タスクをワーカーに割り当てることをディスパッチと呼びます。) ディスパッチのレートにはトークンバケットアルゴリズムという流量制御のアルゴリズムが使われており、大量のトラフィックが来てもバケットサイズ、レート設定で定めた基準以下に抑える(均一にする)仕組みになっています。 ディスパッチ後、ワーカーから2xxのHTTPレスポンスが返ってくるとタスクは完了されたとして消去されます。2xx以外のレスポンスコードが返ってきた場合やリクエストがタイムアウトした場合はリトライに移行します。 Cloud Tasksのユースケースはたくさんありますが、 公式のガイド には”Managing third-party API call rates”が含まれています。 このようなキューイングのサービスは他にもあり、AWSだとAmazon SNS、AzureだとAzure Queue Storageなどがあります。 今回の構成 通常であれば以下のように直接外部APIを呼び出す構成になると思います。 しかしこの場合はCloud Tasksが外部APIの認証情報を持ち、ログはCloud Tasksの実行ログとして落ちます。 そこで今回は次のように一度自分のサービスをproxyのように経由させて外部APIの認証を乗せています。 これによって 外部APIの認証情報をCloud Tasksに入れる必要がない リクエスト/レスポンスをロジックにかけて処理することができる ログを自サービスに落とせる 失敗した場合に記録したり、失敗の詳細をSlackに通知したりできる などのメリットがあります。 つまりCloud Tasksを純粋にタスクの非同期タスク実行管理の目的で使っています。 このやり方のデメリットとしてはCloud Tasksからの呼び出しで自サービスを経由する分だけ、自サービスのトラフィックは増加します。 今回非同期化の対象となった部分は全体を通してリクエスト数はそこまで多いわけではなく、平常時と高負荷時の差がかなり激しいケースだったためこの部分はあまり大きな問題ではありませんでした。 また副次的な効果としてリトライ機構があるので、サービスが不安定になったりメンテナンスに入ったとしても非同期タスクが失われることを考慮する必要がありません。 まとめ 今回は非同期化可能な外部API呼び出しの流量制御においてCloud Tasksを使った例を紹介しました。 一般的なユースケースに比べて少し特殊な例だったかもしれませんが、今後非同期タスク実行を検討するときの選択肢として本記事の知識がお役に立てば幸いです。 私は今まで非同期実行についてそこまで深く考えることはなく、Cloud Tasksを使用するのも初めてだったのでとても勉強になりました。 こういうパターンで他に良い方法や面白い知識があれば、教えていただけるとうれしいです👀 それでは、ありがとうございました。 明日の記事は@krisさんです。引き続きお楽しみください。 ※ 追記: Cloud Tasksからメルカードバックエンドへの通信経路や認証については省略しています
アバター
はじめに この記事は、 Merpay Tech Openness Month 2023 15日目の記事です。 こんにちは。メルペイ加盟店精算チームのバックエンドエンジニア @r_yamaoka です。 今日は現在自分がリードして取り組んでいるテストコードの改善について紹介したいと思います。 抱えている課題  私が所属している加盟店精算チームのマイクロサービスは加盟店さま向けサービスとして欠かせないものであり、メルペイ最初期から存在するサービスです。他のマイクロサービスにあまり無い特徴として多数のバッチ処理を行っている点が挙げられます。  お客さま(メルペイユーザー)がお店で行った決済は、一定の頻度で集計し決済手数料を差し引いた上で加盟店さまの銀行口座へ振り込むことになります。 最終的な振込金額を算出するまでの流れとしては 個々の決済金額のリコンサイル(会計マイクロサービスとの金額照合) 日次集計 締日集計・返金分相殺・振込データ作成 振込指示 といった複数の処理を順に跨ってデータが処理されていきます。またこれら決済金額を直接集計するもの以外にも実行された振込の結果を取得してシステムに反映させたり、各バッチが作成したデータに誤りや矛盾が無いかをチェックしたりするバッチがあり、そのデータの流れは極めて複雑です。  こうした複数のデータ集計を伴う処理は前提条件が複雑になりがちで通常のテストだけでは動作を担保することが難しく、時折トラブルにより集計データの修正や整合性の確認をエンジニアが手作業で行うケースが発生し、品質面の改善が必要な状況となっています。  また長い時間を経てデータ構造やアーキテクチャーが最善とは言えない状態にもなっておりその解消のためにリアーキテクチャーを検討しています。この場合、変更したコードが既存の動作を破壊していないことを担保するテストコードは非常に重要ですが、前述した複雑さによりテストの網羅性が低くまたテストの可読性が低いことも相まって安全なリアーキテクチャーに自信を持てていないのが現状です。 解消のためのアプローチ テスト粒度とその責務を分類する  テストの粒度は一般に「単体テスト」「結合テスト」等と2つ程度に別けられることが多いかと思いますが、これらは実質的に大小関係の定義のみで実際に何をどこまで担保するのかについては人・組織によりバラつきがあります。  私のチームのテストコードも例に漏れず主に「ユニットテスト」「E2Eテスト」が存在しますが、ユニットが簡素な代わりにE2Eで非常に多くのパターンを実装して網羅していたり、反対にユニットは充実している代わりにE2Eが非常に簡素であったりと統一感がありません。  このためテストケースの認知負荷が高く日々の開発で苦慮しており、まずは粒度の分類とその責務を定義しコードを整理するための基準を作ることにしました。  この問題に対する解の一つとして とあるGoogle Testing Blogの投稿 ではSmall, Medium, Largeと3つに分類しそれぞれでどの機能をどこまで使うのかについて定義されています。 (注: 13年前の記事なので現在もこのような分類を運用しているかは不明です)  当初はこれに倣ったテスト構成を考えていたのですが、SMLでは規模の分類に絞った命名であり大分類として何をテストするのかが依然として分かりづらく感じました。そのためSMLを参考にしつつテスト範囲が狭い順に「ユニット」「コンポーネント」「インテグレーション」という説明的な命名を採用し、解釈のバラつきはドキュメントの作成と丁寧な説明でカバーすることとしました。  ユニットは個々の関数やメソッドを対象としたテストで、データや条件分岐の面で高い網羅性を持たせることを主眼においています。コンポーネントは各種APIやバッチ単体を対象とし、マイクロサービスを構成する個々のコンポーネントが正しく動作するか確認します。インテグレーションは複数のバッチを跨いで最終的に正しいデータが得られるかを確認します。  基本的な考え方としてはカバーする範囲が狭い低コストなテスト(個々の関数・メソッド等の粒度)ほど網羅性を高くし、反対に範囲が広く高コストなテスト(API・バッチ等の粒度)ほど網羅性を低くするピラミッド型としています。 参考(Testing Pyramid): https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html  範囲が広いテストで網羅性も担保しようとするとコードの複雑性が増してメンテナンスが困難になるだけでなく実行時間が伸びます。また安定性を欠いた所謂FlakyTestになりやすく逆に開発の足を引っ張りかねないため、どの粒度でどこまでを担保するのかを明確にするのが重要です。  各分類にどの程度労力をかけるべきかについては様々な議論があるかと思いますが、 Google Testing Blogの記事を参考に 概ね以下のように考えています。 ユニット: 5 コンポーネント: 3 インテグレーション: 2  コンポーネントとインテグレーションテストの割合が増えていますが、これは前述の通り複数の処理をまたがって行われる複雑な処理を検証しなければならないという特性をカバーする意図です。  以下に3つの分類とその責務をまとめます。 ユニットテスト 責務: コードの細部において高い網羅性を持った検証を行う 条件分岐や投入データのパターンを可能な限り網羅する レイヤー内に閉じたテストを指す レイヤーを跨ぐコード(usecaseからrepositoryの呼び出し等)はモックorスタブで対応する 外部コンポーネント(マイクロサービス、Pub/Sub、GCS等)との通信もモックorスタブで対応する  ※コードのアーキテクチャーとしては、概ね一般的なレイヤー分けをしたクリーンアーキテクチャーと考えて頂いて差し支えありません。 コンポーネントテスト 責務: サービスのコンポーネント単体として一気通貫な動作(各APIのリクエスト〜レスポンス or バッチ単体での起動〜終了まで)を検証する 仕様書を網羅すること 試験環境(各個人の端末やCI環境)内で加盟店精算サービスを稼働させるテストを指す 外部マイクロサービスとの通信を要する箇所は極力 bufconn と社内テストフレームワークでエミュレートする 参考: https://engineering.mercari.com/blog/entry/gears-microservices/ インテグレーションテスト 責務: 複数のバッチを通して行われたデータ処理の正当性を検証 基本的な動作環境はコンポーネントテストに準ずる バッチAで処理したデータをバッチBで処理しその値をチェックする、というようなテスト 網羅性は追求せず主に正常系と重要な異常系のみに絞る 記述スタイルを統一する  粒度の分類ではテストコードを整理し認知負荷を下げることを目指していますが、個々のテストの記述方法を統一することで更にその効果を高めることができます。そのためテスト実装の細かいスタイルについても話し合い、これもチームとして合意しました。  参考として以下にいくつか例を挙げます。 値の検証にはアサーションを使用する  Goでは標準でテスト結果のアサーション機能が提供されていません。これは「エラーメッセージは重要なので自ら考えて書くべき。記述のコストは高いがエラー分析やオンボーディングが楽になるので回収できるはず」というGoの思想によるもので、標準提供の関数とif文等の条件分岐を使い素朴な形で実装することが推奨されています。 参考: https://go.dev/doc/faq#assertions  しかし、自分が経験した限りでは手動ではどうしても手間がかかることと複数のエラー要因について配慮したメッセージを記述しようとすると冗長になることが多く、結局のところ t.Errorf(“want %v, got %v”, want, actual) というようなあまり中身の無いエラーメッセージになりがちです。またerrorやstructの検証のようなコードはそれなりに難解な記述になってしまい、あまり恩恵を感じられていません。  それであるならいっそアサーションライブラリをを利用する方が簡便に記述でき、エラー分析はデバッガー等を駆使することでカバーすればよいというのが今のところの自分の考えです。  ライブラリには testify と go-cmp を採用しています。基本的には前者でチェックしますが、主にstructやproto messageはエラーメッセージの可読性や記述の容易さから後者でチェックしています。  未だ議論の余地がある事柄と思いますので、これが絶対の正解というわけではないですが少なくとも我々のチームとしてはこちらの方が合理的であるという判断のもと採用することにしました。 原則としてテーブル駆動で記述する  既存のテストコードには以下のような記法が散見されています。 TestXXX(t *testing.T) { t.Run(“test pattern1”, func(t *testing.T) { // do something }) t.Run(“test pattern2”, func(t *testing.T) { // do something }) }  前述の通り、加盟店精算サービスのテストは前提条件が複雑でモックやデータベースのセットアップの記述が難しいため、この記法にも一定の合理性があると言えます。しかし、このまま網羅性を向上しようとすると重複部分が多く保守性に難が生じることが予想されます。また再利用部分が無いため個々のテストケースを最後まで読まないとどのような検証が行われているのかわからなく認知負荷が高いことも欠点です。  そこで基本に立ち返りテーブル駆動で統一することに決め、複雑なセットアップは以下のように各ケース毎にfuncで定義することにしました。 参考: https://github.com/golang/go/wiki/TableDrivenTests setupMock func(ctrl *gomock.Controller) (*mock.MockXXXService, *mock.MockYYYService) prepareQueries func(ctx context.Context) []*spanner.Mutation  こうすることでパターンの記述は少々長くなってしまいますが、ケースが増えても重複が膨れ上がることがなく検証内容とパターンを別けて読み込め認知負荷を下げることができます。 テストでも命名を省略しない  まず最初のケースですが下記サンプルのk, vとは何でしょうか?testCasesはテーブル駆動のケース定義と考えられるので、大概map[string]struct{ … }型と類推はできますが必ずしもそうとは限りません。vという変数は生存期間が極短い使い捨て変数として使いたくなる名前なので、下に続く検証部分が長くなった中でうっかり別の用途として使ってしまうと混乱を引き起こすかもしれません。 for k, v := range testCases { t.Run(k, func(t *testing.T) { ….  代わりにname, tc等とするとどうでしょう。明らかに理解が容易になったことがわかるかと思います。k, vとタイプの手間はほぼ変わらないのでこの僅かな手間は惜しまない方がよいです。  なお、必ずしも直接的な命名を採用する必要は無く、可読性が担保できるのであればどのようなものでも問題ありません。IDEやVSCodeの自動生成の場合テストケースは tt とされることが多いようなのでこういった慣例やチームの標準に倣うのもよいでしょう。 for name, tc := range testCases { t.Run(name, func(t *testing.T) { ….  次のケースです。このretはreturn(戻り値)から命名されたのだと思いますがさて何が入っているのでしょうか?errorかもしれませんし新規発行されたユーザーIDか、はたまたUser型のstructのポインターかもしれません。 ret := createUser(ctx, userName) assert.NotEmpty(t, ret)  これもやはり説明的な変数を採用し、何が入っているか一目でわかるようにするべきです。 userID := createUser(ctx, userName) assert.NotEmpty(t, userID)  流石にプロダクションコードでこういった命名がされることはないのですが、特に小規模なテストではついやってしまいがちです。しかし、書いた時点では正しく認識できていても2,3日もすれば書いた当人すら忘れてしまいますし、コードは日々成長していくものなので少々の手間は惜しまず、将来に渡って理解容易性を損ねないよう常に細部まで気を配るべきです。 自らサンプルを実装する  これまでの取り組みによってテスト改善のための方針をまとめ、チームのエンジニアに認識してもらうことはできたはずですが、本件をリードしているエンジニア(私)以外のメンバーの頭にある姿は微妙に異なっている可能性が高いです。また0から書いたコードをレビューに出すのは少々勇気が必要かもしれません。  そこで既存テストが無い部分については、最初にサンプルとなる実装を行い他のエンジニアが参照できるようにしておき、既存のものがある場合でもいくつか新しい形への移行を行います。こうすることで全員の認識を揃え、バラつきをより抑えることができるでしょう。その後は積極的にコードレビューに参加し、あるべき形へ誘導していくことも重要です。 おわりに  変化の激しい今日ではシステムは常に改善されていくものであり、その礎となるテストコードは決して軽視できません。既存のテストが膨大なためこの取り組みはまだ完了していません。しかし、一部新しい書式で書かれている部分については良好な感触を得ており、品質の向上とリアーキテクチャーの遂行に貢献できると確信しています。  今回の記事が皆様のテストコード改善の参考となれば幸いです。  明日の記事は @panorama さんです。引き続きお楽しみください。
アバター
はじめに こんにちは、mercari.go スタッフの monkukui です。 6月15日にメルカリ主催の Go 勉強会 mercari.go #22 を YouTube でのオンライン配信にて開催しました。 この記事では、当日の各発表を簡単に紹介します。動画もアップロードされてますので、こちらもぜひご覧ください。 Goの標準ライブラリに学ぶジェネリクス 1つめのセッションは tychy16 さんによる「Goの標準ライブラリに学ぶジェネリクス」です。 発表資料: https://speakerdeck.com/tychy/gonobiao-zhun-raiburarinixue-buzienerikusu Go1.18 でリリースされたジェネリクスの機能に関して、標準ライブラリでの使用例を題材に紹介しました。ジェネリクスの使い所や、使うことで得られる恩恵などを掘り下げたあと、ジェネリクスを使うことが難しいユースケースなどにも触れました。 Go1.21 で標準パッケージに追加される slices/map の紹介や、Go におけるジェネリクスの今後の展望などの話もあり、非常に興味深い話が盛りだくさんでした。 業務でジェネリクスの導入を検討している方にはとても参考になる発表になっているかと思いますので、興味がある方はぜひご覧ください。 Hashicorp/raftからraftを学ぶ 2つめのセッションは toshinao_ さんによる「Hashicorp/raftからraftを学ぶ」です。 発表資料: https://speakerdeck.com/t10471/hashicorp-raftkararaftwoxue-hu raft とは、複製されたログを管理するための分散合意アルゴリズムであり、etcd や cunsul などで用いられています。発表の前半では、raft に関する 2 つの論文を要約し、raft アルゴリズムの詳細な説明がされました。発表の後半では、Hashicorp/raft パッケージの具体的なコードに触れながら API の仕様や実装の詳細についての説明がされました。 raft に関する理論的な特徴から、Hashicrop/raft の具体的な実装まで広く深く紹介されており、非常に興味深い発表でした。 raft に関する論文や、参考書籍などは以下を参照してください。 https://github.com/ongardie/dissertation#readme 最初に公開された博士論文 https://raft.github.io/raft.pdf ↑ の論文からコンセンサスアルゴリズムの部分を抽出した論文 https://www.oreilly.co.jp/books/9784873119977 raft を使ったシステムのハンズオンが記載されている Go再入門 3つめのセッションは ques0942 さんによる「Go再入門」です。 発表資料: https://speakerdeck.com/ques0942/golangzai-ru-men 元々 Go メインで開発を行っていたが、一度 PHP 使いに転向し、再度 Go に入門した経験を持つ @ques0942 さんによる、 Go を学び直す上で得られた知見や、考え方の変化について紹介します。 発表の前半では、Go のインターフェースの使い方について紹介しました。他の言語と比較しながら、その使い方について掘り下げました。発表の後半では、Go のエラーハンドリングについて紹介し、標準ライブラリと、よく使われていましたがアーカイブされてしまった pkg/errors や、サードパーティライブラリの morikuni/failure について、それぞれの特徴に触れながら説明が行われました。 他言語を用いた経験が豊富な ques0942 さんだからこそ見えてくる Go の特徴などが語られており、非常に興味深い発表でした。 おわりに 今回は、Go 言語の様々なライブラリを題材として、Go を用いて開発する人にとって幅広く有用な内容をお送りしました。内容はどれも奥深く、運営としても非常に勉強になりました。 ライブで視聴いただいた方も録画を観ていただけた方も本当にありがとうございました! 次回の開催もお楽しみに! イベント開催案内を受け取りたい方は、connpassグループのメンバーになってくださいね! メルカリconnpassグループページ
アバター
この記事は、 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 技術書典チャンネル でも様々な情報を公開しているので、事前に過去の放送分を確認してみると良さそうです。 今回はなんとか人も集まって色々な締め切りにも間に合い素敵な本を出すことができましたが、反省点も多かったので、次に活かせればと思います。
アバター