HAProxyによるブランチ毎ABテスト基盤

こんにちは、新卒エンジニアの宮地(miyachik)です。
業務ではネイティブアド配信プラットフォーム UZOUの裏周りをやっています。

今回はSpeeeKaigiで発表した(フロントの変更なら)エンジニアの工数を必要とせず、アプリケーションの開発言語を問わない汎用的ABテスト基盤の話をします。 SpeeeKaigiについてはこちら↓

tech.speee.jp

前提

今回のABテスト基盤では

  • ブランチを分けた状態で個別にABテストができる(masterにマージする必要なし)
  • フロントの軽微な修正はエンジニアにはエンジニアの工数は割かない(ディレクターとデザイナーのみで行えるようにする)
  • API側などbackendのロジックに対するABテストも行えるようにする
  • 開発言語にとらわれずABテスト基盤導入をすることが可能

を実現することが出来ます。表側はRailsで裏側にAPIサーバーがいる、みたいなときにも試すことが出来ます。(簡単な変更のみならば既存のgemなどを使えば良いのですが、どうしても要件を満たせないことが出てきてしまうので…)

まずは最上段の振り分けについて話したいと思います。

やったこと

  • HAProxyによるABテスト振り分けと再抽選の防止
  • HAProxyの設定ファイルをRailsアプリケーションから動的に吐き出し、アプリケーションをデプロイとHAproxyの設定を再読込するタスクを作成
    • ABテストサーバをbackendから切り離し
    • 対象のABテストブランチをABテストサーバにデプロイ
    • 生成したHAProxyの設定を再読込
    • フロントのダウンタイムはゼロになるように

HAProxyについて

HAProxy とはプロキシサーバであり、ソフトウェアロードバランサーです。 HAProxyが何であって、何でないかは公式のドキュメントに書いてあります。 ドキュメントにもある通りHAProxyにはロードバランサーとしての機能があり、HAProxy自身がCookieを発行することができます。そしてCookieがセットされているかを判断することもできます。 また、HAProxy単体でABテスト振り分けに関する機能は実現可能なため、Front/backendのアプリケーションの種類は問われません。

インフラ構成図

ざっくりとした構成は以下です。各sideごとのサーバ台数はHAProxyのbackend定義により行えます。 f:id:tigger501st:20170315110949p:plain

ABテストパターンが反映されたブランチはab_sideにのみにデプロイされ、結果が芳しくない場合にはmergeせずに次のABテストに進むことが出来ます。

HAProxyによるABテスト振り分け

振り分けについてはHAProxyのACLとbackendの複数定義により行っています。

ACL(Access Control List)

公式ドキュメント HAProxyのACLは非常に柔軟なアクセスを振り分けることができます。 下記のように書くことでCookieを判断して振り分けるbackendを変更することが可能です。

    acl normal hdr_sub(cookie) ab-test-pattern=0
    acl ab-test-pattern hdr_sub(cookie) ab-test-pattern=1
    use_backend normal_side if normal
    use_backend ab_side if ab-test-pattern
    default_backend first_side

ab-test-patternというCookieが存在していない場合(初回アクセス時)には first_side に振り分けら、Cookieが存在しており && 値が1なら ab_sideへ、値が0なら normal_sideへ振り分けられます。

backendの定義

backendの定義は下記の様に行い、初回アクセス時にはCookieを挿入し、重み付け付きの振り分けに関してはweightで行います。この様にすればweightによってABテストサーバーに振り分けられたユーザはその証拠としてCookieの値として 1が書き込まれます。

下記の例ではnormal:AB = 75:25になっています。normalの確立 = ホスト名末尾が 0に行く確立の総和です。なので、設定を吐き出すときにはnormal_sideのweightは設定されている(normal_sideのホスト数)/(設定したいnormal側のweight)である必要があります。 デプロイされているアプリケーションのブランチが違うので、アプリケーション側で ab_side側かnormal_side側のアクセスなのかは判断する必要はありませんが、事故を防ぐ場合Cookieの値を参照することによりユーザがどちら側に抽選されたのかを判断することができます。 初回アクセス時にはCookieはセットされておらず、X-HOST-NAME を使用しHeaderからホスト名を参照しab_sideかnormal_sideかを判断することが出来ます。

