Mirrativ tech blog

株式会社ミラティブの開発者(サーバサイドエンジニア,iOSエンジニア,Androidエンジニア,機械学習エンジニア,インフラエンジニア, etc...)によるブログです

ミラティブサーバチームで行っている障害振り返りを紹介します!!

こんにちは、サーバエンジニアの夏(なつ)です。今回はミラティブのサーバチームで行っている障害振り返りを紹介したいと思います。

ミラティブのサーバチームではサービスに障害が発生した場合、その後、担当者を決めて障害の振り返りのたたき台を作成し、チーム内で振り返りを行って、今後の改善に活かす努力を続けています。

今回はその振り返りの目的やフォーマット・注意点についてお話したいと思います。

目的

システムを運用していれば障害はつきものです。ましてや改善を続けるならば、その代償として不確実性が障害として表面化し、放置していけば徐々にユーザの信頼を失っていくことになります。かといって、障害の防止にコストをかければかけるほど、費用対効果は見合わなくなり、障害を絶対に起こしてはならないという心理的圧力はメンバーのメンタルを擦り減らしていきます。そのため障害の振り返りでは障害の詳細や原因をチームメンバーと共有しつつ、本来価値提供したいことに対して、障害への対応コストを妥当な範囲に収めるためにはどうしたらいいかが議論されます。

f:id:hottestseason:20210402035914p:plain

また、チームで振り返ることにより、個人のミスをチームの経験へと昇華することができます。とくに障害の引き金を引いてしまったのが自分であれば、不注意や確認不足を悔やみ冷静に振り返り辛いのは当然ですし、現実から目を背けてしまいたくなる場合も多々あります。チームに共有することで、異なるバックグラウンドをもつメンバーから建設的なアドバイスを聞けるかもしれません。組織全体を1つのシステム、個人のミスもシステム内で一定の頻度で発生する事象として捉えることで、障害振り返りを通して組織の強化を続ける努力をしましょう。

振り返りフォーマット

ミラティブのサーバチームでは障害の振り返りとして「影響」「背景」「発生・復旧フロー」「原因」「被害の最小化・再発防止策」をまとめてConfluenceに蓄積しています。

どの情報も他の人が再現確認できるよう、エビデンスとセットで書くように意識しています。 (Slackのリンク、調査時のコマンドや出力結果、監視画面のリンクやスクリーンショットなど)

影響

発生日時や影響を受けた機能・対象者を書いておきます。

背景

サーバチームを対象に、障害を理解する上で必要なドメイン知識を共有します。エンジニア以外にも伝わるように書く必要はありませんが、他事業部や歴史的経緯を知らないエンジニアにもこの後の章を理解できるように説明する必要があります。

発生・復旧フロー

何がどう作用して障害を引き起こし、その後どう検知され、どう作業して障害が復旧したのかを時系列に沿って書きます。 障害を検知したチャネルを明確にすることで、そのチャネルの透明性を評価できます。

  • 前兆が見つかってから、調査開始されるまでのチャネルに不要な伝言ゲームが起きていないだろうか
  • 有用そうなチャネル上のノイズが多く、無意識にミュートされていないだろうか
  • 今後そのチャネルの優先順位を上げるべきだろうか

また、復旧フローを時系列に沿って書くことで、どこの対応に時間がかかったかが把握でき、支配的かを洗い出すことで復旧時間短縮に向けて優先順位を決めることができます。

原因

どういう因果関係で障害が発生したのかを詳細に書きます。

被害の最小化・再発防止策

発生・復旧フローや原因をもとに、今後組織として再発防止できるような仕組み化や、早期発見や原因の解像度を向上できるような仕組みを検討します。

障害の芽はできるだけ初期段階で潰せれば、それだけ復旧コストが安く済みますので、以下のようにフェーズごとに何かできないかを考える必要があります。

  • 開発時
    • 危険なコードは型やアーキテクチャレベルで書けないようにする
    • CI上の自動テスト時に検知する
    • コードレビューで指摘する
    • オンボーディングの資料に盛り込む
  • 機能検証時
    • 過去に何度か障害が発生したケースをQAチームに共有し、検証項目に盛り込んでもらう
  • 本番リリース後
    • Canaryリリース時に気付ける体制にする
    • エラーログを充実させる
      • ユーザからのお問い合わせで気付くよりも、エラーログで早めに気付ける方が、影響範囲や復旧コストが少なくて済む
    • リリースしてから数年後にデータ量の増加に伴って、計算量が爆発する場合もあるので、レスポンス速度の劣化やslow query、explainの実行結果などを監視する

逆に以下のようなことが再発防止策に入っていないか注意が必要です。

  • 場当たり的な対応になっていないか
    • HTTP 5xxの監視に引っかかったからといって、エラーを握りつぶすような解法になっていないか
      • エラーを握りつぶしたからといってユーザ影響が解消するとは限らないし、むしろ不具合の検知が遅くなる可能性がある
  • 開発速度を過剰に制限していないか
    • 罰則的な対応になっていないか
    • 本来価値提供したいことに影響が出ていないか
  • 細心の注意で確認を入念に行うなどのような対応になっていないか
    • 漠然としており、障害が再発した場合でも、確認が足りていませんでしたと同じ振り返りにしかならない
    • 確認やダブルチェックが必要ならば、手順書やチェック項目まで落とし込む必要がある
      • 確認箇所が曖昧だと、どこかの現場猫よろしく「後の人がちゃんとみてくれてるだろうからヨシ!」「前の人がレビューをとおしてるんだから絶対ヨシ!」みたいなことになりかねない
      • 自動化できるのがベストだが、工数や優先順位の都合上すべてが自動化できるとは限らない
  • 重厚長大な再発防止になっていないか
    • 解決できるスコープに対して労力を多大にかけていないか
    • 一見良さそうに見えるが、銀の弾丸などないので、導入した場合でも別の問題が出る可能性がある
    • また、実際にサービスに組み込んで、既存のシステムを刷新するためには相当なコストがかかる
    • 但し、重厚長大な再発防止を否定しているわけではなく、短期で結果が出ないし、多大なコストを払うかもしれないが、中長期的な戦略に立った時に改善に繋がるのであれば、コスパを意識した上で導入するのはあり

再発防止策の具体例

ミラティブサーバチームがここ1年半ぐらいの障害振り返りを通して行ってきた代表的な再発防止策としては以下のようなものが挙げられます。
(詳細に関してはまたいつかテックブログに書いたり書かなかったりする予定です)

  • Partition Pruningの効いていないクエリの洗い出し・修正
  • DDLのマイグレーションツール上のノイズを減らし、人間が確認すべき項目に集中できるように改善
  • 特定の時刻に発火する処理は、ゆらぎを自動で付与して負荷が重ならないように改善
  • Pull Requestのテンプレートに新規で発行されるクエリのEXPLAINを記入する項目を追加
  • Webの負荷がスパイクしないように、全体へのPush通知をゆっくり送る修正を追加
  • 開発環境で時刻を変更できる機能を追加
  • INSERT IGNOREを非推奨に
  • チーム内のレビュー方針に「その機能で障害があった時に自分で調査・復旧できるか」を追加
  • OpenAPIのadditionalPropertiesを必須に
  • アプリケーションサーバ側でのSlow Queryの通知
  • ユーザがいる環境での検証作業を最小限に
  • アーキテクチャの再設計・Goへの移行
  • マスタデータの運用フロー改善

特に最後3つの「ユーザがいる環境での検証作業を最小限に」「アーキテクチャの再設計・Goへの移行」「マスタデータの運用フロー改善」はミラティブサーバチームにおける中長期での最重要技術戦略だと位置づけており、障害振り返りで培った経験を元に、現在もプロジェクト化してチーム全体で取り組み続けております。

tech.mirrativ.stream

おわりに

