Flinkコンテナ環境で殺されたOOMについて詳しく説明する

実稼働環境では、Flinkは通常YARNやk8sなどのリソース管理システムにデプロイされます。プロセスはコンテナ化された方法(YARNコンテナまたはドッカーコンテナ)で実行され、そのリソースはリソース管理システムによって厳密に制限されます。一方、FlinkはJVMで実行され、JVMはコンテナ化された環境と特に互換性がありません。特に、JVMの複雑で制御しにくいメモリモデルでは、リソースの過度の使用によりプロセスが簡単に強制終了され、Flinkが発生する可能性があります。アプリケーションが不安定であるか、使用できない場合もあります。

この問題に対応して、Flinkはバージョン1.10でメモリ管理モジュールをリファクタリングし、新しいメモリパラメータを設計しました。ほとんどのシナリオでは、Flinkのメモリモデルとデフォルトは、ユーザーがプロセスの背後にある複雑なメモリ構造を保護するのに十分です。ただし、メモリの問題が発生すると、問題のトラブルシューティングと修復にはより多くのドメイン知識が必要になり、通常は通常のユーザーになります。近づかないでください。

この目的のために、この記事では、JVMとFlinkのメモリモデルを分析し、Flinkのメモリ使用量が作業で遭遇してコミュニティコミュニケーションで学んだコンテナ制限を超える一般的な理由を要約します。Flinkのメモリ使用量は、ユーザーコード、展開環境、さまざまな依存バージョン、およびその他の要因と密接に関連しているため、この記事では主にYARN展開、Oracle JDK / OpenJDK 8、Flink1.10 +について説明します。さらに、コミュニティの質問に答えてくれた@宋辛童(Flink 1.10+新しいメモリアーキテクチャの主な著者)と@唐️(RocksDB StateBackendの専門家)に特に感謝します。これは著者に多大な利益をもたらしました。

JVMメモリパーティション

ほとんどのJavaユーザーにとって、日常の開発でJVMヒープを処理する頻度は、他のJVMメモリパーティションよりもはるかに高いため、他のメモリパーティションはまとめてオフヒープメモリと呼ばれることがよくあります。Flinkの場合、過剰なメモリの問題は通常オフヒープメモリに起因するため、JVMメモリモデルをより深く理解する必要があります。

JVM 8 Spec [1]によると、JVMによって管理されるメモリパーティションは次のとおりです。

JVM8メモリモデル

上記の仕様で指定されている標準パーティションに加えて、JVMは、特定の実装で高度な汎用モジュール用にいくつかの追加パーティションを追加することがよくあります。HotSopt JVMを例にとると、Oracle NMT [5]の標準に従って、JVMメモリを次の領域に分割できます。

  • ヒープ:各スレッドで共有されるメモリ領域には、主にnewオペレータによって作成されたオブジェクトが格納されます。メモリの解放はGCによって管理され、ユーザーコードまたはJVM自体で使用できます。
  • クラス:仕様のメソッド領域(定数プールを除く)、Java8のメタスペースに対応するクラスのメタデータ。
  • スレッド:仕様のPCレジスタ、スタック、およびNatviveスタックの合計に対応するスレッドレベルのメモリ領域。
  • コンパイラ:JIT(Just-In-Time)コンパイラが使用するメモリ。
  • コードキャッシュ:JITコンパイラによって生成されたコードを格納するために使用されるキャッシュ。
  • GC:ガベージコレクターが使用するメモリ。
  • シンボル:仕様の定数プールに対応する、シンボル(フィールド名、メソッド署名、インターン文字列など)を格納するためのメモリ。
  • Arena Chunk:JVMはオペレーティングシステムメモリの一時バッファ領域に適用されます。
  • NMT:NMT自身のメモリ。
  • 内部:ユーザーコードによって要求されたネイティブ/ダイレクトメモリを含む、上記の分類を満たさないその他のメモリ。
  • 不明:不明なメモリ。

