ありがとう、MYM 安らかに眠れ

  • このエントリーをはてなブックマークに追加
Yahoo! JAPAN Tech Advent Calendar 2018の2日目の記事です。一覧はこちら

ヤフーの社内では2011年のHackDayで開発されたMYMというチャットシステムが稼働しています。今では大多数の社員から日常的に利用されるシステムになりましたが、世の中の流れなどもありSlackへ移行することになりました()。この記事ではそんなMYMの紹介から始まり、思い出の尽きないMYMの実装を振り返りながら、チャットシステムの開発の面白さについて熱く語りたいと思います。

申し遅れましたが、Node.js 黒帯の栗山太希(@Ajido)です。得意なことはNode.jsを使ったシステム開発です!

MYMの紹介

技術的な話

特徴

MYM(Modern Yahoo Messenger)は一言で表すと Slack のようなチャットシステムです。チャットルームを作成してメッセージを送受信する基本的な機能に加え、それに付随するさまざまな機能が存在します。その中でも目に見えて分かるSlackとの違いとなると、下記のような要素が挙げられます。

  • ド派手な "いいね" エフェクト
  • JavaScriptを差し込み、さまざまな機能を加えることができる拡張機能
  • カスタマイズも可能な豊富なキーボードショートカット
  • 公式キャラクターのビルドインスタンプ

また稼働率の高さ・接続の速度などはMYMが優位な状況です。ただしこれはヤフーのインフラが社内に最適化できているからとも言えます。

さらに社内専用にチューニングされた独自の機能も多数存在します。例えば情報の持ち出しが禁止されているエリアからの閲覧時に適用されるReadOnly機能や、特殊な契約を考慮したアクセス制限機能などです。共有ファイルのパスをリンクに変換したり、誤投稿を防止するための拡張機能なども存在しています。

派手な "いいね" エフェクト

コメントの "いいね" ボタンを押すとコメント全体がくるりと回転します。この「くるり」はすべてのユーザー間で共有され、誰かが "いいね" すると全員の画面でコメントがくるりとします。裏側に人がいることを感じて欲しいという思いが、この16行のCSSに込められています。

.kururi {
  animation: kururi .75s 0s 1;
}
@keyframes kururi {
  0% {
    transform: scale(0.8) rotateY(90deg);
    -webkit-transform: perspective(800) scale(0.8) rotateY(90deg);
  }
  40% {
    transform: scale(0.8) rotateY(0deg);
    -webkit-transform: perspective(800) scale(0.8) rotateY(0deg);
  }
  100% {
    transform: scale(1.0);
  }
}

また "いいね" は「匿名」で「何度でも」押すことができます。この仕様の是非については社内でも議論が尽きません。どの仕様を選択しても一長一短があり、それならば盛り上がり過ぎてしまう方に振り切って、都度コミュニティで議論される状況こそが理想であるという方針になっています。

利用状況

月間の利用状況と総合的な利用状況を簡単に集計してみました。

集計項目
月間アクティブユーザー数 10,000
月間ファイルアップロード数 310,000
月間アクティブボット数 4,300
月間アクティブルーム数 26,000
月間メッセージ数 4,800,000
集計項目
ユーザーの総数 18,000
メッセージの総数 3.1億
ボットの総数 14,000
部屋の総数 150,000
ピーク時の同時接続数 15,000

特筆すべきはボット数かもしれません。ボットは何らかの操作を自動で行うために作られたプログラムで動作するアカウントなのですが、なんとそのボットの数(14,000)がユーザーの数(18,000)に迫りつつあります。MYMのボットは通常ひとつの部屋にリンクしているため、おそらく同じような機能を持ったボットが大多数だと思われますが、ボット数 14,000 はインパクトのある数字ですね。

興味深い部屋の数々

MYMはこの8年間で計150,000の部屋が生み出されました。業務系・技術系・雑談系を問わず、異彩を放つ部屋は数え切れないほどあります。ここでは数多くの部屋の中でも、ひときわ特徴的な部屋を紹介してみようと思います。

もちろんすべての部屋を把握しているわけではないため、あくまで把握している範囲内での紹介となることをご了承ください。

トピックス編集

Yahoo!ニュース トピックスの編集業務に活用されている業務系の部屋です。トピックス枠に表示される記事の選定・見出しの作成と校正・プッシュ通知の配信・災害情報の更新など、社会的にも重要な役割を担っています。