新しい挑戦を続けている以上、障害をゼロにすることはできません。また、ときにはチーム内で話しあっても、上手い解決策が見つからない場合も多々あると思います。しかし、個人のミスとして片付けるのではなく、チームの経験として獲得し、向き合い続けることができれば、障害を通して反脆い組織へと成長できると信じています。

We are hiring!

ミラティブでは新機能によるユーザへの価値提供と同じくらい基盤の安定化に向き合えるエンジニアを募集しています。

  • Goで大規模サービスの開発をしたい
  • サーバーシステムの基盤の整備をしたい
  • ゲーム×ライブ配信サービスの開発をしたい

www.mirrativ.co.jp

speakerdeck.com

Mirrativのコラボ通話&配信のクライアント/サーバー間の仕組みを徹底解説

こんにちは。サーバーエンジニアのユンです。

今回Mirrativは「コラボ配信」という機能を開発しました。他の配信者(最大3人)と音声でつながり、視聴者とも同時にコミュニケーションを楽しむことができる機能です。

コラボ配信機能の紹介記事 | Mirrativ公式ブログ

f:id:canto87:20210225102438p:plain
コラボ配信機能

現在のところ、一部の配信者さんにご利用いただき、さらなる機能改善を進めています。

もともと、Mirrativでは「コラボ通話」という配信者と視聴者が通話する機能があります。コラボ通話はWebRTCの機能を使っていて、iOS/Android側はlibwebrtcにパッチを当てたものを利用しています。またSTUN/TURNはTwilioを利用しています。

コラボ通話をしているときユーザーは配信者、コラボ通話者、視聴者の3種類に区別され、配信者とコラボ通話者はWebRTCで音声が双方向になり、お互いの声をWebRTCを通して送受信しています。コラボ通話者が2人以上の場合メッシュ状にP2P接続させています。視聴者はRTMPで配信者の画面映像、配信者のマイク入力により合成されたゲーム音&配信者の声&コラボ通話者の声を受信して視聴することができます。

f:id:canto87:20210319110527p:plain
コラボ通話での配信者とコラボ通話者と視聴者

今回はそのコラボ通話機能と配信機能を組み合わせてコラボ配信の機能を作りました。 通常の配信では配信者と視聴者が1対Nの関係性を持ち、映像と音声は配信者から視聴者への一方通行になっており、視聴者はコメントを通じてコミュニケーションを行っています。コラボ配信は自分の配信をしながら、他の配信者と(最大3人)音声でつながり、その配信者&視聴をしている視聴者とコミュニケーションができます。 配信者同士&その視聴者とコミュニケーションを取ることで配信中のコミュニケーションをもっと楽しむ、配信内容によっては一緒にゲームをプレイすることも可能になります。

f:id:canto87:20210319110750p:plain
通常配信とコラボ配信での配信者とコラボ配信者と視聴者

この記事ではそのコラボ通話とコラボ配信の技術の中でクライアント/サーバー間の仕組み、そしてコラボ配信の実装時の工夫したところを中心にお話します。

ここから使う用語の定義

  • 配信者 : 配信を行っているユーザー
  • コラボ通話者 : 配信者とコラボ通話でつながっているユーザー
  • コラボ配信者 : 配信者とコラボ配信でつながっているユーザー
  • API Server : WebAPIを提供するサーバー
  • Pub-Sub Server: リアルタイムメッセージ配信を提供するサーバー

コラボ通話がつながるまでのクライアント/サーバー間の仕組み

f:id:canto87:20210321160953p:plain
視聴者のコラボ通話申請からコラボ通話接続まで

コラボを開始する際にコラボ通話者は配信者に対してAPI Serverにコラボ申請を送ります。API Server側はコラボ申請を受け取った時点で配信者↔コラボ通話者の関係性とコラボ状況(コラボ申請中)とコラボ通話用のPub-Sub Server情報(Pub-Sub Server接続用のキー)を作成してコラボ通話用のデータベースに保存します。

保存後API ServerではPub-Sub Serverを通してコラボ申請が届いたことを配信者に伝えて、配信者のアプリケーション側ではWebRTCClientの作成と承認したことをAPI Serverに送ります。API Serverでは承認したことをデータベースのコラボ通話用テーブル上にあるデータからコラボ状況を更新(承認)して、Pub-Sub Serverを通してコラボ通話者に承認情報を送り、Pub-Subからの承認情報を受信したコラボ通話者は同じくWebRTCClientを作成します。

また、配信者↔コラボ通話者でのWebRTCによるP2Pの接続が確立されたら、配信者側はAPI Serverにコラボ接続が完了してコラボが開始したことを送信します。

コラボ通話者を追加する場合

コラボ通話では配信者を含めて最大4人の視聴者までつなげることができます。それぞれがWebRTCで接続され、音声データのやりとりをしています。 それに備えてサーバー側は「コラボ通話がつながるまでのクライアント/サーバー間の仕組み」を行った後、既存につながってるコラボ通話者ともコラボをWebRTCで接続する処理を追加で行います。

f:id:canto87:20210321161652p:plain
コラボ通話者の追加

コラボ通話者Aがもともと配信者とコラボ通話を行っていて、後からコラボ通話者Bがそこに加わるようなことになり、コラボ通話者Bは上のフローと同様に配信者とコラボを接続した後、その時点でコラボに接続しているユーザーの一覧(この場合はコラボ通話者Aのみ)をサーバーから取得し、コラボ通話者Aとの接続を行います。

コラボ配信がつながるまでのクライアント/サーバー間の仕組み

f:id:canto87:20210321162209p:plain
視聴者のコラボ配信申請からコラボ配信接続まで

コラボ配信はコラボ通話機能と配信機能を組み合わせて作りました。その背景もあり、ユーザーがコラボ配信を申請するタイミングでは配信前の視聴者の可能性があるので、その場合は既存の配信処理もコラボ配信がつながるまで同時に進行する必要と、既存のコラボ通話と配信を区別する必要もありました。

それで、今回は視聴者がAPI Serverにコラボ申請を送る際にサーバー側でその申請がコラボ通話かコラボ配信かを区別するコラボタイプをパラメータに追加して、コラボタイプのパラメータがコラボ配信の場合に配信者↔コラボ通話者の関係性とコラボ状況(コラボ申請中)と情報(Pub-Subのキー)を作成してコラボ通話用に作られたデータベースに保存するとともに、配信を開始するための配信情報を作成およびデータベースに保存するようにしました。 また、レスポンスにはコラボ配信時に使用する配信情報を渡してWebRTCの接続処理までに配信開始処理を行うようにしています。

配信者とコラボ配信者のWebRTC接続の切断がしばしば発生

コラボ配信の実装後、動作確認時に配信者とコラボ配信者の間でWebRTCの接続が切断され、配信は続いているが、コラボ配信をしている配信者間の声が聞こえない問題が発生しました。 原因はコラボ配信を行ってる際にクラッシュなどでアプリケーションが異常終了をしたり通信環境の問題などでWebRTCの接続がなくなるが、データベース上ではコラボ開始状態から変化がないことが問題でした。

コラボ通話の場合、通話者は切断が起きた際にアプリケーション側でコラボ通話を終了させ、ユーザーがもう一回コラボ通話を申請することで再度開始するようにしていました。しかし、コラボ配信の場合はコラボ配信者側に視聴者が入っていることもあるので、コラボ通話のようにもう一回コラボ配信を行うように案内をすることは難しい状況でした。そのため、コラボ配信を終了させずにWebRTCの再接続をするフローを追加することにしました。 データベース上ではコラボ配信が開始している状態になっていたので、該当のユーザーのリストをクライアントに送り、そのリストの上にいるユーザーの中でWebRTCでの接続がされてないユーザーに対してWebRTCでの再接続処理&クライアント/サーバー間での再接続処理を行うようにしました。