backend first_side
    mode http
    cookie ab-test-pattern insert nocache
    balance roundrobin
    http-send-name-header X-HOST-NAME

    server web1-0 127.0.0.1:80 weight 25% check cookie 0 inter 1000 fall 2
    server web2-0 127.0.0.2:80 weight 25% check cookie 0 inter 1000 fall 2
    server web3-0 127.0.0.3:80 weight 25% check cookie 0 inter 1000 fall 2
    server web4-1 127.0.0.4:80 weight 25% check cookie 1 inter 1000 fall 2

あとはnormal_sideにはnormal側のホストのみを宣言すればnormalにしか行きません。 ただし、ab_sideにはnormal側のホストを含めて宣言しないと、万が一ABテストサーバ(今回の場合web4-1)が落ちていた場合に該当するホストが見つからずアクセスできなくなってしまうので注意が必要です。(check cookieをしている場合でもホストが落ちていると振り分けは行われず別のサーバへ振り分けられる)

以下が実際に吐き出されるファイルです。負荷などを気にする場合は適切にmaxconn等の設定を追記してください。ブランチ名はab-test-patternの想定です。 normal_sideのIPなどはRailsのconfigファイル内に配列で書いています。

global
    log         127.0.0.1 local2 info
    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     10000
    user        haproxy
    group       haproxy
    daemon
defaults
    log     global
    timeout connect 60s
    timeout server 1m
    timeout client 1m
    timeout check  5s

# web
listen frontside
    maxconn 4000
    fullconn 4000
    bind 0.0.0.0:80
    mode http
    acl normal hdr_sub(cookie) ab-test-pattern=0
    acl ab-test-pattern hdr_sub(cookie) ab-test-pattern=1
    use_backend normal_side if normal
    use_backend ab_side if ab-test-pattern
    default_backend first_side

backend first_side
    mode http
    cookie ab-test-pattern insert nocache
    balance roundrobin
    http-send-name-header X-HOST-NAME

    server web1-0 127.0.0.1:80 weight 25% check cookie 0 inter 1000 fall 2
    server web2-0 127.0.0.2:80 weight  25% check cookie 0 inter 1000 fall 2
    server web3-0 127.0.0.3:80 weight  25% check cookie 0 inter 1000 fall 2
    server web4-1 127.0.0.4:80 weight  25% check cookie 1 inter 1000 fall 2

backend normal_side
    mode http
    balance roundrobin

    server web1-0 127.0.0.1:80 check inter 1000 fall 2
    server web2-0 127.0.0.2:80 check inter 1000 fall 2
    server web3-0 127.0.0.3:80 check inter 1000 fall 2

backend ab_side
    mode http
    cookie ab-test-pattern insert nocache
    balance roundrobin

    server web1-0 127.0.0.1:80 check cookie 0 inter 1000 fall 2
    server web2-0 127.0.0.2:80 check cookie 0 inter 1000 fall 2
    server web3-0 127.0.0.3:80 check cookie 0 inter 1000 fall 2
    server web4-1 127.0.0.4:80 check cookie 1 inter 1000 fall 2

HAProxyの設定変更はRailsアプリケーションから、指定したブランチのdeployはcapistranoで実現しました。

処理の内容としては

  • HAProxyからab_sideを切り離したconfigをreload
  • 指定したブランチをデプロイ
  • 生成したHAProxyのconfigファイルを再読込

です。HAProxyはgraceful restartが可能なので、デプロイによる表側への影響はありません。

まとめ

実現したこと

  • 既存の構成を崩さない形での振り分け器の導入
  • HAProxyによる、Cookieを使用したアクセス先サーバーへの制限(再抽選防止)
  • HAProxyの設定ファイルを自動書き出し&deploy時読み込み

実現できていないこと

  • オートスケールの考慮
  • deployする毎にインスンタンスのIPが変わる場合の対処
  • 完全なmasterとの同期システム

Q&A

Q.ABテスト用にカラムを追加したいどうする?

A.現状、表側の変更のみを考えているため、考慮していなかった。おそらくdeployフローでmigrationを用意しておくことで対策。

Q.Masterとの同期はどの程度で考えていますか?

A.複数のテストが平行して走る際は、deploy時にmasterとの同期考えているが、それだけではおそらく不整合が出てしまうケースがあるので、考慮が必要。考えきれていなかった。