配信する記事の選定とレビュー・議論が24時間365日行われており、結果的にこの部屋はMYMの中でも最大のデータ量を誇っています。ボットの活用度も他の部屋と比べると群を抜いており、さまざまな工程がボットによって自動化されています。

アダルト画像

サービスに投稿された不適切な画像を検閲する業務系の部屋です。内製の学習モデルを利用したアダルト画像検出が行われており、ボットから報告された画像に対して人間が OK or NG の判定を行い返信することで、学習データが追加登録されていきます。

この部屋の未読を消化している最中に後ろに立たれた場合、業務規定違反を疑われても仕方がありません。要注意です。

ひとことフィード

MYMはプロフィール欄に「ひとこと」を入力する機能があります。これはSlackのステータス機能に近いものです。そのひとことに入力されたメッセージが投稿され続ける雑談系の部屋が「ひとことフィード」です。

メッセージの反映はリアルタイムではなく数分遅れ、文字数や改行などにもさまざまな制限が入る過酷な仕様なのですが、この部屋が登場してから「ひとこと」の投稿数は激増しました。どうやらこのフィードを利用したゆるいコミュニケーションが存在するようです。さまざまな仕様や制限によって生み出された疎結合な仕組みこそがポイントなのかもしれません。

asa1min

時事ネタの分析から、ビジネス、デザイン、法律、心理学、人材育成・マネジメント、果てはジョークネタまで、幅広い情報が共有される部屋です。この部屋は朝イチで社員が私見を添えつつ、気になるニュースを共有するところから始まりました。様々な情報とそれに対する見立てまで得られることもあってか、約3,000人が入室する社内でも注目の部屋です。

「ヤフーにとってこのニュースはこういう意味合いがある」といった話や、「今のヤフーにはこういう点が欠けているのでこうしよう」、「社会に貢献する会社になるためにはどうしたらよいか」など、新鮮で多種多様なニュースが投げ込まれ、常に議論が絶えません。

がん★大切な人のために

癌(がん)に関する情報が共有される部屋です。去年この部屋に投稿されたピロリ菌の話を見て、まさかと思い内視鏡検査に行ったのですが、見事ピロリ菌に感染していました。現在は無事に除菌できています。

このような普段は追う習慣がないけれど無関係ではない分野の情報を流してくれる部屋は大変貴重です。この部屋を作成した方は、重病・難病の検索結果の改善に取り組んでおり、サービスを通した改善だけではなく、ともに働く仲間のためにも情報を提供したいと思い立ち、部屋を作成されたそうです。

パパママサポート

子育て中のパパとママが集う交流場所です。

  • 習い事の送迎の時間が取れません。みなさんどうしていますか?
  • 子供がカブトムシを捕獲してきました。欲しい方がいたら差し上げます。
  • 卒園式の進行役になったのですが、出し物の案がでません...!

などなど、子育ての悩みや迷いを共有できる場として活用されています。多くの仲間や経験者に相談できる場所があるということは非常に心強いものです。この部屋は子育てをしながらも働きやすい環境づくりを目標とする有志の方々によって作成されました。

運用スタンス

MYMには特殊な運用方針がいくつか定められています。ここではそれらの一部を紹介したいと思います。

機能要望への対応方針

MYMに機能を追加する方法は、後述する拡張機能を使った方法と、MYMのソースコードにパッチを送り変更する方法の二種類が存在します。またそれとは別に要望室という部屋が用意されており、この部屋に要望や提案を投げ込むことで、それらの機能が実装されたりされなかったりします。

当然ですが、この部屋で要望されたすべての機能を実現することは不可能です。ですが要望に応えられない状態が長引くと、信頼は失われ要望すら出てこない状態になってしまいます。そのため「要望を出したら新しい機能が爆速で実装される」という信頼を得るために「10の要望がたまったら、そのうち1つは優先度を無視して即座に実装する」という方針がありました。要望の窓口をチケット管理ツールなどにせず、要望室という全員が閲覧可能なチャットルームで行い、対応状況を逐一報告しているのもこの方針の効果を最大化するためです。

もっとも今となっては要望のほとんどが簡単に実装できるものではなくなったため、これは初期の頃の方針でしたが、初期だったからこそ有用な方針だったとも言えます。