f:id:canto87:20210321163204p:plain
コラボ配信者からの再接続
再接続処理の場合にはコラボ配信申請にパラメータ is_reconnect=1 が付与されており、これによってコラボ申請が再接続かを確認するようにしました。 また、コラボ配信者のアプリケーションが異常終了したのちに再接続処理を行う場合、コラボ用Pub-Sub Serverの接続を再度行う必要がありましたので、コラボ配信申請のレスポンスにコラボ配信用Pub-Sub Serverの情報ももう一回渡して接続を行うようにしました。

おわりに

今回はMirrativのコラボ通話とコラボ配信機能のクライアント/サーバー間の仕組みについてお話しました。

特にコラボ配信については、新しい体験を与える大きな規模の機能を0から実装するのではなく既存のユーザーが日頃よく使ってる機能の組み合わせで開発を行うことで、約2ヶ月で開発終了することができ、新しい体験をユーザーに提供することができました。

しかしながら、今のコラボ配信やコラボ通話をメッシュ状にP2P接続させる仕組みだと端末の負荷的にこれ以上参加者を増やせないため、さらなる品質の改善に向けてSFUの導入なども検討しています。

We are hiring!

このように新しい体験をユーザー提供し続けるミラティブではサーバーエンジニアを募集中です!

  • ゲーム×ライブ配信サービスの開発をしたい
  • サーバーシステムの基盤の整備をしたい といった方のご応募をお待ちしております!

www.mirrativ.co.jp

【Unity】MirrativのギフトとUnityを活用したリッチな演出の紹介

こんにちは、Unityエンジニアの菅谷です。 今回はMirrativのギフト機能について紹介します。特にMirrativ内のアバター(エモモ)を利用したMirrativ特有のギフトについて解説します。

Mirrativのギフト

ギフトは配信を盛り上げるための機能の一つで、視聴者がギフトを贈ることで配信者の画面に演出が表示されます。 代表的なギフト演出を仕組みごとに紹介します。 スタンプギフトやシンプルなアニメーションギフトでは、Lottieライブラリ(iOS/Android)を利用してギフトの画像をアニメーションさせています。 LottieはAfter Effectsから出力したJsonファイルをそのまま利用することができます。

スタンプギフトは画面上にアイコンやアニメーションを表示します。 ギフトは30種類以上あり、様々なシチュエーションに合わせて贈られています。

また、エモモと連動したギフトではUnityを活用しており、配信者のエモモにアイテムを渡すギフトや季節限定のギフトなどがあります。 Lottieでのアニメーションと比べると、3Dでの演出が可能になります。 演出のために専用のモーションや3Dモデルを作成する必要があり、手間はかかりますが表現に大きな幅を持たせることができます。

ギフトにより贈られたアイテムを使ってエモモがリアクションを返します。

季節限定ギフトではギフトを贈った視聴者のエモモが登場し配信者と一緒になって配信を盛り上げます。

今回はUnityの機能をフルに活用した季節限定ギフトの作り方について紹介します。

エモモの着せ替えとUnityでの演出の作り方

エモモは顔や衣装、帽子などのアイテムを組み合わせることで自分だけのアバターを作ることができます。 アイテムはイベントで毎週追加され、すでに5000種類以上あるため組み合わせは無限大です。 季節限定ギフトでは視聴者も自身のエモモをギフトの演出として配信に登場させることができます。 そのため配信者と衣装を合わせたり、演出のシチュエーションによって衣装を変えたりしてエモモを通じたコミュニケーションが行われています。

季節限定ギフトは独自のエモモの着せ替えの仕組みに加えて、UnityのTimelineとCinemachineを活用することで実現しています。 Timelineはオブジェクトのオンオフやアニメーションの再生をシーケンス形式で管理することができるため一連のカットシーンの利用に向いています。季節限定ギフトの演出もカットシーンなためTimelineとCinemachineを採用しました。

Timelineでのシーケンス制御

Timelineでは主に以下を制御しています。

  • エモモのアニメーション
  • エモモ以外に演出中に登場する小物、パーティクル、背景
  • 小物に紐付けたAnimator用のTrigger
  • カメラの切り替えとブレンディング

Timelineは内容が変わらないアニメーションを再生することに長けていますが、登場するエモモは衣装やアクセサリーなどユーザーによって様々です。そのためエモモは既存の衣装着せ替えの仕組みを使って表示し、Timelineのトラックに後から紐付けることで着せ替え機能と専用のアニメーションとを両立させています。

具体的な紐付けの処理は以下となっています。

  1. PlayableDirectorのトラック名を元に名前検索によりエモモに対応するトラックを取得する。
  2. PlayableDirectorのSetGenericBindingによりエモモのAnimatorとトラックのAnimatorを紐付ける。
  3. エモモがTimeline上で操作できるようになるため、アニメーションはトラック内に配置したAnimationClipで指定する。
var binding = playableDirector
                  .playableAsset
                  .outputs
                  .First(c => c.streamName == avatarTimeline.animationTrackName);
playableDirector.SetGenericBinding(binding.sourceObject, avatar.GetComponent<Animator>());

トラックに名前を設定しておく(streamName)

インスペクタで取得したいトラックを指定する(avatarTimeline.animationTrackName)

Timeline上のAnimatorとエモモ(Avatar_40)のAnimatorを紐付ける
AnimationClipはTimeline上で指定する

Cinemachineでのカメラ制御

Cinemachineは複数のカメラの切り替えを行う仕組みです。カットシーンにおけるカメラのブレンディングがスムーズに行えます。10個以上のカメラをTimelineにより切り替えることで演出に広がりをもたせています。また、Timelineのレコーディングモードを利用しUnity上で実際にカメラを動かしながらアニメーションをつけることもあります。Unityでカメラワークが作成できるためトライアンドエラーや細かい調整が高速に行えます。

運用の効率化のために

演出データはアセットバンドル化することでアプリを更新せずに追加できます。また、デザイナーだけで演出が作れるように基盤を整えることで運用の効率化を行っています。ただし、Timelineでの演出作成は自由度が高いため、アセットサイズが大きくなり端末の負荷も高くなりやすいです。そのためエンジニアは演出のデータ構造や負荷に問題がないかをチェックしてからリリースしています。また、アセットバンドル内に不要なアセットがないかや、アセットバンドル間の依存関係をチェックするツールを作成しより効率良く運用できるよう日々改善しています。

まとめ

3Dを利用したリッチな演出はUnityの得意領域です。Timeline+Cinemachineの導入により演出の幅も上がりました。ギフトの演出により配信者と視聴者が一緒になって楽しんでもらえると嬉しいですね。今後もエモモを活かしたコミュニケーションが進むようなギフトを作っていきます。

We are hiring!

ミラティブではUnityエンジニアを募集しています。 Unityをフル活用してリッチな演出を一緒に作りましょう!

www.mirrativ.co.jp

【Android】FlipperのCustomPluginを作成してデバッグ効率を改善する

Mirrativ Androidエンジニアのmorizoooです。MirrativではデバッグツールとしてFlipperを使っています。Flipperはモバイルアプリデバッグのためのデスクトップアプリケーションで、アプリ内のデータの整形や可視化を行うことができます。また、Flipperはネットワークの通信状況を確認するNetworkPluginなど、標準でいくつかの機能が用意されています。詳細についてはこちらをご覧ください。 tech.mirrativ.stream

Flipperは標準機能だけでなく、独自のCustomPluginを作成することもできます。MirrativではCustomPluginを積極的に作成し、バグ調査や開発効率の改善に役立てています。一つ例を上げると、Mirrativではコメントやギフトの機能のために、WebSocketベースの独自のPubSubライブラリを使用しています。以前はここでなにか問題が起こったときにペイロードのJSONをLogcatに出力してデバッグしていました。現在ではFlipperのCustomPluginを利用しており、これによりデータの検索、フィルター、閲覧がしやすくなり、バグの原因を特定するスピードが圧倒的に改善しました。