理想的には、各パーティションのメモリの上限を厳密に制御して、プロセスの全体的なメモリがコンテナの制限内に収まるようにすることができます。ただし、管理が厳しすぎると、追加の使用コストと柔軟性の欠如が発生します。したがって、実際には、JVMは、ユーザーに公開される一部のパーティションに厳しい上限しか提供しませんが、他のパーティションは全体として表示できます。これは、JVM自体のメモリ消費量です。

パーティションのメモリを制限するために使用できる特定のJVMパラメータを次の表に示します(業界にはJVMネイティブメモリの正確な定義がないことに注意してください。この記事のネイティブメモリは、オフヒープメモリの非直接部分を指します。非直接は交換できます)。

表からわかるように、ヒープ、メタスペース、およびダイレクトメモリを使用する方が安全ですが、非ダイレクトネイティブメモリの状況はより複雑です。これは、JVM自体の内部使用である可能性があります(以下で説明するMemberNameTableなど)。これは、ユーザーコードによって導入されたJNI依存関係である場合もあれば、sun.misc.Unsafeを介してユーザーコード自体によって要求されたネイティブメモリである場合もあります。理論的には、ユーザーコードまたはサードパーティのlibによって要求されたネイティブメモリでは、ユーザーがメモリ使用量を計画する必要があり、内部の残りの部分は、JVM自体のメモリ消費量に組み込むことができます。実際、Flinkのメモリモデルも同様の原則に従います。

FlinkTaskManagerメモリモデル

まず、Flink1.10 +のTaskManagerメモリモデルを確認します。

FlinkTaskManagerメモリモデル

明らかに、Flinkフレームワーク自体には、JVMによって管理されるヒープメモリが含まれるだけでなく、オフヒープ自体によって管理されるネイティブメモリとダイレクトメモリにも適用されます。私の意見では、Flinkのオフヒープメモリ管理戦略は3つのタイプに分けることができます。

  • ハード制限:メモリパーティションのハード制限は自己完結型であり、Flinkは、その使用量が設定されたしきい値を超えないようにします(メモリが十分でない場合、OOMのような例外がスローされます)
  • ソフト制限:ソフト制限とは、メモリ使用量が長期間しきい値を下回るが、設定されたしきい値を一時的に超える可能性があることを意味します。
  • 予約済み:予約とは、Flinkがパーティションメモリの使用を制限しないことを意味します。メモリを計画するときにスペースの一部のみを予約しますが、実際の使用が制限を超えないことを保証することはできません。

JVMのメモリ管理と組み合わせると、Flinkメモリパーティションのメモリオーバーフローはどのような結果を引き起こしますか?判断ロジックは次のとおりです。

1. Flinkにハード制限されたパーティションがある場合、Flinkはパーティションに十分なメモリがないことを報告します。それ以外の場合は、次の手順に進みます。
2.パーティションがJVM管理パーティションに属している場合、その実際の値が増加し、JVMパーティションもメモリが不足すると、JVMはそれが属するJVMパーティションのOOMを報告します(java.lang.OutOfMemoryError:Javeヒープスペースなど)。それ以外の場合は、次の手順に進みます。
3.パーティションのメモリがオーバーフローし続け、最終的にプロセスのメモリ全体がコンテナのメモリ制限を超えます。厳密なリソース制御が有効になっている環境では、リソースマネージャー(YARN / k8sなど)がプロセスを強制終了します。

FlinkのメモリパーティションとJVMメモリパーティションの関係を視覚的に示すために、作成者は次のメモリパーティションマッピングテーブルをコンパイルしました。

FlinkパーティションとJVMパーティションのメモリ制限の関係