事故の定義

MYMのメインシステムは複数のデータセンターをまたいだ構成であり、30秒以内の自動フェイルオーバーに対応しています。例えば現在のMYMのマスターノードは東京周辺なのですが、もし東京周辺で何かあったとしても、30秒以内に自動復旧することを意味しています。

そしてMYMにおける事故の定義は30秒以上の停止であり、これはフェイルオーバーの見込み時間である30秒以内の停止を事故扱いにしても、再発を防止することが困難だからです。またMYMは計画的な停止時間を設けず、30秒以上の停止は例外なく再発防止が必要な案件として扱います。社内システムとはいえ、チャットシステムにおけるメッセージ送受信機能の停止は致命的すぎるからです。

少し話は逸れるのですが、MYMの開発当初はもちろん今のような構成ではなく、これまでに数え切れないほどの事故を経験した結果、今の構成に落ち着きました。

対応年 内容
2016年 ハードウェアの最適化
2015年 ネットワーク構成の最適化
2014年 拠点間の非同期レプリケーションに対応
2014年 国内の三拠点にサーバを設置

特に上記の最適化が完了するまでは頻繁に事故が発生していました。しかし2016年の最適化が完了してからは、メッセージの送受信を担うメインシステムでの長時間障害は発生しておらず、今年度も99.998%と比較的高い稼働率を維持することができています。

結果的にMYMは平常時の高い稼働率を目標としてこの構成に落ち着いたのですが、この構成は弊社における非常災害時の継続稼動と復旧の優先度を定義したガイドラインの "Tier1" に近い構成でもありました(下図参照)。非常災害時には衛星電話やメールの他、有力な社内連絡ツールとしてMYMも利用されているのですが、その理由はこのような経緯からも来ています。

Tier 概要
Tier0 人員の3交代制を想定した24時間365日の複数拠点運用体制
*下位のTier要件を継承
Tier1 ホットスタンバイの冗長サイトと5分以内の自動フェイルオーバー
2時間以内の対応と業務引き継ぎが可能な運用体制
*下位のTier要件を継承
Tier2 400km以上離れたクロスリージョンの冗長構成
24時間以内の対応と業務引き継ぎが可能な運用体制

データの第三者提供禁止

MYMの内部データはいかなる理由があれ、社内外の第三者には提供しないことを運用ルールとしています。これは例えば全員の会話履歴や他者の入室している部屋のリストを、何らかの学習データとして活用したいという依頼が来たとしても、データは一切提供しないということを意味します。自分自身、プライベートな書き込みを数え切れないほど行っていますので、第三者提供禁止は当然のルールといえます。

ただしコンプライアンス違反などの内部通報に関連する案件は、社内で定められた厳格な規定と手続きに則ったデータの開示要求が可能です(法務関係者に限る)。これは健全な組織を維持する優れた仕組みなのですが、JSONのような奇抜(?)なフォーマットでデータを受け渡しできないところが、技術者としては少し厄介なポイントでしょうか。

コンポーネント構成

下記の表に記載しているコンポーネントが、MYMを構成するすべての要素です。クライアントはさらに細分化できるのですが、ここでは割愛しています。この次のセクションから各コンポーネントの中でも特徴的な部分を紹介していくのですが、その全体像として参考にしてください。

コンポーネント名称 概要
mym-client MYMのクライアント実装。シングルページアプリケーション
mym-ios WebViewベースのiOSアプリ
mym-android WebViewベースのAndroidアプリ
mym-server メッセージの送受信を行うWebSocketサーバー
mym-apis APIトークンの発行とAPIの提供を担うシステム
mym-push モバイルアプリに対するプッシュ通知システム
mym-search メッセージの全文検索システム
mym-auth SSOとクライアント証明書認証を行うための認証プロキシ
mym-paster ファイルアップローダ
mym-peer ビデオチャットに利用するWebRTCのシグナリングサーバー
mym-infra 構成管理ツールのレシピ

チャットシステムを構成する特徴的な要素

チャットシステムを構成するさまざまなコンポーネントの中でも特徴のある要素をピックアップして、どのように実装しているか紹介したいと思います。

クライアントサイド

