こんにちは。プロダクトエンジニアリング部の武井です。 普段はLIFULL HOME'Sの賃貸領域の開発をしています。
現在、LIFULL HOME'Sの賃貸領域ではシステムの基盤刷新を行っています。 詳細については以前の記事をご覧ください。
今回はこの基盤刷新に伴い、新たなABテスト実施システムを構築したので、その概要を紹介したいと思います。
LIFULL HOME'S におけるABテスト
LIFULL HOME'Sでは、ユーザーにとってより使いやすいポータルサイトを目指し日々UI/UXの改善を行っています。 その効果測定の手段としてABテストを取り入れており、常時いくつものABテストを実施しています。
旧基盤でのABテストのしくみはシステム立ち上げ当初に実装されて以来、現在まで10年以上に渡って使われ続けています。 今回の基盤刷新に伴い、このABテストを実施するしくみを新基盤上に構築し直す必要がありました。
ABテストのシステムは旧基盤と同様に今後10年以上使われ続ける可能性があり、非常に影響度の大きいシステムになることが予想されます。 そのため今後の開発効率を左右する重要な開発プラットフォームを作るという意識で開発に臨みました。
インフラ層での振り分け
システムの構築にあたって、A/Bのユーザーをどのように振り分けるかをまず考えました。
最初に出た案は、KubernetesにおけるIstioの加重ルーティングを利用したインフラ層での振り分けです。
IstioはKubernetes上のトラフィック管理などを担うservice meshです。LIFULL HOME'Sの多くのプロダクトはKubernetesの共通基盤上で動作しており、service meshとしてIstioを利用しています。
このIstioのVirtualServiceという機能を使うことでユーザーの振り分けができます。
たとえば、以下のようにmanifestを記述するとv2
のバージョンに25%
、v1
のバージョンに75%
のユーザーをそれぞれ流すことができます。
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: reviews-route spec: hosts: - reviews.prod.svc.cluster.local http: - route: - destination: host: reviews.prod.svc.cluster.local subset: v2 weight: 25 - destination: host: reviews.prod.svc.cluster.local subset: v1 weight: 75
しかしこの方法では、複数のテストを同時に並行して実施する際、運用上の問題が予想されました。 たとえばテスト1とテスト2を同時並行すると、A/Bそれぞれの掛け合わせで以下の4種類のバージョンのPodが必要になります。
- テスト1A * テスト2A
- テスト1A * テスト2B
- テスト1B * テスト2A
- テスト1B * テスト2B
このようにテストの同時実施数を増やしていくと、O(2N)で用意するべきPodの種類が増えていき、管理が非常に困難になることが予想されます。 さらに一つのテストについて振り分けのパターンがA/B/C...と3パターン以上になることもあり、こうなるとより複雑性が増します。
このような運用上の懸念から、Istioを用いたインフラ層での振り分けは断念しました。
アプリケーション層での振り分け
他にも複数の案を検討した結果、インフラ層での振り分けは行わず同一のバージョンのPodでリクエストを受けた後、アプリケーションの中でA/Bのユーザーを振り分けコードレベルで処理を分岐させる案に落ち着きました。
詳細は割愛しますが、コードのイメージは以下のようになります。
システム側で設定ファイルとユーザー情報の突き合わせを行い、A/Bテストの情報が集約されたab
というオブジェクトを生成します。このオブジェクトが持つisB()
のようなメソッドを用いてコード上でパターン分岐を行う形になります。
if (ab.get(TEST_1_ID).isB()) { // テスト1でのBパターンの処理 ... return } // テスト1でのAパターンの処理 ...
この場合、アプリケーションのバージョン自体は一つで済みますが、if文で分岐を行う必要があります。複数のテストを同時並行するとより分岐条件が増え、コードの複雑性が高くなりますが、この複雑性を現時点では許容することにしました。
ABテストを実施する以上、必ずどこかでユーザーを振り分けるための複雑性を引き受ける必要があります。この複雑性をインフラ層という離れた場所ではなく、アプリケーションのコードという我々が普段開発していて変更しやすい場所に持ってきたことは暫定的に良い選択だったと感じています。
使いやすいインタフェースの追求
新システムを構築する上で、このシステムを利用する際の開発者体験や使いやすさを意識してインタフェースをデザインしました。
以下は開発したシステムにおける設定ファイルの一例です。 このように設定を定義すると、アクセスしてきたユーザーを自動的に振り分けるシステムになっています。 タイトルや資料へのリンクなども含めて宣言的に定義することで一目してテスト内容を把握できるようにしています。
export const abTestConfig: AbTestConfig = { [AB_TEST_IDS.sampleProject]: { title: '【賃貸事業部】サンプルテスト', specUrl: [ 'https://jira.jp/wiki/XXXXX', 'https://docs.google.com/spreadsheets/YYYYY', ], startDatetime: '2023-08-01 11:00:00', endDatetime: '2023-08-14 13:30:00', classification: [ { patternValue: 'a', weight: 50, measurementValue: 'sample_test_a', }, { patternValue: 'b', weight: 50, measurementValue: 'sample_test_b', }, ], beforePattern: 'a', afterPattern: 'b', }, };
この設定はTypeScriptのObjectとして定義し、型を当てるようにしています。 また、この設定ファイルに対するlinterを実装し、細かい設定内容もチェックしています。 これらのチェック機構によって、PullRequest作成時点で設定漏れや間違いに気付けるようになっており、誤った設定でデプロイしてしまうことを防げます。
このほかにも、利用開始方法や注意点などを記載した詳細なドキュメントを作成したり、テスト実施状況を可視化する簡単なダッシュボードを作成したりしました。
開発工数の削減
このシステムを構築するにあたって、旧基盤の単純な模倣ではなく少しでも開発工数を削減することも意識しました。 なぜなら今回構築するシステムは今後長い間使われる可能性があり、わずかな違いでもレバレッジが効き、結果的に大きな効果をもたらすためです。
旧基盤でのABテスト実施システムを分析したところ、振り分け部分と分析部分が連動していないことが改善点として挙がりました。振り分け部分とはユーザーをA/Bのように振り分ける部分、分析部分は振り分けた後のユーザーがコンバージョンに至ったかなどの分析用メトリクスを送信する部分を指します。旧基盤ではこれらが連動しておらず、テストを実施するたびにメトリクスを送信する処理を書く必要がありました。
そこで新システムでは設定ファイルに分析用の項目measurementValue
を設け、これを読み取って自動的にメトリクスを送信する設計としました。
export const abTestConfig: AbTestConfig = { [AB_TEST_IDS.sampleProject]: { title: '【賃貸事業部】サンプルテスト', ... classification: [ { patternValue: 'a', weight: 50, measurementValue: 'sample_test_a', // この値を自動的に送信する }, { patternValue: 'b', weight: 50, measurementValue: 'sample_test_b', // Bパターンのときはこちら }, ], ... }, };
この改善によって、実装や確認の工数削減、実装漏れの防止などにつながります。
まとめ
普段はユーザーが直接触る機能の開発が中心となるため、今回のように社内の開発者に向けたシステムの開発は新鮮で貴重な経験ができたと感じています。最近ではこのABテストの実運用が始まっており、前よりも使いやすい!という声をいただいています。
このように、LIFULL ではユーザーや社内の開発者など多様なステークホルダーの体験を考えたものづくりを行い、ともに成長していける仲間を募集しています。よろしければこちらのページもご覧ください。