LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

This post is also available in the following languages. English

LINE NEWSのA/Bテストをどう再設計したか

A/Bテストは、LINE NEWSの意思決定において重要な役割を果たしています。どの機能修正を加えるかや、どの実験的機能をリリースするかを決定するために利用します。LINE NEWSはLINEのネイティブアプリにシームレスに組み込まれたウェブアプリケーションです。日本の人口の5分の3以上 (*1) がLINE NEWSを定期的に利用しており、すべての意思決定による影響の規模は大きいです。

以前のA/Bテストシステムは、目的を達成していたものの、効率的であるとは言えませんでした。特にフロントエンドでは、頻繁な、時として必要以上に複雑なコードのデプロイが必要でした。次のデプロイ日までプロダクトの動作や外観を変更することができないため、機能の概念化からリリースまでのリードタイムが遅くなっていました。A/Bテストの運用をより柔軟で迅速かつ効率的にするために、私たちはシステムを再設計し、いくつかのソフトウェア運用のコンセプトと戦略を採用しました。この記事では、リリースとデプロイメントの分離を含む、これらのコンセプトをどのようにソースコードに反映させたかを紹介します。今回議論する戦略は、フィーチャーフラグ (feature flags) 、リモートコンフィギュレーション (remote configuration) 、カナリアリリース (canary releases) です。

私たちのA/Bテストの使用方法

これらの戦略を説明する前に、私たちが新機能に対してA/Bテストをどの様に使っているかの典型的な流れを説明します。まず、新機能がユーザー体験を改善するという仮説を立てたとします。この仮説を検証するために、ユーザーをコントロールグループ (新機能なし) とトリートメントグループ (新機能の複数のバリエーション) に振り分け、各グループからユーザーの行動データを収集します。データを統計的に分析した後、結論を導き出し、最も最適だったバリエーションを徐々にリリースしていきます。

既存システムの問題点

LINEでは、LINE NEWSをはじめとするいくつかのプロダクトで、Libraという社内のA/Bテストツールを使用しています。このツールの役割は、実験の作成と、実験グループへのユーザーの割り当てです。このツールで作成された値は、分離されたコンフィギュレーションサーバーに送信され、保存されます。

以前のLINE NEWSのA/Bテストのコードベースでは、実験名と実験グループは社内のA/Bテストツールで作成され、フロントエンドに冗長にハードコーディングされていました。開発者は、すべての機能バリエーションの仕様とA/Bテストの名前とグループを個別に紐付けるTypeScriptコードを毎回書いていました。複数のA/Bテストを実行するときには、条件分岐も複雑になっていました。

運用面では、A/Bテストの手順が柔軟性に欠け、反復的であることが問題でした。既存のA/Bテストにわずかな変更を加えるだけでも、新しいコードをデプロイする必要がありました。私たちは2週間ごとに定期デプロイメントを行っているため、新機能のテストとリリースに数カ月かかることもありました。ソースコードの頻繁な変更も、より多くのQAリソースを必要としました。

実験的機能を完全にコントロールする

私たちが必要としていたのは、実験的な機能の動作や外観のアップデートを、予定されたデプロイ日以外にリリースできるアプローチでした。私たちのソリューションの実装の詳細を掘り下げる前に、まずソフトウェア開発と管理のためのいくつかの戦略について話します。

フィーチャーフラグを使うことで、ソースコードを変更することなく、またデプロイメントを必要とすることなく、機能を有効化または無効化することができます。言い換えれば、ソースコードのif文のような条件文の真偽値 (trur/false、オン/オフ) に、値を動的にリモートで挿入する仕組みです。歴史的には、「リリース」と「デプロイ」という用語は同じ意味で使われてきましたが、フィーチャーフラグによってフィーチャーリリースとコードデプロイメントが区別されるようになりました。

フィーチャーフラグだけでも大きな価値をもたらすことができますが (*2) 、私たちは単に機能をオン・オフするだけにとどまりたくありませんでした。私たちは、機能の動作や外観を制御するための他のパラメーターを追加することを目指しました。そこで私たちは、リモートコンフィギュレーションという概念を探りました。

リモートコンフィギュレーションは、フィーチャーフラグのより複雑で高機能なバージョンです。通常、ソフトウェアのコンフィギュレーションは、デプロイされたソースコードか環境ファイルに記述されます。しかし、GoogleのFirebaseのようなサービスは、コンフィギュレーションのリモート管理を普及させました。リモートコンフィギュレーションは、特にリリースに審査を必要とするモバイルネイティブアプリケーションの開発において利用されています。

サードパーティのツールに頼らず、社内の技術で構築する