まずはチャットシステムにおけるクライアントサイドを構成する特徴的な要素を紹介したいと思います。下記項目の紹介も予定していたのですが、今回は書ききれなかったため、いつかまた機会があれば書いてみようと思います。

  • Workerスレッドを使った検索機能
  • 過去ログのスクロール制御
  • 既読処理
  • WebRTCを使ったビデオチャット
  • モバイルアプリ(iOS・Android)
  • 入力補完機能

ちなみにWebRTCを使ったビデオチャットは、チャットシステムの開発における各要素の中で、最も難易度の高い要素だと確信しています。MYMはもともとSFU型のビデオチャットを検討していたのですが、実装の難易度が非常に高く困難だったため、簡易実装のP2P型を採用しました。そのため複数人ビデオチャットの負荷が尋常ではないレベルなのですが、done is better than perfect を念頭にとにかくビデオチャットを提供してみたいという気持ちがありました。

Markdown記法

メッセージに特殊な記法を定義して、スタイルを適用したりリンクに変換したりする仕組みです。Markdown記法というタイトルにはしたものの一般的なMarkdown記法のすべてに従っているわけではありません。MYMでは replacer というhelper関数がこの機能を担っており、さまざまな記法が定義されています。

// 取り消し線
tag.add('strike', /~~([^\n\r]+?)~~/g, (matched, text) => {
  return '<del>' + util.escape(text) + '</del>';
});

// インラインコード
tag.add('inlineCode', /`([^\n\r]+?)`/g, (matched, text) => {
  return '<code>' + util.escape(text) + '</code>';
});

// TODO リスト
tag.add('todo', /^( *- ?)(\[[ X]\])(.*)$/gm, (matched, before, state, after, pos, all, index) => {
  return before + '<a class="todo-checkbox" data-index="' + index + '" href="javascript:void(0)">' + state + '</a>' + util.escape(after);
});

// 引用
tag.add('quote', /^((?:> )+)([^\n\r]*)/g, (matched, head, content) => {
  const level = new Array(head.length / 2 + 1);
  return level.join('<blockquote>') + util.escape(content) + level.join('</blockquote>');
});

記法の識別子・正規表現・変換のロジックをそれぞれ設定することで記法を定義していきます。変換の順序はまず正規表現に対応するマーカーを設置し、その後HTMLに変換する下記のような流れになっています。

// 1. プレーンテキスト
"Node.jsは初手`async/await`"

// 2. マーキング処理
"Node.jsは初手\0inlineCode0\0"

// 3. HTML変換処理
"Node.jsは初手<code>async/await</code>"

このプロセスを踏むことで、変換済みのテキストに対して他の変換処理が被ることを避けています。ただ引用とインラインコードのように、それぞれの記法は併用される可能性もあります。この場合は変換の優先順位を考慮する必要があるため、実際はもう少し複雑な処理が組み込まれています。

しかしながら replacer はXSSの温床にもなっており、正規表現での定義も複雑すぎるため、少なくともMarkdownに関しては何かしら変換ライブラリを使った方が良かったと今更ながら感じています。

キーボードショートカット

さまざまな操作をキーボードから行うためのショートカットが定義されています。これは keybindManager というオブジェクトが担っており、ショートカットは下記のように定義されています。

// 最初のメッセージに移動
commands.g = {};
commands.g.g = () => {
  room.focusComment('first');
};

// 最後のメッセージに移動
commands.G = () => {
  room.focusComment('last');
};

// 前のメッセージを選択
commands['<up>'] = () => {
  room.focusComment('prev');
};

// 次のメッセージを選択
commands['<down>'] = () => {
  room.focusComment('next');
};

// 次の部屋に移動
commands['<S-down>'] = () => {
  list.focusList('next');
};

// 前の部屋に移動
commands['<S-up>'] = () => {
  list.focusList('prev');
};

keybindManager.commands のプロパティにショートカットキーとそのショートカットを実行した時の処理を定義します。この機能の開発者は重度のEmacsユーザーなのですが、Emacsライクにキーバインドを設定できるようにしている部分にこだわりを感じます。また commands.g.g ように連続したキーバインドを美しく設定できるところもすてきです。

let timer = null;
let target = keybindManager.commands;