前のロジックによると、すべてのFlinkメモリパーティションの中で、自己完結型ではなく、独自のJVMパーティションにメモリハード制限パラメータがないJVMオーバーヘッドのみが、プロセスをOOMkillする可能性があります。さまざまな用途のために予約されたメモリの寄せ集めとして、JVMオーバーヘッドは確かに問題が発生しやすいですが、同時に、他の領域からのメモリの問題を軽減するための分離バッファとしても使用できます。

たとえば、Flinkメモリモデルには、ネイティブ非直接メモリを計算するときにトリックがあります。

ネイティブの非直接メモリ使用量は、フレームワークのオフヒープメモリまたはタスクのオフヒープメモリの一部として説明できますが、この場合、JVMの直接メモリの制限が高くなります。

タスク/フレームワークのオフヒープパーティションにはネイティブ非直接メモリが含まれる場合があり、メモリのこの部分は厳密にJVMオーバーヘッドであり、JVM -XX:MaxDirectMemorySizeパラメータによって制限されませんが、FlinkはそれをMaxDirectMemorySizeでカウントします。予約されたダイレクトメモリクォータのこの部分は実際には使用されないため、ネイティブ非ダイレクトメモリ用にスペースを予約する効果を実現するために、上限なしでJVMオーバーヘッド用に予約できます。

OOMKilledの一般的な原因

上記の分析と一致して、実際にOOM Killedの一般的な原因は、基本的にネイティブメモリのリークまたは過剰使用に起因します。仮想メモリのOOMKilledは、リソースマネージャの設定によって簡単に回避でき、通常は大きな問題はないため、以下では、物理メモリのOOMKilledについてのみ説明します。

RocksDBネイティブメモリの不確実性

ご存知のとおり、RocksDBはFlinkによって制御されないJNIを介してネイティブメモリに直接適用されるため、実際、FlinkはRocksDBのメモリパラメータを設定することによってそのメモリ使用量に間接的に影響を与えます。ただし、Flinkは現在、これらのパラメーターを推定していますが、これらはあまり正確な値ではありません。これにはいくつかの理由があります。

1つ目は、メモリの一部を正確に計算するのが難しいことです。RocksDBのメモリには4つの部分があります[6]:

  • ブロックキャッシュ:非圧縮データブロックをキャッシュするOSPageCacheの上のキャッシュのレイヤー。
  • インデックスとフィルターブロック:インデックスとブルームフィルターは、読み取りパフォーマンスを最適化するために使用されます。
  • Memtable:書き込みキャッシュに似ています。
  • Iteratorによって固定されたブロック:RocksDBトラバーサル操作(RocksDBMapStateのすべてのキーのトラバースなど)をトリガーすると、Iteratorは、それによって参照されるブロックとMemtableがライフサイクル中に解放されないようにし、追加のメモリ使用量をもたらします[10]。

最初の3つの領域のメモリは構成可能ですが、Iteratorによってロックされるリソースはアプリケーションのビジネス使用パターンに依存し、ハード制限はありません。したがって、FlinkはRocksDBStateBackendメモリを計算するときにこの部分を考慮しません。

2つ目は、RocksDB Block Cache [8] [9]のバグです。これにより、キャッシュサイズを厳密に制御できなくなり、設定されたメモリ容量を短時間で超える可能性があります。これは、ソフト制限に相当します。

RocksDBのメモリの過剰使用は一時的なものであるため、この問題の場合、通常はJVMオーバーヘッドのしきい値を上げてFlinkにメモリを予約させるだけで済みます。

glibcスレッドアリーナの問題

もう1つの一般的な問題は、glibcの有名な64 MBの問題です。これにより、JVMプロセスのメモリ使用量が大幅に増加し、最終的にYARNによって強制終了される可能性があります。

具体的には、JVMはglibcを介してメモリに適用され、メモリ割り当ての効率を向上させ、メモリの断片化を減らすために、glibcは共有メインアリーナとスレッドレベルのスレッドアリーナを含むアリーナと呼ばれるメモリプールを維持します。スレッドがメモリを申請する必要があるが、メインアリーナが他のスレッドによってロックされている場合、glibcはスレッドが使用するために約64 MB(64ビットマシン)のスレッドアリーナを割り当てます。これらのスレッドアリーナはJVMに対して透過的ですが、プロセスの全体的な仮想メモリ(VIRT)と物理メモリ(RSS)に含まれます。