今回の記事では、実装例としてクライアント一覧でJSONを表示するCustomPluginの作成方法についてお話します。

続きを読む

CSS Variablesを使ってWeb LP制作のエンジニア作業時間を0にした話

こんにちは。Webフロントエンジニアの駒木です。

Mirrativでは毎週の様に運営主催イベントやゲーム会社様とのコラボ企画イベント等が開催されます。 そのイベント情報をユーザーへお伝えするメディアとして、イベント毎にWebページ いわゆる LP ( Landing Page ) を制作・公開しています。

f:id:eightbeeeaaat:20210128003034p:plain
Mirrativで公開している多種多様なLP
ですが毎週の様に新しいイベントが企画・開催されますので、LPをエンジニアが都度制作していてはとても追いつきません。

そこでミラティブではCSS Variablesを活用することで、イベントの魅力が伝わるWeb LPをエンジニアが作業することなく制作・運用できる体制を構築しています。

本記事ではここまでに至った過程も含めお伝えします!

続きを読む

【iOS】ミラティブにウィジェット機能を実装した際の開発Tips

こんにちは、iOS エンジニアの千吉良です。iOS14 にはウィジェット機能が新しく搭載*1されて、アプリ側で対応をすることで iOS 端末のホーム画面に独自のウィジェットを置けるようになりました。ミラティブでも、たまにはオシャレしたいよねということで昨年ウィジェット機能に対応しました。まだまだ対応しているアプリは少ないですが、ホーム画面に置いておくとアプリへの愛着も増すし、 SwiftUI での開発が経験できて今後対応していくであろう新しい開発環境を経験できるという点にもメリットがあります。今回はミラティブで導入したウィジェット機能について、いくつかの実装に触れてご紹介します。

続きを読む

Goで開発した配信サーバーのメモリ使用量問題に向き合う

こんにちは。ストリーミングチームの松本です。

Mirrativのストリーミングチームは、低遅延配信や、通知ぼかしというような機能を追加するため、配信のorigin serverの前段にtranscoder serverというものを導入してきました。

tech.mirrativ.stream

tech.mirrativ.stream

transcoder serverはGoによる内製のミドルウェアであり、主に映像の変換を行う目的で作られました。現在は配信プロトコルの変換(既存プロトコル -> 低遅延プロトコル)などを行っています。また、実際にはサーバー上のDockerコンテナ内で動作しています。

f:id:hma2moto:20210120154004p:plain

transcoder serverを展開していくにあたり、メモリ使用量が常に増え続ける問題が起きていたため、その際に直面したGoの実メモリ使用量に関する話を書きたいと思います。

メモリ使用量の増加問題

ミラティブでは Sharding を行っているため、簡易的な構成図では下記のようになっています。

f:id:hma2moto:20210120155256p:plain

これに対してtranscoder server の動作するサーバーインスタンスでは数日〜数週間動作させているとプロセスのメモリ使用量があるタイミングで急激に増えその後低下しない状態になっていき、結果的にメモリ使用量の監視アラートが通知されるという問題が起きていました。

アラートを検知した際には対象の配信shardをサービスから外す・翌日になり対象の配信shardのすべての配信が終了してからtranscoder serverを再起動する。というような対応をおこなっていました。

Goのヒープ使用量調査

このようなメモリ使用量の大幅な上昇は開発環境上では再現していませんでした。

まず最初に疑ったのは、Goのアプリケーション自体がメモリリークしている箇所があるのではということでした。特に goroutineリークにより、実行中のgoroutineが残ってしまい、そこで利用されているメモリが開放されていないのではということです。

transcoder serverに対しては、pprofをhttp apiでリクエストできるようにしています1。そのため、transcoder serverの動作するDockerコンテナ内に入りさえすれば任意のタイミングで現在実行中のアプリケーションのpprofの結果を取得することができるようになっています。

以下のようにして取得しました。

go tool pprof http://localhost:6060/debug/pprof/heap
go tool pprof http://localhost:6060/debug/pprof/goroutine

結果としては、Goのヒープに実際に処理に必要なもの以外で数百MB、数GBのような割当があるものは見つからず。また、goroutineリークも見つけることはできませんでした。

実メモリ使用量が減少しないのはなぜ?

Goのヒープ上のメモリは不要になったタイミングで開放されているのですが、メトリクス上で確認できるメモリ使用量のグラフは減少せず一定のままになっています。また、メモリ使用量は常に一定ではなく急激に増える箇所もありました。

transcoder serverのプロセスについてtopで確認できるRSSやpsコマンドで確認できるRESも高い値のままです。一体何が実メモリ使用量として割り当てられているのかを調べる必要がありました。

以下はTelegrafで取得した統計情報をソースとして グラフ化したものです。

transcoder serverのメモリ使用量内訳(対応前)

f:id:hma2moto:20210119162619p:plain

以下は赤線が使用メモリ(total memory - free memory)量(左軸)、黄線が使用率(右軸)

transcoder serverのメモリ使用量と使用率(対応前)

f:id:hma2moto:20210119162513p:plain

Go 1.12以上 + Linux 4.5以上においてMADV_FREEが利用される際の問題

実メモリ使用量が増え続ける理由の一つとして、Goのmadvise呼び出しに関する以下のような問題がありました。

  • Go1.12で、LinuxのKernel 4.5以上2ではmadviseのシステムコールについてデフォルトでMADV_FREEを使用するようになっていました。
  • ページフォルトが頻繁に発生しないことで理論上はMADV_DONTNEEDよりもMADV_FREEのほうがパフォーマンスが良くなるはずでしたが、実際にそのメリットを享受できているデータがほとんどないようでした。また、統計ツール上にあらわれる値は次に述べるように監視を行うエンジニアにとってユーザーエクスペリエンスの低下を招いていました。github.com

  • MADV_FREEを利用したことによる問題の例

    • 不要になった実メモリがすぐに開放されていないため、メモリリークではないのに メモリリークに見えることがある
    • 実メモリ使用量は増えつづけるため、実際にメモリリークしていることに気付きにくい
    • LazyFreeの値をRSSの値から差し引けばMADV_DONTNEED指定した際と同等の値になるが、それを知る方法が /proc/<PID>/smaps を参照するしかなく非常にわかりにくい。github.com

上記の問題は Go 1.16 において GOOS == "linux" の場合に debug.madvdontneed = 1 が付くようになる(デフォルトでMADV_DONTNEED) ことで対応されるようです。

また、Go 1.12〜1.15をLinuxで利用する場合においては、環境変数で GODEBUG=madvdontneed=1 を設定することにより、MADV_DONTNEEDが利用されるようになります。

ミラティブでは transcoder server に MADV_DONTNEEDを利用するようにした結果、メモリ使用量が増加傾向にあることは変わりませんでしたが、時間帯によって実メモリの使用量は増減していることがわかるようになりました。

transcoder serverのメモリ使用量内訳(MADV_DONTNEED設定後)

f:id:hma2moto:20210119163203p:plain

transcoder serverのメモリ使用量と使用率(MADV_DONTNEED設定後)

f:id:hma2moto:20210119162611p:plain

まとめ

Go1.12以降ではLinux上において、メモリをOSへ返却する際の実装が監視ツールとの相性が悪く結果としてUXの低下を招いていました。

ミラティブでは配信サーバの運用上、メモリ使用量が減らない状況にあり原因究明をしようとしていましたが、MADV_DONTNEEDを使うことでメトリクス上でメモリ使用量がわかりやすくなりました。

残念ながら解決しようとしてたtranscoder serverのメモリリークの問題はまだ残っています。根本的な解決に向けて今後さらに深堀りが必要です。

We are hiring!