document.addEventListener('keydown', (event) => {
  const type = keybindManager.convert(event);
  clearTimeout(timer);

  if (typeof target[type] === 'function') {
    const ret = target[type](event);
    if (ret !== false) {
      event.preventDefault();
      event.stopPropagation();
      target = keybindManager.commands;
    }
  } else {
    target = target[type];
    timer = setTimeout(() => { target = keybindManager.commands; }, 1000);
  }
});

設定したキーボードショートカットは keydown イベントに対応し、上記のような関数(擬似コード)を通して処理されます。この keybindManager は後述する拡張機能からも参照できるため、拡張機能を通して好きなキーボードショートカットを追加できるようになっています。

拡張機能

スクリプトファイルを差し込み、MYMの機能を拡張する仕組みです。公式にメンテナンスされている「公式拡張」と、社内の有志が開発した「野良拡張」が存在します。

公式拡張は一覧画面から有効にすると動き始め、野良拡張はスクリプトファイルを登録することで動き始めます。MYMは設定画面の項目数を最小限に抑えて、初心者でも簡単に扱えるようにしているので、上級者向けの機能は基本的に設定ではなく公式拡張として提供されています。一方、野良拡張は自分好みのCSSを適用してスタイルを変更するなど、個人に合わせた独特な拡張が多く見られます。

ここからは特徴的な公式拡張をいくつか紹介していこうと思います。

ライブプレビュー

入力中のメッセージのプレビューを表示する公式拡張です。MYMは当初シンプルな記法しかありませんでしたが、今ではさまざまな記法が追加されています。その結果、プレーンテキストから投稿後の表示を予測できなくなってきたため、入力中のテキストがどのような投稿結果になるかをリアルタイムに表示する拡張が生まれました。

ここ全社ですよ・@allして大丈夫?

MYMにはすべてのユーザーが入室している「全社」という部屋が存在します。これはSlackでいう #general チャンネルにあたり、現在約18,000人の入室者がいます。ここで紹介する拡張は主に誤投稿を抑制するためにあり、全社部屋のような人数の多い部屋では特に有効な機能です。例えば部屋の入室者全員にプッシュ通知を飛ばす強力なコマンドの @all を利用するとき、事前に投稿が間違いではないか確認する画面を出してくれたりします。

汎用入力補完

これは特定の文字列のエイリアスを設定できる拡張機能です。MYMにあるアカウント名や絵文字のサジェスト機能を拡張するものであり、有志が作成してパッチを送ることで公式に取り込まれた拡張でもあります。このエイリアスは部屋に対応する設定と個人に対応する設定があり、上の画像のように部屋の主要なメンバーを呼び出す機能としても利用できるほか、任意の文字列を呼び出す便利機能としても利用できます。

サーバーサイド

ここからはチャットシステムにおけるサーバーサイドを構成する特徴的な要素を紹介したいと思います。下記項目の紹介も予定していたのですが、今回は書ききれなかったため、いつかまた機会があれば書いてみようと思います。

  • Identicon
  • 認証プロキシ(SSO・クライアント証明書認証)
  • API
  • プッシュ通知(APNs・FCM)
  • 冗長性の確保
  • 部屋のアクセスコントロール
  • サムネイラ

WebSocket

WebSocketはウェブサーバーとブラウザの双方向通信を実現するための通信規格です。今やチャットシステムの構築には欠かせない技術と言っても良いかもしれません。

WebSocketを利用する上で最大の難所は、スケール性能の確保の難しさにあります。よくあるオンラインゲームのように完全にサーバーを分割して、サーバーごとに接続数の上限を設定し、それぞれのサーバー間で一緒に遊ぶことはできないという仕組みにする場合は問題ないのですが、全員が同じ空間でチャットするシステムを構築するためには、受信したメッセージを複数のサーバーやプロセスに伝搬させるシステムが必要です。

これには一般的にPub/Subモデルのメッセージングシステムが利用されるのですが、そこにもまたスケール性能の問題がつきまといます。このスケール性能だけに焦点を当てた場合、最良の選択肢は多段のブローカーが構築できるメッセージキューなのですが、運用コストの肥大化は覚悟しなければなりません。結果的にMYMでは当時下記システムをそれぞれ比較して、必要十分なメッセージの流量が処理でき、キューがあるため配信も保証できて、実装も簡単だった Tailable Cursor を採用しました。

  • Redis
  • ZeroMQ
  • RabbitMQ
  • Kafka
  • MongoDB Tailable Cursor

