開発基盤チームが目指す事 #pixiv_night

（当日はesaのプレゼンテーションモードで発表しました）

（[pixiv night in Fukuoka #02 - ピクシブを取り巻く技術がわかる一夜！ - connpass](https://pixiv.connpass.com/event/52116/) の発表資料です）

## 自己紹介

  * 各種SNSをcatatsuyでやっている
    * かたついと呼ばれることが多い
  * ピクシブ株式会社で開発基盤チームと広告チームの兼任
    * 2014年度新卒（2013/10入社）
    * pixivの__技術的な改善__が主な業務（後で詳しく）
  * 単著『pixivエンジニアが教えるプログラミング入門（星海社新書）
    * ピクシブ社内の非エンジニア向けのプログラミング研修の書籍化
  * pixiv社内ISUCONやISUCON6本選の問題作成

## pixivのチーム分け

  * pixivというサービスは巨大
    * `www.pixiv.net`/`touch.pixiv.net`/スマートフォン用APIなどなど
    * 提供しているサービスも多い
  * pixivという1つのサービスに対して複数のチームが存在
    * チーム分けは定期的に変わる
    * 現在は投稿体験・web閲覧・アプリ閲覧・プレミアムなどで分けている
      * これらのチームはざっくり言うと『担当しているユーザー視点でのサービスを改善して伸ばす』のが目標

## pixiv.git

  * pixivのソースコードを管理しているgitリポジトリはpixiv.gitという1つのリポジトリ
  * pixiv.gitにはpixivだけではなく、社内の様々なサービスが入っている
    * sensei/pixivision/pixivFANBOX（pixivの一機能）などなど
    * 複数のチームがpixiv.gitに対してコミットし、デプロイを行っている
    * 共通して扱える処理はpixiv-libディレクトリ以下にまとめられている
  * ログイン用のaccounts.pixiv.net、OAuth、外部公開用のAPI、スマートフォン用APIなどもある
    * BOOTH/pixivFACTORY/pixivSketchなど社内のサービスでもOAuthやAPIで認証・pixivとユーザーデータなどのやり取りをしている
    * pixivコミックも別の仕組みでpixivとユーザーデータなどのやり取りをしている
  * サービスに直接関係せず、技術的な観点でpixiv.gitや、pixivというサービス、pixiv以外の社内の様々なサービスを改善するチームが必要
    * 開発基盤チームの必要性

## 開発基盤チームの目標

  * 目標一例
    * 機械学習
    * 検索・おすすめ改善
    * pixivのHTTPS化
    * pixivが使用するミドルウェア刷新・PHPのバージョンアップ
      * Kyoto TycoonからRedisへの移行（後で詳しく）
      * PHP7化
    * 開発効率の改善
  * ざっくり言うと技術的な観点でサービスを改善していくチーム
    * とはいえ開発基盤チームだけで全部やるには量が多すぎるので、一部のタスクは他のチームとも協力しながら分担している
    * インフラチームとの協力が不可欠な作業がかなり多いため、インフラチームとの関係を密にしている

## 開発基盤チームの仕事内容

  * 具体的で生々しい情報の方が面白いと思うので、実際に自分がやった（ている）ことを話します
  * あくまでも仕事内容の一例です

## KT廃止の機運

  * pixivではキャッシュとしてKyoto Tycoon(以下KT)をずっと使っていた
    * memcachedプロトコルでマルチマスターレプリケーションが可能
    * オンメモリのmemcachedと違い、永続データを扱えるので再起動してもデータが消えない
    * pixivはキャッシュが消えるとまともに動作しない箇所がかなり多い（！）
      * キャッシュに頼りすぎるのはWebアプリケーションとして非常にまずい
         * とはいえやりがちな構成
      * pixiv自体が膨大な量のキャッシュに依存しているので今回修正は見送り
      * 個人的にはいずれ開発基盤チームとしてキャッシュの使い方改善はやりたいタスクの1つ
      * 現状の構成ではキャッシュをオンメモリのmemcachedに保存すると、memcachedが死んだときに長時間復旧が出来なくなる
      * pixivのキャッシュサーバーとしては永続データ・レプリケーションで複数台構成に出来ることが必須
  * KTがpixivの規模に耐えられなくなってきた
    * KT内部のGCに時間がかかる
    * KT内のデータが何故か壊れて、pixivが障害になることが何回かあった
      * KT自体のメンテナンスが停止しているため、修正される見込みがない
      * 自分たちでメンテナンスするよりも、他のKVSに移行した方が未来がありそう
    * 早急にKT以外のキャッシュサーバーに移行する必要性を認識

## memcachedプロトコルとPHP

  * memcachedプロトコルを扱うpeclモジュールはmemcachedとmemcacheの2つある
  * pixivではmemcacheモジュールを使ってきた
  * memcacheは2013-04-07から更新がない
    * PHP7に未対応
  * memcachedモジュールはPHP7に対応
  * PHP7化に向けてmemcacheモジュールを廃止する必要がある
    * memcachedモジュールへの単純な置き換えでは対応が出来ない
    * memcachedプロトコルはコマンド名・キー・flag・expire time・バイト数を指定する
      * `<command name> <key> <flags> <exptime> <bytes>`
    * flagの値はアプリケーションによって決められる
      * memcache・memcachedモジュールは圧縮形式などをflagの値から判断している
      * flagの対応に互換性が無いため、単純な置き換えでは正しく動作しない
      * 並行して新しいキャッシュサーバーにもsetするようにして置き換えていくしかない
      * 2度手間になるのでKTからの移行とmemcachedモジュールへの移行を同時にやりたい

## KTからの移行先

  * 当初はMySQL InnoDB memcached pluginが有力候補だった
    * memcachedプロトコルが使える
    * 社内に大規模MySQL運用の知見があり、MySQLに統一できるのは運用上のメリットがある
    * レプリケーションやデータの永続化など求めている機能が全て揃っていた
  * パフォーマンス面・安定性などをインフラ部で検証
    * パフォーマンス面はMySQL5.7系を使い、my.cnfの設定次第で問題が無くなりそうだった
    * 安定性についての問題が解決できず、MySQL InnoDB memcached pluginの導入を断念
  * Redisへの移行が決定
    * レプリケーション・永続データなど必要な機能が揃っている
      * プロトコルは独自で、単純なKVS以上に様々なデータ型やコマンドが使える
    * pixivのソースコードの変更量を考えると、memcachedプロトコルにこだわる理由が薄い
      * 並列で2つのキャッシュサーバーにsetする必要があり、peclモジュールも変更する必要がある
      * 今回Redis移行しても作業量として大差がないと判断
        * 移るモジュールがphpredisかmemcachedかの違い
    * Redisは社内のRailsプロジェクトで採用事例が多く、検証の結果、パフォーマンス面・安定性にも問題が無かった
      * アプリケーションサーバーとKTのコネクションを減らすために使っていたtwemproxyはRedisにも対応していた
      * ただしtwemproxyで使えるRedisのコマンドには制限がある
        * memcachedプロトコルでも出来るレベルのことしかしないのでとりあえず問題なし

## KTからRedisへの移行

  * pixivではキャッシュにしか存在しないユーザーデータが存在（！）
    * これは__キャッシュではない__
    * Redis移行の前に全てのユーザーデータをMySQLへ移行
  * [pixivのデータストア/キャッシュ戦略 その2 - pixiv inside](http://inside.pixiv.net/entry/2014/02/27/001022)
    * 古い記事だが、大筋は変わってない
    * KVSClientを通してキャッシュのキー名からsetするサーバーを指定する
    * KVSClient側でKTにsetする際にRedisにもsetするように
    * KVSClientを使わずにKTにsetするコードもあったので、getする際にKTとRedis両方からgetし、値が食い違っていたらログに流すように
      * fluentd経由でMongoDBにログを集めて、社内のログビューワーで見れるようなログ収集基盤があるのでそれを使用
      * ひたすら抜け漏れを探して修正

## phpのRedisクライアントのphpredisモジュールのシリアライズについて

``` php
$redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);   // don't serialize data
$redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);    // use built-in serialize/unserialize
```

  * シリアライズオプションについて
    * デフォルトは`Redis::SERIALIZER_NONE`
      * 全て文字列に変換されてSETされる
      * 配列はArrayという文字列になる
    * `Redis::SERIALIZER_PHP`
      * 数値1は`i:1;`というPHP組み込みのserialize関数の返り値が入る
      * 文字列も同様に`s:4:"abcd";`のようになる
      * getする際に自動でdeserialize関数を実行する
    * Redis::SERIALIZER_NONEとRedis::SERIALIZER_PHPを混ぜる
      * Redis::SERIALIZER_PHPでsetした後に、Redis::SERIALIZER_NONEでgetするとserializeの返り値の文字列が返ってくる
      * Redis::SERIALIZER_NONEでsetした後に、Redis::SERIALIZER_PHPでgetすると文字列として取得できる
        * 誤作動するパターンは見つけられなかった
  * pixivでは配列（PHPなので連想配列を含む）をシリアライズせずにsetしている箇所が大量にあった
    * ライブラリ内部でset時にシリアライズ、get時にデシリアライズする挙動に頼っていた
  * pixivのコードではKVSClient経由でKTへのコネクションを使い回せるようになっている
    * PHPなので1リクエストの間で使い回される
    * シリアライズのオプションをリクエスト中に切り替えるのは予期しない事故につながりうる
    * 常に`Redis::SERIALIZER_PHP`を使うことにした

## 起こった問題

  * memcachedとRedisにはインクリメントするコマンドがある
    * キーが存在しない場合の扱いが異なる
    * memcached
      * `the item must already exist for incr/decr to work; these commands won't pretend that a non-existent key exists with value 0; instead, they will fail.`
      * 既に存在するアイテムに対してしか使えない。__存在しない場合、値に0が入っているキーとして振る舞うことはない__。代わりに失敗する。
      * https://github.com/memcached/memcached/blob/master/doc/protocol.txt
    * redis
      * `If the key does not exist, it is set to 0 before performing the operation.`
      * キーが存在していなければ、__そのオペレーションを実行する前に0をセットする__
      * https://redis.io/commands/incr
  * これまでのコードはmemcachedプロトコルを意識していたため、インクリメントしたいキーで存在していない場合は0をsetするコードになっていた
    * `Redis::SERIALIZER_PHP`で0をsetすると`i:0;`になる
    * インクリメントできるのは数値として解釈できる文字列がvalueに入っている場合のみ、なので`i:0;`はインクリメントできない
    * 返り値はphpredisのドキュメントにはINTが返るとあるが、インクリメントに失敗したらfalseが返ってくる
      * 接続に失敗するなど致命的な問題が起こった場合は例外が飛ぶ
      * 特に例外が飛ぶわけではなかったので、動いていないことに気付くのが遅れた
  * pixivとピクシブ百科事典は別リポジトリだが、pixivからsetした値をピクシブ百科事典のリポジトリから参照していたため、動きを追うのが難しくなっていて、更に気付くのが遅れた
  * `Redis::SERIALIZER_NONE`にして0をsetすればインクリメントできるが、前述の理由から途中でシリアライズオプションを変更したくない
    * 今は0をsetする場合はdelete、0以外の値をsetする場合はdeleteしてからincrByでその数値を指定している
    * 事故防止のため、KVSClient側でインクリメント専用のキーにはincrementとdeleteしかできないように、インクリメントしないキーにはインクリメントできないように変更

## KT移行の進捗

  * 負荷が大きく、問題が起こっていたキャッシュ用途のKTはRedisに移行したが、セッションなどまだKTを使っている部分は多い
    * これから移行したい

## HTTPS化

  * 現在Redis化よりも、pixivのHTTPS化を最優先にしている
  * 近日中にtouchがフルHTTPS化する予定
    * www(PC版)はもう少し待ってください…
  * これについても様々な挑戦中
    * まだ途中なのでこれについては次の機会で！

## 最後に

  * pixivというサービスは歴史があり、多くのユーザーを抱えるため、技術的に解決しなければならない問題も多い
    * 今回紹介した事例はほんの一例です
  * 技術面でサービスを支えることに興味を持ってもらえれば幸いです