デフォルトでは、Arenaの最大数はcpuコアの数* 8です。通常の32コアサーバーの場合、最大16 GBを占有しますが、これは印象的ではありません。消費されるメモリの合計量を制御するために、glibcは環境変数MALLOC_ARENA_MAXを提供して、Arenaの合計量を制限します。たとえば、Hadoopはこの値をデフォルトで4に設定します。ただし、このパラメータはソフト制限にすぎません。すべてのアリーナがロックされている場合でも、glibcはメモリを割り当てるために新しいスレッドアリーナを作成し[11]、予期しないメモリ使用量を引き起こします。

一般的に、この問題はスレッドを頻繁に作成する必要があるアプリケーションで発生します。たとえば、HDFSクライアントは書き込まれるファイルごとにDataStreamerスレッドを作成するため、スレッドアリーナの問題が発生しやすくなります。Flinkアプリケーションでこの問題が発生したと思われる場合、確認する簡単な方法は、プロセスのpmapに64MBの倍数の連続したanonセグメントが多数あるかどうかを確認することです。たとえば、下の図の青色の65536KBセグメントは非常に可能性が高いです。アリーナです。

pmap 64MBアリーナ

この問題の修正は比較的簡単です。MALLOC_ARENA_MAXを1に設定するだけです。つまり、スレッドアリーナを無効にして、メインアリーナのみを使用します。もちろん、これのコストは、スレッド割り当てメモリの効率が低下することです。ただし、Flinkのプロセス環境変数パラメーター(containerized.taskmanager.env.MALLOC_ARENA_MAX = 1など)を使用してデフォルトのMALLOC_ARENA_MAXパラメーターをオーバーライドすることは不可能な場合があることに注意してください。その理由は、非ホワイトリスト変数(yarn.nodemanager)です。 env-whitelist)競合が発生した場合、NodeManagerはURLをマージすることによって元の値と追加された値をマージし、結果としてMALLOC_ARENA_MAX = "4:1"になります。

最後に、より徹底的な代替ソリューションがあります。それは、glibcをGoogleのtcmallocまたはFacebookのjemallocに置き換えることです[12]。スレッドアリーナの問題が発生しないことを除いて、メモリ割り当てのパフォーマンスは向上し、断片化は少なくなります。実際、Flink 1.12の公式イメージでも、デフォルトのメモリアロケータがglibcからjemellocに変更されました[17]。

JDK8ネイティブメモリリーク

Oracle Jdk8u152より前のバージョンには、ネイティブメモリリークバグ[13]があり、JVMの内部メモリパーティションが拡大し続ける原因になります。

具体的には、JVMは文字列シンボル(Symbol)のマッピングペアをメソッド(Method)およびメンバー変数(Field)にキャッシュして、検索を高速化します。マッピングの各ペアはMemberNameと呼ばれ、マッピング関係全体はMemeberNameTableと呼ばれ、java.langで定義されます。このクラスはinvoke.MethodHandlesを担当します。Jdk8u152より前は、MemberNameTableはネイティブメモリを使用していたため、一部の廃止されたMemberNameはGCによって自動的にクリーンアップされず、メモリリークが発生していました。

この問題を確認するには、NMTを介してJVMメモリの状態を確認する必要があります。たとえば、作成者は400MBを超えるオンラインTaskManagerのMemeberNameTableに遭遇しました。

JDK8MemberNameTableネイティブメモリリーク