もし今から再実装するとしたら、スケール性能重視の場合は Pulsar を、そうでないなら最近追加された Redis Streams も良さそうだと考えています。また機能比較をしてみないとですね。気付いたらWebSocketから大きく話が逸れてしまいました。

ファイルアップローダ

ドラッグ&ドロップされたファイルをアップロードして、そのファイルにアクセスするための一意のURLを発行するファイルアップローダです。

MYMのサブコンポーネントとして提供されており、Paster と呼ばれています。アクセス制御はなく、そのURLを知っている人なら誰でもアクセスできるというPastebinに似た仕組みです。ただしアップロードしたファイルの削除はアップロードした本人しかできません。

このようなシステムで冗長構成を組むためには、アップロードされたファイルを一元管理するストレージが必要です。例えばWebDAVなどを利用して強引にローカルファイルシステムを活用する方法もありますが、可用性や実装コストを考慮するなら、やはりおすすめはパブリッククラウドのオブジェクトストレージです。ヤフーにはS3互換の内製オブジェクトストレージ Dragon がありますので、MYMでもこちらを積極的に活用しています。

実装も非常にシンプルでして、アップロードされたファイルをそのまま putObject または getObject しています。Node.js を利用しているため、可能な限り streamObject を使ってメモリを節約したり、削除の時の本人確認にはオブジェクトの Metadata を利用している部分などがポイントでしょうか。

また個人的に面白いポイントといえば、一意なURLを発行する時のロジックが挙げられます。具体的にはユニークなIDを発行する仕組みと、IDを短く圧縮する仕組みのふたつを指すのですが、下記はそういったIDの生成または圧縮のロジック例の一覧です。

はじめて一意のIDを生み出す方法を見たときはなるほどと感動しました。上記のような衝突確率が0のロジックもあれば、0ではないロジックもあります。short puid のような異質なロジックも存在します。圧縮に関しては基数の変更がベースになりますが、そこにもhashidsのような English curse words を回避する仕組みが存在していたり、この分野は何かと知的好奇心を揺さぶってきます。

メッセージの全文検索にはElasticsearchを活用しています。MYMでは形態素解析のインデックスと 3-Gram のインデックスを併用しており、通常は形態素解析のインデックスを利用、検索クエリのキーワードがすべて3文字以上の場合は N-Gram インデックスをOR条件で利用しています。またスコアリングは行っておらず、時系列でデータを出力しています。

この構成は当時、下記要素のバランスを考えながら選びました。要件は再現率を重視するがノイズも減らしたい。少ない文字数での検索も可能で、検索の速度もそこそこ速いというものでした。インデックスのサイズと書き込みコストはある程度なら捨てても良い要素でしたので、それらのデメリットは受け入れることにしました。

優先度 要素
1 検索漏れが少ない(再現率が高い)
1 少ない文字数での検索が可能
2 ノイズが少ない
2 検索の速度が速い
3 辞書データなどのメンテナンスコストが少ない
4 インデックスのサイズが小さい
5 書き込みのコストが少ない

フィルタには一般的な正規化の他に、アカウントIDをフルネームに変換してソースに追加する処理を加えています。これはIDよりも実名の方が検索クエリとして利用されることが多かったからです。

ちなみにSlackの検証を開始した際に、社内でも特に不満の多かった機能が日本語検索の精度でした。そこでSlackの検索を担当しているエンジニアと日本語の検索精度について議論したところ、ひと月後(2018年06月)にはMYMの検索に似たロジックを取り込んでもらうことができました。Slackのエンジニアのスピード感に驚き、Slackが世界的に流行った理由を垣間見た気がしました。

未読数の一覧

入室している部屋の未読数を表示することは、チャットシステムでは普通の機能だと考える人は多いのではないでしょうか。ですがこの機能の実装はなかなか困難です。もっとも簡単な実装は毎回 COUNT を行うことですが、確実にそこがボトルネックになります。10,000人がそれぞれ50部屋に入室している場合、サーバーの再起動直後などで同時接続が発生すると、瞬間的に500,000回の条件付きカウントを行うことになります。まさにMYMでもこの状況が発生してしまい、朝の10時(ヤフーのコアタイムの開始時間)にMYMが大幅に遅延するという状態に陥ったため、以下のような対策が考えられました。

  • サマリーテーブルを作成する
  • カウント結果をキャッシュする
  • 部屋別の最終更新日時と最終既読日時をキャッシュする
  • 未読数を遅延表示させる
  • コアタイムを撤廃する