ミラティブではGoでライブ配信をゴリゴリ開発できるストリーミングエンジニアを募集中です!

www.mirrativ.co.jp


  1. 配信サーバ以外のミラティブ本体ではprofefe の導入 を行っています。

  2. transcoder serverが動作しているインスタンスのOSはUbuntuでありカーネルは4.5以上です。また、transcoder serverのアプリケーション本体は、Dockerコンテナ上で動作しています

【インフラ】Mirrativのデータベースを最小限の影響で切り替える運用の紹介

こんにちは、ミラティブのインフラを担当している清水です。 今回はミラティブのデータベースのマスタをどのようにフェイルオーバさせているかノウハウをお伝えしようかと思います。

ミラティブではデータベースにMySQLを利用しており、マスタ・スレーブ構成で冗長化しています。 マスタ・スレーブ構成の優れている点はデータをフルダンプすればデータベースを完全に複製でき、マルチマスタ構成で発生しうるデータ不整合を気にかけなくて良い点です。 データベースのクラスタリングには MySQL Cluster や GaleraCluster などもありますが、マスタ・スレーブ構成はストレージエンジンに依存せず素のMySQLで運用できるので、クラスタ固有の制約にハマったりせずシンプルに運用できるのも強みです。

ただし、マスタ・スレーブ構成の鬼門となるのがマスタのフェイルオーバです。 スレーブは参照のみリクエストを処理するので1台停止しても別のスレーブから再び参照すればよいだけなので復旧が容易です。 一方でマスタは、データの書き込み処理を行っているため、フェイルオーバ時にはデータ不整合なく書き込み先を切り替える必要があります。

生きているスレーブをマスタに昇格するにしても、

  1. スレーブが複数台ある場合はデータ欠損を最小とするため、もっともRelaylogのポジションの進んだスレーブを探し出してマスタ昇格候補にする
  2. スレーブが複数台ある場合はRelaylogのポジションにズレがないか確認し、ズレが発生していたら欠損しているバイナリログを手動で解消させる
  3. マスタ昇格候補のスレーブにレプリケーションを張り直す

といった作業が発生します。

とても慎重且つ神経を使う作業が要求されますが、作業中にユーザさんはサービスを完全な状態で利用できないわけで、焦りや緊張でオペレーションミスを誘発しかねません。

そこで、ミラティブではマスタ切り替えにMHA for MySQL(Master High Availability Manager and tools for MySQL)というHAツールを利用して、データ不整合を最小限に解消させ、安全且つ短時間にフェイルオーバを行えるようにしています。

MHA for MySQL(Master High Availability Manager and tools for MySQL)とは

MHA for MySQL(以下、MHAと略す)はDeNA社がオープンソースとして公開している MySQL の HAツールで、githubにソースコードが公開されています。

MHAはMySQLサーバにmha4mysql-nodeをインストールし、外部サーバからmha4mysql-managerに含まれるスクリプトを動かしてフェイルオーバを行います。 インストール方法は本家 mha4mysql wiki で解説されているので本記事では割愛します。

MHAを利用すれば複雑なマスタのフェイルオーバ作業をワンコマンド化することができ、デーモンとして動かしておけば自動化させることもできますが、動かすためにいくつか注意点もあります。

一つ目はスレーブでもBinlogを吐くようにしておく必要があることです。 これはMHAがマスタ・スレーブをフェイルオーバさせた時にマスタからBinlogを回収してスレーブとの差分を埋めてくれるためで、Binlogが吐かれていないとスレーブがマスタ昇格後にMHAを実行できなくなってしまうからです。

二つ目はマスタと昇格対象のスレーブのスペックを揃えておくことです。 マスタ昇格後にスペックが下がってしまうようなことがあると、もともと捌けていたリクエストを昇格後に捌くことができず障害になりかねないからです。

ミラティブではマスタと昇格対象のスレーブはスペックを揃えてBinlogを吐くように運用していて、いつでもスレーブをマスタに昇格できるようにしています。

MHAの処理の流れを追ってみる

MHAを実行した時にどのような制御をしてマスタのフェイルオーバを行っているのか紹介します。 ここの所を理解しておけばMHAの実行に失敗してもパニックにならず落ち着いて作業できるかと思います。

mha4mysql-managerに含まれるmasterha_master_switchを利用してマスタをフェイルオーバした時の処理の流れを見て行きましょう。

マスタが停止している場合

マスタが停止してしまったときのMHAの処理を見ていきましょう。 MHAは切り替えをPhaseで管理しており、マスタが停止している場合はPhase1~5まで進んでフェイルオーバが完了します。

  • Phase 1: Configuration Check Phase..
  • Phase 2: Dead Master Shutdown Phase..
  • Phase 3: Master Recovery Phase..
  • Phase 3.1: Getting Latest Slaves Phase..
  • Phase 3.2: Saving Dead Master's Binlog Phase..
  • Phase 3.3: Determining New Master Phase..
  • Phase 3.3: New Master Diff Log Generation Phase..
  • Phase 3.4: Master Log Apply Phase..
  • Phase 4: Slaves Recovery Phase..
  • Phase 4.1: Starting Parallel Slave Diff Log Generation Phase..
  • Phase 4.2: Starting Parallel Slave Log Apply Phase..
  • Phase 5: New master cleanup phase..

Phase 1: Configuration Check Phase..

f:id:masaya-shimizu:20201221153601j:plain

Phase1はMHAのconfを検証してくれます。 MHAのconfは環境ごとに異なりますが、概ねこのような設定を記述します。

[server default]
user=${MYSQL_USER}
password=${MYSQL_PASSWORD}
repl_user=${REPL_USER}
repl_password=${REPL_PASSWORD}
remote_workdir=/path/to/workdir
master_binlog_dir=/path/to/mysql
ssh_user=${SSH_USER}
master_pid_file=/path/to/mysqld.pid
master_ip_failover_script=/path/to/master_ip_failover_script
master_ip_online_change_script=/path/to/master_ip_online_change_script
shutdown_script=/path/to/shutdown_script
report_script=/path/to/report_script

manager_workdir=/path/to/workdir
manager_log=/path/to/mha/log

[server1]
hostname=${SERVER1}
ip=${ADDRESS1}

[server2]
hostname=${SERVER2}
ip=${ADDRESS2}
candidate_master=1

[server3]
hostname=${SERVER3}
ip=${ADDRESS3}
candidate_master=1

[server4]
hostname=${SERVER4}
ip=${ADDRESS4}
candidate_master=0

MHAのconf内容が間違っていたり、スレーブが停止していたり、スレーブにssh接続できないときはPhase1で中断されます。 この段階でマスタ切り替えは行われていないので、落ち着いてconf内容と実際に動いているマスタ・スレーブ構成を見直してみて下さい。

Phase 2: Dead Master Shutdown Phase..

f:id:masaya-shimizu:20201221154024j:plain

Phase2は停止したマスタを完全停止させます。 ハングアップしたと思われていたマスタが実は生きていて、マスタ切替中にアプリケーションからデータの書き込みが発生してデータ不整合が発生することを防いでくれます。

Phase2に入るとまず master_ip_failover_scriptが--command=stop|stopssh 引数とともに実行されます。

/path/to/master_ip_failover_script  \
  --command=stop|stopssh \
  --orig_master_host=${ORIG_MASTER_HOST} \
  --orig_master_ip=${ORIG_MASTER_IP} \
  --orig_master_port=${ORIG_MASTER_PORT}

Phase2で実行されるmaster_ip_failover_scriptはこれからマスタを完全停止するための事前処理を記述して実行します。 例えば、停止したマスタのレコードを引けなくしたり、これから停止するマスタの情報を通知させたりできますが、何もさせたくない場合は処理を記述しなければよいです。