このようなテクニックを実装する一般的な方法は、LaunchDarklyやOptimizelyのような、ユーザー割り当て、データ追跡、データ分析機能を備えたサードパーティ製のA/Bテストスイートを使用することです。しかし、LINE NEWSでは、ユーザーの行動データをLINE社内のデータ追跡・管理システムに送信し、LINEのビジネス部門が適切な権限でデータを活用できるようにしているため、これらの選択肢の実現は事実上不可能でした。サードパーティのツールの一部だけを社内のツールと組み合わせることは、不要なハック的対応や不必要な冗長性を増やします。その代わりに、LINEのオープンソースのコンフィギュレーションサーバーであるCentral Dogmaが必要な機能を提供していたため、そこへフィーチャーフラグとコンフィギュレーションを保存しました。LINE NEWSでは、この一連のデータ (フラグとコンフィギュレーション) をフィーチャーコンフィギュレーション (feature configuration) と呼んでいます。

フィーチャーコンフィギュレーションの使用

新しいシステムでは、フラグとコンフィギュレーションを同じ場所に格納することにしました。例えば、ある新機能について、フラグは isEnabled パラメーターであり、他のパラメーターはその機能の動作や外観を制御をします。

私たちは、全く新しいCMSを構築するのではなく、シンプルに設計されたJSONフォーマットを利用することにしました。仮定として既存のUIにはパーソナライズされた記事が水平方向にスクロール可能なユーザーインターフェースであるカルーセルUIに表示されていて、それらの同じ記事を垂直方向に表示する新しいUIを試したいとします。私たちはそれをタイムラインUIと呼ぶとします。コンフィギュレーションサーバー上では、データは以下のようになります:

{
  "timelineForyou": {
    "isEnabled": true,
    "numberOfArticlesToLoadInitially": 30,
    "title": "Recommended based on your interests"
  }
}

クライアント側のコードは単純に以下のようになります:

if (timelineForyou.isEnabled) {
  // ...
}

タイムラインUIをロールアウトすることを決めたと仮定すると、次のステップは最初にローディングする記事の数を決めることです。あまり多くの記事を読み込むと、最初のページの読み込みが遅くなります。記事の数が少なすぎると、記事が「遅延ローディング」される頻度が高くなり、ユーザー体験が低下する可能性があります。この場合、以前のシステムでは、ソースコードを修正してデプロイする必要がありました。この新しいシステムでは、リモートリポジトリから簡単に numberOfArticlesToLoadInitially の値を調整することができます。また、タイトルを「Recommended based on your interests」から「For You」のように変更することもできます。

以下の画像は、「For You」セクションがどのように変わったかを説明しています。左の例では、パーソナライズされた記事がカルーセルで表示されています。右の例 (現在のUI) では、同じコンテンツが縦に表示されています。これは、私たちの実験の最初と最後だけを示していることに注意してください。実際の過程では、多くの試行錯誤がありました。例えば、人々が「For You」セクションをどのように認識し、どのようにインタラクションするかは、トップページの他の部分の見た目に影響される可能性があります。このような影響要因をコントロールするために、私たちは複数の分離したA/Bテストを実施しました。

フィーチャーコンフィギュレーションの設定を簡単にするための規約

わかりやすく限られた規約を用意しました。開発者がコンフィギュレーションを設定するために知っておく必要があるのはこれらですべてです。これらの規約は以下の通りです:

  • 適切なコンフィギュレーションを特定の実験に結びつけるために、JSONファイルの名前は実験名でなければなりません。例えば、exp0001という名前の実験の場合、JSONファイルの名前はexp0001.jsonとなります。
  • JSONオブジェクトの各階層には、実験グループ (1番目) 、機能名 (2番目) 、コンフィギュレーション (3番目) という専用の役割があります。

以下にexp0001.jsonの中身を例示します:

{
  "control": {
    "timelineForyou": {
      "isEnabled": true,
      "numberOfArticlesToLoadInitially": 30,
      "title": "For You"
    }
  },
  "treatment": {
    "timelineForyou": {
      "isEnabled": true,
      "numberOfArticlesToLoadInitially": 10,
      "title": "For You"
    }
  }
}

自動化による安全性の確保

ヒューマンエラーをなくし、バグを減らすために、いくつかのプロセスを自動化しました:

  • フィーチャーコンフィギュレーションをコンフィギュレーションサーバーに直接書き込むこともできますが、安全性を確保するための追加レイヤーとして、GitHub Actionsを使うGitHubリポジトリを作成しました。コンフィギュレーションへの変更は、プルリクエストで他の開発者によってレビューされ、バージョン管理されます。
  • 開発者がフィーチャーコンフィギュレーションGitHubリポジトリのプルリクエストに変更をプッシュすると、GitHub Actionsは私たちが書いたテストを実行して、JSONフォーマットが完全かどうかをチェックし、エラーを回避します。また、名前の衝突を避けるために、機能名 (第2階層のキー) がリポジトリ内のすべてのファイルで互いに排他的であることもチェックします。
  • プルリクエストがフィーチャーコンフィギュレーションGitHubリポジトリのメインブランチにマージされると、コンフィギュレーションサーバーのコンフィギュレーションが自動的に更新されます。
  • フロントエンドのコードがLINEユーザーの暗号化されたユニークなIDでリクエストを送信すると、LINE NEWSサーバーはコンフィギュレーションサーバーにアクセスして実験に関するデータを取得し、ユーザーがどの実験グループに属しているかを判断します。そしてそのユーザーに必要なコンフィギュレーションのみを提供し、フロントエンドのTypeScriptコードがそのコンフィギュレーションの通りにUIを構築できるようにします。