サマリーテーブル案はメッセージの書き込み時に、利用者ごとの未読数をインクリメントするという方法ですが、未読数の表示は高速になるものの、書き込みのコストが高くなるというデメリットが存在します。とはいえこれは最適解に近い案ですので、対策できなかった場合の最終手段としました。メールのようにデータを受信者全員に複製配布する方法もこれと同じです。またカウント結果をキャッシュする案は「未読数が正しかったり正しくなかったりするのは論外」ということで却下に、同様に未読数の遅延表示も不便なので却下となりました。

結果的に部屋別の最終更新日時と既読日時をキャッシュする案を試すことになったのですが、これは最終既読日時と最終更新日時を比較することで、カウントする必要がない部屋はカウント処理を行わず 0 を返すという仕組みです。応急処理に近い対策ですが、この仕組みのおかげでカウントクエリは劇的に減り、機能を損なわないままパフォーマンスを維持できるようになりました。

アンケート

MYMには未読数の一覧表示に限らず、局所的な最適化が随所に施されています。例えば入室者同士で投票を行うアンケート機能があるのですが、この機能は数百人から数千人が瞬間的に同時投票することがあり得ます。

わかりやすい事例として、弊社では最近ワイキューというサービスが始まりました。
https://twitter.com/waiq_yahoojp

社内では先行してこのサービスのユーザーテストをしていたのですが、そのテストにはMYMのアンケート機能が利用されました。これは選択式のアンケートを利用したクイズに10秒以内に答えていくという形式だったため、1,000人近い社員が同時に投票することになりました。

これをそのまま処理してしまうと大量のメッセージが瞬間的にブラウザに送り込まれ、ブラウザが固まってしまいます。そのためMYMのアンケートには流量をコントロールする3秒間のタイマーが設けられており、1,000人の同時投票が行われてもその3秒後に1回だけ1,000人の投票結果がまとめられたメッセージが送られるという仕組みになっています。

チャットシステムの開発者の立場としては、急激な負荷にクイズとは違う意味でハラハラしましたが、こういったテストをこれだけの規模で行えるのはヤフーならではだなぁと感じたイベントでした。

インシデントから学ぶ設計の失敗

ここでは設計の失敗を痛感した特徴的なインシデントをふたつ紹介したいと思います。

うおぽぉ事件(2013)

ある日を境にMYMが突然遅延するという障害が発生し始めました。遅延は数秒で解消するのですが、不定期に再発します。ログを確認してみると、MYMの遅延に合わせてログ出力まで停止していることが分かりました。

停止する前のログ出力を確認してみると、ある投稿に「いいね」されています。しばらく経過を観察すると、どうやらその投稿が「いいね」されるとMYMが停止しているようでした。どのような投稿なのか調べてみると、その投稿は「うおぽぉ」というよく分からない部屋にありました。この部屋にはひとつのアスキーアートが投稿されており、その投稿が何故か「いいね」され続けていました。

その投稿のいいね数は 76,812 と当時の他の投稿と比べても群を抜いて多く、桁違いのいいね数でした。遅延の原因がわかった開発者はそっとMYMのプログラムに手を入れ、いいねのレスポンスをArrayから { length: 76812 } に変換して、問題は無事に解消されました...

ここまでが当時の事の顛末なのですが、これはNode.jsの仕組みに依存した問題でもあります。Node.jsではJSONオブジェクトのシリアライズとデシリアライズは基本的に同期処理であり、同期処理はNode.jsのパフォーマンスを維持する要であるイベントループを停止させます。小さなデータで短時間の停止なら問題ないのですが、いいね数 76,812 の投稿は { iine: ['userid', 'userid', 'userid' ...] } のようなデータとなり、非常に巨大なJSONです。

つまりMYMの遅延はうおぽぉの投稿が「いいね」された時のJSONオブジェクトのシリアライズで発生していました。この失敗から学べることは「時間経過やサービスの成長などに伴い、扱うJSON(ワンショットのデータ)が肥大化するような設計にしてはいけない」ということになるかと思います。これは非常に一般的な話でもありますが、Node.jsではその特殊なアーキテクチャの都合上、悪影響のダメージが劇的なので特に注意しなければなりません。