mha4mysql-managerに master_ip_failover というサンプルスクリプトが付属しているので、サンプルを参考にしつつ自前で処理を記述してみましょう。 Perl製ですが、同じ引数を受け取ることができれば別言語でも実装可能です。

続いて、マスタを完全停止させるためshutdown_script が実行されます。

/path/to/shutdown_script \
  --command=stop \
  --host=${HOSTNAME}  \
  --ip=${ADRESS}  \
  --port=${PORT} \
  --pid_file=/path/to/mysqld.pid

mha4mysql-managerに power_manager というshutdown_scriptがスクリプトが付属していますが、ミラティブのMySQLデータベースはGCP(Google Cloud Platform)で動いており、GCPと連携して確実にマスタを停止させたかったのでGo製のツールを自作しています。 このGo製のマスタ停止ツールはssh越しにMySQLの停止を試みて、失敗した場合はGCPからインスタンスを強制停止してくれます。

例ですが、shutdown_scriptはこんな感じで実装しています。

package main

import(
  "fmt"
  "log"

  "gopkg.in/urfave/cli.v1"

  "infra-tool"
  "infra-tool/util"
  "infra-tool/mha"
)

func mha_shutdown(c *cli.Context) error {
  ...
  sshPrivateKey := c.String("ssh-private-key")
  maxRetry      := uint64(c.Int("max-retry"))
  project       := c.String("project")

  if util.FileExists(sshPrivateKey) != true {
    return fmt.Errorf("ssh-private-key not exist: %s", sshPrivateKey)
  }
  if maxRetry < 1 {
    maxRetry = 1
  }

  // port22 に接続できないと--ssh_user が引数に渡されないのでrootを引き渡す
  sshUser := c.String("ssh_user")
  if c.String("ssh_user") == "" {
    sshUser = "root"
  }

  mhaOptions          := mha.MHAShutdownOptions{}
  mhaOptions.Command  = c.String("command")
  mhaOptions.SshUser  = sshUser
  mhaOptions.Host     = c.String("host")
  mhaOptions.Ip       = c.String("ip")
  mhaOptions.Port     = c.Int("port")
  mhaOptions.PidFile  = c.String("pid_file")

  log.Printf("debug: command: %s", mhaOptions.Command)
  log.Printf("debug: ssh_user: %s, host: %s, ip: %s, port: %d, pid_file: %s",
    mhaOptions.SshUser, mhaOptions.Host, mhaOptions.Ip, mhaOptions.Port, mhaOptions.PidFile,
  )

  if mhaOptions.Command == "stopssh" || mhaOptions.Command == "stop" {
    if err:= shutdownStopsshCommand(mhaOptions, sshPrivateKey, maxRetry, project); err != nil {
      return err
    }
  }

  return nil
}

func shutdownStopsshCommand(mhaOptions mha.MHAShutdownOptions, sshPrivateKey string, maxRetry uint64, project string) error {
  if err := mha.KillMySql(mhaOptions.Host, mhaOptions.Ip, mhaOptions.SshUser, sshPrivateKey, maxRetry, mhaOptions.PidFile, project); err != nil {
    log.Printf("warn: %s", err.Error())
    if err := mha.ShutdownInstance(mhaOptions.Host, project); err != nil {
      return err
    }
  }
  return nil
}

func init(){
  addCommand(cli.Command{
    Name: "mha-shutdown",
    Usage: "mha shutdown_script",
    Flags: []cli.Flag{
      cli.StringFlag{
        Name: "ssh-private-key",
        Usage: "/path/to/.ssh/id_rsa",
        Value: watch.DEFAULT_MS_SSH_PRIVATE_KEY,
        EnvVar: "INFRA_WATCH_MS_SSH_PRIVATE_KEY",
      },
      cli.IntFlag{
        Name: "max-retry",
        Usage: "maximum number of times to retry on failure",
        Value: watch.DEFAULT_MS_MAX_RETRY,
        EnvVar: "INFRA_WATCH_MS_MAX_RETRY",
      },
      cli.StringFlag{
        Name: "project",
        Usage: "specify gcp project",
        Value: watch.DEFAULT_MS_PROJECT,
        EnvVar: "INFRA_WATCH_MS_PROJECT",
      },
      cli.StringFlag{
        Name: "command",
        ...
      },
      cli.StringFlag{
        Name: "ssh_user",
        ...
      },
      cli.StringFlag{
        Name: "host",
        ...
      },
      cli.StringFlag{
        Name: "ip",
        ...
      },
      cli.IntFlag{
        Name: "port",
        ...
      },
      cli.StringFlag{
        Name: "pid_file",
        ...
      },
    },
    Action: mha_shutdown,
  })
}

Phase2で失敗した場合はmaster_ip_failover_scriptまたはshutdown_scriptの実行に失敗しているので、スクリプトをデバッグしてみてください。 スクリプトでマスタを落とし切ることができずエラー判定となる場合は手動で落としてしまうのも手です。

Phase 3: Master Recovery Phase..

Phase3はスレーブをマスタに昇格させるための下準備を進めるフェーズです。 3.1 ~ 3.4 まであるのでそれぞれ見ていきましょう。

Phase 3.1: Getting Latest Slaves Phase..

f:id:masaya-shimizu:20201221155914j:plain

Phase3.1は全てのスレーブのRelaylogポジションをチェックしてもっともポジションの進んでいるスレーブを探し出します。 図ではSlave2がもっともポジションの進んだsalveです。

Phase 3.2: Saving Dead Master's Binlog Phase..

f:id:masaya-shimizu:20201222175743j:plain

Phase3.2は停止したマスタにsshログインを試行し、もっともRelaylogポジションの進んだスレーブと停止したマスタのBinlogポジションの差分を回収します。 インスタンスが停止してしまっている場合はsshログインできないのでスキップされます。

f:id:masaya-shimizu:20201222175900j:plain

停止したマスタからBinlogの回収に成功した場合は全てのスレーブに差分を転送します。

Phase 3.3: Determining New Master Phase..

f:id:masaya-shimizu:20201221161135j:plain

Phase3.3パート1はマスタの昇格候補となるスレーブを決定します。 もっともRelaylogポジションの進んでいるスレーブが昇格候補となりますが、MHAのconfに candidate_master=1 を定義すると優先的に特定のスレーブを昇格候補とすることができます。

Phase 3.3: New Master Diff Log Generation Phase..

f:id:masaya-shimizu:20201221161240j:plain

Phase3.3パート2はもっとものRelaylogポジションの進んでいるスレーブとマスタ昇格候補スレーブのRelaylogの差分を取り出し、マスタ昇格候補スレーブに転送します

Phase 3.4: Master Log Apply Phase..

f:id:masaya-shimizu:20201221161656j:plain

Phase3.4はマスタ昇格候補のスレーブに停止したマスタから回収したBinlogの差分と、もっともRelaylogポジションの進んでいるスレーブとの差分を適用します。

ここまで進むともう後戻りはできません。差分適用に失敗したら戻すのは困難なので壊れていない他のslaveからdumpを取ってマスタ・スレーブを作り直した方が早いです。 無事終わることを見守りましょう。

差分適用に成功したらmaster_ip_failover_scriptが--command=start引数とともに実行されます。

/path/to/master_ip_failover_script  \
  --command=start \
  --ssh_user=${SSH_USER} \
  --orig_master_host=${ORIG_MASTER_HOST} \
  --orig_master_ip=${ORIG_MASTER_IP} \
  --orig_master_port=${ORIG_MASTER_PORT} \
  --new-master_host=${NEW_MASTER_HOST} \
  --new_master_ip=${NEW_MASTER_IP} \
  --new_master_port=${NEW_MASTER_PORT} \
  --new_master_user=${NEW_MASTER_USER} \
  --new_master_password=${NEW_MASTER_PASSWORD}