JDK-8013267 [14]の後、この問題を修正するためにMemeberNameTableがネイティブメモリからJavaヒープに移動されました。ただし、C2コンパイラのメモリリークの問題[15]など、JVMには複数のネイティブメモリリークの問題があるため、作成者のような専用のJVMチームを持たないユーザーにとっては、最新バージョンのJDKにアップグレードすることが問題を解決する最良の方法です。 。

YARNmmapメモリアルゴリズム

ご存知のとおり、YARNは/ proc / $ {pid}の下のプロセス情報に従って、コンテナプロセスツリー全体の合計メモリを計算しますが、mmapの共有メモリには特別な点があります。mmapメモリはすべてプロセスのVIRTにカウントされます。これについては疑いの余地はありませんが、RSS計算にはさまざまな基準があります。YARNおよびLinuxsmapの計算規則に従って、ページは2つの標準に分けられます。

  • プライベートページ:現在のプロセスのみがマップ(マップ)されたページ
  • 共有ページ:他のプロセスと共有されているページ
  • クリーンページ:マップされてから変更されていないページ
  • ダーティページ:マップされてから変更されたページデフォルトの実装では、YARNは/ proc / $ {pid} / statusに従って合計メモリを計算します。これらのページが同時に存在する場合でも、すべての共有ページはプロセスのRSSにカウントされます。複数のプロセスによるマッピング[16]。これにより、実際のオペレーティングシステムの物理メモリから逸脱し、Flinkプロセスが誤って強制終了される可能性があります(もちろん、ユーザーコードがmmapを使用し、十分なスペースを予約していないことが前提です)。

この目的のために、YARNはyarn.nodemanager.container-monitor.procfs-tree.smaps-based-rss.enabled構成オプションを提供します。これをtrueに設定すると、YARNはより正確な/ proc / $ {pid} / smapを使用します。メモリ使用量を計算する際の重要な概念の1つはPSSです。簡単に言うと、PSSの違いは、メモリの計算時にこのページを使用するすべてのプロセスに共有ページが均等に分散されることです。たとえば、プロセスが別のプロセスと共有される1000個のプライベートページと1000個の共有ページを保持している場合、プロセスの合計ページ数は1500です。YARNのメモリ計算に戻ると、プロセスRSSは、それによってマップされたすべてのページRSSの合計に等しくなります。デフォルトでは、YARNはページRSS式を次のように計算します。`` `Page RSS = Private_Clean + Private_Dirty + Shared_Clean + Shared_Dirty` ``ページはプライベートまたは共有であり、クリーンまたはダーティであるため、実際には上記の発表の右側にある少なくとも3つの項目は0です。smapsオプションをオンにすると、式は次のようになります。 `` `Page RSS = Min(Shared_Dirty、PSS)+ Private_Clean + Private_Dirty` ``簡単に言えば、新しい式の結果は、Shared_Clean部分で繰り返される計算の影響を取り除くことです。smaps計算に基づくオプションをオンにすると、計算がより正確になりますが、ページをトラバースして合計メモリを計算するオーバーヘッドが発生します。/proc/${pid}/statusの統計を直接取得するほど高速ではありません。したがって、mmapの問題が発生した場合は、 FlinkのJVMオーバーヘッドパーティションの容量を増やすことをお勧めします。

総括する

この記事では、最初にJVMメモリモデルとFlink TaskManagerメモリモデルを紹介し、次にOOM Killedが通常ネイティブメモリリークに起因するプロセスを分析し、最後にRocksDBメモリ使用量の不確実性を含むネイティブメモリリークのいくつかの一般的な原因と解決策をリストします、Glibcの64MBの問題、JDK8 MemberNameTableのリーク、およびYARNのmmapメモリの不正確な計算。作者のレベルが限られているため、すべての内容が正しいことを保証することはできません。読者の意見が異なる場合は、メッセージを残して一緒に話し合ってください。

 

元のリンク

この記事はAlibabaCloudのオリジナルのコンテンツであり、許可なく複製することはできません。

おすすめ

転載: blog.csdn.net/weixin_43970890/article/details/112672702