情報の制限による認知的負荷の最小化

以前のシステムでは、フロントエンドのコードがリクエストしたあと、LINE NEWSサーバーが全ての実験データを提供していました。このため、前節で述べたように、新システムではバックエンドが自動的に処理するのと同じ作業をフロントエンドのコードで行う必要がありました。つまり、新しいアーキテクチャーでは、任意の実験名などの実験情報はフロントエンドでは見ていません。フロントエンドの開発者は、どのタイプの変数がコンフィギュレーションに含まれているかだけを気にします。

さらに、実験グループのバリエーションに関するコンフィギュレーション情報を専用のリポジトリに保存することで、開発者はUIの実装とコンフィギュレーションの管理をする時点を2つに分けることができるため、それぞれの時点での認知的負荷を軽減することもできます。

リライアビリティの維持

自由と柔軟性は、しばしばソフトウェアに複雑さをもたらすことがあります。リスクを最小限にするために、私たちは以下のようにしています:

  • フォールバックはフロントエンドの専用モジュールに記述します。通常はそれぞれの機能に対して isEnabled: false という値を設定するだけですが、コンフィギュレーションサーバーのダウンタイムでも、アプリケーションはデフォルトの動作で稼動します。
  • QAデバイスに対してのみ機能をオンにすることで、プロダクション環境でQAを行うことができます。
  • テストが終わってもソースコードに残っている条件分岐は技術的負債となるため、できるだけ早く条件分岐を削除し、 (コンフィギュレーションの有無にかかわらず) フラグの数を最小限に抑えます。

この新システムにより、以下のことが実現しました:

  • QAコストは変更が発生する箇所に存在します。コード変更を減らすことで、QAリソースをより効率的に使用することができます。

  • フィーチャーフラグとリモートコンフィギュレーションのもう一つの大きな利点は、ビッグバンリリースを避けることです。これは次の投稿において良いトピックになるかもしれません。

Reactによる実装詳細

Reactのコンテキストを使用してフィーチャーコンフィギュレーションを提供し、UIコンポーネント・ツリーのどこからでも直接使用できるようにしています。複雑なステート管理システムは使わないことにしました。コンフィギュレーションはセッション中のユーザーのアクションによって変わることはないので、よりシンプルなReactコンテキストは便利でした。プロップドリルを避けることで、コードの変更も最小限に抑えることができます。

以下はコードの簡略化した例です。まず、フロントエンドのコードに書かれたデフォルト値を初期値としてコンテキストを作成します。初期値の設定はオプショナルですが、推奨されます。

featureConfigurationContext.ts:

import { createContext } from 'react';
import { defaultFeatureConfiguration } from '../defaultFeatureConfiguration';
  
export const FeatureConfigurationContext = createContext({
  ...defaultFeatureConfiguration,
});

次に、デフォルト値をリモートリポジトリから送信された値で上書きし、提供します。

App.tsx:

import { FeatureConfigurationContext } from './FeatureConfigurationContext';
  
const featureConfiguration = {
  ...defaultFeatureConfiguration,
  ...featureConfigurationData,
};
  
function App() {
  return (
    <FeatureConfigurationContext.Provider value="featureConfiguration">
      ...
    </FeatureConfigurationContext.Provider>
  );
}

使い方は簡単です。

Feature.tsx:

import { FeatureConfigurationContext } from '../../FeatureConfigurationContext';
   
const { numberOfArticlesToLoadInitially } = useContext(
  FeatureConfigurationContext
);
   
function Feature() {
  return (
    <SomeJsxElement>
      // Here you can use numberOfArticlesToLoadInitially
    </SomeJsxElement>
  );
}

カナリアリリース

最後に、カナリアリリースとは、リスクを軽減するために、ある機能を一部のユーザーに限定して展開する戦略です。LINE NEWSはこの方法を何年も使ってきました。新しいアーキテクチャーでは、どの実験グループに対してどの機能を有効にするかをターゲットできるため、機能をより詳しくコントロールできるようになりました。何か問題が発生した場合は、その機能をオフにするだけで、それ以上の被害を避けることができます。

1. 月間アクティブユーザー数7700万人/月間ページビュー数154億回 (当社調べ、2021年8月現在)
2.
LINEのA/Bテストツール「Libra」は、実験のオン・オフを切り替えることができます。実験が非アクティブに設定されている場合、その機能のデフォルトの動作は、通常、フラグの値がオフの場合の動作と同じになります。つまり、実験と機能をカップリングすることで、フィーチャーフラッグと似たようなことが実現できます。以前からこの方法でプロジェクトを運営していました。しかし、実験のアクティブ/非アクティブのステータスを利用して機能のオン/オフを切り替えるのは、Libraの適切な使い方とは言えず、毎回慎重な計画が必要でした。新しいアーキテクチャーでは、この問題も解決しました。