Phase 3.4で実行されるmaster_ip_failover_scriptはアプリケーションの書き込み先をマスタ昇格候補のスレーブに切り替えるための処理を記述します。 DNSで制御している場合はマスタのレコードを切り替えたり、IPで書き込み先を制御している場合はIPを付け替えたりします。

Phase 4: Slaves Recovery Phase..

Phase4はマスタ昇格候補のスレーブとその他スレーブの差分を埋めてレプリケーションを張り直します。

Phase 4.1: Starting Parallel Slave Diff Log Generation Phase..

f:id:masaya-shimizu:20201222180020j:plain

Phase 4.1はマスタ昇格候補のスレーブとその他スレーブのRelaylogの差分を生成してそれぞれのスレーブに転送します。

Phase 4.2: Starting Parallel Slave Log Apply Phase..

f:id:masaya-shimizu:20201221162216j:plain

Phase 4.2は各スレーブで停止したマスタから回収したBinlogの差分と、もっともRelaylogポジションの進んでいるスレーブとの差分を適用します。 ここで差分適用に失敗してしまってもマスタ昇格候補のスレーブは復元が完了しているので、そこからダンプを取ってスレーブを作り直しましょう。

f:id:masaya-shimizu:20201221162329j:plain

差分適用に成功したらマスタ昇格候補のスレーブにレプリケーションを張り直します。

Phase 5: New master cleanup phase..

Phase5はマスタ昇格候補のスレーブで reset slave all が実行されて、停止したマスタとレプリケーションを張っていたときの情報がクリーニングされます。

Master failover to ${HOSTNAME}(${ADDRESS}:${PORT}) completed successfully.

メッセージが表示されればマスタ切り替えは完了です。おつかれさまでした。

マスタが起動している場合

マスタが停止せずともスレーブをマスタに昇格させたい場合もよくあります。 例えば、CPUやメモリといったサーバのスペックを増強したり、コスト最適化のためにディスク容量を減らしたり、サーバの性能劣化による入れ替えを行いたいケースなどです。

停止メンテナンスを伴う時間を確保すれば切り替えはできますが、ユーザへの告知、サービス連携している協力会社さんへの連絡、停止中のユーザアクセスの停止が発生するためできればやりたくはありません。 MHAはマスタが起動状態でも切り替えられるように作られているので停止メンテナンスを伴う時間を確保せずとも切り替えることができます。

それでは、マスタが起動している場合のmasterha_master_switchの挙動を見ていきましょう。 マスタが起動している場合のPhaseは1,2,5で、マスタが停止している場合と異なるのはBinlogとRelaylogの差分回収と適用が無い点です。

  • Phase 1: Configuration Check Phase..
  • Phase 2: Rejecting updates Phase..
  • Phase 5: New master cleanup phase..

Phase 1: Configuration Check Phase..

f:id:masaya-shimizu:20201221222549j:plain

Phase1はマスタが停止している時と概ね同じ挙動をします。 スレーブが停止していたりssh接続できないときはPhase1で中断されるので、落ち着いてMHAのConf内容とマスタ・スレーブ構成の状態を見比べてみましょう。

マスタが停止している場合と異なる挙動はPhase1で FLUSH NO_WRITE_TO_BINLOG が実行されてBinlogの書き出しが行われる点です。 書き込みが多いとIO詰まりを誘発しかねないので、書き込みの少ない時間帯にあらかじめ1台ずつ FLUSH NO_WRITE_TO_BINLOG を実行しておくと安全です。 もし IO に余裕がある環境であれば、 cron などで FLUSH NO_WRITE_TO_BINLOG を定期実行しておき、 Binlog を定期的に書き出しておくのも有効かもしれません。

Phase 2: Rejecting updates Phase..

f:id:masaya-shimizu:20201221222847j:plain

Phase2はマスタ切り替え中のデータ不整合を防ぐためにアプリケーションからの書き込みをブロックします。 書き込みのブロックはmaster_ip_online_change_scriptが --command=stop|stopssh 引数とともに呼び出されて行ってくれます。

/path/to/master_ip_online_change_script \
  --command=stop|stopssh \
  --orig_master_host=${ORIG_MASTER_HOST} \
  --orig_master_ip=${ORIG_MASTER_IP} \
  --orig_master_port=${ORIG_MASTER_PORT} \
  --orig_master_user=${ORIG_MASTER_PORT} \
  --orig_master_password=${ORIG_MASTER_PASSWORD} \
  --new_master_host=${NEW_MASTER_HOST} \
  --new_master_ip=${NEW_MASTER_IP} \
  --new_master_port=${NEW_MASTER_PORT} \
  --new_master_user=${NEW_MASTER_USER} \
  --new_master_password=${NEW_MASTER_PASSWORD} \
  --orig_master_ssh_user=${ORIG_MASTER_SSH_USER} \
  --new_master_ssh_user=${NEW_MASTER_SSH_USER}

mha4mysql-managerに master_ip_online_change がサンプルスクリプトとして付属しているので環境にあわせてカスタマイズしてみましょう。

ミラティブではアプリケーション用のMySQLユーザを以下表のとおり書き込み用と参照用を分けており、 Goで実装したmaster_ip_online_change_script が書き込み用ユーザをアンダースコア付きのユーザ名にrenameして新規の書き込み用のセッションを落としています。

書き込みユーザ 参照ユーザ
master writeuser readuser
slave1 _writeuser readuser
slave2 _writeuser readuser
slave3 _writeuser readuser

持続的な接続があると効果が無いので注意が必要ですが、ミラティブのアプリケーションは切り替えを考慮して処理毎に都度、接続を切断して接続が残らないように実装しています。

一般的なサービスではmysql接続時のオーバヘッドを減らす目的でkeepaliveで実装されていますが、ミラティブはフェイルオーバ発生時のダウンタイムを極力減らす目的でコネクションプールでも長時間接続が残らないようにしています。 持続的な接続に比べオーバヘッドも含んでしまいますが、接続がmax-connectionになるまで溜まることもほとんどなくなります。

万が一書き込みを復旧させたい時でも、MySQLユーザをrenameしているだけなので切り戻しも簡単です。 また、書き込み先を1箇所に限定できるので切り替え中に意図せぬスレーブへアプリケーションが書き込んでしまう事故も防げます。

master_ip_online_change_script で安全に新規書き込みの接続を落とすことができたら、 FLUSH TABLES WITH READ LOCK でテーブルロックされて完全に書き込みできない状態となり、マスタの切り替えが開始されます。

まず、master_ip_online_change_scriptが --command=start とともに呼び出されます。 ここではマスタ昇格先のスレーブで書き込みを行えるようにするための処理を記述しておきます。 ミラティブの場合ですと、書き込み用ユーザをアプリケーションが利用できるようにrenameして、DNSを切り替えてAレコードを昇格したマスタに向けるように実装しています。

/path/to/master_ip_online_change_script  \
  --command=start \
  --orig_master_host=${ORIG_MASTER_HOST} \
  --orig_master_ip=${ORIG_MASTER_IP} \
  --orig_master_port=${ORIG_MASTER_PORT} \
  --orig_master_user=${ORIG_MASTER_USER} \
  --orig_master_password=${ORIG_MASTER_PASSWORD} \
  --new_master_host=${NEW_MASTER_HOST} \
  --new_master_ip=${NEW_MASTER_IP} \
  --new_master_port=${NEW_MASTER_PORT} \
  --new_master_user=${NEW_MASTER_USER} \
  --new_master_password=${NEW_MASTER_PASSWORD} \
  --orig_master_ssh_user=${ORIG_MASTER_SSH_USER} \
  --new_master_ssh_user=${NEW_MASTER_SSH_USER}

master_ip_online_change_scriptの実行が完了したら set global read_only = 0 が実行されて書き込みが行える状態となります。

f:id:masaya-shimizu:20201221223255j:plain