過半数拠点の同時障害(2018)

MYMのデータベースは過半数のサーバーがダウンすると完全に停止します。3拠点でそれぞれ1台、計3台のクラスタを構築していますので、同時ダウン許容量は1となり過半数は2です。下記リンクにある Three-member Replica SetThree data centers と同等の構成です。

ここで説明するインシデントはダウン許容量の限界である1を超えた、3拠点のうち2拠点が同時にダウンした衝撃的な事件でした。主な原因はある特定のリージョンが定期的に通信不可となり、頻繁にレプリケーションが解除される状態を放置していたところにあります。発生するたびに人間がレプリケーションを再開させる運用がしばらく続いていたのですが、この日は別のリージョンのサーバーの故障と、この特定リージョンのレプリケーション解除が重なってしまい、結果的に過半数がダウンした状態となりました。

インシデントの解消にはレプリケーションクラスタを復旧するしかないのですが、サーバーを用意することは簡単でもデータの同期には非常に時間がかかります。そのためこの時はリーダー選出の投票権のみを持ち、データを持たないArbiterノードを急遽増設して強引にレプリカセットを再構築しました。レプリカセットの再構築は手に汗握るコマンドの連続です。

その後、故障したサーバーが復旧して、無事元の状態に戻り事なきを得たのですが、この失敗から学んだことは「問題の根本的な解決から逃げてはいけない」という当然のことでした...。また設計に関しても3拠点でそれぞれ1台はギリギリを攻めすぎていたと感じています。最初は最小限の3台構成で、実際に過半数ダウンが発生したら5台のクラスタに昇格させようと考えていたのですが、半ば人為的とはいえ本当に発生するとは思っておらず完全に油断していました。

チャットシステムの開発を通して

チャットシステムは最初、たった数行のコードで目に見えて動くものが完成します。刺激的ですよね。そこに欲しいと思った機能を付け足していってみると、信じられないほど多くの課題があることに気付きます。ですがそれらの課題は大抵世の中で既に論じられおり、問題に関する情報が体系立てられて説明されています。そんな情報を取り込み、試すことで、チャットシステムは目に見えてアップグレードされていきます!

学習した情報は自らの知識にもなり、他のシステムを開発する際にも、より効率的に要領よく開発ができること間違いなしです。チャットという誰もが触れたことのある分野での開発には親近感が湧き、そんな中で生まれる課題には解決の必要性を感じるはずです。必要性を感じる学習は楽しさが桁違いです。

もしウェブシステムを開発したいけど何から始めたら良いか分からないという方がいたら、チャットシステムを開発してみてはどうでしょうか? それはおもしろいものだと自信を持っておすすめできます。ただこの記事を読んでいる方にそんな人はいないかもしれません。だからもし周りにそういう人がいたら勧めてみてください。チャットシステムの開発は学習材料として最適ですよ、と。

最後に

ここまで読んでいただいた方も飛ばしてきた方もありがとうございます。これまでMYMは新しい機能を開発したり、パフォーマンスを改善したりするばかりで、振り返りというものは初めてした気がします。

システムを構成する特徴的な要素も、これまでに発生したインシデントもまだまだ書ききれないほどあるのですが、今回はここで一区切りにしたいと思います。

MYMの実装当時は部門を超えてひとつのシステムを改善するような文化はあまり見られませんでした。ですが今ではさまざまな人が機能追加やバグ修正を行い、システムを改善していく文化が根付いてきたように感じます。MYMを通して気付くことができたそんな文化が僕は気に入っているので、またいつかOSSなどでチャットシステムを作成してみたいですね!


※ ヤフーでの全社導入のお知らせ|Slack Japan株式会社のプレスリリース

※ 企業向けツールにも関わらず、アクティブユーザーは800万人超 Slackがコミュニケーションハブとして愛される理由

※ 「Slack」を全社導入、社内コミュニケーションの活性化狙うヤフーの取り組み


TechBlog事務局推薦者

Yahoo! JAPANでは情報技術を駆使して人々や社会の課題を一緒に解決していける方を募集しています。詳しくは採用情報をご覧ください。

  • このエントリーをはてなブックマークに追加