こんにちは。ストリーミングチームの松本です。
Mirrativのストリーミングチームは、低遅延配信や、通知ぼかしというような機能を追加するため、配信のorigin serverの前段にtranscoder serverというものを導入してきました。
transcoder serverはGoによる内製のミドルウェアであり、主に映像の変換を行う目的で作られました。現在は配信プロトコルの変換(既存プロトコル -> 低遅延プロトコル)などを行っています。また、実際にはサーバー上のDockerコンテナ内で動作しています。
transcoder serverを展開していくにあたり、メモリ使用量が常に増え続ける問題が起きていたため、その際に直面したGoの実メモリ使用量に関する話を書きたいと思います。
メモリ使用量の増加問題
ミラティブでは Sharding を行っているため、簡易的な構成図では下記のようになっています。
これに対して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のメモリ使用量内訳(対応前)
以下は赤線が使用メモリ(total memory - free memory)量(左軸)、黄線が使用率(右軸)
transcoder serverのメモリ使用量と使用率(対応前)
Go 1.12以上 + Linux 4.5以上においてMADV_FREEが利用される際の問題
実メモリ使用量が増え続ける理由の一つとして、Goのmadvise呼び出しに関する以下のような問題がありました。
- Go1.12で、LinuxのKernel 4.5以上2ではmadviseのシステムコールについてデフォルトでMADV_FREEを使用するようになっていました。
- これはページフォルトの頻度を減らして実際にメモリを再利用する際にのみメモリの開放を行うことでパフォーマンスの向上を得ようとする試みでした。
- MADV_FREEは、実際にそのメモリ領域が必要になったタイミングで開放されRSSに反映されることになります。
ページフォルトが頻繁に発生しないことで理論上は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設定後)
transcoder serverのメモリ使用量と使用率(MADV_DONTNEED設定後)
まとめ
Go1.12以降ではLinux上において、メモリをOSへ返却する際の実装が監視ツールとの相性が悪く結果としてUXの低下を招いていました。
ミラティブでは配信サーバの運用上、メモリ使用量が減らない状況にあり原因究明をしようとしていましたが、MADV_DONTNEEDを使うことでメトリクス上でメモリ使用量がわかりやすくなりました。
残念ながら解決しようとしてたtranscoder serverのメモリリークの問題はまだ残っています。根本的な解決に向けて今後さらに深堀りが必要です。
We are hiring!
ミラティブではGoでライブ配信をゴリゴリ開発できるストリーミングエンジニアを募集中です!
-
配信サーバ以外のミラティブ本体ではprofefe の導入 を行っています。↩
-
transcoder serverが動作しているインスタンスのOSはUbuntuでありカーネルは4.5以上です。また、transcoder serverのアプリケーション本体は、Dockerコンテナ上で動作しています↩