TECH PLAY

株式会社メルカリ

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

269

こんにちは!Merpay Engineering Enagement Team の@mikichinです。 来たる8月22日(火)から8月24日(木)までの3日間にわたり、「Merpay & Mercoin Tech Fest 2023」をオンライン開催します! テーマは「Unleash Fintech」。メルペイ・メルコインのこれまでの技術的な取り組みはもちろん、メルカリグループのFintech事業における新たな挑戦をお伝えします。メルペイ・メルコインが今後どのように“Unleash(解放)“していくのか、ぜひご自身の目と耳で確かめてください!! 肝心なトークセッションは、昨年全20セッションだったところ、今年は全33セッションにパワーアップ! 本記事では、22日のトークセッションの見どころをご紹介!2日目はBackend、Data & Modelingを中心としたテーマをお届けします! まだ申し込みをされていない方も、興味のあるセッションがあるはずです。お申し込みは こちら からお願いします。 [12:10〜12:40] メルペイのあと払いとスマートマネーを支える返済基盤マイクロサービスの進化 本セッションでは、事例を通して既存マイクロサービスの分割やデータマイグレーションなどをご説明します。マイクロサービスアーキテクチャの最適化に興味や課題を持っている方にとって参考になれば幸いです。 [12:40〜13:10] 拡張性を備えたソフトウェア設計 In this session Rupesh will share the challenges and accomplishments while building EGP(Engagement Platform) as an internal product for Marketers at mercari group. How is it different from the usual product development lifecycle. How we could leverage its foundational design to build new features incrementally, and most recently, the batch feature which was never thought of to be built on EGP ! [13:10〜13:40] 発行枚数100万枚を支えたメルカードGrowth施策の裏側 メルカードの Growth に関連する施策は多岐に渡り、多くのチームが関わっています。そのような中、コミュニケーションやプロジェクト管理における課題に対してどのような工夫をしてきたか、自分たちの開発がいかにビジネス的に貢献しているのかという点でどのようなモチベーションややりがいを感じているかといった内容をお話します。 [13:40〜13:55] メルカードの常時ポイント還元開発の裏側 本セッションでは、まず始めに、メルカードの常時ポイント還元の仕組みについて概要を話します。ポイントの付与判定は、常に多くのマイクロサービスとの関わりが発生しています。そのため、現場で発生した細かな判断など開発の苦労話もありますが、幾つかピックアップしてお話します。 Fintechならではの開発現場の空気感を感じ取っていただければ幸いです。 [13:55〜14:10] メルペイ加盟店売上精算の仕組み このセッションではメルペイ加盟店の売上を精算するマイクロサービスが、メルカリShopsやメルコインなどのサービスを導入するために行ってきた開発や直面してきた課題点などをお話します。複雑な集計を伴うシステムの課題点や考慮点などが、同じような課題を抱えている方々の参考になると嬉しいです。 [14:10〜14:25] GoによるSQLクエリテストの取り組み 本セッションではBigQueryのSQLクエリのテスト方法を実装方法や動作デモを交えて紹介する予定です。複雑なSQLクエリのテストに課題感を持たれている方々の参考になると嬉しいです。 [14:35〜15:05] 発生可能な取引の属性データを用いた素早い不正検知 このセッションでは不正検知システムの機能や仕組み、運用事例などを紹介する予定です!不正行為の防止において最新技術を使った解決策として参考になると嬉しいです! [15:05〜15:35] Fintechにおける機械学習の品質保証とリスク管理 リスク管理のルール遵守と現場の生産性を両立させるために、ルール作りの時点でどのような工夫をしているか。エンジニアとしてどのようにリスク感度を高めようとしているかをお話します。 与信モデル更新マニュアルも作成しており、ブログでご紹介しています。本セッションご視聴前に、ぜひご一読ください。 https://engineering.mercari.com/blog/entry/20230622-d8b521dd2e/ [15:35〜16:15] Merpay & MercoinにおけるLLM活用の取り組み Fintech企業における最新技術を活用した取り組み事例や、取り組みを通じて見える知見についてお伝えできればと考えています! [16:15〜16:30] BigQueryのデータ監視社内サービスを作った話 BigQueryデータを監視したいという要件をお持ちの方のほか、社内に複数のデータ利用者がおり、彼らにセルフサービスのデータサービスを提供したいという方にとって参考になると嬉しく存じます。 [16:30〜17:00] 社内用GitHub Actionsのセキュリティガイドラインを作成した話 「社内でのGitHub Actions利用の広がりにあわせて、安全安心に使うためのガイドラインを作成したい」 そんな思いでGitHub Actionsのセキュリティガイドラインは、社内の有志メンバーによって策定されました。今回の発表ではこのガイドラインの内容を一部紹介するとともに、社内での活用事例やガイドライン作成のプロセスなどもご紹介します。GitHub Actionsを利用する方々が安心安全に利用するための一助になれば幸いです。 https://engineering.mercari.com/blog/entry/20230609-github-actions-guideline/ [17:00〜17:30] BigQueryのコンピューティングリソース管理の取り組み BigQueryを運用されている方、特にSLOT管理の課題に直面している方々に向けて課題に対する具体的な取り組みをご紹介します。 「Merpay & Mercoin Tech Fest 2023」のお申し込みは こちら から。 イベント詳細 開催日時: 2023年8月22日(火)〜24日(木)12:00-17:30 概要: IT企業で働くソフトウェアエンジニアおよびメルペイ・メルコインの技術スタックに興味がある方々を対象にしたオンライン技術カンファレンスです。事業との関わりから技術への興味を深め、プロダクトやサービスを支えるエンジニアリングについて知ることができる、メルペイ・メルコインにとってこの夏一番のお祭りです。 テーマ: Backend、Architect、SRE、Data Platform&Management、Machine learning、Frontend、iOS、Android、QA/テスト、 組織づくりなど 参加費:無料 場所:オンライン 参加方法:こちらの ページ にてお申し込みください。 【 公式サイト 】 本イベントに関する追加情報があれば、随時 @mercaridevjp でお知らせしますので、気になる方はぜひフォローをお願いします。
こんにちは!Merpay Engineering Enagement Team の@mikichinです。 来たる8月22日(火)から8月24日(木)までの3日間にわたり、「Merpay & Mercoin Tech Fest 2023」をオンライン開催します! テーマは「Unleash Fintech」。メルペイ・メルコインのこれまでの技術的な取り組みはもちろん、メルカリグループのFintech事業における新たな挑戦をお伝えします。メルペイ・メルコインが今後どのように“Unleash(解放)“していくのか、ぜひご自身の目と耳で確かめてください!! 肝心なトークセッションは、昨年全20セッションだったところ、今年は全33セッションにパワーアップ! 本記事では、22日のトークセッションの見どころをご紹介!1日目はKeynoteからはじまり、Client and Anti-Fraudを中心としたテーマをお届けします! まだ申し込みをされていない方も、興味のあるセッションがあるはずです。お申し込みは こちら からお願いします。 [12:10〜13:10] Keynote & How to Unleash Fintech 新たな体制となったCTO、VPoE3名によるメルカリグループのFintech事業における プロダクトや組織についてざっくばらんにお話します。わくわくする未来の話とFintechサービスならではの信頼性高いシステムを開発する上での「あんしん・あんぜん」な取り組みなど、メルペイ・メルコインのエンジニアリング組織の魅力をお届けできれば幸いです!ぜひ、Keynoteとあわせてお気軽にご視聴ください。 [13:10〜13:40] 1週間リリースを支えるAndroid自動テスト運用のその後 大規模なアプリのテスト保守をチームで解決しようとしている話です。常に変化し続けるアプリのリリースコストを削減すべく、有志で集まってテスト作成に励んでいる様子をご紹介します。 以前公開した「1週間リリースを支えるAndroid自動テスト運用」を事前にご一読いただくと、本セッションをよりお楽しみいただけると思います! https://engineering.mercari.com/blog/entry/20211210-merpay-android-test-automation/ [13:40〜14:10] Merpay iOSのGroundUp Appへの移行 既存のメルペイのコードを新しいメルカリのコードベース上に確実に移植するために、段階的なアプローチで作業を進めてきた様子を具体的なモジュール構造の解説を交えながら紹介します。既存のコードを壊すことなく新しいコード上に安全・確実に移植するという、まるで渋谷駅山手線線路切り替え工事のようなプロジェクトをいかにやり切ったか。お楽しみください。 [14:10〜14:25] Merpay iOSにおけるSwift Concurrency対応の挫折と今後 本セッションではプロジェクト失敗の経緯を説明します。失敗の理由はさまざまありますが、Swift Concurrencynoの特性からすべてのコードを一気に書き換える判断をしたことや、メルペイiOSコードの移植作業によって元のコードを変更する動機がなくなったことが挙げられます。 Swift Concurrencyの特性や組織のコードベースの変更の中でどうプロジェクトを判断したのかを示す予定です。 Swift Concurrency対応や大規模なコード変更のプロジェクトを考える上の参考になればと思います。 [14:35〜15:05] SwiftUIでビットコインの価格チャートを改善・再実装した話 本セッションではSwiftUIを使った実装の技術的な側面だけでなく、再実装決定までの経緯やデザイナーと改善サイクルを回していく流れについても触れます。SwiftUIでアニメーション付きのチャートの実装についてや、チームでの運用の話に興味がある方はぜひセッションをご視聴ください。 [15:05〜15:20] フロントエンドチームのスキルテスト評価システム改善の取り組み メルペイのフロントエンドチームの採用フローの一つにスキルテストがあります。スキルテストの評価観点や評価方法をどのように作成し、改善し、運用しているのかを紹介します。 [15:20〜15:50] WYSIWYGウェブページビルダーを支える技術的マジックの裏側 We will share our journey on how we built a WYSIWYG Webpage Builder, from concept to launch. With a flexible component system and conditional rendering functionality, our WYSIWYG page builder streamlined workflows, eliminated the need for coding and technical skills to create beautiful and responsive webpages, and allowed our company to create 150% more webpages than without it. We will describe our practices for building a WYSIWYG page builder, such as finding a balance between versatility and complexity. Attendees will leave with a deeper understanding of how to leverage shared knowledge to create an efficient and effective page builder. [15:50〜16:20] メルカリのユーザージャーニーにおける不正防止の取り組み As part of Trust and Safety [ TnS ] backend team, it is our goal to provide a safe environment for transactions to mercari customers. So I would like to focus upon some architectural designs and discussions in choosing certain components in our fraud-prevention system and want to provide a glimpse into real-time-fraud-detection work we are doing with Apache Flink. [16:20〜16:50] 日本におけるお客さま本人確認と今後の技術的課題 In this talk, Mann, Chris and Tim of the KYC Team, in charge of Mercari customer identity verification services, will discuss identity verification history in Japan, its most recent and biggest evolutions through the 2018 amendment of the “Act on Prevention of Transfer of Criminal Proceeds” and the potential incoming challenges that Japan might have to face in the near future due to innovative technologies such as Deepfake. [16:50〜17:20] メルカリへのFIDO導入の経緯とこれからの展望、課題から得た学び フィッシング耐性のある認証として注目されているFIDOやパスキーの導入にメルカリがどのように取り組んでいるかを知っていただき、実際に実装を進めた中で感じた苦悩や課題、そこから得た学びをLiveディスカッションの中でお楽しみいただけます。 「Merpay & Mercoin Tech Fest 2023」のお申し込みは こちら から。 イベント詳細 開催日時: 2023年8月22日(火)〜24日(木)12:00-17:30 概要: IT企業で働くソフトウェアエンジニアおよびメルペイ・メルコインの技術スタックに興味がある方々を対象にしたオンライン技術カンファレンスです。事業との関わりから技術への興味を深め、プロダクトやサービスを支えるエンジニアリングについて知ることができる、メルペイ・メルコインにとってこの夏一番のお祭りです。 テーマ: Backend、Architect、SRE、Data Platform&Management、Machine learning、Frontend、iOS、Android、QA/テスト、 組織づくりなど 参加費:無料 場所:オンライン 参加方法:こちらの ページ にてお申し込みください。 【 公式サイト 】 本イベントに関する追加情報があれば、随時 @mercaridevjp でお知らせしますので、気になる方はぜひフォローをお願いします。
この記事は、 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 )。