TECH PLAY

タイミー

タイミー の技術ブログ

264

はじめに こんにちは、フロントエンドエンジニアの樫福 @cashfooooou です。 先日、 和田卓人氏(以下、 t_wada さん)に「質とスピード」というテーマで講演をしていただきました。 この講演にはエンジニア以外の方々も参加してくれました。 僕は学生時代に t_wada さんの テスト駆動開発 についての講演を聞いたことがあり、それ以来 テスト駆動開発 を取り入れるようになりました。 今回の講演でも、なにか気づきが得られるとうれしいなあとワクワクしながら参加しました。 はじめに こんな講演でした 冒頭で投げられた問い 犠牲にされがちな「品質」とはなにか 内部品質を犠牲にしたときのスピードの損益分岐点はどこか 講演会の振り返り エンジニアの振り返り エンジニア以外の参加者の感想 おわりに こんな講演でした 講演の内容を簡単にまとめてみました。 t_wada さんが公開されている こちらの資料 もぜひ参考にしてください。 冒頭で投げられた問い 講演の冒頭で二つの問いが投げられました。 「スピードを得るために品質を犠牲にします」と言うときの「品質」とは? 品質を犠牲にしてスピードが得られる「短期」と逆にスピードが得られなくなる「中期」の境目は? 犠牲にされがちな「品質」とはなにか SQuaRE という品質モデルでは、品質を大きく「利用時の品質」と「製品品質」に分けます。 利用時の品質は実際にソフトウェアを利用するユーザにとっての品質を指し、製品品質はソフトウェアの開発者にとっての品質です。 さらに、「製品品質」は「外部品質」と「内部品質」とに分類できます。 外部品質はソフトウェアの正しさや使いやすさなど利用時の品質に直接影響を与えるもので、内部品質はコードの読みやすさや理解のしやすさなど利用時の品質に直接的な影響を与えないものです。 「スピードを得るために品質を犠牲にします」といって犠牲にするのは「内部品質」で、もっと踏み込むと「保守性」です。 保守性とは「テスト容易性」「変更容易性」「理解容易性」などから構成されます。 では保守性とスピードは トレードオフ の関係なのでしょうか?答えは NO です。 保守性が高まると開発しやすくなりスピードが上がります。スピードが上がることで学習する機会が増えて保守性が高まるのです。 質とスピードは相互に高め合う関係にあることがわかりました。 内部品質を犠牲にしたときのスピードの 損益分岐点 はどこか 内部品質を犠牲にしたときスピードが得られるのは1ヶ月以内だと言われています。 内部品質を犠牲にしてもたったそれだけしかスピードが得られないならば、常に内部品質の高い実装を目指したほうがよいですね。 講演会の振り返り エンジニアの振り返り 講演会の終了後に、各チームで「品質の高い理想的な状態とはなにか?」について話し合ってもらいました。 あるチームは Miro を使ってアイディアを出し合い、「テスト容易性」「変更容易性」「理解容易性」の観点で話しました。講演会と話し合いを通じて次のような感想をいただきました。 バックエンドエンジニアの方 もともと明文化されてなかった「品質の高い状態」の共通認識が取れたと思います。 思い返すと、これまで議論をするときに「品質の高い状態」の認識のズレのせいでうまく進まなかったことがあったかもしれないなあと感じました。 講演会を聞いて品質についてチームで振り返ったことで、この人はこんな観点を気にしてたんだ!という気付きに繋がりました。 今後、チームで議論をしていくときには「どういう観点をもって話しているか」をより意識した良いコミュニケーションがとれて品質を高めることができそうです。 エンジニア以外の参加者の感想 営業部の方 「スピードと質が相互に高め合う関係にある」という話は営業にも通ずると感じました。 私達にとっての「スピードが速い」状態とは「営業活動の回数が多い」こと、「質が高い」状態とは「営業活動に再現性がある」ことではないかと考えています。 営業職はお客さんと話して初めて気づくことがたくさんあるので、いい営業活動をするために場数を踏むことは必須です。 場数を踏んで得られる知見は、データに起こし効果検証を経た上で社内に共有するようにしています。こうすることで自身の営業活動の再現性を高めるともに 、初めて営業活動をする人でも既知の問題をクリアできる角度の向上や 工数 の短縮に繋がります。結果として、未知の問題に取り組む機会も増やすことができます。 この「場数をこなすから再現性が上がる、再現性が上がるから場数がこなせる」という好循環を意識していきたいですね。 広報の方 エンジニアの方がどんなことに興味を持ってるのか、気になって参加してみました。 「品質とスピードのどっちが大切か…」という悩みが解決してよかったです。 さらに、エンジニアの方々も同じような悩みを抱えていると知ってより親近感が湧きました。 私は仕事で原稿を書くことが多いのですが、原稿の品質は人に読まれることで判断されます。 講演内で「スピードを速くすることで学習する機会が増え、高品質に繋がる」という話がありましたが、 自身の仕事では、「まず原稿を書き上げてしまって、レビューや推敲が何度もされている状態」に置き換えられると思いました。 時間をかけてもいい記事が書けるとは限らないですからね。 学習機会を増やして品質を高くするという考えは大切に普段 からし ていきたいです。 エンジニア以外の皆さんにも、とても勉強になったと言っていただきました! 皆さんの感想を伺いながら、部署やチームにとっての質やスピードを定義すること、メンバーがその定義を理解していて共感できていることが大切だと感じました。 また、部署を横断した取り組みについては、お互いの質とスピードの定義について理解し合い、双方の質とスピードが高くなるような取り組み方が模索できると理想的だなあと思いました。 ゆくゆくは会社全体で「質とスピードの高いサービスの提供」ができるようになることを目指していきたいです。 おわりに t_wada さんをお招きし「質とスピード」というテーマで講演をしていただきました。 講演会に加えてチームでの振り返りを行ったことで、今後のいい開発体験に繋がりそうです。 エンジニアだけでなく会社全体に浸透する文化にしていきたいなあとも思っています。 今回の講演会をきっかけに、みんなの質とスピードについての考え方が変わりました。 次は「どのよう質とスピードを向上させるか」という技術的な課題にも取り組んでいきたいです。
アバター
不定期な割り込みタスクは見落としやすく、振り返りづらい Slack + Notionで、割り込みタスクを管理する CSメンバーはNotionに起票後、Slackで報告 エンジニアメンバーは、Daily Standupで優先度をつけ着手 職種をまたいだ依頼フローをもっと整えたい ※このブログは Cocoda さんに寄稿したものです。 タイミーでバックエンドエンジニアをしている edy です。 スキマ時間にバイトができるアプリTimeeを運営しています。 timee.co.jp エンジニアとしてサービス開発に関わる中で、日々の スクラム などで「計画的に行っているタスク」とは別に、「CSなど別チームから急に依頼されたタスク」に対して、どんな優先度で、どのように向き合っていくとよいのか頭を悩ませていました。 試行錯誤した結果、タイミーではNotionを活用してそのような「割り込みタスク」に対処する運用フローをつくり、うまく回り始めています。 お客様のサポートをするCSメンバーは、Notionに起票後、Slackのワークフローを使ってエンジニ アメンバー にお知らせ 起票を受けたエンジニアは、「対応可能なチケット数」に応じて、対応するかどうかや、優先度を決定し、解消に向かう 今回は、CSメンバーとエンジニアを滑らかにつなぐ、NotionとSlackを使った割り込みタスクの運用方法について、その背景やプロセスをまとめていきます。 プロダクトの改善フローや、体制づくりに悩まれているPMやエンジニアの方々の力になれると嬉しいです。 不定 期な割り込みタスクは見落としやすく、振り返りづらい これまで、「サイトの情報が古いから変更をお願いします」といった割り込みタスクはSlack上にのみ流れていて、依頼する人によって依頼の仕方もマチマチ、依頼の方法も不明、エンジニアも都度対応するのは大変、といった状況でした。 過去のSlackのみの運用(2019年頃) スレッドの件数がかさむことがしばしば発生していました。 そこで、ストック情報を扱うツールとしてNotionを活用することにしました。 Notionは普段プロジェクトやタスクの管理、ドキュメントツールとしても使っているので、割り込みタスクもNotionで管理することに。 Slack + Notionで、割り込みタスクを管理する タイミーの割り込みタスク管理は、NotionとSlackを使い「割り込み可能なチケット数」を決めて、チケット数の範囲であれば、割り込みタスクに対応していくような流れになっています。 CSメンバーはNotionに起票後、Slackで報告 まずは、サポートメンバーが、プロダクトの不具合や要望をNotionに起票。 割り込み可能なチケット数は、エンジニアの負担や割り込みタスクの数から、現在は2週間で14と決めています。 そのため、CSメンバーは残りチケット数を見ながら起票を進めていきます(厳密には、CSのリーダーが「チケットを使ってでも解消したい」というふうに優先度を決めてくれています。)。 割り込み可能なチケット数は、Notionの計算機能を使って「14 - 現在のチケット数」で算出しています。 割り込みタスクの種類には、3種類を用意しています。 データ変更依頼 … 古い情報の差し替えなど 仕様確認 … ボタンの文言がどんなロジックで変わるか、など 調査依頼 … 原因がわからない不具合の調査依頼など そして、Slackワークフローを使って、割り込みタスクをエンジニアに伝達。 Slackワークフローでは、手順書を読んだかどうかとNotionのURL、依頼内容を簡潔に伝えてもらうように。 また、CSメンバーはアルバイト等タイミーでの業務にまだ精通していない方々もいるため、 依頼方法について共通の認識を持ち、依頼を一定の水準にするための手順書も用意 しています。 依頼の際は、必ずこれを見てもらい、依頼をしてもらうようにしています。 データ変更依頼、調査依頼、仕様確認の3パターンそれぞれに対応するNotionテンプレートを用意しており、エンジニアが対応する際に必要なプロパティの入力 チェックボックス や背景や期限の理由などの記入欄が自動生成されるようになっています。 これにより、情報の抜け漏れ防止やコミュニケーション往復回数の削減、また情報の標準化に繋がっています。 報告した依頼内容は、要望を報告する #product_エンジニア依頼 チャンネルに流れてきて、必要な場合は担当するエンジニアがスレッドでコミュニケーションをするようになっています。 エンジニ アメンバー は、Daily Standupで優先度をつけ着手 こうして、Notionには割り込みタスクがチケットごとに溜まっていきます。 割り込みチケットの一覧。各ページ内に、割り込みタスクが入れられています。 エンジニアチームで、毎朝やっているDaily Standupで対応する割り込みタスクを決定し、優先度も併せてつけて対応を進めていきます。 外部の顧客に価値提供するタスクや納期が差し迫っているタスクは優先度を高くしており、社内運用向けであったり、期日の交渉が可能なものは優先度を低くして、過度に開発のスケジュールを圧迫しないように努めています。 「priority」のプロパティで、1~5の5段階で数字を割り振り、5が最も優先度の高いタスクとして扱えるようにしています。 依頼内容について、もっと細かく把握したい場合は、Slackのスレッドでやりとりをしていきます。 実際のやりとりの例 まとめると、 割り込み可能なタスクの数を「チケット数」として決める 依頼の際は割り込み可能な数の中で、CSメンバーがNotionに起票し、Slackで報告 エンジニ アメンバー が優先度を決めて、適宜コミュニケーション というフローになっています。 こうして、CSメンバーはより割り込みタスクの依頼もしやすく、エンジニ アメンバー も突発的なタスクに対応することがなくなりました (エンジニアの方々には多いかもしれませんが、突発的なタスクはストレスになることも多いのでなるべく避けたい...)。 職種をまたいだ依頼フローをもっと整えたい 普段行っている業務や、所属しているチームが異なると、どうしてもチームや職種をまたいで依頼することが億劫になったり、そもそも依頼方法が分からなかったりします。 ここまでの事例紹介でも扱った通り、依頼プロセスの入り口を日常業務で多用するSlackのワークフローに設置したことで、スイッチングコストを抑えながらシームレスに依頼手続きを開始したり、依頼方法が分からなくてもワークフローを起動すれば文言に従って情報を入力するだけで手続きが済むようになりました。 現在では、さらにNotionのプロパティを CSV でエクスポートし、過去数ヶ月でどういった種類の依頼がなされているかの 定量 的な分析も行っています。 頻度や削減できる 工数 などを総合的に判断し、必要あればエンジニアがリソースを投下して機能追加をしています。 ストック情報を活用して根本的に割り込み依頼を発生させないようなアクションを取るための意思決定材料にもなっています。 今のフローも手作業な部分もあり、改善の余地があるのですが、社内の利用者の皆さんにより素早く価値が届けられるようなフローに改良していこうと思います。
アバター
こんにちは、タイミーでプロダクトマネージャを務めている高石 ( @tktktks10 ) です。 戦略やロードマップの策定から、プロダクトの成果を最大化するための課題発見や優先順位付けを日々の業務としていますが、今回はその中でも顧客と直接顔を合わせる「ユーザーインタビュー」を起点とした取り組みの話をしようと思います。 ユーザーインタビューの積み重ねから組織のアライメントを生み出す タイミーでは最近新たに入社頂いたPMMの影響もあり、ユーザーインタビューの頻度を大幅に増やしています。きっかけは単に顧客解像度を上げようという至ってシンプルなものでしたが、横断的に継続する中で次第に部署や役職を超えた共通の顧客像 *1 (セグメント)が出来上がり、最近では全社戦略やプロダクトロードマップ、個別の施策にも引用されるまでになってきました。 一言で言えば、「 横断的なユーザーインタビューの積み重ねから組織として顧客像を定義し、戦略や施策に活用している話 」となります。 役割にかかわらず、同じ顧客像を向くことで価値提供の前提が揃います 組織全体で共通理解のある顧客像は、全社戦略からチームレベルの施策全てにおける主語となり、自ずと各所の取り組みが同じ方向を向く、所謂アライメントが取れている状態を生み出します。アライメントが取れている状態ではあらゆる場面でコミュニケーションコストが削減される他、 マーケティング 、プロダクト開発、カスタマーサクセスまでなど、 一気通貫 した課題解決に繋がります。 ユーザーインタビューやペルソナ、個別具体に関する記事は多いものの、実際に構築から運用までを紹介した実例を見かける機会は少ないように思い、今回はタイミーにおける実際のプ ラク ティスをまとめました。 *1 厳密には顧客とユーザーの双方が存在しますが、ここではまとめ上げられたもの呼称として"顧客像"(セグメント)に統一することとします 『組織として』顧客を理解し続ける意味 組織がスケールして部署やチームの数も増えると、それぞれのミッションや取り組みも新たに生まれるでしょう。この過程において全員の向き先を揃える顧客像が不足していれば、皆が少しずつ異なる顧客像を持ち始め、異なる方向に向かい出してしまいます。顧客像や注力対象がズレた状態では、連携する際のコミュニケーションコストが増大する他、部署やチーム間で時には対立を生んだりなどの問題が発生します。優れた組織設計の元ではこれらの摩擦が起きづらいものですが、それでも少なからず分業や連携というものは発生するのが現実です。 このようなサイロ化が進む状況で、いくらサイロの中にいるチームや個人から顧客像を打ち立てても、中々根本の問題は解消しません。 組織がスケールする中で個人やチーム同士を繋ぎ止めて速度を落とさないためには、役割や役職を跨ぐ形で組織として顧客を理解し、戦略や各部のアクションに還元し続ける必要があります。 以下、実際に横断的なインタビューの積み重ねから全社共通の顧客セグメントを構築しているサイクルと、得られた顧客セグメントを戦略や施策に還元したことで得られた成果の順で紹介したいと思います。なお、タイミーはBtoCのサービスですが、今回の取り組みは toC 側を対象にしたものです。 実際に行っているインタビューのサイクル 継続的な取り組みですのでサイクルという表現になり、 下記1~3のプロセスを日常的に回しています 。全体を通して、具体から抽象を見出すことを意識した設計になっています。 1. 共通理解を築きたい人とインタビューを行う 主にインタビュアー2人のデプスインタビューを継続的に行っています。1人が主な進行を担当し、もう1人が視点をフォローするようなサポートをします。現在ミニマムで週5回ですので、毎日1回はどこかしらのペアがインタビューを行なっているくらいの頻度になります。 この時、 共通理解を築きたい人(裏を返せば、理解に溝がある人)を同席相手に選定します 。プロダクトマネージャである自分は、戦略の議論が必要な場合経営陣や関連部署のメンバー、 バックログ レベルで解像度を上げたい場合開発チームと同席することが多いですし、 マーケティング 部では既存メンバーが新しく入社したばかりのメンバーとオンボーディングの一環として同席しています。実際の顧客を目の前にして時間を共有することは、内輪の会議からは得がたい強力な共通理解を築く方法です。 また、インタビュー自体にも様々なテクニックはありますが、基本的には 事実を聞くように徹します 。後述する共通理解を築く上で、主観ではなく事実を把握しておくことは非常に重要です。個人的には下記の アンチパターン 集 *2*3 などは参考になりました。 *2 良いユーザーインタビュー、悪いユーザーインタビュー|Mizuho Kushida|note *3 失敗から学んだ、ユーザーインタビュー23の心得|Goodpatch Blog グッドパッチブログ 2. インタビューで得られた事実について考察をし、ユーザーの特徴を簡潔にまとめる Tips: 「利用を始めたきっかけ」「現在の使い方」「今後の利用見込み」の3点を具体性を持って書くことが多いです インタビューが終わったらその直後、遅くとも記憶が新しい翌日までに考察の振り返りを行います。インタビューで得られた事実を基に、ユーザーの利用開始文脈や現状解決している課題、解決しきれていない課題などに対して協力して解釈(厳密には仮説)を当てます。 当然異なる2人が解釈を当てるので、双方自分には無かった観点が飛び交います。この過程ではお互いの観点を共有して見方を増やし解釈の精度を上げることにフォーカスしますが、ユーザーを起点にしてお互いの考え方を知る時間でもあります。 考察が一通り終わった後は、ユーザーの特徴を3~4行で簡潔にまとめあげます。このまとめが思いの外重要であり、2人で協力して特徴をサマライズすることで「この人はこういう目的でタイミーを使っていたよね」といった、ユーザーに対する共通理解が生まれると同時に、後からこの文書をみたときにユーザーの顔やインタビュー内の会話をスッと思い出せる入り口にもなります。 3. インタビュー結果を集合させ、顧客像を定義・アップデートする 個々のインタビューとその考察で得られた情報を、 集合知 にするプロセスです。 最初こそまとめ方は模索しつつ皆でインタビュー結果をホワイトボードに貼り出して考察を重ねたりしていましたが、総インタビュー数が30~40回を超え始めたところで利用目的、即ち解決している課題が大きく異なる顧客像が2つ浮かび上がり、皆が腑に落ちる形でセグメントとして 言語化 ・定義されました *4 。結果こそシンプルですが、過程は決して事務的な作業ではなく、殆どが会話によるワークショップ的な時間を通して出来上がったものになります。 ユーザーストーリー マッピング や結果の共有会を通じてジョブ別に分類 利用目的が異なるというのがポイントで、これは解決しているジョブが異なることを意味します。一般的にセグメントといえば職業や年齢などから成るデ モグラ ベースの物が多いかもしれませんが、今回定義されたのは解決している課題別、即ちジョブベースのセグメントであり、プロダクトの4階層を借りるならばWhyに相当するもの *5 です。 なお、一度セグメントが定義された後はここに立ち返る形で新たなインタビュー結果を照合し、継続的に内容や理解をアップデートし続けています(現在では開始から4ヶ月で計100~150人程になりました)。この2つのセグメントが組織内の共通言語になっており、後述する戦略や施策などの前提として利用されることになります。 *4 本記事では取り上げていませんが、実際はある程度 定量 データで裏付けをとりながら定義を進めています *5 何のためにペルソナをつくるのか - 4つの使い分け|小城久美子 / ozyozyo|note 得られた顧客像の活用例とその成果 ここまでの過程を プロダクトに関わる全ての役割・役職が協力することで、個人でもなくチームでもなく『組織として』顧客像を作り上げている のがポイントです。自分たちで作っているので都度背景を説明する必要もなく、以降の活用フェーズにおいて既知の共通言語として利用することができます。 全社戦略やロードマップにおける注力セグメントとして利用されることで、部署やチームを 一気通貫 した課題解決に繋がる 以前からも全社戦略やロードマップは運用されていたものの、顧客像の認識は今より曖昧であり、部署やチームの施策レベルで目的の分散が起きてしまう課題が起こりつつありました。今思い返すと、データを元にした会話は多かったものの、インタビューなど顧客のWhyに触れる定性的な時間を共にする機会は少なかったように感じます。 既に共通言語になっている顧客像を戦略の骨子に利用する 一方、最近戦略やロードマップがアップデートされたタイミングで今回作り上げた顧客像を引用したことに加え、事業の方向性やプロダクトビジョンと照らし合わせた上で注力するセグメントと目指すべき方向性も自ずと決定されました。これは、中長期的・組織的な目線において「 誰をどんな状態にしたいのか 」の方針が立ったことに相当します。重ねての強調になりますが、そもそも組織的に作り上げた顧客像であるため、この時点で既に関係者間で共通理解がある状態です。したがって、後からドキュメントによる追加の補足や翻訳作業などもほとんど必要ありません。 結果的に、獲得施策からプロダクト開発、マーケットインまで共通の顧客像を持つことで、以前よりも正しい顧客に正しいプロダクトを届けるための連携ができるようになってきました。部署やチームの前に顧客像とフォーカスが決まっていることで、組織全体を活かした課題解決に繋がっています。 部署間の前提が揃うことで、プロダクト開発の優先順位付けが効率化した こちらはもう少し具体に近い、日々の バックログ に対する優先順位付けの話です。 共通の顧客像とフォーカスができる前は、そもそも向いている顧客像が若干ズレていて視点が広がりすぎているために、ステークホルダからの要望に対して都度目線の調整したり断ることが多かったように思います。もちろん個別の施策に対してコストの軽いものから随時実行するという判断もできなくはないですが、フォーカスの定まらない表層の改善だけが連続してしまうことになります。本質的な課題を解決し中長期的な成長を目指すには、同一のゴールに対して継続的に改善を試みる 選択と集中 が必要であるため、そういった状況になることは好ましくありません。 一方、共通の顧客像とフォーカスが決まってからは自ずと施策レベルの方向性も揃い始めたため、目線の調整や断るケースがかなり減りました。また、プロダクト開発における直近の注力テーマをステークホルダに理解してもらうことで、その間は開発を必要としないプロダクト外で並走できる改善に取り組んでもらうなど、むしろ協力する機会も増えてきたように思います。根っこの目線が揃うことでコミュニケーションコストが削減されるのはもちろん、以前より効率的な分業もできるようになりました。 開発時に引用され、ものづくりにおける意思決定の促進に繋がる 前述した優先順位付けにもつながりますが、顧客像とそのフォーカスが決まっていることで、 バックログ 自体も自然と流れを持った構成になります。また、これはまだ一部ですが、 バックログ リファインメントで開発チームが顧客セグメント引用することで、自律的に機能開発の方向性を決定できるケースも出てきました。ただし、UI/UXレベルの意思決定を精度良く高く行うには、もう一段階具体に寄ったWhatレベルの顧客像が必要であるとも感じます。この辺りは最近取り組み始めたUXリサーチやプロトタイピングを通じて、組織としての仮説検証能力を上げていきたいポイントでもあります。 今回の取り組みを始める際にやってよかったこと 最初はとにかく量を重ねること 正直なところ、インタビュー開始当初は目的設計も曖昧でした。しかし今思い返すと、 顧客解像度が低く目的設計が曖昧になるためインタビューが起こりづらいが、インタビューが起こらないため顧客解像度が上がらない負のスパイラル に陥っていたように思います。 一方、ひとたび顧客の解像度が上がりはじめると自ずと新たな疑問(仮説)が生まれます。その疑問を解消するために次のインタビューを設計して…、というポジティブフィードバックサイクルが生まれれば、後は勝手に自走するでしょう。まずは目的が曖昧でも良いので、量を重ねてみることをおすすめします。 インタビューの参加コストを抑える仕組みを同時に作ったこと こちらは以前出した記事の内容になるのですが、タイミーでは思い立てば誰でもインタビューに参加できる社内の仕組みが構築されています。実際インタビューを行うにはインタビュイーの選定から実際の募集、謝礼の受け渡し、当日までの連絡など、様々な附帯業務が発生する訳ですが、これらをインタビューを支えるチームがサポートすることで、参加者はインタビューのコンテンツだけに集中できるようになっています。 前述したポジティブフィードバックサイクルに乗るためにも、インタビューを行う人とインタビューの設定をサポートする人を分離したことは、今回の取り組みに一役買ったと感じています。 おわりに 最初は何気なく始めたユーザーインタビューでしたが、今では拡大し続ける組織において顧客の共通理解を保ち続ける重要なプロセスとなりました。 プロダクトマネジメント においても、共通の顧客像が主語になることで、組織でものづくりをする力がついてきているように感じます。 とはいえ、タイミーは急成長中のプロダクトで課題もまだまだ転がっています。もっと具体的に話を聞いてみたい方、ユーザーインタビューに限らずプロダクト開発や プロダクトマネジメント について興味がある方、もちろんタイミー自体に興味のある方、職種問わずお気軽にお話しましょう!( Twitter からDMを頂けると助かります)
アバター
前編(トランザクション範囲の最小化)へ はじめに こんにちは。タイミーのバックエンドエンジニア中野です。 前編では締めの バッチ処理 における トランザクション の範囲を最小化した技術的改善をご紹介しました。 トランザクション の範囲を バッチ処理 全体から最小限の範囲に変更したことにより、 バッチ処理 が失敗した場合に請求レコードの処理が途中まで完了している状態が発生するようになりました。後編では、処理対象の請求レコードに対し状態を持たせることで バッチ処理 全体での冪等性を担保し、 バッチ処理 が途中で失敗した場合でも安全に処理を再開できるようにした取り組みをご紹介します。 はじめに 締めのバッチ処理とは 現状の課題認識 実施した施策 冪等性とは 冪等性を実現する方法 バッチ処理への適用 達成できたこと 今後の課題 スループット向上とリソース最適化 まとめ 締めの バッチ処理 とは まずは前編のおさらいになりますが、弊社の締めの バッチ処理 に関して説明します。締めの バッチ処理 とは、月初に定期的に実行されるオンラインバッチであり企業への先月の請求を確定させる処理になります。 具体的な締め処理までのプロセスを記述すると以下の通りになります。 企業への請求が発生した段階で「請求テーブル」にレコードが作成されます 毎月月初の「締め」の バッチ処理 により、「請求テーブル」のレコードを「確定請求テーブル」に複製することで企業への請求を確定させていきます つまり、締めの バッチ処理 とは月初時点の請求テーブルのスナップショットを撮る行為であるとみなすことができます。 現状の課題認識 前編の技術的改善により トランザクション の範囲を バッチ処理 全体から最小限の範囲(レコード単位)に変更しました。これまでは バッチ処理 の途中で処理が失敗した場合には トランザクション の ロールバック により次の2つの状態が担保されてきました。 確定請求レコードが全て作成される状態 トランザクション が成功した場合に遷移する状態。「請求テーブル」のレコードがすべて処理され「確定請求テーブル」に過不足なくレコードが作成されている。この状態に遷移した時に確定請求テーブルに依存している利用明細画面の表示が一斉に更新される。 確定請求レコードが1つも作成されていない状態 バッチ処理 開始前または処理が失敗したに遷移する状態。 バッチ処理 が途中で失敗した場合には トランザクション の ロールバック により処理開始前の状態のままとなる。確定請求テーブルに依存している利用明細画面の表示は バッチ処理 実行前の表示のままとなる。 トランザクション をレコード単位に変更することで、 バッチ処理 全体での原子性が担保されなくなる状況が発生しました。下図右側の失敗時の通り、途中までレコードが作成されている状態が存在するようになりました。 トランザクション を最小化したことによる影響 途中まで「確定請求テーブル」のレコードが作成される状態が出現したことにより生じうる課題は下記の通りです。 処理再開時に重複して確定請求レコードが作成される問題 これまでは バッチ処理 が失敗した際に、 トランザクション の ロールバック 機構により バッチ処理 開始前の状態に戻るため失敗の原因を排除後に再度最初から バッチ処理 を再開することできました。しかし、 バッチ処理 全体での トランザクション を廃止し、途中まで「確定請求レコード」が作成された状態が発生したことで、再度処理を実行すると重複した確定請求レコードが作成される可能性がでてきました。 利用明細への影響 企業管理画面では、翌月の請求予定額及び月ごとの確定した請求額を閲覧する機能を提供しています。この翌月の請求予定額が「請求テーブル」に依存しており、締め処理がされていない請求テーブルのレコード合計を表示するようになっています。一方で、月ごとの確定した請求額は「確定請求テーブル」に依存しています。 バッチ処理 全体において トランザクション が適用されていた場合は、 トランザクション がコミットされたタイミングで処理結果がデータベースに反映されるため、 バッチ処理 完了時に利用明細の表示が一斉に更新されてきました。しかし、 バッチ処理 全体の トランザクション を廃止し逐次的に処理結果をデータベースに反映するようになったため、企業が利用明細を開いたタイミングによって翌月の請求予定額が変動する問題が発生しました。 締め処理と利用明細画面のテーブルへの依存関係 このように処理が途中で失敗するケースは バッチ処理 だけに限った話ではなく、アプリケーションの開発・運用していく中で様々な原因に起因し発生します。例えば、プログラムのバグ、データ欠損や異常値、メモリ不足などが挙げられます。そのため処理が失敗した場合に備えてアプリケーション設計を考えていく必要があります。 実施した施策 上記の課題を解決すべく、締めの バッチ処理 を再設計するにあたり冪等性を意識しました。 冪等性とは 冪等性とは「ある操作を1度実行しても、複数回実行しても同じ結果になる」性質のことを言います。冪等性が担保されていると、仮に処理が途中で失敗したときに処理をリトライしても最終的な結果が1回で処理が成功した場合と変わらないことになります。今回扱う締めの バッチ処理 では、リトライ時に処理済みの請求データに基づく確定請求レコードが重複して作成される可能性があります。そのため、 バッチ処理 に冪等性を持たせることが非常に重要となってきます。 バッチ処理 において冪等性を実現する方法を次に2点紹介します。 冪等性を実現する方法 すでに処理が完了した対象をリトライ時に処理しない方法 バッチ処理 の対象に未処理もしくは処理済みの状態をもたさせることで、リトライ時に処理済みの対象をスキップさせる方法があります。リトライ時に未処理のみを対象に処理を開始できるため、 バッチ処理 全体の完了時間を早められる反面、状態を管理する必要があるため条件分岐のロジックが入るなどコードの複雑さが増す傾向にあります。 すでに処理が完了した対象も含めてリトライする方法 1度目の バッチ処理 によって処理された対象も含めて処理する方法です。例えば、処理した結果をデータベースに永続化している場合はすでに存在しているレコードを削除し新規でレコードを作成し直す処理を1つの トランザクション 内で実行します。そうすることで重複したレコードは作成されず冪等性が担保されます。留意すべき点としては1つひとつの処理対象レコード自体が冪等性を有しており外部サービスに依存していないことです。1と比較し分岐処理によるコードの複雑性は回避できる反面、リトライ時に最初から処理の実行を再開する必要があるため完了時間が遅延する傾向にあります。 バッチ処理 への適用 今回は1の方法で冪等性を実現しました。具体的には、請求レコードに「締め前」「締め中」「締め後」の状態を持たせることで締め処理済みかどうかの判定を行うことができ、たとえ途中で バッチ処理 が失敗した場合でもリトライ する際に締め処理済みのレコードをスキップすれば確定請求レコードの重複作成を回避できます。また、請求テーブルに状態を持つことで「締め後」の状態に遷移した際に依存先の利用明細画面の表示を切り替えるだけで、締め処理が途中で失敗した場合の締め途中の中途半端な状態の金額が閲覧される問題を回避できるようになりました。 達成できたこと 締めの バッチ処理 に冪等性を考慮した設計改善を行うことで次の成果が得られました。 処理が失敗した場合にリトライしても重複した請求が発生することを防止できる 請求レコードに状態を持たせることで バッチ処理 の処理状態に依存している利用明細画面の表示を制御できるようになった 副次的な効果として バッチ処理 全体の完了時間の短期化を実現できました。 バッチ処理 が失敗した際に、処理が失敗した原因を排除後に単にリトライすれば処理を再開できるようになり、結果的に バッチ処理 完了までの時間の短縮化につながりました。 今後の課題 最後に、これから改善を考えている課題を提示した上で バッチ処理 の改善に関する記事の「締め」とさせていただきたいと思います。 スループット 向上とリソース最適化 バッチ処理 は通常処理負荷が高いためサーバーのCPUやメモリなどのリソース消費が高くなる傾向になります。無限にリソースが利用できれば良いですが、通常はコスト制約が存在するため利用できるリソースにも制約が生まれます。そこで処理時間とリソースの制約条件に基づき バッチ処理 のパフォーマンスをチューニングする必要があります。よくある バッチ処理 の事例として、以下の処理を考えます。 データベース からデータを読み取る アプリケーションのインメモリでデータを加工処理する データベースへ加工処理したデータを挿入する 上記の場合に、データベースのレコード1件ごとにRead/Writeクエリを走らせると、大量のデータ量を扱う場合に処理時間の致命的な遅延を招きます。そのため通常はバッチ数を設定し一定件数ごとに処理を行います。バッチ数を大きく設定した場合は一度にインメモリで処理できる件数が増加するためデータベースとのIOを削減でき処理の スループット が向上しますが、メモリのリソース消費が大きくなります。 バッチ処理 の処理時間とメモリのリソース制約を鑑み最適なバッチ数を設定していく必要があります。 まとめ バッチ処理 の一連の技術的な改善において以下の学びを得ました。 一般的な システム開発 において トランザクション の範囲は最小限に限定する必要がある。今回の事例のようにレコードに広範囲のロックがとられる可能性があり、オンライン処理へ影響を与える。 バッチ処理 全体における トランザクション を廃止した結果、 バッチ処理 全体の原子性が担保されなくなった。原子性が担保されなくなったことにより、依存先の利用明細画面の表示に一貫性のない状態が発生した。そこで、処理対象レコードに処理済みの状態を持たせることで バッチ処理 全体の冪等性を担保できるようになった。結果的に、処理失敗時のリトライが容易になり異常状態からの回復が迅速に行えるようになった。 今回は バッチ処理 における トランザクション の改善及び冪等性の設計に関してご紹介しましたが、 バッチ処理 にはここで紹介しきれなかった様々な論点があります。 僕たちの バッチ処理 改善の戦いはこれからだ(完)連載打ち切り tech.timee.co.jp
アバター
後編(冪等性の設計導入)へ はじめに こんにちは。タイミーのバックエンドエンジニアの中野です。 よく Gopher くんに似てると言われます。 本記事では月次で実行している「締め」の バッチ処理 に関する一連の技術的改善について掲載します。弊社のプロダクト「タイミー」は著しい事業成長に伴いデータ量が急増してきています。そこで今回はデータ量の急増を背景とした中長期的な バッチ処理 の設計改善にどのように取り組んできたのかをご紹介したいと思います。 バッチ処理 に関する技術的改善の記事は前編・後編の2部構成をとっています。前編は バッチ処理 における トランザクション の改善をテーマに、後編では バッチ処理 に冪等性の設計を導入したことをご紹介したいと思います。 今回は前編の トランザクション の改善をテーマにご紹介します。すでに本番稼働しているアプリケーションにおいて トランザクション の範囲が大きい場合にどのような問題が発生したのか、そしてどう解決していったのかを中心に取り上げます。 読み手としては、今後データ量の急激な増加が見込まれるプロダクト開発の中長期的な設計・運用を模索している方を想定しています。また読み手に該当しない場合でも、現在タイミーがどのような事業を推進しているのか興味を持ってもらえるように記載しましたので是非最後までお付き合いください。 はじめに バッチ処理について バッチ処理改善のきっかけ 締めのバッチ処理とは 弊社と私のチームが扱っている領域 「締め」の概念 現状の課題認識 実施した施策 トランザクションA トランザクションB トランザクションC 達成できたこと まとめ バッチ処理 について ある程度の量のデータを一括で処理することを「 バッチ処理 」と呼び、通常は日時、週次、月次で定期的に実行されます。オンライン処理 *1 を稼働もしくは停止させたまま バッチ処理 を実行するかによって、 バッチ処理 は次の2種類に分けられます。 オンラインバッチ オンライン処理を継続して稼働させた状態で実行する バッチ処理 です。オンライン処理と同じリソース(例えばデータベース)へアクセスが発生する場合はオンライン処理へ影響を与える可能性があります。 オフラインバッチ オンライン処理を停止させた状態で実行する バッチ処理 です。例えば、夜間にメンテナンス期間を設けることでシステムのダウンタイムを許容し実行する バッチ処理 が考えられます。 今回説明する締めの バッチ処理 ではオンラインバッチを前提としています。 バッチ処理 改善のきっかけ 弊社では毎月初に締めの バッチ処理 を実行しています。具体的な処理内容の説明は以降のパートに譲りますが、月次で定期的に実行されるオンラインバッチを想定してください。約2年前の設計当初は問題なく稼働していましたが、プロダクトが成長するにつれてバッチの処理時間が長期化してきました。処理時間が長期化するにつれて、これまで見えてこなかった問題が起きるようになりました。その問題とはオンライン処理におけるLockWaitTimeoutエラーの発生です。実は改善前の バッチ処理 では処理全体で トランザクション を貼り実行していたため、 バッチ処理 で作成されるレコードまたは外部キー制約において値が登録されている親テーブルのレコードにおいて占有・共有ロックが行われていました。レコードにロックがとられている場合、 トランザクション がコミットされるまでの間に、他の処理からロックされているレコードに対しての読み書きに処理待ちが発生する場合があります。 バッチ処理 のロックがとられる時間が長期化した結果、オンライン処理でLockWaitTimeoutエラーが発生しユーザーに500エラーが返る事態が発生しました。 この問題をどのようなプロセスを経て改善していったのか、まず弊社の締めの バッチ処理 に関する説明をした上でご紹介します。 締めの バッチ処理 とは 弊社と私のチームが扱っている領域 締めの バッチ処理 を弊社でどう改善してきたのか説明する前に、事前知識として弊社及び「締め」の概念に関して簡単にご紹介します。 弊社で開発しているプロダクト「タイミー」は「働きたい時間」と「働いてほしい時間」をマッチングするスキマバイトサービスです。私のチームでは図1グレー部分の企業・クライアントに対し求人を円滑に掲載するための機能を主に開発しています。 用語説明 クライアント :タイミー上に求人を掲載する主体。クライアントの管理画面ではワーカー管理や出退勤管理などの機能を提供している。 企業 :クライアントがタイミーを利用した際に発生する請求を管理する主体。法人単位であることが多く、企業は複数のクライアントを持つ。企業管理画面では請求書や利用明細閲覧の機能を提供している。 各チームが扱っている領域の説明 「締め」の概念 タイミーで働いたワーカーの給料はタイミーが立替払いを行なっています。そのため毎月月初の特定の時点で先月分の給料およびタイミーのサービス利用料を確定させ企業に請求を行っています。この毎月月初で請求を確定させる行為を「締め」と呼びます。 ここではより詳細に「締め」でどのような処理を行なっているのかを説明します。ワーカーの稼働により報酬が確定した段階で「請求テーブル」にレコードを作成し管理しています。毎月月初の「締め」の バッチ処理 により、「請求テーブル」のレコードを「確定請求テーブル」に複製することで企業への請求を確定させていきます。簡単に説明すると、締めの バッチ処理 は月初時点の請求テーブルのスナップショットを撮る行為であると捉えてください。 また企業の利用明細画面では翌月の請求予定額及び月ごとに確定した請求額を閲覧する機能を提供しています。翌月の請求予定額は締め処理が行われていない「請求テーブル」のレコードを参照しており、確定した月の請求額は「確定請求テーブル」を参照しています。 請求関連テーブルと利用明細画面との依存関係 現状の課題認識 締めの処理対象である請求テーブルの特徴として、タイミーの事業成長に伴い急激にデータ量が増えてきたことが挙げられます。締めの バッチ処理 が実装された約2年前から比較すると月次で処理すべきデータ量が20倍近くになっていることが分かります。 *2 請求テーブルのデータ量推移 2年前の バッチ処理 設計当初はデータ量が少なかったこともあり、 バッチ処理 全体に トランザクション を適用していました。利用明細画面の表示が締め処理により作成される確定請求テーブルに依存していたため(図「請求関連テーブルと利用明細画面との依存関係」を参照)、 トランザクション の性質を利用することにより次の2つの状態を担保していました。 確定請求レコードが全て作成される状態 トランザクション が成功した場合に遷移する状態。 この状態に遷移した時に翌月の請求予定額及び締めにより確定した月の請求金額の利用明細画面の表示が一斉に更新される 確定請求レコードが1つも作成されていない状態 処理開始前または バッチ処理 が失敗した場合に遷移する状態。 バッチ処理 が途中で失敗した場合に トランザクション の ロールバック により処理開始前の状態に戻ります。翌月の請求予定額及び締めにより確定した月の請求額の利用明細画面の表示は バッチ処理 実行前のままとなる。 このように トランザクション における原子性の特性を利用することで、 バッチ処理 が失敗した場合でも処理途中までの確定請求レコードが中途半端に作成される状態は存在しません。そのため、確定請求テーブルに依存している利用明細画面の表示上の金額も上の2つの状態しか取りえないことになります。 これまで バッチ処理 全体における トランザクション 貼ることで上記のような恩恵を受けてきましたが、次のような課題も出てきました。 バッチ処理 の影響によるオンライン処理のエラー発生 タイミーは現在モノリシックなサービスのため、オンラインバッチで処理を実行するとオンライン処理と同じデータベースに対し トランザクション 処理を行うことになります。 トランザクション は、他の トランザクション 処理の影響を受けないようにするためレコードまたはテーブルに対してロックをとることがあります。レコードに対してロックが取られた場合に他の トランザクション からのレコードに対する更新系の処理は停止し待たされる状況が発生します。一定時間ロック状態が継続しロックが解放されない場合にLockWaitTimeoutエラーが発生し処理が失敗します。このLockWaitTimeoutエラーが発生すると、ユーザーが意図した処理が実行されないばかりか、エラー発生までユーザーは処理が待たされていることを認知できないため意図しない誤操作を行いかねません。 バッチ処理 失敗時の処理完了時間の遅延 バッチ処理 を運用していると様々な要因により処理が異常終了します。例えば、データ量の増加に伴いコンテナのメモリ量が不足し処理が失敗する問題や請求テーブルのデータに異常なデータが混在しているなどです。処理が失敗した場合に トランザクション の ロールバック により締め開始前の状態に戻るため、処理が失敗した原因を特定後に再度締めの開始から処理を再開する必要がありました。結果的に締めが完了までの時間が長期化し企業への請求確定メールが遅延することも課題としてありました。 実施した施策 トランザクション の範囲を バッチ処理 全体から最小限の範囲に限定しました。 では、最小限の範囲とはどう評価すべきでしょうか。 トランザクション はアプリケーションから見て一貫した状態から次の一貫した状態へ遷移させる作用とみなすことができます。ここで言及した最小限の範囲とは、一貫した状態から次の一貫した状態へと遷移させる トランザクション のうち最小の処理を持つ トランザクション の適用範囲を指します。 言葉で定義するとわかりにくいため、次の事例を考えます。 請求レコードに対応する確定請求レコードの作成 確定請求レコードを作成が完了した場合に処理済みの請求レコードを管理するための処理済みフラグを更新 上記1, 2の処理を締め対象月の請求レコードに対し逐次的に処理していきます。 トランザクション の最小限の範囲を考えるにあたり次の3つの トランザクション (A, B, C)を例に考えます。 トランザクション A まず初めに状態aから状態bへの トランザクション による遷移を考えます。これは今まで締めの バッチ処理 で扱ってきたパターンです。a, bの状態はアプリケーション上許容される状態のため一貫した状態であると言えそうです。ただし、締め処理全体の処理であるため最小の処理を持つ トランザクション とは必ずしも言えなそうです。 状態a: 締め対象月の確定請求テーブルのレコードが1つも作成されていない状態 状態b: 締め対象月の確定請求テーブルのレコードが全て作成され請求レコードも全て処理済みに更新されている状態 トランザクション Aによる状態遷移 トランザクション B 次に、状態cから状態dへの トランザクション による遷移を考えます。c, dの状態は確定請求レコードの作成と請求レコードの処理済みフラグの更新が両方とも実行されており、アプリケーション上許容される状態です。そのためc, dは共に一貫した状態であると言えそうです。さらに最小の処理を持つ トランザクション は存在するのでしょうか。 状態c: ある請求レコードに対応する確定請求レコードが作成されていない、かつ請求レコードが処理済みに更新されていない状態 状態d: ある請求レコードに対応した確定請求レコードが作成され、かつ請求レコードが処理済みに更新された状態 トランザクション Bによる状態遷移 トランザクション C 最後に状態eから状態fへの トランザクション による遷移を考えます。これが トランザクション による最小の処理と言えそうです。しかし、前提として確定請求レコードの作成と処理済みのフラグ更新は両方とも処理が成功するか、もしくは両方とも処理が失敗すべきかの状態が担保される必要があり、確定請求レコードの作成のみが処理されている状態fはアプリケーション上許容されません。これは一貫した状態への遷移とは言えなそうです。 状態e: 請求レコードに対応する確定請求テーブルのレコードが作成されていない状態 状態f: 請求レコードに対応する確定請求テーブルのレコードが作成されるも請求レコードの処理済みのフラグが更新されていない状態 トランザクション Cによる状態遷移 上の3つのケースから一貫した状態から次の一貫した状態への最小限の処理を持つ トランザクション 処理は、 トランザクション Bとなります。このようにして、締めの バッチ処理 に対して最小限の トランザクション 範囲を限定し変更していきました。 達成できたこと これまで1度の締めの バッチ処理 に対してLockWaitTimeoutエラーを20 ~ 30件近く観測していましたが、 トランザクション の範囲を最小限に限定することで0件にまで抑制することができました。しかし、 バッチ処理 全体における トランザクション を廃止したため バッチ処理 全体における原子性が担保されなくなりました。原子性が担保されなくなったことにより、請求テーブル及び確定請求テーブルに依存している利用明細画面の表示に問題が発生しました。どのような問題が発生し、どう解決していったのか後編で詳細をまとめていますので、是非後編もご覧ください。 まとめ データ量の増加が見込まれるシステムでは、それに限らず一般的な システム開発 において トランザクション の範囲は最小限に限定する必要があります。今回の事例のようにレコードに広範囲のロックがとられる可能性があり処理の遅延を招くことがあります。 後編(冪等性の設計導入)へ *1 : バッチ処理 とは対照的にユーザーからのリク エス トに応じて逐次処理を実行し即時的にレスポンスを返す処理をオンライン処理と呼びます。 *2 : 縦軸は バッチ処理 が導入された当初(2020/4)の月次データ量を1としたときの倍率を表しています
アバター
この記事はタイミーのPMM(Product Marketing Manager)のishinabeが担当します。 PMM??と思った方もいるかもしれないので軽くどんなミッションを持っているのかを説明しておくと、デプス調査や定量分析などなどを絡めて顧客課題やジョブの発見から、その深さ・ボリュームの推定、リリースする機能のマーケットイン(機能に価値を感じてもらえる顧客に機能を認知してもらい使ってもらうこと)あたりを主な任務としてIssue度が高いものから解決しようと動いています。 また、今回の記事のテーマのように、デプス含めた顧客理解を組織にインストールすることも重要なミッションとして捉えています。 ※前提、世の中的にもまだ役割がかっちりと定まっている訳ではないので、私も関連チームと会話しつつ模索しながら動いている段階です(この記事の話はどちらかというと世に言うUXリサーチャーっぽい動きかも知れない)。 さて、本題の「ユーザーインタビュー参加コストを極小化する仕組み」お話しです。 なお、以降「ユーザー」→「ワーカー」と表記します。タイミーのサービスのユーザーは、お仕事内容を掲載して働く人を募集する「クライアント」と、お仕事に申し込んで働く「ワーカー」に大別されるためです。 何をやったか やりたいという意思表示をしたらあとは当日参加するだけ 裏側で処理されていること インタビュー参加のサポート なぜやったか 取り組みへのフィードバックなど 今後の展望 最後に自己紹介とおさそい 何をやったか やりたいという意思表示をしたらあとは当日参加するだけ 今の状態から話すと、ワーカーインタビューが 毎日、各1時間ずつ枠が確保されている ので、「やりたいという意思表示をGoogleカレンダーで示す」だけでインタビューに参加できるようにしました。具体的には以下のキャプチャの通りです。 「 IaaS(Interview as a Service) 」ですね、はい。 裏側で処理されていること ワーカーインタビュー用のカレンダーを作る ワーカーインタビューを定期的に開催する アドホックだと文化にならない インタビュー実施の数営業日前に、私が参加者の有無をカレンダーにて確認 希望のインタビュイー像があるかを参加者に確認(なければおまかせで募集) 基本的にはタイミーのサービス上でお仕事としてインタビュイーを募集 マッチングしたワーカーさんとインタビューURLなどのやり取りを実施 インタビュー用のNotionを作成し、マッチングしたワーカーさんの基本情報・お仕事のログを記載した上で参加者へ共有 謝礼に関する社内申請・精算の実施 インタビュー参加のサポート 参加者にインタビュアーとしての希望の参加度合いを確認し、それに叶うようにPMM/PdMが進行をサポート(つまりファシリテートはPMM/PdMがやるので、前準備や知識は必要なし) インタビューTipsをNotionにまとめ、初心者でもざっと心得を把握できるようにしている なぜやったか 状況としては2つあったと認識してます。 プロダクトチーム(PdM&エンジニア)として、以下の課題を感じていた 企画〜実施までの工程が重く、現実的にそれを実現するのは大変(に感じていた) インタビュー目的を設定して項目を作って インタビュイーを募集してやりとりして 社内の承認や費申請などを通して 当日の準備をやって 終わったら報酬のお支払いなどのやり取りをして などなど・・・ 目的を明確に設定していないとインタビューが起こりづらく、ちょっと聞きたいだけだったり、プロダクトゴール外の普遍的な顧客理解が進みにくい 私が、ジョインして以降(勝手に)インタビューをしまくっていた 昨年12月にPMMとしてジョインして以降、加入当初はまず全体感を把握するために幅広にインタビューしながら、明確にミッションを持ってからはそのIssueを深掘りするために特定セグメントのインタビューを重ねていました(数えたら営業日以上の回数インタビューしていた) フリーダムなムーブをしていた自分を、嗅覚のするどいPdMの  たかし (@tktktks10) | Twitter  がめざとく見つけて「折角だからうまく合流して仕組み化できれば良くない?」という話になりました(すごく簡略化しましたが、PdM&PMMとして、組織がユーザーの課題やジョブに深く向き合いながらサービス改善していく文化を作りたいという信念があり、その足がかりとしたい意図も合致して特に引っかかりもなくやろうとなりました) 取り組みへのフィードバックなど PdMの @tktktks10 からのお言葉 「 インタビューの企画設定コストを極限までas a serviceとして削ぎ落としたことで、開発チームが本質的なコンテンツに集中しながら顧客と話せるようになったのが尊い 」 他チームからも問い合わせが いわゆるセールスや渉外の役割を持ったチームからもワーカーインタビューをしたいという問い合わせが来ました。浸透させていきたい...。 CEOの小川も参加しました。大事。 記事を書いている途中に最近入社したBIチーム・マーケチームのメンバーも参加してくれました 参加人数(のべ) 正確にいつから始めたか調べるのが面倒なのでざっくりですが、2月下旬くらいからプロダクトチームに声をかけて、のべ20回超(5月11日時点)はプロダクトチームの人に参加してもらいました(※UUではありません。参加回数にまだまだばらつきはあります) 今後の展望 今後として、「仕組み化」という文脈ではプロセス上にまだ自分が挟まってしまっているので各工程で自動化できる余地を探りつつ、プロダクトチームが顧客の声を直接聞くという営みの価値の高さは実感できたので、より組織に浸透させる後押しをしていきたいなと思っています。 別途、インタビューをどう開発プロセスや組織の意思決定にフィードバックしたかは別記事で触れられればと思います。 また、実際のサービスやプロトタイプのユーザビリティを「定期的に」観察できる場も用意できると良いかなと考えています(Issue/Jobの探索はこの記事の取り組みで一定できるようになっているので、Solutionへの解像度を高める取り組みも浸透させたいの意)。 というわけで、顧客と向き合うの最高ですよ、まずは騙されたと思って5回やってみてください!踏み出したとして1回や2回そこらでやめない方がいいです。 そしてできればタイミーで一緒にやりましょう。 最後に自己紹介とおさそい 前職ではPdM(Product Manager)をやっていましたが、タイミーでPMMロールにチャレンジすることになりました。役割は冒頭に書いた部分を中心に据えて動いていますが、まだまだ模索中、かつ、流動的な状況です。 部署的な話でいうと組織図上はマーケティング部の中にいますが、気持ち的にはプロダクト半分、マーケ半分で、スクラムの各イベントに参加したり、こうしてTech Blogに寄稿したりしています。 昨年12月にジョインして約半年になりましたが、事業としての成長スピードもえげつなく、やれることが溢れるようにあると感じています。が、とはいえPMMは今のところ私一人なので拾いきれてないものも多く、ご一緒する方を探し求めています。 まだPMMというロールの浸透度が薄く、選考フローに乗ってくる絶対数がほぼないという現状なので、 少しでも興味があればぜひお話しできればと思ってます (意見交換目的でもぜひ...)。 product-recruit.timee.co.jp また、少なくともこの4月までプロモーションマーケの方にも関わっており、そちらの雰囲気を知りたいマーケ志望の方。もしくは、PMM視点でプロダクトチームの雰囲気を知りたいエンジニアの方など、幅広にお声がけお待ちしております。 それでは。
アバター
はじめに こんにちは、フロントエンドエンジニアの樫福 @cashfooooou です。 タイミーでは toB 向け管理画面を作成しています。 半年ほど前、タイミーでは顧客からのサービスへの要望を集め、管理するシステム(以下、要望回収システム)を作りました。 顧客の課題から新しい機能について考え、顧客により価値のあるものを届けるための施策です。 システムの実装には Notion という SaaS を活用しました。 最小限の実装で良い機能・良い運用が作れたと思っています。 この記事では、要望回収システムの実装に取り組んだ経緯から、実際の運用の例まで紹介します。 同じように顧客の要望回収を行いたい方はもちろん、 SaaS を使ったミニマルな機能開発の参考になれば幸いです。 はじめに Notion 従来の回収システム 課題 解決したい課題 Notion API を用いた課題解決 制約 データベースの構成 回収システムの Notion の構築 運用 まとめ Notion Notion は、タスク管理や Wiki の作成に適した Web アプリケーションです。 ユーザの手によってカスタマイズしやすく UI も使いやすいので非常に便利です。 www.notion.so 弊社ではエンジニアだけでなく、社全体のドキュメント管理の大部分で Notion を用いています。 Notion 好きの社員も多く、 最近は Notion の活用術について CTO の kameike が インタビュー を受けたりしました。 従来の回収システム 従来の回収システムは Google フォーム と Google スプレッドシート を組み合わせたものを利用していました。 課題 従来の回収システムは顧客から寄せられた要望を閲覧できますが、次のような複雑な操作が難しいです。 似た要望同士を紐付けて管理する 対応の優先順位を決める プロジェクトやタスクと要望を紐付ける 結果として、 要望が集まれば集まるほど確認作業の手間が増える というジレンマに陥り、運用が十分に回らなくなってしまいました。 解決したい課題 従来のシステムの課題をもとに、次の方針を立てました。 要望に対して操作をしやすく 似た要望の紐付け 進捗の管理、優先度をつけられる 要望からタスクに繋げやすく なるべく実装が少なく 我々のチームは普段 Notion を用いてタスク管理しているので、それとうまく連携が取れると運用が楽になって嬉しいです。 加えて、要望回収システムは弊社のサービスのコア ドメイン との直接的な関係が薄いのでなるべく手間をかけずに実装したいと考えていました。 Notion API を用いた課題解決 この議論をしていたのは2021年7月ごろでしたが、その2ヶ月ほど前に Notion API がパブリックベータとして公開されました。 developers.notion.com パブリックベータとはいえ、レコードの閲覧や作成ができるのでいろんなことができます。 寄せられた要望を Notion 上でを管理できると、Notion を用いた既存のタスク管理との連携も容易になりました。 また、似た要望をまとめるような操作も Notion のリレーションなどを使って実現できます。 そして何より、これらのロジックもUIも、そのほとんどを Notion の機能を用いて実現できるので実装の手間がとても小さくて済みます。 今回実装した要望回収システムは、 Notion API を用いて次のような構成になりました。 要望システムの構成 顧客は弊社のサービス画面の「プロダクトへの要望フォーム」から要望を投稿します。 投稿された要望は API サーバから Notion API を経由して Notion のデータベース に蓄積されます。 要望が投稿されると Slack に通知が届くので、エンジニアは Notion の Web ページから顧客の要望を確認することができます。 このシステムを導入する上で実装すべきことは 顧客の投稿フォームの実装 Notion API を用いた Notion の DataBase への投稿機能 Slack への投稿 のみです。 Notion の機能のおかげで下のような恩恵が受けられました。 DB のカラムの変更などが、エンジニアの 工数 を割かなくても手軽にできる データの操作のための UI や API の設計・実装が不要 Notion API の登場によって、抱えてた問題の全てが解決しました。 制約 こちらのページ でNotion API には 平均3リク エス ト/秒のレートリミットについて触れられています。 将来的に料金プランに応じてレートリミットが変わる予定もあるようです。 Notion API を利用する際にはこのあたりに注意することをおすすめします。 弊社のサービスに寄せられる要望は多くても一日10件程度のなので、レートリミットの問題は無いと判断しました。 データベースの構成 システムを実装するにあたって、顧客から寄せられる要望の分析をしました。 寄せられた要望を見ると、「〇〇の条件下で△△してほしい」や「✗✗機能を追加してほしい」のような具体的な解決案が送られてくることが多いです。 これらの具体的な解決策に直接対応していっても、プロダクトが抱えている課題を本質的に解決するのは難しいことが多いです。 そこで、似た要望をまとめて抽象化し、プロダクトの課題として扱うのがよさそうだと考えました。 以上のことを踏まえて、要望の管理のために次のようなテーブルを作成することにしました。 *1 図中の矢印は Notion のテーブルのリレーションを表しています。 データベースの構成 「要望」は顧客から寄せられたメッセージそのものです。 「課題」は要望の根底にあると考えられるプロダクトの課題です。 要望と課題はそれぞれ進捗度合いを表すステータスを持っていて、進行度合いを確認できます。 タグは類似の要望や課題を整理するのに役立ります。 また、要望・課題・backlog はそれぞれリレーションを持っていて、情報の管理に役に立ちます。 回収システムの Notion の構築 こちらのページ に、上の図のテーブルを再現してみました。 寄せられた要望、そこから深ぼった課題、実際にエンジニアが着手するタスクの例を埋めています。 (簡単のために、 twitter のような架空の SNS に対して寄せられた要望という設定です。) 要望データベース 「要望」テーブルの一番右の列は、要望と紐付いている「課題」テーブルのアイテムが表示されています。 同様に、「課題」テーブルには紐付いている「backlog」テーブルのアイテムが表示されています。 更に、Notion でテーブルの各レコードが Notion Page になっているのも大きな利点です。 要望・課題について議論をしたときに直接 議事録を書き込むことができるので、後で確認するときに情報が漏れる心配がないです。 議事録の作成には Database templates が便利です。 ページ内の課題のレコードを開くと議事録のサンプルも確認できるようになっています。 Database Template を使った議事録の作成 実際に Notion を触っていただくとよくわかると思いますが、データの更新、ソートやフィルタ、他のテーブルとのリレーションなど、実際に実装するとかなりの 工数 のかかりそうな機能が手軽に使えるのは本当に嬉しいです。 運用 エンジニアや PM が定期的に要望テーブルを確認してタグを割り当てたり課題・タスクの作成・紐付けを行っています。 課題の作成・紐付けも Notion のデータベースのフィルタやソート機能を使って、過去に作成した同様の課題と比較しながら作業ができるので無駄が少ないです。 テーブルのレコードがそれぞれが Notion Page となっていることの恩恵を強く感じます。 Database templates を用いた議事録やテーブルの管理がかなり楽で助かります。 まとめ プロダクトの要望回収システムを Notion API を用いて実装しました。 Notion の使い勝手がよいので実装の手間がかなり小さく、よい選択ができたと思っています。 今回のような社内向けの機能の開発において Notion API は、最小限の実装のコストで十分な機能が実現できる Notion のポテンシャルを見せつけられました。 加えて、すでに社内の情報管理のために Notion を導入しているのであれば、連携することでさらなるうまみが得られるチャンスです。 コア ドメイン ではない機能の開発や新機能の仮説検証など、あまり 工数 をかけずにミニマルな実装をしたいときには Notion をはじめとした SaaS を活用してみてはいかがでしょうか。 Notion 最高!一番好きな SaaS です! *1 : 実際に運用しているテーブルからいくつかのプロパティを落としています。
アバター
はじめに はじめまして、 Android エンジニアのmurata( @orerus )です。 アイラ系 ウイスキー を愛していますが、肝臓が弱まってきた為最近は専ら0.5% ハイボール を愛飲しています。 本記事では、タイミーのモバイル アプリ開発 におけるSLO(サービスレベル目標)を設けているメトリクスのちょっとした改善事例について紹介します。 SLOとは何かといった話やタイミーで運用しているSLOについては こちらの記事 にて詳しく紹介していますので是非ご覧ください! 本記事の概略 タイミーのワーカーチームでは、モバイル アプリ開発 における指標の一つであるクラッシュフリーレートをSLI(サービスレベル指標)としてSLOの運用を行っています。 しかし、長く運用する中で、SLO運用に期待されている「当たり前品質と攻めたリリースのバランスを取る」「当たり前品質の低下をいち早く検知する」「適切なタイミングでプロダクト品質への改善圧をかける」といった役割が果たされていないと感じられるケースが度々発生していました。 そこで、モバイル アプリ開発 メンバーが普段観測しているクラッシュフリーレートのメトリクスを改善することにより問題を解消した事例について紹介します。 目次 はじめに 本記事の概略 目次 現状と課題 行った改善策 結果変わったこと 最後に APPENDIX 現状と課題 タイミーではSLOを運用しているメトリクスが日に1度slackに投稿されるようになっています。 実際にタイミーで運用している、とある日の改善前のクラッシュフリーレートのメトリクスがこちらです。 表1. 過去30日間、7日間におけるクラッシュフリーレート ① : 過去30日間、7日間におけるクラッシュフリーレート ② : プラットフォーム/アプリバージョン単位でのメトリクス詳細 ※今回は触れません 表の左から プラットフォーム ( iOS or Android ) アプリバージョン クラッシュに遭遇しなかったユーザー数 クラッシュに遭遇したユーザー数 クラッシュフリーレート こちらのメトリクスにおいて、ワーカーチームでは以下のSLOを策定していました。 iOS : クラッシュフリーレート 99.95% 以上 Android OS: クラッシュフリーレート 99.60% 以上 ここで先程のメトリクスを見てみると、 Android 側はすでに過去30日間におけるクラッシュフリーレートにおいてSLO違反を起こしてしまっており、プロダクト品質への改善圧をかけるべき状態となっていることが分かります。 しかしこの時、 Android 開発メンバーの間では「直近で品質改善の施策を行ったので既に改善されているはずだ」という認識を持っていました。 このような事例の他、幾つかの事例からこのメトリクスによるSLO運用には以下の課題があることが分かりました。 メトリクス単体で指標値の変化の傾向が掴めず、問題発生の検知が遅れ早期の対応が行えない SLO違反を引き起こしている要因をメトリクスから把握することが難しい 品質改善などの施策の結果がメトリクスに反映されるまで時間がかかり、かつ反映されるまでの時間の予測も立ちづらく、施策実施後もSLO違反による改善圧がしばらくかかり続けてしまう これらの課題を眺めていると、いずれも過去30日間/7日間のクラッシュフリーレートという施策の実施から結果が反映されるまでに時間のかかる指標のみに頼ってSLOを運用している為に発生しているものである、という予測が立ちました。 行った改善策 先述の課題解決の為、以下を目的としたメトリクス改善を行うことにしました。 指標値の変化の傾向をメトリクスから掴めるようになる 指標値が大きく変化したタイミングを的確に把握できる その結果、追加したメトリクスがこちらです。(こちらのメトリクスは表1と同日に計測したもの) 表2-1. クラッシュフリーレートSMA 表2-2. クラッシュフリーレートSMAグラフのみ ① : プラットフォーム単位での過去60日間におけるメトリクス クラッシュフリー率 : 1日毎のプラットフォーム単位でのクラッシュフリーレート 7日間SMA : 各日付を含む過去7日間の 移動平均線 (Simple Moving Average) 30日間SMA : 各日付を含む過去30日間の 移動平均線 ② : プラットフォーム/日付単位でのメトリクス詳細 ※今回は触れません 表の左から 日付 クラッシュに遭遇しなかったユーザー数 クラッシュに遭遇したユーザー数 クラッシュフリーレート 7日間SMA 30日間SMA ※上記メトリクスの算出方法については文末のAPPENDIXにて紹介します この表2のメトリクス追加により、表1のメトリクスと組み合わせて以下のことが分かります。 Android 表1の「30Daysクラッシュフリーレート」においてSLO違反を起こしているが、表2の「7日間SMA」から過去1〜2週の間に改善施策が行われていて改善傾向にある よって今後のメトリクスに置いても「30Daysクラッシュフリーレート」が改善されることが予測される iOS 「30Daysクラッシュフリーレート」がSLO違反スレスレの値ではあるが、こちらも「7日間SMA」「30日間SMA」を見ると上昇傾向にある為、直ちに対策を取る必要性は無さそう 上記の例はポジティブな結果のみのサンプルではありますが、ネガティブなケースでも同様に傾向を把握することができそうです。 結果変わったこと 今回のメトリクス改善を行い、実体験として変化があったと思うものを以下に挙げます。 SLO違反の予兆や改善の傾向が把握できるようになった 施策の結果を客観的な指標値で速やかに把握することができるようになった 過去のメトリクスを見返す必要がなくなった メトリクスに対する 心理的 変化が発生した 1 〜 3 の結果、当初のSLO運用の目的である「当たり前品質と攻めたリリースのバランス」の判断をより精度高く判断することができるようになった為、この改善はやってよかったと感じています。 また、1 〜 3 の変化については当初の目的通りですが、4については思わぬ変化でした。以前のメトリクスでは点でしか指標値を見ることができず、結果をSLO違反しているかしていないかのゼロイチでしか受け取ることができていませんでした。それが今では線で指標値を見ることができるようになり、プロダクトの当たり前品質の機微な変化を捉えやすく、またメトリクスを施策の結果を速やかにフィードバックしてくれるパートナーとして認識できるようになったため、自然と前向きにメトリクスを確認しに行くことができるようになりました。 最後に 前項にも触れた通り、SLOおよびメトリクスは改善圧をかけてくる敵ではなく、プロダクトの当たり前品質という健康状態を維持する手助けをしてくれるパートナーです。是非味方につけて、プロダクトの体験改善の為の攻めたリリースと当たり前品質の維持を効率よく両立していきましょう! また、現在はモバイル アプリ開発 においては今回取り上げたクラッシュフリーレートというアプリのクラッシュにまつわる指標でしかSLOを運用できていませんが、クラッシュには至らずとも機能が正しく動いていないケース *1 、機能は動いているがユーザーが満足に使えていないケース *2 など、プロダクトの体験改善の為に注視していくべき観点はたくさんあります。今後も体験改善に繋げることのできる指標を測定可能にし、攻めたリリースを行うためのSLO運用を行っていく所存です。 その為に脳みそを貸してくれる仲間、また美味しい ウイスキー 銘柄を教えてくれる仲間を絶賛募集中です 😆 APPENDIX 表2の①のグラフを作成する為に使用している SQL を記載します。 こちらで抽出した結果を Google Data Studioを用いてグラフ化しています。 なお、こちらの SQL はFirebase CrashlyticsのデータをBigQueryに転送している環境( 参考URL )であればこのまま利用可能です。 ※ スキーマ 名は各自の環境に合わせて修正ください -- TemporaryTable1. BigQueryからクラッシュ情報を抽出 WITH userCrashes AS ( SELECT event_date, user_pseudo_id, platform, MAX ( event_name = ' app_exception ' ) hasCrash, MAX ( event_name = ' app_exception ' AND ( select value.int_value = 1 from unnest(event_params) where key = ' fatal ' ) ) hasFatalCrash FROM `analytics_17541xxxx.events_*` -- 各環境のスキーマ名に修正してください WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE( " %Y%m%d " , DATE_SUB( CURRENT_DATE ( " Asia/Tokyo " ), INTERVAL 60 DAY)) -- 過去60日間 AND FORMAT_DATE( " %Y%m%d " , DATE_SUB( CURRENT_DATE ( " Asia/Tokyo " ), INTERVAL 1 DAY)) AND platform = " ANDROID " -- 測定したいプラットフォーム IOS or ANDROID GROUP BY event_date, user_pseudo_id, platform ), -- TemporaryTable2. TemporaryTable1から日付単位でクラッシュ数やクラッシュフリーレートを集計 userCrashesCount AS ( SELECT event_date, platform, IF (hasCrash, ' crashed ' , ' crash-free ' ) crashState, IF (hasFatalCrash, ' crashed fatal ' , ' crash-free ' ) fatalCrashState, COUNT ( DISTINCT user_pseudo_id) AS crashFreeUsers, ( SELECT COUNT ( DISTINCT user_pseudo_id) FROM userCrashes AS uc2 WHERE uc2.event_date = userCrashes.event_date AND uc2.platform = userCrashes.platform ) - COUNT ( DISTINCT user_pseudo_id) AS crashUsers, ROUND ( COUNT ( DISTINCT user_pseudo_id) / ( SELECT COUNT ( DISTINCT user_pseudo_id) FROM userCrashes AS uc2 WHERE uc2.event_date = userCrashes.event_date AND uc2.platform = userCrashes.platform ), 4 ) AS crashFreeUserShare, ( SELECT COUNT ( DISTINCT user_pseudo_id) FROM userCrashes AS uc2 WHERE uc2.event_date = userCrashes.event_date AND uc2.platform = userCrashes.platform ) AS users FROM userCrashes -- TemporaryTable1 WHERE hasCrash = false GROUP BY event_date, platform, crashState, fatalCrashState ORDER BY event_date ) -- 抽出結果. Table2から30日間SMA、7日間SMAを算出 SELECT platform, -- プラットフォーム event_date, -- 日付 crashFreeUsers, -- クラッシュに遭遇しなかったユーザー数 crashUsers, -- クラッシュに遭遇したユーザー数 crashFreeUserShare, -- クラッシュフリーレート CASE WHEN 7 = COUNT (crashFreeUserShare) OVER ( ROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) THEN AVG (crashFreeUserShare) OVER ( ROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) END sevenDaysSMA, -- 7日間SMA、過去7日分の数値が取れない日付の分は空にする CASE WHEN 30 = COUNT (crashFreeUserShare) OVER ( ROWS BETWEEN 29 PRECEDING AND CURRENT ROW ) THEN AVG (crashFreeUserShare) OVER ( ROWS BETWEEN 29 PRECEDING AND CURRENT ROW ) END thirtyDaysSMA -- 30日間SMA、過去30日分の数値が取れない日付の分は空にする FROM userCrashesCount -- TemporaryTable2 ORDER BY event_date なお、上記 SQL を構築するにあたり、 こちらのStackOverFlowの回答 を参考にさせていただきました。 *1 : API の4xxエラー等の監視は別途行っています! *2 : API の422エラー(バリデーションエラー)の発生件数を監視することで、分かりやすいUI/UXを実現できているかを確認しています
アバター
はじめに はじめまして、 Android エンジニアのsyam( @arus4869 )です。 普段は愛知県からフルリモートで勤務していますが、最近は褪せ人として荒野を駆けています。 本記事の概略 本記事では、タイミーの Android プロジェクトで挑戦しているモジュール分割の取り組みについて紹介します。 また内容の理解を促すため、マルチモジュールについても軽くおさらいします。 本記事では、以下の方を読者として想定しています。 他社のモジュール分割の手法に興味がある方 マルチモジュールなプロジェクトに挑戦してみたい方 はじめに 本記事の概略 現状のタイミーとアプリについて タイミーのアプリ開発におけるチーム構成 課題 モジュール分割とは 「マルチモジュール」なプロジェクトの例 分割のステップ モジュール分割のメリット モジュール分割のデメリット 現在のアーキテクチャ analytics pubsub component style core repository repository-impl Feature Moduleによる機能分割について FeatureModuleにおける画面遷移について考えていること おわりに 現状のタイミーとアプリについて タイミーの Android アプリでは、レイヤー単位でのモジュール分割はある程度行っていますが、機能単位でのモジュール(以降、Feature Moduleと呼びます)分割が行われていないのが現状です。 現在タイミーの開発組織は「マッチング領域」と「スポットワークシステム領域」という2つの領域でわかれています。今回は私が所属している「マッチング領域」の話です。 マッチング領域ではLeSSを採用しており、POと2つの開発チームが存在します。また、そのチームの枠を超えて技術的な課題を横断的に解決するため技術領域ごとにCommunityが存在します。現状マッチング領域の Android エンジニアは全員 Android Communityに所属しており、Community内で話し合った方針に基づきアプリケーションの アーキテクチャ の改修や リファクタリング を行っています。その中の一つが Android アプリのモジュール分割です。これを行うことで将来発生する開発人数・コード規模の増加などに伴って発生する様々な問題に対処しやすくなると考えています。 LeSSについて詳しく知りたい方はこちらの The LeSS Company B.V が提供している イントロダクション が参考になると思います。 タイミーの アプリ開発 におけるチーム構成 課題 現状抱えている課題は大きく2つあります。 開発組織の拡大に伴い、複数人での並列的な開発により生じる課題 広範囲で強制化されていないコードが存在しているため、コードを触る開発者の人数が増えることによって、 アーキテクチャ の強制が効かなくなる可能性がある コンフリクトの増加 別開発チームのレビューにかかる時間が増加 サービスの拡大に伴いコード量が増加することで生じる課題 ビルド速度の低下 リファクタリング における影響範囲が読みづらくなる コードが読みづらくなる 他にも様々な課題が考えられそうですが、モジュール分割を適切に行うことによって、上記課題の改善がされることを期待しています。 モジュール分割とは タイミーの Android プロジェクトで採用しているモジュール分割の取り組みについてお話しする前にモジュール分割について簡単におさらいします。 Android Studio などの IDE で新規プロジェクトを作成すると、最初にApplication Module(app ディレクト リを持つモジュール)が作成されます。この状態はシングルモジュールなプロジェクトと呼ばれます。 このApplication Moduleを複数のモジュールに分割することが可能であり、モジュール分割されたプロジェクトを「マルチモジュール」なプロジェクトと呼びます。 本記事における「マルチモジュール」なプロジェクトは、1つの「Application Module」と「Application Module」から分割された複数の「Library Module」から構成されます。 また「Library Module」からも複数の「Library Module」を分割することが可能です。 他にも「Dynamic Feature Module」などのモジュールがありますが、本記事では取り扱わないこととします。 モジュール分割の具体的な方法については、Doroid Kaigiの codelabの既存のアプリをマルチモジュール化 するを参照してみると良いと思います。 「マルチモジュール」なプロジェクトの例 モジュール分割をする際は、既存の「Application Module」にあるレイヤーや機能を分割することで、思考の関心事を分離することができます。そのためモジュール分割を実施する前にどう分割するかを計画する必要があります。 またモジュール分割する際の注意点として、モジュール間の依存関係を単一方向に保つ必要があるため、抽象化の範囲を適切に考えておく必要があります。 モジュール分割を行なうために例えば以下のようなステップが考えられます。 分割のステップ コードの共通部品をモジュールに分割する 関心を切り離したいレイヤーを決めて分割の優先順位をつける 優先順位ごとにモジュールを分割する 古いUIのコードを別のパッケージに移す UIのコードを「Feature Module」として機能単位に分割 優先順位の付け方は諸説ありますが、ステップを踏んで適切にモジュール分割をすることによって、様々な恩恵を受けることができます。 モジュール分割のメリット モジュール分割を行なうことで受けられる恩恵のひとつとしてビルド時間の短縮化が挙げられます。 既にビルドが行われ編集されなかったモジュールは、キャッシュ化されます。 そのため、ビルド時には編集されたモジュールだけを コンパイル すれば済むので、ビルド時間の短縮につながることいわれています。(Gradleによるビルドシステムの恩恵が大きいと考えられます) モジュール分割では、ビルド時間の短縮だけでなく、以下のように様々なメリットが考えられます。 コードを触る範囲を狭め、ある程度強制することができる 関心ごとを分離できるため、コードを触る範囲が明確になる 関心ごとを分離できるため、機能の実装に集中できる 関心ごとを分離できるため、新規開発者が参画しやすくなる モジュール分割でコードの抽象化が行われることによる恩恵が大きいと思います。また、先述で挙げた課題についても改善に繋げることができると思います。 他にも様々なメリットがありますが、計画を立てずにモジュール分割を目的にするとモジュール分割に失敗し、逆にデメリットになる場合もあります。 モジュール分割のデメリット モジュール間の依存が増えると、依存間の関係性が複雑になることによりビルド時間が伸びる可能性があります。 そのため、結局ビルド時間の短縮に繋がらなくなるので過度な抽象化には注意が必要です。 モジュール分割による抽象化は正解がないので、規模が大きくなるにつれ、計画を練る難易度が高くなります。 そのため小さくステップを踏んで、少しずつモジュール分割を進めていくのをお勧めします。 現在の アーキテクチャ タイミーの Android アプリでは、1つの「Application Module」(以下、appモジュール)と20の「Library Module」で構成されています。 ここでは全体のモジュールの構成と代表的なモジュールの役割について簡単に紹介します。 全体のモジュールの構成としては、下図の通りです。 基本的にはappモジュール(Application Module)がほとんどのモジュールと依存しています。appモジュールにはレガシーなコードがまだ混在しているので、新旧それぞれのコードを別パッケージに振り分け、新パッケージ内に内包されているUIのコードが分割したモジュールと依存するようになっています。 レガシーなUIのコードはappモジュール内のviewパッケージ 新規に実装するUIのコードはappモジュール内のuiパッケージ analytics 分析基盤のモジュールです。Firebaseの他にAdjustやReproなどのアナリティクスに特化したモジュールです。 pubsub Publishersからメッセージを送り他の画面でSubscribeするモジュールです。 募集画面などでお気に入りしたイベントを他の画面に通知させたりしています。 Rxで仕組みを実現させています。 component 部品として独立したモジュールを内包しています。 imageviewer license pickimage style ActivityやUI コンポーネント 等のstyleがAppThemeとして定義されているモジュールです。 core Andoroid コンポーネント の開発で使用する共通的なプログラム等が内包されているモジュールです。 repository ドメイン モデルをリソースとして返すインターフェースが内包されているモジュールです repository-impl repositoryモジュールのインタフェースを実装した ドメイン モデルの具象を取得するプログラムが内包されているモジュールです。 各モジュールについて簡単に触れさせていただきましたが、また別の機会にて具体的に紹介できたらと思います。 タイミーのモジュール分割の現状ですが、先述の「分割のステップ4」にあたる「古いUIのコードを別のパッケージに移す」に位置しており、「分割のステップ5」の「UIのコードをFeature Moduleとして機能単位に分割する」ことを目指している最中です。 Feature Moduleによる機能分割について 本記事で取り扱う「Feature Module」についてですが、「機能としての画面」をモジュール単位で分割することを指しています。 例えば下図の「検索」や「募集一覧」が「機能としての画面」にあたります。 このような「機能としての画面」を「Feature Module」で機能分割して利用する場合、下図のような構成になります。 このように機能単位で分割することによって、自チームが検索機能を実装する場合、募集機能を実装している別チームに影響を与えずに同時に開発可能な領域を増やすことができます。 機能単位で独立して実装できるので、並列のチームを抱える開発組織において、モジュール分割でのメリットと同時に強力な恩恵を受けることができると考えています。 このような恩恵を受けるため「Feature Module」による機能分割を目指しています。 FeatureModuleにおける画面遷移について考えていること 「Feature Module」による機能分割を成功させるためには、画面導線を秩序立って配置する必要があります。そのため、画面の中に複数機能への遷移があった場合にモジュールを組み合わせしやすい形にしておきたいと考えています。 下図のような画面遷移が例として挙げられます。 しかし、相互依存しないような画面遷移について考えなくてはならないため、どのように「Feature Module」を分割すれば良いか悪戦苦闘しています。 下図は相互依存するような画面遷移の例になります。(実際に下図のような画面遷移はしないのであくまで例です。) Gradle上でモジュールの参照を定義した場合、循環参照になってしまい相互依存してしまうため、単純に「Feature Module」同士での依存は、上図の画面遷移を考慮した場合に実現不可能です。 相互依存しない画面遷移を実現するためには、「画面遷移の抽象化」を考えなければなりません。 現状においては、中間にモジュールを挟むことによって「画面遷移の抽象化」を図りたいと考えており、下図のような構造をイメージして具体の実装を考えています。 「画面遷移の抽象化」についてはまだ道半ばなため、次回以降の記事で取り上げたいと思います。 おわりに 本記事では、タイミーの Android プロジェクトで採用しているモジュール分割の取り組みについて紹介させていただきました。 タイミーは挑戦できる土壌がある会社で、脳みそをたくさん使うことができます! モチベーションの高い仲間たちと様々なことにチャレンジしてみませんか? 是非興味をお持ちの方は気軽にタイミーへ応募してみてください! iOS Communityの @sky_83325 もマルチモジュールについての記事を書いてますので、こちらも興味があれば読んでいただけらと思います! tech.timee.co.jp
アバター
はじめに はじめまして、バックエンドエンジニアのぽこひで ( @pokohide ) です。 最近の日課はゲーム実況者「 兄者弟者 」の「DYING LIGHT 2 STAY HUMAN」と「エルデンリング」を見る事です。 本記事ではタイミーで長年使われていた、マイクロサービスとして切り出されたチャットサーバ(以降、旧チャットサーバと呼びます)をタイミーの中核を担うモノリシックなRuby on Railsサービス(以降、タイミー本体と呼びます)に移行した話です。 今回は移行した経緯、気にした点などを紹介します。 対象にしている読者は以下の方々です。 レガシーなシステムと向き合っている人 無人化システム ※1 に疲弊している人 ※1 : 無人化システムとは この記事 に出てくる造語で「 誰も詳細は知らないが、なぜか動いているシステム 」を意味する はじめに チャット機能とは 旧チャットサーバとは なぜやるのか やること・やらないこと やること やらないこと 移行計画の検討 移行計画まとめ 結果 最後に チャット機能とは タイミーは「働きたい時間」と「働いてほしい時間」をマッチングするスキマバイトサービスです。マッチングすると事業者(クライアント)と働き手(ワーカー)はチャットを通じてやりとりができます。 タイミーリリース当初から提供されており日常的に使われている機能ですが、マッチング後に働くまでの流れといったテンプレートメッセージを事業者が手動で送信していたり、画像等のファイルを送受信不可能だったりと色々な課題も抱えてもいました。 この機能の改善はマッチングから働くまでの無駄な時間の削減に繋がり価値が高いため、弊社ではコアドメインと考えています。 旧チャットサーバとは 旧チャットサーバはGo言語で書かれておりアカウントのAuth、プロフィール情報の保存、メッセージの送受信、既読の有無といった機能を有するサービスです。 以下の図は、アプリ上でメッセージタブを開いた時の流れを簡略化したものです。 旧チャットサーバの持つアカウントはタイミー本体のアカウント情報をハッシュ化したものを利用しているため、タイミー本体は旧チャットサーバのアカウントやルームを取得できるが逆は行えないといった欠点があります。 そのため、タイミー本体からマッチング中の募集を取得してそれに対応するルームを取得するといった流れになっています。この際、GETなのにDBにWriteが走る可能性があったり悲しい事にもなっていました。 以下の図は、ルームを選択してメッセージ投稿までを簡略化したものです。 旧チャットサーバでは受信者へのプッシュ通知やメール送信は行なっていないため、メッセージ登録時にタイミー本体にリクエストを行なっています。 他にもプロフィール更新はタイミー本体を介して旧チャットサーバにリクエストを送るため、同一アカウントに対して複数の経路からAuthが実行される事で認証の奪い合いになるといった事象も発生していました。 なぜやるのか 端的に言うと コアドメインにも関わらず継続的改善ができない状況 だったからです。 上述したものも含めツラミをまとめたのがこちら クライアント、バックエンドどちらからも認証を行うため認証の奪い合いが起こる アカウント情報がハッシュ値のため特定が難しく分析が困難 チャット → マッチングが疎結合で体験的改善が走らない 障害やエラーが発生しても対応が困難 単一機能を提供することを前提とした設計のため追加の開発が困難 色々なツラミを抱えていましたが、今回は次のような目的と制約を決めました。 目的 - チャットを**継続的改善**と**タイミーとのシームレスな連携**が可能な状態にすること - チャット情報を**分析可能な状態**にすること 制約 - 旧チャットサーバに**一切手を加えない** やること・やらないこと 移行の流れを決める前に今回の目的と制約の中でやる事・やらない事を決めました。 やること 将来を見据えた設計で旧チャットサーバの機能を踏襲したチャットの実装 今までと変わらないメッセージ体験の提供 ルームやメッセージなどは同じ機能を持つ 古いバージョンのアプリをサポート やらないこと メッセージの既読情報の同期 旧チャットサーバ側のRDBに保存される過去データの移行 厳密な同期処理の実現 旧チャットサーバはメッセージ作成時にタイミー本体にリクエストを送っていますが、中身はメッセージ本文とアカウント情報のみで既読などの情報は含んでいません。旧チャットサーバに手を加えない制約のため、無理な同期はせずこれはやらないこととしました。 他にも、タイミーでは報酬確定後24時間でルームが閉じます。このため、見えないデータである過去データの移行は行わない事に決めました。 タイミーにとってのチャットは重要な機能ではありますが、SNSやメッセージアプリに比べて高精度な同期性を求められるほど頻繁に連絡が行われているわけではありません。早く届くことよりも確実に届けられることが重要です。そこで、高精度な同期はベストエフォートとしつつ結果整合性を重要視しました。 移行計画の検討 「マイクロサービスとして新規開発する」「メッセージ関連の外部SDKを導入する」といった方法も考えられましたが、タイミー本体とのシームレスな連携をするために チャットをタイミー本体に移行する ことを選択しました。 また、チャットはWebブラウザからもアプリからもアクセスされるサービスです。アプリにはバージョンの概念があるため移行に向けて必ずアップデートの浸透待ちを必要とします。 そこで今回は、タイミー本体で新しくチャットAPI(以降、新チャットサーバと呼ぶ)を実装して、Webブラウザとアプリの特定バージョン以降はタイミー本体を利用させつつ、アプリの推奨・強制アップデートを活用して旧チャットサーバを利用するバージョンを徐々に減らしていく手法を考えました。 その間、新旧チャットサーバを行き来することが考えられるのでデータ同期が必要となります。ここで既存のチャットサーバからのWebhookや解放されているAPIを利用します。 実際には旧チャットサーバのAuthは不安定なところがあるため、新→旧の同期処理は非同期で行われています。正しくデータが同期される事を保証したいのですがリトライ上限に達して非同期ジョブが終了してしまう可能性もあります。それを考慮して上限に達した場合はログを残し、それを検知可能にする事で見つけ次第、泥臭く対応する事にしました。 お気づきの方もいるかもしれませんがこのデータ同期は完璧ではありません。なぜなら、旧チャットサーバはメッセージの登録を契機にWebhookリクエストを行います。そのため、悲しい事に新 → 旧で同期を行うと旧 → 新と全く同じメッセージが返ってきてしまいます。 さらに上述の通り、このWebhookリクエストの中身はメッセージ本文とアカウント情報のみです。同じアカウントが全く同じ内容のメッセージを送信した場合、それらが違う事を識別できません。 そこで新チャットサーバ側で、 5分以内に全く同じ内容のメッセージがある場合は何もしない といった処理を加えました。これにより 新 → 旧 → 新のメッセージ投稿のループに対応できる 同じアカウントから同じ内容のメッセージは識別できないのであえて識別せず、5分以内であれば1つのメッセージとして扱う ようにしました。これは今回の移行に際して諦めたことです。 最終的にはアップデート浸透待ち期間を経て、アプリから旧チャットサーバへのアクセスがなくなったことを確認し、データ同期処理をやめて完全に新チャットサーバのみで稼働させることで移行を完了させるといった計画を立てました。 移行計画まとめ タイミー本体で新しくチャットAPI(新チャットサーバ)を実装する アプリで新チャットサーバを利用する 推奨・強制アップデートを利用して旧チャットサーバから新チャットサーバに徐々に移行する 旧チャットサーバへのリクエストがなくなったことを確認したのち、データ同期処理を止める 以下は余談です。 今思えば、アプリのバイナリ単位で向き先を変えるのではなく、ストラングラーパターン ※2 を用いて段階的に向き先を変えることで制御可能でロールバックも容易になり、より安全な移行ができたなと思いました。 ※2 : 既存のアプリケーションと新しいアプリケーションを振り分ける「ストラングラーファサード」というレイヤーを設けて機能の特定部分を新しいアプリケーションに徐々に置き換えながら移行する手法 https://docs.microsoft.com/ja-jp/azure/architecture/patterns/strangler-fig 結果 以下のような時系列で特に大きな障害なく旧チャットサーバと完全にお別れができました。 2020年12月 2021年3月 2021年4月 ~ 2021年11月 2022年1月 推奨・強制アップデートのフロー構築も同時に準備していたためアップデート浸透待ちに半年以上を要した結果となりました。 タイミーとのシームレスな連携も可能となり「チャットを通じたお問合せ」「複数人のチャット」「システム的なメッセージ」「メディアのやりとり」といった将来を見据えた拡張性の余地も持たせることができました。 また、副次的な効果として全エラーの約3割を占めていた旧チャットサーバとのAuth時の401エラーも0件になりSentryのノイズも減りました。 最後に 今回はコアドメインなのに継続的改善が行えない状況をタイミー本体に移行する事で解決しました。リスクの伴う決断でしたが、継続的な体験改善が行えるようになったのでより使いやすいアプリ開発を頑張っていきます。 技術的負債の解消や継続的な体験改善に興味がある方は是非、声をかけてください! product-recruit.timee.co.jp
アバター
はじめに 以前のデータ基盤 3つの問題解決と振り返り 問題1: データパイプラインの更新遅延 解決策 実装 振り返り 問題2: 分析チームへのクエリ修正依頼の増加 解決策 実装 振り返り 問題3: ETLパイプラインにおける加工処理の負債 解決策 実装 振り返り これからの品質に関する改善 はじめに 初めまして、タイミーのDRE (Data Reliability Engineering) チームの土川( @tvtg_24 )です。 本記事ではデータ品質の保守に着目してここ1年くらいで試行錯誤したことを振り返っていきたいと思います。 対象にしている読者は以下の方々です。 データ品質について考えている方 データ分析の品質担保に困っている方 ETLからELTへの基盤移行を考えている方 この記事は  Data Engineering Study #11「6社のデータエンジニアが振り返る2021」 - connpass で発表させていただいた内容を詳細に説明したものになります。 登壇スライド:  データ基盤品質向上のための一年 - Speaker Deck 動画:  https://youtu.be/q9HA1S3vmcE?t=7578 以前のデータ基盤 点在しているデータソースを統合した分析するためにEmbulk, Digdagを中心に用いて1年前に構築し終えたデータ基盤がこちらになります。 約1年前のデータ基盤 1番左の各種データソースからEmbulk, Digdagを用いて、BigQueryにデータを収集し、Redashやデータポータル、BigQuery上で分析を行ってもらっていました。 EmbulkはデータパイプラインにおけるETL (Extract, Transform, Load) を差分更新で実行し、T処理では主に個人情報のマスキングなどを行っていました。 これでデータ基盤はしばらく安定すると思っていたのですが、すぐ様々なエラーに遭遇することになります。 3つの問題解決と振り返り 以降ではタイミーで起こったデータ品質に関する問題解決を時系列順に紹介していきます。登場するツールの詳細な説明は省き、問題を解決するための意思決定、設計を中心に取り上げ、振り返りを行います。 問題1: データパイプラインの更新遅延 データ基盤を運用し始めてすぐ、BigQueryやBIツールの使用者からデータの更新が遅れているとの報告が多数入るようになりました。 報告が入った際にはすでにどこかの分析で重大な問題が起こっていることが多く、DREチームがすぐに対応することが多かったです。 このような事象が多く起こると、分析者はデータの品質にいつも不信感を抱きながら分析をすることになりますし、DREチームとしても新規の開発をストップして障害対応をすることが多くなるので、お互いにとっていいことはありません。 いくつかデータパイプラインにバグ監視を入れていたのですが、ETLの過程、インフラなど監視すべき部分が多く、全てのデータパイプラインをカバーすることは不可能でした。 解決策 DREチームと分析者との間で、ソースデータごとの更新頻度に関するSLAを結び、適時性の監視システムを導入したことで解決しました。 適時性をSLIとして定義し、SLA, SLOを以下のスライドの定義にしました。 例えばデータソースAに関する適時性のSLA, SLOはそれぞれ1日、2時間と定義し、SLOをSLAより厳しめにすることでSLAをしっかり保守できるようにします。 DREチームとしてのSLI, SLA, SLO定義 適時性の定義はDMBOKという本を参考にしています。一般的なデータ品質の定義については以下の記事にまとめてありますのでぜひ参考にしてください。 DMBOKとデジタル庁を参考にデータ品質について調査、考察してみた - Qiita 実装 適時性監視システムはPythonのコードをGithub Actionsで定期実行することで実現しています。 具体的には以下の手順で実装されています。 各データソースごとのSLOを定義 BigQuery上のテーブルの最終更新時間をそれぞれ取得 テーブルの$最終更新時間 > 現在の時刻 - SLO$を比較し、この式がfalseであればSlackでDREチームに通知 BigQueryの最終時間の取得にはこちらを用いています。 Python client library  |  Google Cloud 振り返り 適時性に関するSLI, SLA, SLOの導入により、SLOを破った段階でDREチームが障害対応をするので分析者からの報告の前にバグの解決が可能になりました SLO, SLAを破らない限りはDREチームは障害対応しないので、障害に対して即対応することなく、新規開発と障害対応のバランスがうまくとれるようになりました SLAを破った際にポストモーテムをすることでデータパイプラインのコード品質、障害対応のフローなどの品質も向上しました 問題2: 分析チームへのクエリ修正依頼の増加 タイミーではRedashの権限が全社員に付与されています。そのため、全社員が自発的にクエリを発行し、実行し、スケジューリングを行っていてデータ活用が盛んです。しかし、間違ったクエリや古い定義のクエリを再利用してしまうことで意思決定の誤りが発生することも少なくありませんでした。 分析チームにはそれらのクエリの修正依頼が多数きており、会社の人数が増えるにつれ、依頼が増加していくことは自明でした。 解決策 この問題にはLookerを導入することで対応を試みました。Redashでは各個人が個別で分析指標のクエリを書いていましたが、Lookerはあらかじめ管理者が分析指標を定義し、一元管理することができます。そうすることで分析者は定義された指標をGUI上で選択するだけで分析やダッシュボード作成ができます。 Lookerの導入により、Redashで行われていた分析の品質を高くできるため、RedashからLookerに移行することでクエリ修正依頼の削減を目指しました。 LookMLはコード管理もできるため、指標の定義をバージョン管理することもできます。また、LookMLで定義した一部の定義をBigQuery側に戻し、BigQueryに集計テーブルを作成することもでき、これを再び他のBIツールで参照することでBIツール全体のデータ品質向上が見込めるDWHを構築することもできます。 タイミーではDREチームの自分と分析チームの方の計2人でLookMLの実装をし、重要な指標の繋ぎ込みを3ヶ月くらいかけてやりました。 実装に関してはディレクトリ構成や、モデル構成のベストプラクティスが各社で異なるのでかなり試行錯誤しましたが、Lookerのサポートやコミュニティの知見を借りながら進めることができました。 実装 具体的な実装は以下のようになっています。様々なデータを収集しているDatalakeを、LookMLによる実装でLookerに繋ぎます。LookMLの詳細は以下になります。 Looker  |  Google Cloud 集計処理が重たい指標があり、分析結果の取得までに時間がかかることがあります。これらの指標はDerived TableとしてLookerからBigQueryのDWH用データセットに永続化することで解決しています。Derived Tableの詳細は以下になります。 Looker  |  Google Cloud Lookerを含むデータ基盤 振り返り Lookerの導入により、重要な指標に関しては簡単に分析とダッシュボード作成ができるようになりました。また、集計処理が重たい指標に関してDerived Tableを用いてBigQueryに永続化することで、対象の分析をする際に各BIツールにかかる負担を軽減することができました。 実際にやってみるとRedashをLookerに移行する作業は予想より大変でした。Lookerには1つ1つ指標を裏側で定義する必要があるので、Lookerで定義されていない指標を含むRedashのダッシュボードに関してはそもそも移行することができません。また、移行すべきダッシュボードと不必要なダッシュボードの選定をするために、様々な部署を巻き込んだり、ダッシュボードのメタデータ集計をする必要もあります。 こちらは未完了なため、2022年の課題として取り組んでいきます。 問題3: ETLパイプラインにおける加工処理の負債 個人情報マスキングなどの加工処理をEmbulkで行っていました。データ量やデータパイプラインが増えるにつれ、データ加工がEmbulkに依存していることで様々な負債が溜まってしまいました。 Embulkによる加工はアプリケーションが用いるデータベースが主なデータソースになっています。データベースのテーブル数は多く、全てのテーブルに対して同じ加工処理をかけていたので、テーブルやデータの増加と共に加工処理のロジックが膨れ上がり、コード量も増加します。そうなると、Embulk内で閉じている加工処理は分析チームから把握しづらいものとなり、DREチームとしてもデバッグがしづらくなりました。 また、Embulk以外のデータ転送ツールであるtroccoなどを用いて転送されるデータソースにも個人情報が含まれるため、マスキングをする必要が生じました。Embulkとtroccoそれぞれで加工処理を行ってしまうと、コードの共通化やコード管理がしづらくなってしまいました。 解決策 この問題の解決には加工処理を転送中に行うのではなく、転送後に行うELT基盤の構築が必要となります。ELT基盤により加工処理が一元管理でき、加工処理のロジックも分析チームから把握しやすくなります。ELT基盤を実現するツールは多く存在しますが、我々はdbt Cloudを選びました。 我々の要件としては、2人チームであることから導入コスト、メンテコストが少ないこと、事例が豊富であること、キャッチアップコストが少ないことなどがありました。 比較したツールでは特にDataformとdbt Cloud、dbt CLIに注目しました。 Dataformが良い点としては、GCPの傘下に入っていることや、無料で利用できることがありますが、dbt Cloud, dbt CLIと比べ導入事例が少なく、コミュニティが狭いため、今回は採用を見送りました。dbt Cloudはdbt CLIと比べ、5000円/人月のコストが発生しますが、コストを踏まえてもインフラやスケジューラがフルマネージドであり、メンテコストが少ないことが我々の要件を満たすため、dbt Cloudを導入することにしました。 dbt Cloudにより今までETL構成だったデータパイプラインをELT構成に変更し、EL処理はEmbulkやtrocco、T処理はdbt Cloudと、役割を分担することができます。 またdbt Cloudは加工処理の様子をドキュメント化することができたり、SQLライクな言語で書かれているため、分析チームがどのような加工をしているかを把握しやすいです。テストの機能も豊富でDREチームが開発に安心して取り組むこともできます。 ツールの選定には、ADR (Architectural Decision Records) というフレームワークを用いました。ADRのフォーマットを用いて機能要件、非機能要件を洗い出し、ツールを比較し、関係者の合意を得ました。 Architectural Decision Records (ADRs) | Architectural Decision Records 実装 BigQueryのプロジェクトとしては、加工処理をしていないデータを保存するためのDataLake用プロジェクトと、加工処理後のデータを保存するDWH用プロジェクトの2つを用意する必要があります。 DataLake用プロジェクトは最小限の人がアクセスできるようにGCPのIAM構成を調整しました。 Embulkのマスキング実装をdbt Cloudに全て移行しました。dbt Cloudの加工処理自体はデータ転送後に行いたいため、Digdagワークフローの最後の1ステップでdbt CloudのAPIを叩くことで加工処理を実行しています。 今までのEmbulkの加工処理は全テーブルに対して処理をかけていましたが、dbt Cloudの実装では加工が必要なテーブル、カラムのみに対して処理をかけています。 dbt Cloudの導入 振り返り 今回はADRのフォーマットでツール選定をし、3ヶ月後に選定結果の評価をしました。ADRにより、ツール選定の意思決定が綺麗に残せたこと、関係者への合意が取りやすかったこと、ツールを選定するだけで終わるのではなく、数ヶ月後にその結果の評価ができました。 dbt Cloudの導入でELT構成が実現したことによって、どのパイプラインから届くデータにもdbt Cloudで加工処理を行い、加工処理ロジックも一元管理できるようになりました。 dbt Cloudにはデータ品質に関するテストが用意されていて、not null制約などのテストが簡単に実行できます。この辺りも守るべき品質を定義しつつ、上手に使っていこうと思っています。 これからの品質に関する改善 ここ一年に取り組んだ3つの品質改善について紹介をしてきました。 最終的なデータ基盤構成図は以下のようになりました。 まだまだ品質における課題はたくさんあり、2022年にトライしていきたいことはいくつもあります。 例えば、データ品質におけるSLIは適時性だけでは足りず、データの中身の重複、欠損などの品質に関する指標を定義して保守していく必要があります。また、DWHの生成に現在Lookerとdbt Cloudの使用をしていますが、これらの役割を定義し、DWH, データマートを上手に構築していく必要もあります。 また、弊社は機械学習の導入が本格的に始まっていくので、MLOps基盤の品質担保もしていく必要があります。 最後に、DREチームは現在2人チームでチームメンバーを募集中です! 応募お待ちしてます! product-recruit.timee.co.jp
アバター
はじめに プロダクトチームの克海です。PdMの補佐をしながらプロダクトのデータアナリストをしています。 本記事ではアプリでのABを始めようといしている方に向けてのABテストの実施の流れと事例についてまとめた記事になります。 ABテストとは? ABテストとはランダム化比較試験ともいれる実験手法です。検証対象をランダムにグループ化して別々の介入をすることで「介入の効果」を図る手法の一つです。 メール マーケティング や広告、ウェブページの改善などのデジタル マーケティング や医学現場でも使われている手法で、多くの実験では変更を加えない「コン トロール グループ」と、変更を加える「テストグループ」を作り、実験を行います。実験を開始して2つのグループの違いを検証します。2つのグループに対して介入以外同一条件な状態を作ることで、バイアスがない状態で比較することができます。 実験を繰り返すことで、ユーザーが求めている価値の知見をため、サービスやアプリの最適化していくことができます。今回の記事では、ABテストした流れと結果について記載します。 目次 はじめに ABテストとは? 目次 なぜABテストを始めるのか? 取り組みの流れ 課題の洗い出し 実装のアイデア出し 実装 計測・振り返り タイミーでの実際のやり方 事例1 日付移動のスワイプを追加する 課題の洗い出し 実装のアイデア出し 実装 計測・振り返り 事例2 市区町村に関連する情報を一覧に追加する 課題の洗い出し 実装のアイデア出し 実装 計測・振り返り まとめ なぜABテストを始めるのか? もしデータがあるなら、データを見る。 もし意見しかないのなら、私の意見で行く。— Jim Barksdale ( Netscape 社の元CEO) この引用はデータ分析においてすきな名言です。 タイミーでは、サービスの認知度が増えたことや全国展開により一定以上のユーザーの利用がある状態になりました。 ユーザーの利用データも集まるようになり、開発チームも 定量 的データの分析を利用した開発の意思決定や開発の効果測定をし開発へフィードバックを利用して、開発価値を最大化していく必要がありました。 取り組みの流れ 改善を始める前に、取れているデータを整理したり、必要に応じて追加のデータを集める作業を行いました またチームとして現状の数字の感覚を掴んでもらうためにも、 ダッシュ ボードを作成し、毎日主要な数字を見てもらえる状態を作りました。数値の理解度が上がった段階で下記の流れで、ABテストを開始しました。 課題の洗い出し 定性的なユーザーからのお問合せや、 定量 的なデータを分析しユーザーの課題をいくつか発見します。 実装のア イデア 出し 重要そうな課題に対してチームで解決のア イデア のブレストを行いア イデア を出します。 実装 影響力、 工数 を考慮しながらプロダクトオーナーと相談しながら実装する内容を確定します。 プロダクトオーナーと相談し、実装しています。 計測・振り返り FirebaseAB Testingを利用してABテストを実行し、 Google Analytics を利用して計測しています。 有意差については今回深くは書きませんがABテストの結果が偶然によって起こる割合を出し、結果が誤差ではないことを判断します。 有意差の検定にはDataStudio や Python を使っています。弊社にはDRE(Data Reliability Engineering)チームが存在し、GoogleAnalyticsやプロダクトのデータをBigQuery上から分析する仕組みが整っています。様々な数字の変化をレポートにまとめ、振り返りを行い次の開発やアクションを決めています。 タイミーでの実際のやり方 以降ではタイミーで実際にABテストを行った際に各工程で行ったことをお伝えします。 課題の洗い出しは業務の中で複数並行して調査しています。また、実装のア イデア 出しは効率化のため複数のテーマの ブレインストーミング を同時に行っております。事例の中では関連のある部分を抜粋して取り上げます。 事例1 日付移動のスワイプを追加する 課題の洗い出し アプリのファネル分析をする中で登録から求人を見るまでに大きくワーカーが離脱する箇所がありました。 深堀りしていく中でワーカーが求人を探す段階で、より未来の求人を見るユーザーの割合が少ないことがわかりました。 ユーザーが日付移動に課題がありそうだとと分析しました。 実装のア イデア 出し 過去のUIでは、画面上部のボタンでしか移動することができないため気軽に移動することができませんでした。 その際に考えたア イデア が以下のようなものです。 画面内の任意の位置の横スワイプで日付を変更する カレンダーの右端に右矢印をつける カレンダーの使い方を説明する チュートリアル を作成する 実装 プロダクトオーナーと相談し、標準カレンダーにあるような横スワイプで移動する実装が直感的で効果が高く、かつ 工数 が低いと推定しこのア イデア を採用しました。 計測・振り返り リリース後は今までの数字との大幅な数字のブレが起きていないか数字で確認しながら、有意差を確認できるまで各種数字を見ながら計測していきます。今回追加した機能はアクティブユーザーの約3割が利用しており、複数の日程も見られるようになりました。しかしながら申込み人数の増加は増えず、登録から稼働までのファネルは改善しませんでした。知見としてレポートにまとめ、マッチングを増やすための、別の手段を検討しました。 事例2 市区町村に関連する情報を一覧に追加する 課題の洗い出し 求人の数も増え始めユーザーは多くの求人から、自分に合った求人を選ぶ必要性が出てきました。 改善要望にも、より詳しい勤務地を用いた検索がほしいという声が多くありました。 実装のア イデア 出し 過去のデザインでは 都道 府県に関する検索はできるものの、求人のより詳細な勤務地は詳細画面に進まないと確認することができませんでした。 距離を利用した順番にする エリア指定で検索できる 市区郡に関連する情報を一覧に追加する 実装 勤務地が遠い求人が一覧画面からわかることの影響力を高く評価し、 工数 との比較から市区町村の情報を一覧画面に追加する実装を行いました。 計測・振り返り 申込数が増え案件表示数が減る結果になりました。また市区町村の情報を見つけやすくなったことでお気に入り関しても微増しました。申し込み率に対して有意差ありで約10%も改善しました。レポートにしてチームの知見として蓄積しています。 まとめ 今回は2つのABテストについて紹介しました。弊社では様々なABテストを行っています。仮説通りユーザーに受け入れられるのか、ユーザーが求めているものの解像度を上げ知見を増やしていくためにもぜひ、ABテストに挑戦してみてはいかがでしょうか?是非参考にしてみてください。
アバター
こんにちは、 iOS チームの阿久津( @sky_83325 )です。 今回も 前回の記事 に引き続き、マルチモジュール開発についての記事です。 タイミーでは2019年7月より、機能単位でFrameworkを分割するマルチモジュール開発に取り組んできました。 現在では全体の約8割がモジュール分割され、27個のモジュールよりアプリが構成されています。 ここ数年で iOS におけるマルチモジュール開発に対する関心が高まり、多くのプロジェクトで導入され始めているのではないでしょうか。 マルチモジュール化することで次のような恩恵を受けることが出来ます。 ビルド時間の軽減 影響範囲の限定 ミニアプリを用いた機能単体での動作確認 このような大きな恩恵がある一方で、なんとなく難しそうなイメージがあったり、大規模な開発チームでのみ採用されてる印象があり、導入を見送られているチームも多いのではないでしょうか。 タイミーでは、1~3名という小規模なチームでマルチモジュール開発を続けてきました。 この記事では、実際にそのような小さなチームで運用してみて感じたこと、学んだことを共有したいと思います。 マルチモジュール開発に興味のある方や、実際に導入を検討されている方の参考になれば幸いです。 マルチモジュール開発とは 取り組んだ背景・経緯 目的① ビルド時間の短縮 目的② ミニアプリの導入による開発時のデバッグ効率の向上 どのように移行していったのか 構成はFeature x Layer形式を採用 課題① 既存の密結合な実装 課題② 各機能間の画面遷移をどうするか 実際に運用してみてどうだったのか ビルド時間が短縮された 開発時の動作確認が容易になった 実装が矯正された 仕組みとして疎結合な実装が保証された 1. 動作確認(QA)のコストが減った 2. 実装のキャッチアップが容易になった 3. 負債が限定された 終わりに マルチモジュール開発とは マルチモジュール開発とは、ある機能や責務を持ったレイヤーを独立したモジュールとして実装し、それらを組み合わせて1つの iOS アプリを開発することをいいます。そして、それらのモジュールはFrameworkやLibrary、Swift Packageといった形で管理されます。 他方モノリシックな開発では、ある機能や責務を持ったレイヤー全てが1つのモジュールで実装されます。そして、それらの機能や責務は1つの ディレクト リとして管理されることが多いです。 言い換えれば、マルチモジュール開発ではある責務を持ったコードを ディレクト リとしてではなくSwift PackageやFramework、Libraryといった形式で管理することになります。 そして、モジュール化することでネームスペースが完全に分かれそれぞれの実装を蘇結合に保つことが出来たり、必要な部分のみビルドを実行できたり、特定の機能/役割を再利用したりすることが可能になります。 取り組んだ背景・経緯 当時のタイミーはリリースから1年が経ち、主要機能の開発が落ち着き始めていたタイミングでした。 同時に、ビルド時間の増加などいくつかの問題に悩まされており、それを解決する手段としてマルチモジュール開発に興味を持ちました。 当時考えていた、マルチモジュール化の目的は以下の2つでした。 ビルド時間の短縮 ミニアプリの導入による開発時の デバッグ 効率の向上 目的① ビルド時間の短縮 機能の増加に比例して、ビルド時間が継続的に伸びてしまっていました。 細かなViewの調整に対しても一定のビルド時間がかかってしまうことは時間的にも、開発体験的にもかなり辛いものになっていました。そのような状況の中、必要なモジュール単位でビルドをし、動作確認を出来るマルチモジュール開発に大きな魅力を感じました。 目的② ミニアプリの導入による開発時の デバッグ 効率の向上 またマルチモジュール化に伴い、機能単位で デバッグ を可能にするミニアプリも導入も検討しました。 機能によっては、その画面へたどり着く条件が限られており、動作確認が難しくなってしまっている場合があります。 例えば、ウォークスルーの場合は初回起動時にしか表示されないため、毎度アプリを削除し、再インストールする必要があります。その他にも、商品購入後にしか辿り着けない画面があった場合は、 デバッグ の度に商品の作成&購入をする必要があります。 ミニアプリを作り、特定の機能に直接アクセス出来るようにすることでこのような障壁を取り除けることに魅力を感じました。 この2つが主なマルチモジュール化へ移行した理由ですが、それに加えて 純粋に楽しそうだったから と言うのも1つの大きな理由です。 どのように移行していったのか 構成はFeature x Layer形式を採用 アプリをモジュール分割する際に、どのような構成で分割するかを決める必要があります。 機能(Feature)単位でモジュールを分割する方法や、Domain LayerやData Layer、Presentation Layerというような一定の責務を持ったレイヤー単位で分割する方法がありますが、タイミーでは「ミニアプリの導入による開発時の デバッグ 効率の向上」をマルチモジュール化の1つの目的としていたため、大きな方針としては機能単位でモジュールを分けることにしました。また、既存のモノリシックなアプリ構成も機能単位でフォルダ分けをしていたため、機能単位で分割する方が適していたということも理由の1つです。 基本的には機能単位でモジュールを分割しつつ、機能を横断して共通で使いたい部分は別途モジュールとして切り出しました。 例えば、通信を担当する箇所や、アプリ全体で利用するモデル(Entity)などです。 具体的には次のような構成を目指しました。 課題① 既存の密結合な実装 既存のアプリ構成も機能単位でフォルダを分けていたため、モジュール分割も容易かと思われましたが実際は違いました。 機能間でシングルトンなオブジェクトを共有していたりなど、所々密結合な実装が存在していました。 そのため、既存のプロジェクトを一気にモジュール化することは出来ませんでした。 以下のような手順で徐々にモジュール分割を進めていく方針を取りました。 各機能で共通して使う部分をモジュールに切り出す どこか1つの機能をモジュールに切り出す 以降は新規実装や リファクタリング のタイミングで継続的にモジュール分離する はじめに、機能横断で利用したい共通部分をモジュール化し、その後1つの機能を実際にモジュール化してみました。 ここまでを最初の段階とし、それ以降は リファクタリング のタイミングであったり、新規開発の際に徐々にモジュール移行するようにしました。 そのように順番に移行することでモジュール化された部分には、既存の負債が混ざらないようにしました。 課題② 各機能間の画面遷移をどうするか 徐々に機能をモジュール分割する中で、各機能間の画面遷移をどう実装するかが問題になりました。 一番シンプルな解決策としては、遷移先のモジュールをimportし、遷移先のViewControllerを直接呼び出すことです。機能Aから機能Bへ一方向にしか遷移しないのであればこのような実装方法でも大丈夫かもしれませんが、必ずしもそれが保証されるとは限りません。また、各機能を 疎結合 に保つためにも、機能モジュール間で依存することは望ましくありません。 そのため、全てのモジュールを知っているAppに画面遷移を委ねる必要があります。 画面遷移のロジックを抽象化したインターフェースを定義し、それをAppから各機能モジュールへ注入することで、機能間の画面遷移を実現しました。 具体的には次のように行いました。 Routerのインターフェースを全ての機能モジュールが依存している箇所へ定義する 各機能モジュールではそのインターフェースを呼び出して画面遷移を実行する Appでそのインターフェースの実装をする。 各機能モジュールへそれを注入する。 このようにマルチモジュール開発における依存関係は、うまく抽象に依存させながら解決していくことになります。 実際に運用してみてどうだったのか マルチモジュール化を運用してみて、当初想像していた以上に様々な恩恵を受けることが出来たのでそれらを紹介していきます。 ビルド時間が短縮された ある機能を動かすために必要最低限のモジュールのみをリンクしたミニアプリを用意することで、開発時に都度アプリ全体をビルドする必要がなくなりました。その結果、開発時のビルド時間が大幅に削減されました。 また、テストの実行もモジュール単位で行えるため、時間が大幅に短縮されました。 開発時の動作確認が容易になった ミニアプリを用いることでウォークスルーなどの特定の条件下でしか表示されない画面などにも直接アクセスできるようになったため、簡単に動作確認を行えるようになりました。 さらに、 コンポーネント 化されたViewを確認する開発時専用のアプリを作ることで、より開発効率が向上されました。 実装が矯正された モジュール化することで、より一層レイヤーや依存を意識した開発をする必要が出てきます。 全ての機能を単一モジュールで実装している場合、意図せず、もしくは妥協の末、密結合な実装をすることが出来てしまいます。例えば、シングルトンな インスタンス を作成し機能を跨いで共有することなどです。 しかし、 モジュール分割することでそのようなスコープを超えた実装を仕組みとして防ぐ ことが出来ます。 さらに、そのように「実装の自由」を制限することで、自ずと各モジュールの責務を深く検討するように(検討せざるを得なく)なりました。結果として、プロジェクト全体のコード品質が上がりました。 仕組みとして 疎結合 な実装が保証された 前述したように、モジュール分割することで 疎結合 な実装が仕組みとして強制されます。その結果、変更の影響範囲ががそのモジュール内に限定されます。それによりいくつかの恩恵を得ることが出来ました。 1. 動作確認(QA)のコストが減った 基本的に、あるモジュールを変更した場合は、そこを部分的に動作確認するのみでよくなりました。毎回のリリース時の動作確認コストが減った結果、より継続的に高頻度でリリースすることが可能になりました。 2. 実装のキャッチアップが容易になった 新しくメンバーを採用した場合、そのメンバーが初めての開発の際にキャッチアップすべき領域も限られるため、効率的にオンボーディングを行うことが出来るようになりました。 3. 負債が限定された また、一部の機能や、実装に負債があったとしても、影響範囲がそのモジュール内に限られているため、ほかに負債が伝播することもありません。また、1つ1つのモジュールは小さく出来ているため、最悪そのモジュールを切り捨て、再実装することも可能です。 このように 疎結合 な実装が仕組みとして求められるようになり、様々な恩恵を得られるようになりました。 終わりに 今まではEmbeded Frameworkを利用したマルチモジュール化が主流でしたが、最近だとSwift Package Managerを利用しSwift Packageとしてモジュールを管理することが可能になっています。Swift Packageとしてモジュールを管理する場合、 Package.swift に各モジュールの依存関係を記載していくことになりますが、それ以外は通常のモノリシックな アプリ開発 でフォルダを分割するのとほぼ似たような感覚でモジュール分割を行えるため、今まで以上にマルチモジュール化のコストが下がりました。 Swift Packageを利用したマルチモジュール開発に関しては以下の記事にもまとめてありますので、是非参考にしてみてください。 tech.timee.co.jp
アバター
はじめまして!フロントエンドエンジニアの樫福 @cashfooooou です。 タイミーでは Next.js × TypeScript で toB 向け管理画面を作成しています。 この記事は、 toB 向けの管理画面の開発時に筆者が気づいた コンポーネント 間の責務の明確化の必要性と、 TypeScript の型を用いて責務の分割をサポートする方法の紹介しています。 背景 利用者の様々なニーズに応えるために、 toB 向け管理画面には様々なページが実装されています。 2つ以上のページを実装していると、それぞれのページで実装の粒度がバラバラになることがあります。 一方ではフックの中で実装していたようなロジックが、他方では コンポーネント で実装されている あるページの コンポーネント は複数のファイルに分割しているけど、こちらのページでは巨大な一つのファイルで実装が完結している 属人的な責務の分割が失敗した様子 Next.js は pages/ 配下にファイルを追加すると自動的にルーティングが作成されますが、 ディレクト リ構造についてのそれ以外の制約はありません。 実装やレイヤ分けは実装者に委ねられており、責務の分割をしっかりやるためには実装もレビューも "人が頑張ってやる" ことが求められます。 例えば、 Atomic Design という コンポーネント の粒度とその依存関係を定義したデザイン フレームワーク が存在します。 Atomic Design のように コンポーネント を細かく分割する際には実装者やレビュー担当者によって責務が精査される必要があります。 責務の分割を適当にやったり人によって判断がぶれたりすると、システム全体を見た時に一貫性が失われてしまいます。 コンポーネント の責務が揃っていると次のようないいことがあります。 設計思想に則ってロジックが分割されるので コンポーネント が単一責任を担うようになる 抽象化、具体化をレイヤを意識して行うので結果的に再利用性が高くなる Unit Test や Visual Regression Test の実装や管理のコストが小さくなる コーディング ガイドライン が整い、実装者が迷うことなく書くべきコードに集中できる しかし、新しい コンポーネント を実装するたびに人の判断で コンポーネント の責務が適切に分割されているかを判断するのは簡単ではなく、実装やレビュー担当者の力量や考え方に依存します。 結果として、システムが入退社等に伴うエンジニアの交替に弱かったり考え方の変化によってシステム全体で方針が統一できなかったりします。 責務の境界の曖昧さを何か 機械的 な方法でなくせると、属人的ではなくなり一度決めた方針を貫けるので嬉しいです。 実現方法 コンポーネント の受け取るデータの型を基準にして、適切な コンポーネント の責務の分割を実現します。 適切な型を用いて責務を表現し、型を中心にして一貫性のある責務が明確になっている状態を目指します。 型をもとに責務の分割を行い、属人的ではなくなった様子 責務を表現する型を作るために、 コンポーネント が「何をすべきか」と「何をしてはいけないか」を明確にしてみましょう。 例として、 Atomic Design で 何かのデータをリスト形式で表示する UI を実装することを考えてみましょう。 Atomic Design は 5段階の要素(atoms、molecules、organisms、templates、pages)のうち、 organisms - molecules 間の責務について取り上げてみます。 リストの形式のデータを持つ時はそれぞれの責務のうち、とくに「何をすべきか」は次のようになります。 organisms リストの繰り返しの扱いを担うべき *1 molecules リスト中の単一のデータについて、そのデータの描画を担うべき 一方の コンポーネント の「何をすべきか」がわかると、 他方の「何をしてはいけないか」も見えてきます。 organisms 単一データの描画について担ってはいけない molecules リストの繰り返しを扱ってはいけない 「何をしてはいけないか」という制約を明確にすることは、責務の分割にとても有用です。 実装レベルで 、 molecules が受け取るデータの型が何かのリストの形式 Array<T> になっているならば責務の分割がうまくできていない ことを疑うことができます。 以降では、責務の分割のための具体的な型とそれを用いた コンポーネント の実装まで提示していきます。 具体的な例: API のレスポンスを表示する コンポーネント 一般的なアプリケーションの例として、 RESTful API のレスポンスを表示するページの実装を挙げます。 こちらのデモ と併せて読んでいただけると、より理解が深まると思います。 以下に定義するコードは src 以下に配置されています。 API クライアントはモックを使用しており、取得成功と取得失敗(とそれぞれの取得中)の動作を確認できるようになっています。 API クライアントの定義 次のようなユーザの投稿を扱う型 Post と API クライアント apiClient を使用します。 以降では、ユーザの投稿を一覧表示する PostPage の実装を考えます。 type Post = { postId: number ; content: string ; postedAt: string ; } ; const apiClient = { fetchPosts: async ( userId: number ) => { const { data } = await axios. get< Post [] >( `https://xxx.com/api/{userId}/posts` , { params: { userId } } ); return data ; } , } ; ディレクト リの責務の定義 コンポーネント の ディレクト リとして「 pages 」「 domains 」というものを作ることとします。 それぞれの責務として「何をすべきか」と「何をしてはいけないか」は次のように定めます。 pages コンポーネント 内で API レスポンスを受け取り、状態に応じて表示を変える データの具体的な描画の実装をしてはいけない domains データを描画するテーブルなどの コンポーネント を実装する 渡されたデータを扱うだけで API レスポンスの状態に依存してはいけない ここでいう「 API レスポンスの状態」というのは、取得時の待ち状態やエラーを指しています。 責務を明確にすること、とくに「してはいけない」を明確にすることによって、本来の責務に集中して実装することができます。 型の定義 API レスポンスの状態を扱うために、次の ジェネリック 型 ApiState を定義します。 type ApiState < T > = | { type : 'loading' ; } | { type : 'error' ; errorMessage: string ; } | { type : 'success' ; result: T ; } ; domains の責務の 2つ目である「あくまでデータを扱うだけで API レスポンスの状態に依存してはいけない」 を満たすことはとても簡単で、 ApiState 型のデータを受け取らない とするだけで良いです。 これだけで、実装者もレビュー担当者も domains の コンポーネント のプロパティに注目することで不適切な責務の分割が行われていないか確認できます。 さらに ESLint などで 「 domains の ディレクト リ配下での ApiState のインポートを禁止」というルールを定めれば、 機械的 に判断することすら可能です。 *2 apiClient を用いて Post[] 型のデータを取得するフック、 usePostLists を次のように実装します。 // usePosts は userId を受け取って ApiState<Post[]> を返すフック interface IUsePosts { ( userId: number ) : { postsState: ApiState < Post [] >; } ; } export const usePosts: IUsePosts = ( userId ) => { const [ posts , setPosts ] = useState < Post [] | null >( null ); const [ error , setError ] = useState < string | null >( null ); // エラーになったとき、かつエラーの時のみ string型のエラー文になる useEffect (() => { (async () => { try { const result = await apiClient.fetchPosts ( userId ); setPosts ( result ); } catch ( e ) { setError ( errorToString ( e )); } } )(); } , [ userId ] ); const postsState: ApiState < Post [] > = error ? { type : 'error' , errorMessage: error } : posts === null ? { type : 'loading' } // エラーでなく、かつ取得前なので取得中 : { type : 'success' , result: posts } ; return { postsState } ; } ; フックについても、 API を扱うフックの返り値を「ある型 T に対する ApiState<T> 」とすることで、 API レスポンスを扱うという責務を明確にすることができています。 この時点で、 pages の責務を満たす実装はとても楽になっています。 Post[] 型のデータを受け取って投稿一覧を表示する domains の コンポーネント PostList 、エラーメッセージを受け取ってエラー状態を表示する コンポーネント ErrorView 、読み込み状態を表示する コンポーネント LoadingView という 3つの コンポーネント (実装例は省略)を用いて、 pages/PageListPage は次のように実装できます。 const PostListPage: React.FC < { userId: number } > = ( props ) => { const { postsState } = usePosts ( props.userId ); if ( postsState. type === 'success' ) return < PostList data = { postsState.result } / >; // postsState.result の型は Post[] if ( postsState. type === 'error' ) return < ErrorView errorMessage = { postsState.errorMessage } / >; return < LoadingView / >; } ; たったこれだけのコードで pages の「 コンポーネント 内で API レスポンスを受け取り、状態に応じて表示を変える」という責務を満たしています。 また、 domains である PostsList の「 API レスポンスの状態に依存してはいけない」という制約も満たされていることがわかります。 型を導入することで、その型を中心に責務が明確になっています。 実装者もレビュー担当者も、この型の情報を責務の分割の指標として使うことができます。 ライブラリを使用する際の利点 責務が明確になっている状態であれば、実装の一部をライブラリに差し替えや使用するライブラリを置き換えが影響範囲を小さくすることで容易になります。 たとえば、React 16.6 で実験的に追加された機能の Suspense は、 コンポーネント が API レスポンスの状態に依存しないようにしているという点において上記の実装とよく似た用途で使用することができます。 紹介したような実装方法をとっていて Suspense を使った実装に切り替える場合、 pages と一部のフックを修正するだけで他への影響はほぼ発生しないです。 実装時の注意 これまでに出した例は非常にシンプルなものなので簡単な実装で済みました。 しかし、現実にエンジニアリングで向き合う問題は必ずしもシンプルではないです。 あるタイミングで責務を満たす型を作成したとしても、他の機能の実装中にエッジケース(例外)が見つかることがあります。 エッジケースに対して アドホック に型の修正を行うと、責務の本質から大きく逸れた歪な型になってしまいます。 他方でエッジケースを例外的に扱う癖がついてしまうと、責務を担う型自体が形骸化してしまいます。 型自体を常に精査し続け必要に応じて更新していくことで責務が明確な状態を保つことが、システムの質を高い状態で保つには不可欠です。 しかしながら、無自覚に責務から逸脱するコードを書く心配がなくなり、型の修正がそのまま責務の再定義になる点は、型の恩恵を受けていることを強く実感します。 まとめ この記事では、型を用いた コンポーネント の責務の明確化の方法について紹介しました。 責務を表現する型を用いて、属人的でない責務の明確化が可能になります。 実務で実装をしていると、UI や UX、 ビジネスロジック など、集中しなければならないことがたくさんあって大変だなと感じます。 紹介したような取り組みは本当に集中しなければならないことに集中することにとても役に立ちます。 今後も、システムの品質や開発効率を考えながら開発に取り組んでいきます。 また面白い取り組みができれば紹介したいです。 *1 : いかなるケースでも organisms が Array のデータを持つわけではありません *2 : ESLint で自作ルールを作る上で、こちらのページがとても参考になりました https://zenn.dev/nus3/articles/b2bc110efd0887442c11
アバター
はじめまして、 iOS エンジニアの阿久津 @sky_83325 です。 タイミーでは、機能ごとにEmbedded Frameworkに分割して開発するマルチモジュール開発に取り組んでいます。 現在では、本体AppやAppExtensionの他に7つの共通Framework、そして16個の機能Frameworkという規模になってきました。 今回は、そのマルチモジュール開発をEmbedded Frameworkではなく、Swift Packageを利用した方法に乗り換えてみたので、その成果や学びについて共有できればと思います。 取り組んだ経緯・背景 タイミーでは、技術顧問の @d_date さんと隔週で「ツバメの会」という情報共有の場を設けています。そこでは、直近タイミーで取り組んでいることの共有や相談をしたり、Swiftや iOS 、その他エンジニアリングの最近の話題について議論したりしています。 そのツバメの会で、pointfreeが OSS として提供している isowords というアプリが取り上げられました。 www.notion.so そのisowordsは86ものモジュールから構成されていますが、そのモジュールをSwift Packageとして管理しています。 Swift Packageとして管理することで、ファイルの追加や変更のたびに project.pbxproj が変更される辛みからも解放され、結果としてXcodeGenのようなツールを利用してプロジェクトファイルを生成しなくても、十分に管理することが可能となります。 この構成に大きな魅力と可能性を感じたため、タイミーでもSwift Packageを利用したプロジェクト構成への変更に舵を取りました。 この記事では、次の内容を共有します。 Swift Packageに移行したことで得られた成果 どのように移行していったのか 実際に運用してみた感想 Swift Packageに移行したことで得られた成果 1. XcodeGenを取り除くことができた タイミーでは次の2つの目的のために、XcodeGenを導入していました。 project.pbxproj のコンフリクトを防ぐ フレームワーク 追加の処理をテンプレート化する 特に前者は、多くのプロジェクトでXcodeGenが導入されている理由ではないでしょうか。 複数人で開発を行なっていると、ファイルや ディレクト リの追加に伴い、プロジェクトファイルがコンフリクトしがちです。常にプロジェクトファイルを生成するようにし、プロジェクトファイルを.gitignoreに追加することでコンフリクトの発生を抑えることができます。そういった目的でタイミーではXcodeGenを利用していました。 また、Frameworkターゲットを追加するたびに、毎回同じ作業が必要でしたが、その作業を誰でも同じように行えるようにテンプレート化するためにXcodeGenを利用していました。 XcodeGenの利用に問題があったわけではありません。従来のプロジェクト構成においては、XcodeGenを利用してプロジェクトファイルは常に生成する運用で特別な問題は発生していませんでした。 しかし、Swift Package Manager(以下SwiftPM)を利用し、 ソースコード をSwift Packageとして管理することで、 Xcode Projectにはファイルやフォルダへの参照が発生しません。これにより、Build Settingsの変更やTargetへの依存の追加を除いては、 .pbxproj に変更差分は発生しません。 そして、新しくモジュールを追加する際の処理も、 Package.swift を少し編集するだけとなったので、テンプレートなども不要になりました。 こうして、XcodeGenを取り除くこととなりました。 project.pbxproj は16416行から2295行へなり、XcodeGenのymlも削除されました。 // Before I'm working on timee-ios $ wc -l timee-ios.xcodeproj/project.pbxproj 16416 timee-ios.xcodeproj/project.pbxproj // After I'm working on timee-ios $ wc -l App/timee-ios.xcodeproj/project.pbxproj 2295 App/taimee-ios.xcodeproj/project.pbxproj // Before I'm working on timee-ios $ wc -l XcodeGen/* 102 XcodeGen/common-targets.yml 189 XcodeGen/project.yml 47 XcodeGen/sandbox-targets.yml 179 XcodeGen/scene-targets.yml 58 XcodeGen/setting-groups.yml 73 XcodeGen/templates.yml 178 XcodeGen/unit-test-targets.yml 826 total // After I'm working on timee-ios $ wc -l Package.swift 272 Package.swift 2. 本体Appのビルド時間が90秒から10秒になった Swift Packageに移行した結果、ビルド時間が大幅に改善されました。 これはSwift Package化したことによるものなのか、従来のプロジェクト構成に無駄があったのかはわかりませんが、思わぬ恩恵を受けることになりました。 ビルド時間のBefore/After 追記(2021年9月1日) SwiftPM対応した際に、BuildScriptで実行していたSwiftLintが動かなくなっていました。 そのSwiftLintの実行時間が40秒ほどかかっていたため、それが外れたことによりビルド時間が大幅に削減されていました。 3. アプリのサイズが30.3MBから26.9MBになった こちらも当初は期待してなかった成果でした。 アプリサイズのBefore/After 4. アプリの起動時間 Embedded Frameworkは Dynamic Link されていたので、今回Static Frameworkとしてリンクされることで起動時間が早くなるかもと思っていました。 今回実測はしていませんが、肌感では初回起動含め特に変化は感じていません。 5. その他 プロジェクトのセットアップがかなり容易になりました。まだCocoaPodsを併用していたり、Swift製の コマンドライン ツールの管理にMintを利用しているため、Git Clone後に Xcode を立ち上げそのまま開発を始められるという状況にはなっていません。 ですが、都度XcodeGenのscriptを走らせる必要がなくなったのは大きな開発体験の向上だと感じています。引き続き、MintのMintのSwiftPM移行などを進めていき、 Xcode のみで開発できる状況を目指していければと思います。 どのように移行していったのか それでは、どのようにプロジェクト構成を移行していったのかを見ていきます。 既存のタイミーの構成は次の通りでした。 外部ライブラリの管理は基本的にSwiftPMを使用して行っているが、一部CocoaPodsを利用している 1つのプロジェクトで、Appや複数のEmbedded Frameworkを管理している xcworkspaceを用いて、 Pods とメインprojectを紐づけている。 移行後のゴールとしては isowords を参考に以下のように定めました。 XcodeGenを取り除く ローカルライブラリ及びリ モートラ イブラリはSwift Packageとして管理する(メインprojectではSwift Packageのビルド成果物であるFrameworkをリンクして利用する) 一部SwiftPM未対応のものはCocoaPodsを利用する xcworkspaceでメインproject、 Pods 、Swift Packageを編集可能にする このゴールを達成するために、次の手順で移行を行いました。 XcodeGenを取り除く xcworkspaceの構成を整える Embedded FrameworkをSwift Packageに切り出す CocoaPodsを再度導入する 1. XcodeGenを取り除く まず、XcodeGenを取り除くということを大きな目的の1つにしていたので、この作業から始めました。具体的にはgitignoreの整理と、XcodeGenに関する記述の削除のみです。 .pbxproj はXcodeGenを利用し都度生成していたため、Gitでは管理していませんでしたが、管理するように変更しまし、差分を確認できるようにしました。 2. xcworkspaceの構成を整える 従来の構成は、次の画像のようになっていましたが isowords によせるため、Rootにおいていた各種ファイルを /App 以下に配置しました。 それにより、xcworkspaceからxcodeprojに対する参照がなくなってしまうため、 ***.xcworkspace/contents.xcworkspacedata でpathを更新します。 ワークスペース のインスペクタに何を表示するかは、この contents.xcworkspacedata によって管理されています。 <?xml version="1.0" encoding="UTF-8"?> <Workspace version = "1.0"> <FileRef location = "group:App/taimee-ios.xcodeproj"> // ここのpathを更新する </FileRef> <FileRef location = "group:Pods/Pods.xcodeproj"> </FileRef> </Workspace> ここまでの変更は、SwiftPMなどは一切関係なく単に ディレクト リ構造を変更しただけです。 ここで、一度ビルドが通ることを確認し、前準備を完了とします。 その後、プロジェクトのRootで swift package init を実行し、Swift Package導入の準備を進めます。 I'm working on timee-ios $ swift package init Creating library package: timee-ios Creating Package.swift Creating Sources/ Creating Sources/timee-ios/timee-ios.swift Creating Tests/ Creating Tests/timee-iosTests/ Creating Tests/timee-iosTests/timee-iosTests.swift 同様に、こちらもworkspaceで管理したいため、 contents.xcworkspacedata を編集していきます。 今回は、root ディレクト リの内容を全て表示したいため、 group: を指定します。 <FileRef location = "group:"> </FileRef> ここまでで、Swift Packageを導入する準備が整いました。 この時点での構成は次の通りです。 補足 group: を指定しただけでは App/ の内容もSwift Package側に表示されてしまいます。非表示にするには、非表示にしたい ディレクト リの中に空の Package.swift を作成することで防げます 参考 App/ 以下に本体projectを移行したことで、各種ツールのPathが壊れるかもしれないので適宜修正します。 以降の作業では一時的にCocoaPodsで導入しているライブラリの参照を外して作業を進めています。CocoaPodsがプロジェクトにリンクされている状態で移行を進めると、ある不具合が発生した場合にSwiftPM依存の不具合なのかCocoaPodsによる不具合なのか判断しにくいと考えました。 3. Embedded FrameworkをSwift Packageに切り出す ここまで来れば、あと一息です。 Swift Packageに対応する準備は整ったので、Embedded FrameworkをひとつひとつSwift Packageに移行していきます。 具体的には以下の作業を順に繰り返していくことになります。 Sources/ Tests/ にFrameworkのコードを移行する Package.swift に移行したFrameworkを定義する Package.targets にtargetとtestTargetを追加する 外部ライブラリが必要であれば Package.dependencies を定義し、 Package.target の dependency に追加する Package.products に .library を定義する。これによりアプリターゲットにFrameworkをリンクすることが可能となる。 Xcode ProjectのTargetから不要なものを取り除く Xcode ProjectのTargetに2で定義したFrameworkをリンクする 例として、共通コードをまとめたFrameworkである Common の場合を次に示します。 各種 dependency を指定しtargetを作成したあとに、productsでlibraryを提供することで、本体projectで Common というモジュールとFrameworkを使えるようになります。 // swift-tools-version:5.3 import PackageDescription let package = Package( name: "Frameworks", platforms: [ .iOS(.v13) ], products: [ .library(name: "Common", targets: ["Common"]) ], dependencies: [ .package(name: "Lottie", url: "https://github.com/airbnb/lottie-ios", from: "3.0.0"), .package(name: "RxSwift", url: "https://github.com/ReactiveX/RxSwift.git", from: "6.0.0"), .package(name: "Kingfisher", url: "https://github.com/onevcat/Kingfisher", from: "6.0.0"), .package(name: "ImageViewer", url: "https://github.com/Taimee/ImageViewer", from: "0.2.0"), ], targets: [ .target( name: "Common", dependencies: [ "Umbrella" ]), .target( name: "Umbrella", dependencies: [ .product(name: "ImageViewer", package: "ImageViewer"), .product(name: "Lottie", package: "Lottie"), .product(name: "Kingfisher", package: "Kingfisher"), .product(name: "RxSwift", package: "RxSwift"), .product(name: "RxRelay", package: "RxSwift"), .product(name: "RxCocoa", package: "RxSwift") ]), .testTarget( name: "CommonTests", dependencies: [ "Common" ]) .testTarget( name: "UmbrellaTests", dependencies: ["Umbrella"]), ] ) 同様に、全てのFrameworkを Package.swift を編集しながら移行していくことになります。 単調な作業が進みますが、 .pbxproj がどんどん軽量になっていくことを実感できます。 補足 import UIKit や import Foundation などの記述が漏れているとビルドに失敗するので適宜修正しましょう BundleはPackageごとに作成されるので Bundle.module を利用しましょう 4. CocoaPodsを再度導入する 最後にCocoaPodsを再導入していきます。既にxcworkspaceは作成済みであるため、既存のxcworkspaceを使うようにPodfileで指定します。それ以外は、特に気をつけることはありません。 workspace 'timee-ios' project './App/timee-ios.xcodeproj' これで無事以降が完了しました🎉 実際に運用してみた感想 実際にSwiftPMを中心としたプロジェクト構成にして、1ヶ月弱運用してみてメリット/デメリットが分かってきました。 メリットに関しては、この記事の前半で述べたように、基本的に Xcode を開くだけで開発を始められる体制になったということです。ビルド時間の高速化もかなり大きなメリットでした。 他方、良い面ばかりではなく、比較的新しい技術であるため、いくつかの不具合なども観測されました。具体的には以下の通りです。 不具合だと思われるもの Swift Packageにファイルを新規追加するのが少し辛い CocoaTouchClass を新規追加する際、 UIViewController などを指定することが出来ません。通常は Next を押した後の画面で、親Classなどを指定できると思うのですが、 Next を押すとすぐに Objective-C 用のファイルが作成されます😇 一時的な不具合だと思うので、次の Xcode に期待です。 AppExtensionを利用している場合、binaryTargetを利用することが出来ない AppExtensionを利用しているアプリで、SwiftPMを利用してbinaryFrameworkを利用する場合にAppStoreConnectにアップロード出来ない不具合があります。AppとAppExtensionの両方に対してFrameworkがコピーされてしまうため、AppStoreConnectへアップロードする際に、Bundleの重複でTransporterErrorが出ます。手動で片方を削除するなど対応可能かもしれませんが、CIなどでリリースプロセスを組んでることが多いと思うので、現時点では利用不可と考えるのが無難かもしれません。タイミーでも、これらはCocoaPodsで導入することで対応しました。 forums.swift.org InterfaceBuilderから ソースコード にIBOutlet/IBActionを接続しようとするとクラッシュする これは少し開発体験を損ねています😢 ソースコード 側からIB側に接続することは可能なので、そのようにして対応しています。次の Xcode で修正されると期待します。 これらの不具合はSwiftUIを使えば解決出来ることでもあるので、早めに乗り換えていきたい気持ちが高まりました。 その他 Swift Package内でCustomBuildConfigurationを利用することが出来ない Swift Packageでは、標準の RELEASE DEBUG 以外は利用できません。そのため、なんらかの対応が必要です。環境ごとにプロジェクトを分けてしまうのが良いかもしれません。 forums.swift.org 新作です https://t.co/gQJgGK0i9Q — Date (@d_date) 2021年4月21日 最後に Swift Package中心のプロジェクトに移行しましたが、まだまだやりたいことが沢山あります。 タイミーでは、技術的挑戦を一緒にしていけるメンバーを常に募集していますので、少しでも興味がある方はぜひご連絡ください。 その他参考文献 A Tour of isowords: Part 1 A Tour of isowords: Part 2 Libraries, frameworks, swift packages… What’s the difference?
アバター
こんにちは、サーバサイドエンジニアの @Juju_62q です。 今回は年末から仕込んでいたタイミーのSLOについてと、その時に得た学びを紹介したいと思います。 概略 結論としてタイミーのSLOで大事にしているのは以下の3つです。 プロダクトの緩やかな品質低下を検知できるものであること プロダクトの健全性を大局的に把握できるものであること 罰則はSLOを消費する行為に相反する行動を取ること また、組織で新たなものをやる時は熱量/知識/経験のいずれか2つ以上が必要になることも学びとなりました。 概略 SLO(Service Level Objective)とは なぜSLOを作ったのか SLO導入前の状態 SLOの役割 導入のために行ったこと タイミーで定義しているSLOについて 利用チームについて SLOでどの範囲を扱うかについて 違反時について SLOの見直しについて SLOを導入してよかったこと 失敗と学び 小さく始める やっていきするときに必要なもの 終わりに SLO(Service Level Objective)とは 運用サービスが守るべきと考えられる品質と解釈しています。 SLOはサービスが健全に動いていることを知るための基準であり、これを破るとユーザの期待に応えられてないということを意味します。 また、SLOは観測可能な指標から 定量 判断可能な基準であり、SLOを利用した健全性判断では定性観点が入りません。 なぜSLOを作ったのか サービスが一定の品質を保てないのであれば開発者はサービスの品質を高めることに注力すべきです。 一方で、品質が市場要求に対して過多なのであればもう少し攻めたリリースをしても良いでしょう(質とスピードの話はここでは置いておくことにします)。 この辺りの匙加減は一般に開発者のよしな力に依存しています。 その辺りの記述が SRE本 にあるため引用して紹介します。 通常、リスクと労力の線引きが必要な部分について、既存チームは非公式になんらかのバランスを見出しているものです。ただし残念ながらそういったバランスが最適なものであるかは自明ではなく、関わりのあるエンジニアの交渉スキルだけで下された決定だったりします。 中略 それに対して私たちのゴールは、交渉を生産的な方向へ導くために利用できる客観的なメトリクスを定義し、両者に合意してもらうことです。判断はデータに基づくほど良いのです 品質をチームが意識しやすい形に落とし込み、攻めたリリースを許容しつつ高い品質を維持するためにタイミーではSLOを導入しました。 SLO導入前の状態 良くも悪くもPdMを主導に無邪気に開発をしていたという側面は否めません。 例えば「パフォーマンスは早い方がいいよね」という話をみんなが意識しつつも、それと機能開発で一体どっちが大きなROI(Return on investment)なのか説明するのは簡単ではありませんでした。 サーバのエラーレート、モバイルアプリのフラッシュフリーレートなども同様です。利用者にとっては一定以上待たなくていいこと、エラーが起きないことは当たり前なのにも関わらず機能の追加・改修を差し置いてそれを改善する改善することが妥当であると説明するのは困難です。 この辺りはみんなが暗黙的にやりたいと思いつつも実行に落とすのは少し困難な状態でした。改善に関しても意識が高い人の隙間時間に依存しており、組織としてはかなり脆弱な状態だったと思います。また、パフォーマンスやエラーレートって改善するのが基本的に正義という状況ではあるのでA/Bテストをするという話にもなりませんでした。 SLOの役割 タイミーにおけるSLOの役割はなんでしょう? タイミーではSLOではサービスの当たり前品質を 定量 的に観測するためのものであり、基準として扱っています。 ここで"当たり前品質とは何か?"という話ですが例えば以下のようなものを扱っています(いますべて扱っているか?と言われるとそうではありません)。 モバイルアプリが落ちない お仕事を探す上で、くりかえしお仕事画面と検索画面を行き来してもストレスがない サーバが理不尽なエラーを返さない ユーザが期待する動作をする これらに該当する内容をSLIとして観測しつつ、初動ではチームを締めすぎない範囲で数値を設定、少しずつ絞めていきました。 タイミーのSLOの役割は 当たり前品質と攻めたリリースのバランスを取る 、 当たり前品質の低下をいち早く検知する というものになります。 導入のために行ったこと SLOを導入する下準備以下のようなことをやっています PdM、CSM、カスタマーサポートに話を聞いてビジネス的に守って欲しい部分、品質の ヒアリ ングをする SLI, SLOを作ってPdMとすり合わせる SLOを一覧可能にする SLOについて説明する資料を作り、チームに共有する SLOを改善していく会議体をとりあえず設計する 大きく区別すると ビジネス的に間違ってなさそうなSLOを設計する 運用に携わる人にSLOの説明をする SLOをとりあえず運用・改善可能にする という感じになります。 タイミーで定義しているSLOについて 現在タイミーのワーカーチームでは以下の4つのSLOが運用されています。 全体のアクセスのうちA%が正常なレスポンスを返す(2xx, 3xx, 4xx) 検索 API (ファーストビューの95%ile)がB[msec]を切っている期間がC%以上 モバイルアプリケーションのクラッシュフリーレートがD%以上 定めた API の書き込みアクセスのバリデーションエラーに対する成功率がE%以上 ルールとして、チームが意識するSLOは最大5つとしています。 過去にはタイミーの ビジネスロジック として特に重要な API のエラーレート等も扱っていましたが、その部分が壊れると即障害として扱われるためやめることにしました。 今残っているSLOは 定めたAPIの書き込みアクセスのバリデーションエラーに対する成功率がE%以上 のようにアプリケーションの品質の緩やかな低下を検知できるようになっています。 利用チームについて 前述の通りタイミーのSLOは現在ワーカーに価値を提供するワーカーチームでのみ運用されています。 タイミーは Ruby on Rails で書かれたモノリシックなWebアプリケーションを中心としつつ、Reactで書かれた店舗向け画面、 iOS , Android で提供されるモバイルアプリケーションでサービスを展開しています。 チームについては価値の提供先で分かれています。現在は就労先となる店舗の体験を改善するクライアントチーム、働き手となるワーカーの体験を改善するワーカーチームが存在します。 初めは(構想時点でのチーム分割技術スタックに依存していたこともあり)、サーバサイドエンジニア全体をSLOの影響下に置いていました。 しかしながら、そうした場合に価値提供を意識したSLOの策定が困難だったため自分が所属しているワーカーチームのみを対象としました。 結果としてクラッシュフリーレートの導入が決まったり、サーバサイドのレスポンスタイムでなく レンダリング 速度でパフォーマンスを見ていこうというような価値の対象を強く意識したアグレッシブな意思決定がスムーズにできるようになりました。 まだまだ運用が安定しないので展開はしていませんが、プ ラク ティスがまとまってきた際にはクライアントチームにもSLOを展開できればと思っています。 SLOでどの範囲を扱うかについて 1つのSLOのもつ影響範囲が大きくなるほど、そこに対するアクションは非自明になります。 例えば 検索API(ファーストビューの95%ile)がB[msec]を切っている期間がC%以上 と 全体のアクセスのうちA%が正常なレスポンスを返す(2xx, 3xx, 4xx) で比較した時にその差は明らかです。 前者であれば、検索 API のパフォーマンスの ボトルネック を ボトルネック をあされば良いのに対して、後者はどこで問題が起きているのか?から始める必要があります。 結果としてSLOに違反した、ないししそうなりそうな場合に何をやっていいかがわかりにくくなります。 一方で細かく全ての領域にSLOを用意するとチームにわかりやすいかというとそうではありません。SLOの数が増えると管理するのも意識するのも対応するのも難しくなります。 この トレードオフ にはいろんな考えがあるかと思いますが、タイミーではSLOを大局的な情報を知るためのツールとして扱い、問題が起きているという事実を示唆するために利用しています(調査に時間がかかっても良いという判断)。 違反時について 違反時のアクションはそれほど正確には定めていません。 基本的な決まり事としてはエラーバジェットの消費と相反する行動をしましょうという話をしています。 例えば「A/Bテストを検索部分に入れた結果検索 API のパフォーマンスが低下した。」という事象が過去に発生しました。 この際にした意思決定は、「A/Bテストの期間を十分なデータを貯めるための最小の期間として決めてしまいバジェットの消耗を最小限に抑える。」というものでした。 SLO違反時の振る舞いについてはデプロイを止めるというようなことがよく語られる印象がありますが、デプロイを止めるだけだと悪化の一途を辿るものも多いです。よって基本的に違反時については具体的な行動を定めず、場合によって機能開発や仮説検証を止めて改善に向かうということだけを共通認識として持っています。 違反時の決まり事をふわっとしたものを動きにした結果 定めた API の書き込みアクセスのバリデーションエラーに対する成功率がE%以上 のような短期間では改善できなさそうな項目もSLOに取り込むことができました。 これについては違反時には次の3,4ヶ月でPdMに改善の筋道を立ててもらうということで合意しています。 SLOの見直しについて 現状SLOの評価期間を月次にし、見直しも毎月話して行っています。参加者はCTO, PdM, 希望者としています。 月次の会についてはまだ話すことがたくさんあるので引き続き行う予定です。 もう少し枯れてきた場合、頻度を減らしていくかもしれません。 現在具体的には、以下のような内容を話しています。 先月のSLOについて 数値のおさらい SLOに対するアクションのおさらいと振り返り SLOの過不足について 違反時のアクションについて 話し合い自体のの反省 時間としては1hかけています。また、チーム全体としてはレトロスペクティブ等でSLOの扱い方をチューニングしています。 SLOを導入してよかったこと SLO導入による明らかな変化として"チームが数字の変化に反応して勝手に改善をするようになった"というものがあります。 SLOを定義したことによって、正常、危険、異常というラインが明示され、チーム内での数値的な危機感も同期することができました。 また、SLOをある程度みやすく管理することによって日々の数値の変化にも敏感になりました。 結果として、エラーバジェット減少の兆候を早く汲み取り、自律的にプロダクトの当たり前品質が改善できるようになりました。 失敗と学び SLOをやる中で幾らかの学びがありました。SLOに直接関わる学びについては幾らか前述してきました。 一方でSLOをやっていく中で感じた組織的な学びについて記していこうと思います。 小さく始める 散々言われていますが、小さく始めることは重要です。 ちらっと話しましたが今回僕がした失敗として、チーム分割が行われた時にそれに合わせてスコープを変更しなかったというものがあります。 SLOを元々作り始めた時はタイミーではモバイルアプリを開発するチームとサーバサイドを開発するチームがありました。現在は価値の提供対象で割っていてワーカーチームとクライアントチームとなっています。SLOはチーム分割のすぐ後くらいに実運用を開始しました。 元々サーバサイドチームを見ながら作ったものだったので疑問を持たずにワーカーチームとクライアントチームの2チームを影響範囲にして運用しようとしました。結果として以下のようなデメリットが発生していました。 SLO運用、改善のオーナーシップが曖昧 サービス全体が対象なので何かに特化したSLOを採用しづらい 関係者が多く合意形成が少し煩雑(当時エンジニア20人程度で大したことないにせよ) もちろん一定の強制力を持って推し進めるという手法を使えば、全社に対して一度に適用することができるかもしれません。 しかしながら個人的に自律性を大事にしてSLOを導入していきたかったためその方法を取りませんでした。 最終的には自分の所属するワーカーチームのみをSLOの影響範囲とすることにし、ワーカーに特化したSLOを策定するに至りました。 やっていきするときに必要なもの ここでいうやっていきは以下のようなものです。 やっていき = 枯れていない何かの考え方・技術を導入すること 弊社にとってSLOという概念はこの"やっていき"にあたるものであったと思います。 SLOについて社内では確立された知見はありません。また、SLOはサービス特性や会社のカルチャーによってその形がかなり異なるため、他社の事例も100%踏襲することができません。 ゆえに、会社やチームには経験がありません。 また、導入時は自分が引っ張って諸々の設計等をしていたため、全員に大きな熱量があるわけではありませんでした。 自分がやっていくぞ!!となっている一方、良い感じの方向に倒れるなら乗っかりたいと思う人もいたわけです(これは自然なことだと思います)。 やった方がいいという意識があってもそこに今自分の時間を使うことが妥当でないという方もいました(優先順位の問題)。ゆえに全員に熱量があったわけでもありません。 知識についてはある程度勉強していたり、資料を作ったりしていたのである程度あったのですが、経験=プ ラク ティスが固まっていない以上熱量をかけないと物事が進んでいきません。そこで熱量のある自分がボールを持ちやすいように自身のチームに持たせるという選択をしました。 ただし、全社的なスケールをもくろむという意味で試した結果を経験としておき、必要なタイミングで他のチームが時間をかけずとも始められる準備をしようと話をしています。知識と経験があり、労力が小さいのであれば大きな熱量がなくともSLOを導入できます。 "やっていき"を自律的に進めるためには熱量/知識/経験のいずれか2つ以上が必要になることを今回の件から学びました。 終わりに 今回はSLOの導入に伴った学びや、タイミーでのSLO活用事例を紹介しました。 今後もSLOだけでなくプロダクトやチームの数値を 定量 、定性的に観測し、事実に基づいた改善を素早く行えるように頑張っていきたいと思います。 プロダクトや組織状況の継続的な観測や改善に興味がある人はぜひ声をかけてください!
アバター
こんにちは タイミー CTOの kameike です 昨日発売された、 『ユニコーン企業のひみつ――Spotifyで学んだソフトウェアづくりと働き方』 のレビューです。 書籍の発売前に抽選でもらえるキャンペーンに当選し、献本をいただきました。「謹呈」ののし紙がついた本をいただいたのは初めてでテンションが上がりました。 タイミーも現在の20名のプロダクト組織からスケールしていくフェーズに入ってきており、これは Spotify が組織モデルを樹立しはじめたタイミングと同じであるため非常に参考になりました。 TL;DR 『 ユニコーン 企業のひみつ』には Spotify の組織の マインドセット ・ルールが書かれており、自律的なチームのヒントが多く含まれています 『 ユニコーン 企業のひみつ』に書かれているような組織の実現は継続的な計測と改善がとても大切です 『 ユニコーン 企業のひみつ』はどのようなことが書かれている本なの? 著者であるJonathan Rasmussonは、2014/8/30に Spotify に入社されていて、 Agile-Coachとして入社されたようです 。2017年辺りに退職されており、現在Jonathan Rasmussonは、( 彼のyoutubeチャンネル 見る限り) iOS エンジニアをされている雰囲気を感じています。この本はJonathan Rasmussonが Spotify で経験された4年の経験をしたためた本になっています。 『 ユニコーン 企業の秘密』に書かれているモデルは、2012年頃、30人から250人にスケールした際に樹立されたモデルであり、その後10年程度運用され2600人規模までこの仕組みで組織が運用されているようです。数千人規模までエンジニアを増やしつつ、「デリバリー」を主軸にどう組織 サステイナブル な組織を組み立てるか?という試行錯誤の上に到達した組織のモデル・ マインドセット が書かれています。 英語が大丈夫な人で購入を迷っている人は是非以下のビデオを見てもらうのがとても良いと思います。以下のような組織構造をベースにするとどのようなカルチャーが育つのか?ということがわかります。2本立てになっていますので2本目は関連動画からご覧ください。 www.youtube.com 『 ユニコーン 企業のひみつ』は誰の役に立つの? エンジニア・ ビジネスパーソン の皆様 Spotify の例をとってもこのような組織の考え方は10年近く運用されているので、一定「枯れた技術」になったものかなと思っています。そのため「自律的な組織」的な考え方は、今後10年ぐらいで「gitで開発しています」程度にはある程度スタンダードになると思っています。 この体制は「メンバー/チーム」の裁量が大きくなる仕組みの話なので、より大きな責任でよりクリエイティブに働くことが求められていくと思っています。このカルチャーにキャッチアップすることが、今後のキャリアとして ユニコーン 企業のような企業で就労を希望する場合には非常に大きな鍵になってくると思っています。カルチャーのサンプルとして是非この本をお勧めしたいです。 技術責任者の皆様 他組織の ユースケース が詰まった本として非常に参考になると思っています。自分の組織に展開するヒントが散りばめられています。 経営者やマネージャーの皆様 「ソフトウェアデリバリー」などわかりにくい単語があると思いますがほとんどの章は基本的にググればわかる言葉で構成されています。 ユニコーン 企業と戦う心づもりがある方は是非に読んでほしいなと思っています。 エンジニア組織に限らずメンテナンスし続けるモデルに対してチームで立ち向かうために この本はサービス開発における Spotify の価値観が書かれている書籍です。これは「永遠にメンテナンスしないといけないモデル」に向き合う話でもあると思っています。 近年 CRM が浸透する中、営業や マーケティング のモデル化が進んでいると感じています。このモデルに対して「 銀の弾丸 (唯一の正解)」はなく、自社にフィットさせる改善の繰り返しが必要です。この改善を繰り返すためのヒントが多く書かれている本です。 流動性 の高い優秀な人と一緒に働くために エンジニアは人材の 流動性 がかなり高いため、マッチングが悪いとすぐに転職していく、という経営からのコン トロール が難しいセグメントの人材だと思っています。ただ、これはエンジニアが性質上わかりやすく先行しただけで、あらゆる職種で人材の 流動性 が上がっていくと思っています。 人材の 流動性 が高まり、 自己実現 のために働いている方にとって魅力的な組織とはどのような組織なのか?のヒントが多く書かれている本です。 『 ユニコーン 企業のひみつ』の実現に向けてどう取り組むのか? 『 ユニコーン 企業のひみつ』には「こうするといいよ」だったり、「こういう価値観だよ」という内容が中心に書かれています。 Spotify の良さに関しては『 ユニコーン 企業の秘密』や⬆️の youtube の動画にお任せするとして、このモデルが作られた経緯や時期などを調べてみました。 このモデルは2012年に外部に発信されています。 * このモデルは仕組み含めての設計から展開まで「 アジャイル コーチ」という存在が強く関わってきているようで、このモデルの樹立にはフルタイムの アジャイル コーチであるAnders Ivarssonさんと外部 コンサルタント であるHenrik Knibergさん1名でモデルの樹立をしているようです。 日本のスタートアップでは、私の観測範囲においてあまりフルタイムの アジャイル コーチというのは一般的ではないように感じており。おそらく「VPofE」や「EM」のようなロールの方が担う責務になるのかな?と思っています。大切なのは肩書きではなく、組織の改善に対するフルタイムのコミットメントできるロールであり、その責務を担うロールが本書でも言われている「あらゆるレベルでの継続的改善」継続していくことが大切だと感じました。 サービスの開発のように組織も計測しながら進むことが大切だと感じた Spotify の組織モデルを調べる中で、 Spotify のデータに基づく改善のカルチャーが組織にも適応されているように感じました。 データとサービスの意思決定の関わりは、『 ユニコーン 企業のひみつ』内でも多く述べられてお、2章の経営の意思決定をどう扱うか?のBIDDモデル、第8章にはプロダクトの改善でいかにデータを利用するか書かれています。 このデータに対する姿勢は組織運用に対しても展開されているようです。2012年に公開されたペーパーである、 Scaling Agile @ Spotify にてその様子を窺い知ることがでます。「サービス開発のように組織をデザインしているなぁ」と特に印象に残っている2つの観点を書きます。 ①自律的なチームであることを計測する。改善要求ではなくバックアップする 「Squad(チーム)は自律していること」が実現できているかどうか?は掲げるだけではなく計測が必要になります。 Spotify ではチームが自律的であることを、以下の7つの基軸でモニタリングしていたようです。 プロダクトオーナー アジャイル コーチ 仕事の影響度の強さ リリース容易性 チームに適合するプロセス ミッション 組織的なサポート 調査した内容は以下の図のように、クライテリアごとチームごとのメッシュで健康度がわかるようにモニタリングされます。 https://blog.crisp.se/wp-content/uploads/2012/11/SpotifyScaling.pdf 指標が下がっているチームに対しては、各セグメントにエキスパートを アサイ ンして、改善を行なっていきます。「自律的なチーム」であるSquadが実態として自律的に振る舞えているか?を指標化し定常的にモニタリングし、改善を行なっていたようです。 ②組織の依存性を調査し改善していく Spotify の「Squad(チーム)は自律していること」が実現できているのであれば、何かを実現するにあたって、チームとチームの間に「依存関係」はなく折衝が発生したり、 ブロッキング になることはないはずです。特にTribe(150名程度の部)を跨ぐような依存は自律性を大きく損なってしまいます。 これを防ぐために、Squad(チーム)やTribe(部)の依存関係は以下のシートのように定期的に調査されるとのことです。 この サーベイ によりチームや組織の依存を検知して依存を切るような責務の分離やシステムの改修を進めていきます。 https://blog.crisp.se/wp-content/uploads/2012/11/SpotifyScaling.pdf これはまさしくシステムを 疎結合 にする リファクタリング のような行為で、組織の健全性を保つ良いアプローチのように感じました。 で、タイミーとどう交わるの? 「Squadは自律したチームであること」という方針を定め。その方針を指標として計測し、上記ののような観測と実行のサイクルが実行されていることが Spotify の組織の強さだと感じています。 タイミーも現在の20名のプロダクト組織からスケールしていくフェーズに入ってきており、これは Spotify が組織モデルを樹立しはじめたタイミングと同じであります。そのためタイミーも何かしらモデルを作り 言語化 し、計測し改善していくフェーズに入ってきていると思っています。 大きな組織においては「How」を統一して押し付けるのではなく、指標計測を組織としてバックアップし、改善の主体はチームに委ねることはチームの自律性において非常に大切な要素だとこの本を通して改めて感じました。 タイミーはこれから一年程度かけて、組織の拡充を進めるとともに、チームの指標を継続的にFBすることでチームの状態の改善が行える状態を目指していきます。この変遷を一緒に作っていくことに興味がある方は是非、 こちら などからエントリーなどいただければ嬉しいです...! we are hiring...! 蛇足🐍 日本語訳と英語名の差分に感じた気持ち 英語名『Competing with unicorns』に対して、日本語名『 ユニコーン 企業のひみつ』になっているのもだいぶ マーケティング 上の性質の違いを感じます。 英語名だと ユニコーン 企業が身近な脅威になっている人に響くタイトル、日本語名だと雲の上の存在をの覗く人に響くタイトルになっています。文化の違いというのもあると思いますが ユニコーン 企業との距離感の違いでもあるのかな?と感じて挑んでいかねばという気持ちになりました。また調べている時に感じたことですが、このスタイル自体2012年に発信されており、Edgeのナレッジを得るという意味だと日本語でトレンドをフォローすること自体かなり速度が遅いなぁという気持ちになっています。
アバター
こんにちは、 タイミーデリバリー 開発チームの宮城です。 この記事は JP_Stripes Advent Calendar 2020 の10日目の記事です。 タイミーデリバリーはデリバリーを頼みたい人が安い価格で注文でき、飲食店も安い利用料で注文を受けられるデリバリープラットフォームです。 その決済機能として今回は Stripe を導入しました。 この記事では、決済基盤の技術選定/Stripeを活用したクレジットカード決済と各事業者への入金までの流れ/ Rails での具体的な実装内容 をそれぞれタイミーデリバリーでの活用事例として紹介します。 導入にあたった背景 決済基盤の技術選定基準 Stripeでできること PCI DSSについて 利用したStripeの機能 Custom Account Stripe SDKを利用したRails/Swiftでの実装内容 PaymentIntent Customer Account Connected Accountが決済を行えるようになるまで 決済の全体像 DBに保存する情報とStripeで保持する情報の整合性担保 仮登録フェーズ(Try) 確定フェーズ(Confirm/Cancel) stripe-ruby-mockを使ったTesting 返金処理・経理業務 Stripeダッシュボードでの返金処理 API経由による返金処理 返金のタイミング 入金について よかったこと 決済に関するトラブルが非常に少なかった ドキュメント、サポート、開発ツールが手厚い 失敗したこと 事業者に直接Stripeダッシュボードを見てもらった方が楽なケースが多かった 事業者のStripeへの登録における実装がかなり重く、Expressで提供されているAccountLinkを使った方が良さそう 今アカウントを選ぶとした場合の判断基準 終わりに 導入にあたった背景 タイミーデリバリーでは、 Rails による API サーバーと、Web管理画面としてVue.jsによるSPA、ユーザー向け iOS アプリとしてSwiftを採用しています。 1つの モノリス な Rails アプリで利用者別にネームスペースを区切り、それぞれ JSON を返す API を提供しています。 タイミーデリバリーはプラットフォームビジネスであり、注文者はアプリ上で複数の事業者の商品を閲覧し商品を購入します。 注文者が決済した金額にはプラットフォーム利用料が含まれており、タイミーと事業者双方に分配する必要があります。 まだ弊社には決済機能を導入するノウハウがなかったため、 Rails を使ってどのようにこのビジネスモデルを実現するのかや、そもそも決済機能に求められる通常の要件をどのように達成するのかもわからない状態からスタートしました。 まずは決済基盤に求められる技術選定の基準を作るところから始めました。 決済基盤の技術選定基準 弊社 経理 チームやCSチーム、法務チームと相談しながら要件をまとめ技術選定基準を作り、以下の要件が達成できる状態を目指すことにしました。 注文者の体験 注文者がクレジットカードを使ってアプリ上から商品を事前購入することができる 決済情報(クレジットカードなど)はタイミーが保持せず、ログにも残らない 不正利用や マネーロンダリング の対策ができている 注文者が操作に迷わない(UXが高い) 弊社 経理 チーム、導入事業者の体験 決済された金額から事業者とタイミーに分配できる、またはタイミーから事業者に請求できる 事業者の導入にあたって、契約書の受領からアプリ上に商品を掲載し決済できるようになるまでが簡単で短い 特定の期間で「締め」て、締めたデータは変更されなくなる 必要なデータを後から取り出すことができる 注文者への領収書や店舗ごとの利用明細が表示できる サポートチームの体験 返金のオペレーションが容易にできる 必要なデータを後から取り出すことができる 開発チームの体験 実装コストができるだけ低い サービスが利用できない時間が可能な限り短い(可用性が高い) この時点で「決済を行う」とはここまで考えることがあるのか…と深い闇に迷い込んだ気がしましたが、ここで丁寧に要件をまとめたことで各 ステークホルダー と認識のズレを減らすことができたように思います。 Stripeでできること 上記の技術選定基準をStripeで達成できるか当てはめたのが以下です。 利用者 項目 結果 注文者の体験 注文者がクレジットカードを使ってアプリ上から商品を事前購入することができる クレジットカード、ApplePay、GooglePayなどが可能。 決済情報(クレジットカードなど)はタイミーが保持せず、ログにも残らず、セキュアに管理される API で問い合わせ、決済情報の登録・閲覧・削除が可能。 秘匿情報は全てStripe側で管理し、タイミーが保持するものは結果のみ。 不正利用や マネーロンダリング の対策ができている Stripeに蓄積されているデータを用い、クレジットカードのリスク審査ができる Radarと呼ばれる不正行為検知の機能がある(別料金) 注文者が操作に迷わない(UXが高い) StripeのSwift向け SDK が優秀 カードの入力は1度きりでよく、次回決済時にシームレスに利用できる 弊社 経理 チーム、導入事業者の体験 決済された金額から事業者とタイミーに分配できる、またはタイミーから事業者に請求できる 可能。方法は後述 導入にあたって、契約書の受領からアプリ上で決済できるようになるまでが簡単で短い アカウントを登録するための情報は多いが、入力次第すぐに決済が可能。Stripeが並行で審査を進めている 特定の期間で「締め」て、締めたデータは変更されなくなる これはできなさそうだった。だが 経理 上のオペレーションとしては問題なく処理できたので方法を後述 必要なデータを後から取り出すことができる Stripeの ダッシュ ボード上で可能 注文者への領収書や事業者ごとの利用明細が表示できる 注文者向けの領収書は発行可能。デザインも良い。 事業者ごとの利用明細も可能ではあるが、少々面倒だった。これも後述 サポートチームの体験 返金のオペレーションが容易にできる 返金処理自体は簡単だった。むしろ ダッシュ ボード上でいつでもできてしまうので、弊社アプリケーション側で制限する仕組みを用意した。 必要なデータを後から取り出すことができる Stripeの ダッシュ ボード上で可能 開発チームの体験 実装コストができるだけ低い 開発者フレンドリーを謳っており、 API 経由での利用や拡張が簡単にできる。 サービスが利用できない時間が可能な限り短い(可用性が高い) SLA , SLOは特に公開しているわけではないようだったのですが、担当してくれていたStripeの営業の方に確認したところ「ほぼ落ちないようなもの」とのこと。 プロダクトとしては単一障害点になってしまっていることを ステークホルダー との合意を得ています。 このように概ね要件を満たせていたのと、開発者体験が良いという噂は聞いていたため自分もエンジニアとして興味もあり、今回はStripeを利用することを決めました。 PCI DSSについて ここに書いてある内容は正確性に欠けている可能性があるので、実際に対応を進める場合は必ず専門家に相談しながら進めてください。 決済機能を提供する上で避けて通れないのが PCI DSSへの対応です。 PCI DSSとは、クレジットカード情報を事業者がどのように扱うべきかを定めた情報セキュリティ基準です。クレジットカード情報を自社で保持する場合は300 件を超える PCI DSSの各セキュリティ制御要件を満たさなければなりませんが、カード情報の処理をStripeなどの外部 SaaS に任せる事で事業者に求められるセキュリティ制御要件は22件にまで減ります。そのためにカード情報の「非保持・非通過」を目指します。 その上で PCI に準拠していることの検証を進めていく事になりますが、求められる要件は年間のクレジットカード取引量によってレベルが変わります。新規事業であるタイミーデリバリーは最も低いレベルであるレベル4だったため、Stripe上での本番環境のセットアップを進める最中に要求される自己問診だけで済みました。 PCI DSSについて全てを理解することは困難であり、今回は担当してくださったStripeの営業の方に何度も相談させていただきました。 Stripeを利用する上でどのように PCI に準拠していくかはまずこの資料を読むことをオススメします。 stripe.com 利用したStripeの機能 今回は Stripe Connect を利用しました。 Stripe Connectはプラットフォームビジネスに最適なサービスであり、1人の注文者が任意の事業者の商品を購入し、事業者の口座へ入金しつつプラットフォーム手数料の徴収を行うことができます。 Stripe Connectの詳細や主な利用方法についてはこの記事が参考になりました。一部情報が古い部分があるものの、全体像を掴みやすい記事です。 qiita.com Custom Account Stripe Connectを利用する上で重要なのはアカウントタイプを決定することです。現在はStandard/Express/Customの3タイプが提供されています。詳細な説明はこの記事では割愛します。 アカウントの選び方は公式のドキュメントを見るのが良いかと思います。 stripe.com 今回タイミーでは以下の理由からCustomアカウントを選定しました。 導入事業者に課すプラットフォーム手数料の計算をアプリケーションロジックで行いたかったこと 弊社 経理 チームの都合や導入事業者の 経理 面からの要望により、返金等の請求内容の変更が発生するタイミングを制御したかったこと ネットで検索したところ、Expressアカウントの採用事例がまだ少なかったこと しかし、運用フェーズに入った今思えば、Expressアカウントを採用した方がよかったのではないかと考えています。こちらについては後述する失敗したことで記します。 Stripe SDK を利用した Rails /Swiftでの実装内容 ここからは実際にどのように実装したのかを紹介します。Stripeは REST API で決済を行える事に加えて、各言語ごとに SDK を提供しています。タイミーデリバリーはフロントエンドとしてSwift、バックエンドとして Ruby を利用しており、どちらも SDK が対応していました。 github.com github.com また公式ドキュメントには SDK を利用してPaymentIntentを作成する決済の流れが紹介されているので、ここを熟読することをオススメします。(PaymentIntentについては次項で説明します。) stripe.com PaymentIntent Stripeで決済を行う場合、それぞれの決済を表すのが PaymentIntent API です。ネット上の記事ではCharge API を利用した記事も多く存在していますが、現在はPaymentIntentを利用することが 推奨されています 。 とはいえCharge API にしかサポートしていない決済方法もあるため、自身の ユースケース と照らし合わせて対応しているか確認しつつ、どちらを選択しても問題ない場合はPaymentIntentを利用するのが妥当でしょう。 タイミーデリバリーの場合はファーストリリースではクレジットカードしかサポートしないことを決めていたためPaymentIntent API を選択しました。 PaymentIntentを作成しただけでは決済処理は行われず、その後「承認」を行う事によって決済処理が行われます。 stripe.com Customer 決済を行う主体を指します。タイミーデリバリーの場合は「注文者」と呼んでいます。 Customerオブジェクトには数多くのプロパティがありますが、タイミーデリバリーの ユースケース では、注文者の一意な特定ができかつ領収書の送信ができさえすればよかったため、メールアドレスのプロパティしか利用していません。 stripe.com Account プラットフォームを利用してお金を受け取る主体です。タイミーデリバリーでは、プラットフォームであるタイミーが親アカウント、タイミーデリバリー上で商品を公開する事業者が子アカウント(Connected Accountとも呼ばれます)にあたります。 上述したとおり、タイミーデリバリーではアカウントタイプとしてCustomアカウントを選択しました。 Stripe Connectでは、「プラットフォーム上での売り上げはまず親アカウントの残高に集まり、その後子アカウントの残高へ送金する」という形を取っています。事業者がプラットフォームを利用する際の手数料( ApplicationFee )はこの送金のタイミングで差し引くことが可能です。 Connected Accountが決済を行えるようになるまで 導入した企業が契約から決済を行えるようになるまでのリードタイムは、熱量を下げないためにも重要な観点です。 Stripeの場合、Accountに事業者の各種情報を登録すればその瞬間から決済を行うことができます。まずは本人確認中ステータスとなりますが、本人確認が済む前から利用可能です。 しかしこの情報を入力する項目がかなり多く、事業者ごとに情報を用意していただき入力してもらうのがかなり大変だったので注意しておいた方が良いかと思います。法人の場合は企業情報だけではなく代表者の身分証明書画像のアップロードまで必要でした。 stripe.com 決済の全体像 商品を選択し決済するまでのシーケンス図がこちらです。 決済の流れ ここで考慮すべきは 決済情報をどのようにサーバーで保持せずにStripeに送信するか ですが、そこはStripeのSwift SDK に全て任せることができます。クレジットカード情報の登録、決済の承認の処理前には API からStripeに向けて一時キーを要求し、Swift SDK に返します。Swift SDK のViewControllerからStripeへ直接リク エス トすることで処理を確定します。 DBに保存する情報とStripeで保持する情報の整合性担保 上記の全体シーケンス図から、注文情報の確定とPaymentIntentの作成、承認処理をより細かく記載したのが下記のシーケンス図です。 注文の確定処理のシーケンス図 アプリケーション側では注文情報をデータベースに保存し、Stripe側にはPaymentIntentを作成するリク エス トを送ります。複数サービスを跨いだ トランザクション では、個々のデータの整合性を担保することが大きな課題となります。注文情報は保存できたがStripeへのリク エス トが タイムアウト などで失敗した場合、注文情報が ロールバック されなければ注文者は引き落としがないまま商品を受け取ることができてしまいます。 そのため、今回は仮登録フェーズと確定フェーズの2段階の トランザクション を行う設計にしました。分散 トランザクション における デザインパターン の TCC (Try/Confirm/Cancel)パターンに近いです。 仮登録フェーズ(Try) 仮登録フェーズの トランザクション では、フロントエンドから送られてきた注文情報をもとにPaymentIntentを作成するリク エス トを行い、その後データベースにpayment_intent_idを含めた注文情報を保存します。 PaymentIntentは作成しただけでは決済は行われないため、現時点では仮登録の状態といえます。 アプリケーション側ではOrderモデルを作成し、各種注文情報を保存します。payment_succeeded_atというnullableのdatetime型のカラムを用意しておき、現時点ではnullにしておきます。このカラムがnullの場合は注文者のアプリや店舗側の管理画面には表示しないロジックにしておきます。 実際のコードに近いサービスクラスがこちらです。 class User :: Order :: PaymentPrepareService def initialize (user, params) @user = user @params = params end def run! ActiveRecord :: Base .transaction do find_resources validate! create_payment_intent! create_order! end @payment_intent .client_secret end private ~ 省略 ~ PaymentIntentの作成リク エス トが失敗した場合はordersのレコードは作成されません。一方ordersの保存処理が失敗した場合は作成したPaymentIntentはキャンセル処理をするべきですが、上述した通りPaymentIntentは承認処理をしなければ実際に決済が行われないため、存在したままでも特に支障もないためそのままにしています。 仮登録フェーズでやるべきことは、PaymentItentとordersのどちらの登録処理も成功していれば確定フェーズでConfirm処理が確実に成功する もしくはCancel処理による ロールバック ができる という状態にしておくことがポイントです。 確定フェーズ(Confirm/Cancel) 仮登録フェーズのレスポンスで返した一時キーを利用し、フロントエンドのStripe SDK が確定処理を実行します。Stripeでは各イベントの発火ごとにWebhookを送信することができるため、確定処理の成功/失敗のイベントにおけるWebhook処理を実装します。 ほぼそのままのコードのサービスクラスの実装がこちらです。 class PaymentIntent :: Webhook :: UpdateOrderService VALID_EVENT_TYPE = %w[ payment_intent.succeeded payment_intent.payment_failed ] .freeze # @param [String] payload # @param [String] sig_header # @param [String] endpoint_secret def initialize (payload, sig_header, endpoint_secret) # 全てStripeのWebhookが送信してくる内容。ドキュメントに従えば良い @payload = payload @sig_header = sig_header @endpoint_secret = endpoint_secret end # @raise [Service::ValidationError] # @raise [ActiveRecord::RecordInvalid] def run! validate! ActiveRecord :: Base .transaction do @order = Order .lock.find_by( stripe_payment_intent_code : payment_intent_code) return logging_order_not_presence if @order .nil? send( :"update_order_ #{ event.type.split( ' . ' ).last } !" ) end end private def event @event ||= Stripe :: Webhook .construct_event( @payload , @sig_header , @endpoint_secret ) rescue JSON :: ParserError raise Service :: ValidationError .new([ :invalid_payload ], self ) # アプリケーションで定義している独自の例外クラス rescue Stripe :: SignatureVerificationError raise Service :: ValidationError .new([ :invalid_signature ], self ) end def update_order_succeeded! @order .update!( payment_succeeded_at : Time .zone.now) NotifySlack :: Order :: PaymentSucceededNotifyJob .perform_later( @order .id) end def update_order_payment_failed! @order .discard! # 論理削除 end def validate! check_valid_event_type! end def check_valid_event_type! raise Service :: ValidationError .new([ :unexpected_event_type ], self ) unless VALID_EVENT_TYPE .include?(event.type) end def logging_order_not_presence return if @order .present? Rails .logger.warn( " couldn't find Order with webhook, stripe_payment_intent_code= #{ payment_intent_code }" ) Rails .logger.warn(event) end def payment_intent_code event.data.object.id end end 成功時はOrderモデルをpayment_succeeded_atに現在時刻を入れて更新し、失敗時はOrderモデルを論理削除しています。 決済処理の トランザクション についてはメルカリさんの記事を参考にしています。 engineering.mercari.com stripe- ruby -mockを使ったTesting 外部 API を利用する上では RSpec やローカル環境ではリク エス トにモックを差し込めるようにしたくなります。WebMockなどを使って自前実装する手もありますが、Stripeの場合はモック化のためのgemがいくつか サードパーティ で開発されており、今回はその中でもstripe- ruby -mockを利用することにしました。 github.com 利用方法の詳細はここでは省略しますが、それっぽいモックデータを返してくれることはもちろんのこと、リク エス トで送ったパラメータをインメモリに保持し、その値をレスポンスとして返してくれるので非常に使い勝手が良いです。 しかし入力値バリデーションは完璧ではないため、あくまでレスポンスの型の検証として使うのが良さそうです。 モックデータはこの辺りのコードにまとまっています。 stripe-ruby-mock/data.rb at master · stripe-ruby-mock/stripe-ruby-mock · GitHub 返金処理・ 経理 業務 返金はStripeの ダッシュ ボード上、または API 経由でも行うことができます。 Stripe ダッシュ ボードでの返金処理 ダッシュ ボード上での返金処理 全額返金することも一部返金することもできます。 「関連する送金を差戻す」とは、決済で発生した金額のうち、子アカウントの残高に送金した金額を差戻すことを指します。チェックがない場合子アカウントの残高は売り上げが立ったまま減らないため、この返金はプラットフォーム側が立て替えることになります。 「プラットフォーム手数料を返金」とは、決済で発生した金額のうち、子アカウントから徴収したプラットフォーム手数料を子アカウントに返金することを指します。「関連する送金を差戻す」にチェックを入れつつ「プラットフォーム手数料を返金」にチェックを入れなかった場合、プラットフォーム側の利益が残ったまま子アカウントの残高が減るので、この返金は子アカウントが立て替えることになります。 少しややこしいですが、この二つの チェックボックス にチェックを入れて返金することで注文者が支払いをする前の状態に戻ることになります。テスト環境で返金を複数パターン試してみて理解するのが良いと思います。 API 経由による返金処理 API 経由であっても、 ダッシュ ボードと同じような制御をしつつ返金することが可能です。 Refundオブジェクトを利用します。 stripe.com 返金のタイミング Stripeでは返金はいつでも可能です。いつでもというのは子アカウントの銀行口座に残高を振り込んだ後でも可能であり、アカウントの残高はマイナスになることも許容されます。 しかし 経理 チームからの要求としては「特定の期間で"締め"て、締めたデータは変更されなくなる」ことが望まれていました。月次決算として売り上げを計上した後に返金され、計上された売り上げが変わってしまうと困ります。 そのためサービス方針として返金は24時間以内のみ可能であることを明記し、オペレーションとしてもStripeの ダッシュ ボードからの返金は行わずに基本的に API 経由で行う方針としました。 Stripeの決済金額は最短4日後に利益として確定されます(この件については次節で解説します)。そのため毎月5日には前月の売り上げが確定されることになる形で 経理 チームと合意しました。5日よりも早い方が 経理 としては嬉しいものの、Stripeの仕様上ここまでしかできないと結論づけました。 入金について 上記でStripeの仕様上と書きましたが、これはStripeの自動入金スケジュールが関わってきます。日本の口座の場合「週ごと」と「月ごと」が選択でき、スケジュール予定日時点で振込可能な金額が自動で入金されます。 「振込可能な金額」になるのが最短4日かかるため、例えば5/30が振込予定日だった場合は5/29の決済金額は含まれないことになります。 このStripeの入金の仕様は弊社 経理 チームとして辛く、導入企業からも問い合わせが多かった箇所でもありました。 経理 業務として考えると月内の売上がまとめて入金されることが望ましいですが、Stripeの入金を毎月5日に設定していた場合は月初め1日の売上も含まれてしまうことになります。これを4日にしていたとしても、Stripeの入金処理が始まるのはおそらく UTC 時間の0時ごろであり(Stripeのサポートに確認しましたが実際の時間は答えられないという回答でした。)、完璧な しきい値 で入金対象を選ぶことは難しいです。 経理 チームと相談の上、タイミーデリバリーでは毎月5日の入金とし、 ダッシュ ボード上でダウンロードできる利用明細と入金額を突合して処理してもらう形とすることにしました。 一応この問題を解決する方法はあるにはあり、Stripeの自動入金ではなく API による制御での手動入金を行うことで解決できます。他社さんでStripeを利用しつつ、月末締め翌月末払いを実現しているサービスは手動入金を活用しているようです。 しかし手動入金をしようとなるとアプリケーション側のロジックで入金金額の計算と API リク エス トなどの完全性を担保しなければならず、 PMF していない状態で自前で実装するのはコストやリスクが高いと判断し、今回は利用しませんでした。 よかったこと ここまででStripeの導入について説明してきました。Stripeを利用していてよかったことを説明します。 決済に関するトラブルが非常に少なかった 運用していて半年程度経ちますが、決済にまつわるトラブルや問い合わせはかなり少ないです。特に「クレジットカード決済ができない」ような問い合わせは0でした。Stripeの SDK のUXが高いのもそうですし、カード情報の間違いなどによるエラーはそれぞれ適切にメッセージを返してくれるからだと言えます。 ドキュメント、サポート、開発ツールが手厚い 開発者フレンドリーを謳っている通り、実装していて迷った場合は基本的に全て公式ドキュメントを読むことで解決できました。 ドキュメントを読んでも分からないことはサポートへ問い合わせていましたが、こちらもかなり助けられました。チャット、電話、メールによる問い合わせを受け付けており、メールでの問い合わせは日本語でやり取りすることができます。24時間以内の返信が保証されているのも安心できます。 開発ツールとして管理画面から API のリク エス トログやWebHookの結果を閲覧することができ、こちらも非常に見やすく便利でした。 失敗したこと 一方失敗したなと思えたことはいくつかあり、Customアカウントを選ばない方がよかったかもしれない、と今では思っています。理由は以下の2点です。 事業者に直接Stripe ダッシュ ボードを見てもらった方が楽なケースが多かった CustomアカウントではStripeを利用していることをほぼ完全に隠蔽できるほどにカスタマイズできますが、裏を返せば自身で実装しなければならない箇所がグンと増えます。CustomアカウントではStripeの ダッシュ ボードを顧客に提供することができないため、 ダッシュ ボードで提供されている売り上げや各種決済情報の明細、 CSV エクスポートなどの便利な機能を丸々使うことができず、自身で実装しアプリケーション内で提供する必要がありました。これは最小限の実装で済ませたいスタートアップとしては非常に重く、そもそもStripeを利用していることを隠したいモチベーションもないため、Expressアカウントの方がよかったのではないかと考えています。 事業者のStripeへの登録における実装がかなり重く、Expressで提供されているAccountLinkを使った方が良さそう 上述した通りStripeではAccountに事業者の各種情報を登録すればその瞬間から決済を行うことができますが、その情報が会社情報、代表者情報(身分証明証も含む)など多岐にわたり、導入事業者にその情報を収集してもらうのがかなり大変でした。加えてCustomアカウントではその情報を登録するフォームを自身で実装しなければならず、それぞれの情報のバリデーションまで含めるとかなりの 工数 がかかってしまい、リリース前に想定していた 工数 を想定以上に上回ってしまいました。 Expressアカウントの場合この登録フォームを提供できるAccountLinkと呼ばれる機能が提供されているので、それを使えば上記の 工数 は必要なかったのに…と後悔しています。 今アカウントを選ぶとした場合の判断基準 元々Customアカウントを選んだ理由は以下の3点でした。 導入事業者に課すプラットフォーム手数料の計算をアプリケーションロジックで行いたかったこと 弊社 経理 チームの都合や導入事業者の 経理 面からの要望により、返金等の請求内容の変更が発生するタイミングを制御したかったこと ネットで検索したところ、Expressアカウントの採用事例がまだ少なかったこと Standardアカウントでは返金の判断も事業者に任せてしまうため採用は難しいですが、上2つに関してはExpressアカウントでも可能です。 それに加えて事業者向け利用明細や事業者情報の登録フォームなどの SaaS に任せられる機能もExpressアカウントでは利用できるため、今からStripeを導入するとしたらExpressアカウントを利用すると思います。 どの機能は自身のアプリケーションで実装しなければならず、どの機能を SaaS に任せられるのか、を判断軸として持っておいた方が良いと書き残しこの記事を締めくくりたいと思います。 終わりに いかがだったでしょうか。新規事業でStripeを利用する際に考えたことのほぼ全てをこの記事に集約したつもりです。ぜひ参考にしていただければ幸いです。 15000字を超える超長文になってしまいましたが、ここまでお読みくださり誠にありがとうございました。
アバター
コーポレートエンジニア(兼いろいろ)をやっている @sion_cojp です。 本当はやり遂げた状態を発表したかったのですが、道のりも長かったため、今やっていこうとしてる内容を記事にしてみました。 この記事を見てタイミーのコーポレートエンジニアに興味を持っていただけたら幸いです。 TL;DR 増え続けるXaaS、ユーザ管理、複雑化 残されない手順書 我々がやりたいこと なぜterraform? terraformで管理していくための課題 課題を解決してみる さらにソフトウェア化できそうなところ APIがないXaaS 最後に TL;DR XaaSユーザ管理を全てterraformにしていく 名前とroleさえ設定してapplyすれば権限付与できる形式にしたい 増え続けるXaaS、ユーザ管理、複雑化 GSuite( SaaS )/ AWS や GCP (IaaS) / CircleCI(PaaS)など便利なツールが増えている時代。 弊社では全てのXaaSを網羅するカオスマップが github で管理されており、例えばエンジニア組織が主に使うものはこれだけあります。 オン/オフボーディングの際、社員に権限を手動で追加/削除してる必要がありますが、おそらくこれらのXaaSにログインし手動でポチポチやってる企業が多いかと思います。 「ちゃんと追加した?退職時にremoveしてる?」 追加権限が忙しいマネージャークラスしか無く、追加に時間かかって仕事がスムーズにできなかったり。 removeしてないとセキュリティ的に問題発生します。またユーザ単位課金だと無駄な費用も増えます。 残されない手順書 「ユーザをどう追加/削除したか、またそれはどういう基準か。」 ポチポチは手順や意図が残しづらいです。 どこをどうクリックして...のように全て手順書で管理されてる企業は少ないかと思います。 我々がやりたいこと XaaSのアカウント作成/削除を、オン/オフボーディングの一環としてコーポレート側で一限管理できると良さそうです。 ユーザ名に対し、role(部署やチーム)単位で紐づけて CRUD できるソフトウェアがあれば解決できます。 それらをどう管理するか...基本terraformで管理して、最悪なにかしらの独自ソフトウェアで解決していく方向で考えてみます。 なぜterraform? terraform自体は調べてもらうにして、私なりに今回のメリットは3つ。 1. 今ある状態をstateに残せる 2. providerさえあれば、いろんなXaaSを操作できるフレームワーク 3. 脱属人化を図れそう 1について、後ほど説明します。 2, 3について、 AWS でterraformが書ける人なら他のprovider...例えば GCP /datadogなども扱えることが多いです。 これはとても大事なことで、世の中にいる優秀なコーポレートエンジニアは極少数で、採用が難しいですし、弊社でも私1人です。 特定の言語で独自ソフトウェアを書くよりは、terraformの フレームワーク に落とし込むことで、脱属人化もしやすいです。 最悪なにかしらの独自ソフトウェアで解決しようと思いました。 この文言と矛盾してる理由は、今のところterraformに落とし込むことで問題はなさそうですが、将来どうなるか全く読めない。その場合は作るしかないかなぁと思い、この一文を添えました。 terraformで管理していくための課題 デメリットは2つ。 1. XaaSに対して、providerがない場合がある 2. community providerの場合、開発が滞ってる可能性がある AWS / GCP など有名なものはterraform-providerがあり、活発に開発されてます。 弊社では日本のXaaSも利用してるので、それらのterraform-providerを作る必要があります。 またGSuiteは有名なIaaSですが、 community provider です。 community providerの場合は、活発に開発されてない事が多いです。 1passwordに至っては registry.terraform.io に上がってないため、terraform 0.13で使うには自身で github releaseからproviderをインストールしないといけません。 これらを解決するには、自分たちで OSS でproviderを作成/コミットする課題があります。 課題を解決してみる 下記を試していけそうか実際にチャレンジしてみました。 1. 自分自身でterraform-providerを作ってみる(上述、2,3の課題解決) 2. 一番難しそうなGSuiteのterraform化をしてみる(上述、1の課題解決) 1について、jamfのterraform-providerを書いてみました。 https://github.com/sioncojp/go-jamf-api https://github.com/sioncojp/terraform-provider-jamf https://registry.terraform.io/providers/sioncojp/jamf/latest?pollNotifications=true jamf trial期間中に作成しため、若干バグがありますが、実際にterraform apply->Createができたのは確認しました。 解決は出来ましたが、コーポレートエンジニアが最低限 Golang を扱える必要があります。 「 Golang は扱えるが、terraform-providerの開発が難しいのでは?」というと、どのproviderも作り方は簡単で同じなので勉強すれば...最悪私が教えれば書けると思います。 (hashicorpがtutorialも出してます Setup and Implement Read | Terraform - HashiCorp Learn ) 2について、GSuiteのgroup部分をterraform化することが出来ました。 ただ、documentはあまり整備されておらず、issueも若干たまっており、 OSS コミットしていく必要があります。 1,2 を踏まえて、「 Golang が書けて、あとは OSS 開発/コミットをやっていく気持ちがあれば出来る」というのが所感です。 さらにソフトウェア化できそうなところ > 1. 今ある状態をstateに残せる これを利用して、GSuiteのterraform state情報より組織図の自動generateができそうですね。 またterraform-providerを開発した知識を踏まえて、先ほど出てきたjamfや meraki などコーポレートで管理してるツールのterraform化も出来て似たような課題を解決できそうです。 API がないXaaS API がないXaaSもあります。そこは手順書で管理するしかないと思います。 最後に 現段階では、gsuite/1passwordはterraform importすれば管理出来る状態です。 jamfに関しては、少しお時間かかりますが引き続き開発していく気持ちですので、ご協力よろしくお願いします。 そしてこれらを1人で成し遂げようとしたら多くの時間が必要です。 この記事見てterraform/ Golang を駆使して、コーポレートを自動化することに興味がある方がいらっしゃれば是非一度お話しましょう!
アバター
こんにちは、サーバサイドエンジニアの @Juju_62q です。 今回はタイミーで実践しているECSのオートスケール戦略についてお話ししようと思います。 TL;DR タイミーではTarget Tracking ScalingとStep Scalingを組み合わせてオートスケールをしています Target Tracking Scaling -> 通常のスケールアウト・スケールイン Step Scaling -> スパイク時のスケールアウト 2つを組み合わせることで、様々なリクエストに対し適切なリソースを用意しています タイミーのアクセス量の変化とビジネス要求 タイミーのアクセス量の変化とこれまでのオートスケール タイミー は空いた時間に働きたい人とすぐに人手が欲しい店舗・企業をつなぐスキマバイトアプリです。 したがって、仕事の募集数や働いてくださるワーカーさんの数は世の中の動向に大きく左右されます。 例えば昨年2019年の12月で言えば、忘年会シーズンということもあり飲食店を中心に仕事の募集が大幅に増えました。 重ねてそのタイミングでテレビCMを行いました。伴ってメディア露出も急増しサービスへのアクセスは波があるとは言え常時圧倒的に多かったです。 一方で4月以降はCOVID-19の影響もあり一時的に仕事の募集数、アクセス数共に減少しました。 近頃は職種を多様化させることにより再び盛り上がっていますが、アクセスの傾向は昨年の12月とは異なるものになっています。 具体的には弊社のマーケティングチームが通知やキャンペーンの施策を行ったタイミングでアクセス数が急増するようになりました 1 。 これまでのタイミーのオートスケールではアクセスの特性上滑らかなアクセス量の増減に対応とすることを目的としてきましたが、アクセスの変化量が大きくなったことにより従来のオートスケールでは対応が難しくなりました。 特に7/20に通知を行ったキャンペーンではその影響は顕著で、アクセスの増加からサーバ増加までのラグが10-15分ありました。 ユーザとしては通知が来たからアプリを開いているので、表示が適切にされなかったり待ち時間が長いのは体験がよくないです。 ビジネス要求の変化 前述した通り、タイミーの仕事の募集数、アクセス数はCOVID-19の影響もあり一時的に減少しました。 しかしながらセールスチーム、サポートチーム、マーケティングチームの必死の努力が実り今現在かなり盛り返してきています。 そうした努力の一部にワーカーさんにむけたPush通知やキャンペーンの実施があります。 メディア露出などの明らかに予期できるスパイクについては事前共有をいただいた上でサーバの増強を行ってきましたが、マーケティング施策となるとスピード感も頻度も異なります。 全てのPush通知やキャンペーンを実施する前に開発チームの承認を取るのは非効率です 2 。 一方でプロダクトの技術的な都合でビジネスチームが必死で考えた施策の効果を低下させるのは言語道断です。 以上を踏まえ、スピード感を持ってビジネス施策を実施でき、その効果をなるべく減少させない環境を作るためにオートスケール設定を見直しました。 ECSのオートスケールポリシー ECSのオートスケールポリシーはAWSが提供しているものとしては2種類存在します 3 。 なお、記事内ではオートスケールをコンテナの増減を指す言葉として扱います。VMのインスタンス数を増減させるオートスケールは扱いませんのでご了承ください。 Step Scaling Policy Target Tracking Scaling Policy Step Scaling Policy 指定した閾値に基づいてスケールアウト/インを行うオートスケールです。 スケールアウト/インを段階的に定義できるのが特徴で、例えば以下のような設定が可能です。 CPUの平均使用率が61-70% -> コンテナを1つ増やす CPUの平均使用率が71-80% -> コンテナを3つ増やす CPUの平均使用率が81%以上 -> コンテナを5つ増やす CPUの平均使用率が50%以下 -> コンテナを1つ減らす 適切な値を設定する難易度は低くないですが、うまく使うとリソースを急激に変化させることができます。 Target Tracking Scaling Policy 指定したメトリクスが指定した数値になるようにスケールアウト/インを行うオートスケールです。 イメージとしてはKubernetesの Horizontal Pod Autoscaler が近いと思います。 例えばCPUの平均使用率が60%となるように指定した場合 CPUの平均使用率が70% -> スケールアウト CPUの平均使用率が50% -> スケールイン というような振る舞いをし、CPUの平均使用率が60%になるように努めてくれます 4 。 オートスケールの設定をする時にみなさん頭を悩ませるのがスケールインの閾値だと思いますが、これをある程度いい感じにやってもらえるのが便利です。 タイミーでは12月時点からTarget Tracking Scaling Policyを利用しています。 いい感じにオートスケールしつつスパイクに抗うAutoScale Policyを設定する 以上を鑑みると、リソース使用量が滑らかに変化するオートスケールをTarget Tracking Scalingに任せ、スパイクが発生した場合にはStep Scalingを利用してどかっとリソースを増やすのが良さそうです。 Step Scalingで増やしたコンテナは、Target Tracking ScalingでスケールインしていくためStep Scalingにはスケールアウトの設定だけすればたりそうです。 タイミーのサービスで設定している実際の数値は出しませんが、Terraformでの実装例を載せておきます。 厳密には正確な記述ではありませんが、雰囲気は掴んでいただけると思います。 Target Tracking Scaling resource "aws_appautoscaling_target" "target" { max_capacity = 50 min_capacity = 5 resource_id = "service/${cluster_name}/${service_name}" role_arn = ${iam_role_arn} scalable_dimension = "ecs:service:DesiredCount" service_namespace = "ecs" } resource "aws_appautoscaling_policy" "scale" { name = "${service_name}-scale" resource_id = "service/${cluster_name}/${service_name}" scalable_dimension = "ecs:service:DesiredCount" service_namespace = "ecs" policy_type = "TargetTrackingScaling" target_tracking_scaling_policy_configuration { predefined_metric_specification { predefined_metric_type = "ECSServiceAverageCPUUtilization" } // CPUの平均使用率が60%になるように維持する target_value = 60 // スケールインの間隔は60秒空ける scale_in_cooldown = 60 // スケールアウトの間隔は30秒空ける scale_out_cooldown = 30 } depends_on = [aws_appautoscaling_target.target] } Step Scaling resource "aws_appautoscaling_policy" "spike_scale_out" { name = "${service_name}-spike-scale-out" policy_type = "StepScaling" resource_id = "service/${cluster_name}/${service_name}" scalable_dimension = "ecs:service:DesiredCount" service_namespace = "ecs" step_scaling_policy_configuration { adjustment_type = "ChangeInCapacity" // スケールアウトの間隔は30秒空ける(測定間隔は60秒ですが、バッファを持つため小さい値にする) cooldown = 30 metric_aggregation_type = "Maximum" // CPUの平均使用率が70%-80%の場合コンテナを3つ増やす step_adjustment { metric_interval_lower_bound = 0 metric_interval_upper_bound = 10 scaling_adjustment = 3 } // CPUの平均使用率が80%-90%の場合コンテナを5つ増やす step_adjustment { metric_interval_lower_bound = 10 metric_interval_upper_bound = 20 scaling_adjustment = 5 } // CPUの平均使用率が90%-の場合コンテナを10増やす step_adjustment { metric_interval_lower_bound = 20 scaling_adjustment = 10 } } depends_on = [aws_appautoscaling_target.main] } resource "aws_cloudwatch_metric_alarm" "spike_scale_out_alerm" { alarm_name = "${service_name}-spike-scale-out" comparison_operator = "GreaterThanOrEqualToThreshold" evaluation_periods = "1" metric_name = "CPUUtilization" namespace = "AWS/ECS" period = "60" statistic = "Average" // Step ScalingのCPU平均使用率の閾値の基準は70%とする threshold = 70 dimensions = { ClusterName = ${cluster_name} ServiceName = ${service_name} } alarm_actions = [aws_appautoscaling_policy.spike_scale_out.arn] } この2つの組み合わせで、CPU使用率を普段は60%に維持するようにオートスケールをしつつ、CPU使用率が80%や90%など跳ね上がった際に大きくスケールアウトできるようになります 結果 画像のグラフですが横軸のスケールを合わせるのを失念しておりました。失礼いたしました。 アクセスの増加から、1,2分で大幅なスケールアウトをしています。 結果的にレスポンスタイムの悪化も最小限に止めることができました。 終わりに 複数のスケールポリシーを組み合わせることで、滑らかなアクセス数の増減とスパイクの2つの状況に対応ができるようになりました。 とはいえまだまだ完璧なオートスケールには程遠く、リソースを最適に使えているとはいえない状況です。 より高いコストパフォーマンスを発揮できるようにこれからも勉強していきたいと思います。 アクセスの絶対量の減少により、施策によるアクセス変化がもたらす影響が大きくなった。また、新規登録は継続的にあるので通知対象ユーザ数も増加している。 ↩ 想定影響に応じて事前に連絡をいただいたり、単位時間あたりの流量制御もやってます。 ↩ APIやSDK経由でコンテナ数を増減させることでもっと柔軟な設定もできます。 ↩ 厳密には異なりますが、イメージはできると思います。 ↩
アバター