そして、マスタに昇格したスレーブにレプリケーションを張り直します。

Phase 5: New master cleanup phase..

Phase5はマスタに昇格したスレーブで reset slave all が実行されます。 Switching master to ${HOSTNAME}(${ADDRESS}:${PORT}) completed successfully. メッセージが表示されれば切り替え完了です。

最後に

MHAのフェイルオーバの動きは理解していただけたでしょうか。MHA実行時のトラブルに遭遇した時にお役いただけるとうれしいです。

MHAは非常によくできたHAツールですが、あくまでマスタ・スレーブの構成管理ができている前提で動作します。 ミラティブでは構成管理するためにマスタ・スレーブの構成監視やMHAのconfを動いているマスタ・スレーブ構成から生成していて、いつでもMHAが実行できる環境を整えています。

今回は紹介しきれなかったので、いずれまた紹介できたらなと思います。

We are hiring!

ミラティブでは サービスの拡大と安定化を支えるインフラエンジニアを募集中です! meetup も開催しているため気軽にご参加ください!

www.mirrativ.co.jp

speakerdeck.com

【Go】profefeでContinuous Profilingをやっていく話

こんにちは、サーバーエンジニアの牧野です。 今回はGoで開発しているアプリケーションでContinuous Profilingを実践するために導入した profefe を紹介したいと思います。

Continuous Profilingとは

Continuous Profilingとは、ざっくり言うと本番環境で継続的にプロファイリングすることを指します。Continuous Profilingができると、本番環境でのみ発生するパフォーマンスの問題を捉えることができたり、継続的にプロファイリングすることで問題が発生する前後の状態を比較することができます。

Goには pprof というプロファイリングのための標準パッケージがあり、プロファイリング自体は容易に行うことができますが、Continuous Profilingを実現するとなると、以下のような課題と向き合う必要があります。

  • 本番環境でオーバーヘッドが少なく安全にプロファイリングを実行できるか
  • どこにプロファイリング結果を保存するか
  • 保存したプロファイリング結果をどのようにして検索・抽出するか

今回はこれらの課題を解決するために、profefe というOSSを導入しました。

github.com

Continuous Profilingを支援するサービスとして、Cloud ProfilerDatadog Continuous Profilerといったサービスがありますが、データの保持期間に上限があったりするので、より柔軟な運用をしたいとなるとprofefeのようなOSSが選択肢に入ってくるかと思います。

profefeについて

profefeは、CollectorAgentという2つのコンポーネントから構成されています。

f:id:tatsumack:20201217105318p:plain
https://github.com/profefe/profefe/blob/master/DESIGN.md より引用

Collector

Collectorはプロファイルを受け取るサーバーです。Docker Imageが提供されているので、以下のコマンドで起動することができます。

$ docker run -d -p 10100:10100 profefe/profefe

以下のようにPOSTメソッドでプロファイルを送ると、profefeがプロファイルを保存します。

$ curl -X POST \
    "http://localhost:10100/api/0/profiles?service=<service>&type=cpu" \
    --data-binary @pprof.profefe.samples.cpu.001.pb.gz

プロファイルはpprofのフォーマットに従ってさえさえいれば良く、Go以外の言語でも使用することができます。

プロファイルを検索・抽出するためのAPIが提供されており、たとえば特定の期間のプロファイルをマージした結果を参照することができます。

$ go tool pprof \
   'http://localhost:10100/api/0/profiles/merge?service=<service>&type=<type>&from=<created_from>&to=<created_to>'

また、プロファイル保存時にlabelを指定することができ、プロファイルを検索するときの条件に指定することができます。例えば、labelにアプリケーションのversionを加えて、go tool pprofbaseオプションを利用してversion間のプロファイル差分を見ることができます。

$ go tool pprof \
   -base 'http://localhost:10100/api/0/profiles/merge?service=<service>&type=<type>&from=<created_from>&to=<created_to>&version=0.0.1' \
   'http://localhost:10100/api/0/profiles/merge?service=<service>&type=<type>&from=<created_from>&to=<created_to>&version=0.0.2'

プロファイルを保存するストレージは差し替えが可能になっており、Badger DBAWS S3Clickhouse DBを保存先として指定することができます。
ただ、今回はプロファイルをGoogle Cloud Storage(GCS)に保存したかったので、GCSをprofefeのストレージとして扱う実装を行いました。

profefeのストレージは以下のinterfaceを満たせばよく、integration testも用意されているので、さくっと実装することができました。手持ちの技術スタックに合わせて、ストレージを差し替えることができるのもprofefeの良い点だと思います。

type Storage interface {
    Writer
    Reader
}

type Writer interface {
    WriteProfile(ctx context.Context, params *WriteProfileParams, r io.Reader) (profile.Meta, error)
}

type Reader interface {
    FindProfiles(ctx context.Context, params *FindProfilesParams) ([]profile.Meta, error)
    FindProfileIDs(ctx context.Context, params *FindProfilesParams) ([]profile.ID, error)
    ListProfiles(ctx context.Context, pid []profile.ID) (ProfileList, error)
    ListServices(ctx context.Context) ([]string, error)
}

今回の実装はPRとして送っています。現時点ではまだマージされていませんが、"the change looks good to me."というコメントをいただいているので、そのうちマージされるかと思います。 マージされました 🎉 github.com

Agent

Agentは以下のようにアプリケーションに組み込んで使用します。

import "github.com/profefe/profefe/agent"

func main() {
    _, err := agent.Start("<profefe-url>", "<service-name>")
    ...
}  

Agentはgoroutineを起動し、定期的にプロファイリングを実行し、Collectorに送信します。プロファイリングにはruntime/pprofパッケージが使われています。

デフォルトでは1分おきに10秒間プロファイリングを実行します。Diagnostics - The Go Programming Language にもある通り、pprofは本番環境でも安全に実行できるとのことですが、オーバーヘッドはゼロではないので、プロファイリングの実行時間・間隔を調整して許容できる範囲を探ると良いと思います。ちなみにミラティブではデフォルトの設定のまま使用していますが、profefeの導入前後でCPU使用率に大きな変化はありませんでした。
また、異なるインスタンスで同時にプロファイリングが実行されてシステム全体の性能が劣化することがないように、ランダムにsleepを入れることで、インスタンス間でプロファイリングの実行タイミングを分散するような工夫がされていたりします。

このAgentは必ずしもアプリケーションに組み込む必要はありません。アプリケーションがnet/http/pprofを組み込んでいれば、cronなどで定期的にプロファイリングを実行してCollecterに送信する、といった使い方をすることも可能です。

f:id:tatsumack:20201217105150p:plain
https://github.com/profefe/profefe/blob/master/DESIGN.md より引用

おわりに

ミラティブでは本番環境にprofefeを導入して数週間経過しましたが、特に問題なく使うことができています。 profefeを導入してContinuous Profilingの基盤は整備できましたが、実運用としてどのように実践していくかはまだ固まっておらず、これから模索していくところです。今後知見が溜まってきましたら、またテックブログにて共有できればと思っております。

Continuous Profilingに関しては、GoogleのデータセンターのContinuous Profiling基盤に関する論文があるので、興味を持った方は読んでみると楽しめるかと思います。
Google-Wide Profiling: A Continuous Profiling Infrastructure for Data Centers – Google Research

また、profefeの作者の方がprofefeを作った背景をブログ記事に書いているので、こちらもオススメです。
Continuous Profiling and Go. There are lots of hidden details we… | by Vladimir Varankin | Medium

We are hiring!

ミラティブではサーバーエンジニアを募集中です!

  • Goで大規模サービスの開発をしたい
  • サーバーシステムの基盤の整備をしたい
  • ゲーム×ライブ配信サービスの開発をしたい

といった方のご応募をお待ちしております!

www.mirrativ.co.jp

speakerdeck.com