Nettyは、このように使用する必要があります。ベストプラクティスです。

最近、多くのクラスメートが、プッシュサービス関連の問題について問い合わせるためにWeiboで私に電子メールまたはプライベートメッセージを送信しました。

質問にはさまざまな種類がありますが、質問への回答を支援する過程で、質問を要約しました。これは、次のカテゴリに大まかに要約できます。

  1. Nettyはプッシュサーバーになることができますか?

  2. Nettyを使用してプッシュサービスを開発する場合、サーバーは最大でいくつのクライアントをサポートできますか?

  3. Nettyを使用してプッシュサービスを開発するときに発生するさまざまな技術的な問題。

コンサルタントが多く、比較的集中しているので、この記事の事例分析とプッシュサービス設計の要点のまとめを通じて、実際の作業で回り道をしないようにお手伝いしたいと思います。

1.2。プッシュサービス

モバイルインターネットの時代では、プッシュサービスはアプリアプリケーションの不可欠な部分になり、プッシュサービスはユーザーのアクティビティと定着率を高めることができます。私たちの携帯電話が毎日受け取るさまざまな広告やリマインダーのほとんどは、プッシュサービスによって実現されます。

Internet of Thingsの開発に伴い、ほとんどのスマートホームはモバイルプッシュサービスをサポートしています。将来的には、Internet of Thingsに接続されているすべてのスマートデバイスがプッシュサービスのクライアントになります。つまり、プッシュサービスは将来的に多数のデバイスや端末に直面することになります。に。

1.3。プッシュサービスの機能

モバイルプッシュサービスの主な機能は次のとおりです。

  1. 使用するネットワークは主にオペレーターのワイヤレスモバイルネットワークであり、ネットワークの品質が不安定です。たとえば、地下鉄の信号が非常に悪く、ネットワークが断続的になりがちです。

  2. 大量のクライアントアクセス、および通常は長い接続(クライアントであろうとサーバーであろうと)、リソース消費は非常に大きくなります。

  3. 中国ではGoogleのプッシュフレームワークを使用できないため、Androidの長い接続は各アプリケーションによって維持されます。つまり、各Androidデバイスには複数の長い接続があります。プッシュするメッセージがない場合でも、長い接続自体のハートビートメッセージの量は非常に多く、トラフィックと電力消費の増加につながります。

  4. 不安定:メッセージの損失、プッシュの繰り返し、配信の遅延、プッシュの期限切れが時々発生します。

  5. スパムメッセージが空中を飛び交い、統一されたサービス管理機能が不足しています。

上記の欠点を解決するために、JD Cloudが起動するプッシュサービスなど、マルチアプリケーションシングルサービスシングル接続モードを実装し、AlarmManagerを使用してハートビートをスケジュールして電力とトラフィックを節約できる独自のソリューションを提供している企業もあります。

2.スマートホームの分野での実際の事例

2.1。問題の説明

スマートホームMQTTメッセージサービスミドルウェアは、100,000人のユーザーをオンラインで長寿命に保ち、20,000人のユーザーがメッセージリクエストを送信します。プログラムを一定期間実行した後、メモリリークが見つかりました。これは、Nettyのバグであると疑われました。その他の関連情報は次のとおりです。

  1. MQTTメッセージサービスミドルウェアサーバーには、16Gメモリと8コアCPUがあります。

  2. Nettyでは、ボススレッドプールのサイズは1、ワーカースレッドプールのサイズは6で、残りのスレッドはビジネス用に割り当てられます。割り当て方法は、後でワーカースレッドプールのサイズを11に調整しました。問題は残ります。

  3. Nettyのバージョンは4.0.8.Finalです。

2.2。問題の場所

まず、以下に示すように、メモリスタックをダンプして、疑わしいメモリリークと参照関係を分析する必要があります。

c53035c3391b41558e5328eb02d3a6d1

NettyのScheduledFutureTaskが9076%増加し、約110Wのインスタンスに到達したことがわかりました。ビジネスコードの分析により、ユーザーはリンクがアイドル状態のときにビジネスロジック処理にIdleStateHandlerを使用していることがわかりましたが、アイドル時間は比較的長く、15分に設定されています。 。

NettyのIdleStateHandlerは、ユーザーの使用シナリオに応じて、ReaderIdleTimeoutTask、WriterIdleTimeoutTask、およびAllIdleTimeoutTaskの3種類の時限タスクを開始します。これらはすべて、スケジュールおよび実行されるNioEventLoopのタスクキューに追加されます。

タイムアウト期間が長いため、10Wの長いリンクリンクは10WのScheduledFutureTaskオブジェクトを作成し、各オブジェクトはメモリを消費するビジネスメンバー変数も格納します。ユーザーの永続世代は比較的大きく設定されています。一部の時間指定タスクは永続世代にエージングされ、JVMによってガベージ収集されません。メモリは増大しており、ユーザーはメモリリークがあると誤って信じています。

実際、さらに分析したところ、ユーザーのタイムアウト設定は非常に不合理であることがわかりました。15分のタイムアウトは設計目標を満たしていません。再設計後、タイムアウト時間は45秒に設定され、メモリは正常に回復され、問題は解決されます。

2.3。問題の要約

100回の接続であれば、長時間のタイミングタスクでもメモリリークの問題はありません。新世代では、マイナーGCでメモリ回復が可能です。小さな問題が増幅され、その後のさまざまな問題につながるのは、まさに100,000レベルの長い接続のためです。

実際、ユーザーが長期間のタイミングタスクを持っている場合、どのように対処するのですか?長い接続が多いプッシュサービスの場合、コードを不用意に扱うとゲームが失われます。ここでは、Nettyのアーキテクチャ特性に基づいて、Nettyを使用して数百万のクライアントのプッシュサービスを実現する方法を紹介します。

3.Nettyマスプッシュサービスの設計ポイント

高性能NIOフレームワークとして、Nettyを使用して効率的なプッシュサービス技術を開発することは可能ですが、プッシュサービス自体が複雑であるため、安定した高性能プッシュサービスを開発することは容易ではなく、設計段階を対象とする必要があります。サービスの特徴は合理的に設計されています。

3.1。ハンドルの最大数の変更

何百万もの長い接続の場合、最初に最適化する必要があるのはLinuxカーネルパラメータです。Linuxのファイルハンドルの最大数は、最も重要なチューニングパラメータの1つです。単一のプロセスによって開かれるハンドルのデフォルトの最大数は1024です。ulimit-aを使用して関連するパラメータを確認できます。パラメータ、例は次のとおりです。

[root @ lilinfeng〜] #ulimit -a
コアファイルサイズ(ブロック、-c)0
データセグメントサイズ(キロバイト、-d)無制限の
スケジューリング優先度(-e)0
ファイルサイズ(ブロック、-f)無制限の
保留中のシグナル(- i)256324
最大ロックメモリ(kbytes 、-l)64
最大メモリサイズ(kbytes、-m)無制限の
オープンファイル(-n)1024 
 
......后续未出消

シングルプッシュサービスで受信したリンクが上限を超えると、「開いているファイルが多すぎます」と報告され、すべての新しいクライアントアクセスが失敗します。

vi /etc/security/limits.confを使用して次の構成パラメーターを追加します。変更後に保存し、現在のユーザーをログアウトして再度ログインし、変更されたステータスがulimit-aを使用して有効になるかどうかを確認します。

*ソフトnofile1000000 
*ハードnofile1000000

1つのプロセスで開くハンドルの最大数を非常に大きく変更することはできますが、ハンドルの数が一定の桁数に達すると、処理効率が大幅に低下するため、サーバーのハードウェア構成と処理能力に応じて合理的である必要があることに注意してください。セットアップ。単一のサーバーのパフォーマンスが十分でない場合は、クラスタリングによって実現することもできます。

3.2.CLOSE_WAITに注意してください

モバイルプッシュサービスの開発に従事している学生は、モバイルワイヤレスネットワークの信頼性が非常に低く、クライアントのリセット接続やネットワークフラッシュが頻繁に発生するという経験を持っている可能性があります。

何百万もの長い接続があるプッシュシステムでは、サーバーはこれらのネットワーク例外を正しく処理できる必要があります。設計ポイントは次のとおりです。

  1. クライアントの再接続間隔は、頻繁すぎる接続(たとえば、ポートが解放されていない)によって引き起こされる接続障害を防ぐために合理的に設定する必要があります。

  2. クライアントの繰り返しログイン拒否メカニズム。

  3. サーバーは、ハンドルのリークを防ぐために、I / O例外とデコード例外を正しく処理します。

最後に特に注意しなければならないのは、close_waitが多すぎるという問題です。ネットワークが不安定なため、クライアントが切断されることがよくあります。サーバーが時間内にソケットを閉じることができないと、close_wait状態のリンクが多すぎます。close_wait状態のリンクは、ハンドルやメモリなどのリソースを解放しません。バックログが大きすぎると、システムハンドルが使い果たされ、「開いているファイルが多すぎます」という例外が発生する可能性があります。新しいクライアントは、ハンドルの作成またはオープンを含めてアクセスできません。失敗します。

以下は、close_wait状態の簡単な紹介です。TCP接続をパッシブに閉じる状態遷移図は次のとおりです。

0c5efad0621c41e28d82e5e7ec313fcd

図3-1TCP接続を受動的に閉じる状態遷移図

Close_waitは、接続を受動的に閉じることによって形成されます。TCPステートマシンによると、サーバーはクライアントから送信されたFINを受信し、TCPプロトコルスタックは自動的にACKを送信し、リンクはclose_wait状態になります。ただし、サーバーがソケットclose()操作を実行しない場合、状態をclose_waitからlast_ackに移行できず、システム内に多くのclose_wait接続が存在します。一般的に、close_waitは少なくとも2時間続きます(システムのデフォルトのタイムアウトは7200秒、つまり2時間です)。サーバープログラムによって、何らかの理由で多数のclose_waitリソースがシステムによって消費される場合、通常は、システムがリリースの待機に失敗した瞬間です。

close_waitが多すぎる理由として考えられるのは、次のとおりです。

  1. プログラムがバグを処理し、相手のフィンを受け取った後、ソケットが時間内に閉じられません。これは、特定の問題の特定の分析を必要とするNettyバグまたはビジネスレイヤーバグである可能性があります。

  2. ソケットが時間内に閉じられていません。たとえば、I / Oスレッドが誤ってブロックされたり、I / Oスレッドによって実行されるユーザー定義タスクの割合が高すぎたりして、I / O操作の処理が遅れ、リンクが時間内に解放されません。

以下では、Nettyの原則を組み合わせて、潜在的な障害ポイントを分析します。

設計ポイント1:NettyのI / Oスレッドでビジネスを処理しないでください(ハートビートの送信と検出を除く)。なぜですか?Javaプロセスの場合、スレッドは無期限に拡張できません。つまり、NettyのReactorスレッドは収束する必要があります。Nettyのデフォルト値はCPUコアの数です* 2。通常、I / Oを多用するアプリケーションでは、スレッドの数をできるだけ多く設定することをお勧めしますが、これは主に従来の同期I / O用です。肺をブロックするI / Oの場合、スレッド大きすぎる設定はお勧めしません。最適値はありませんが、I / Oスレッド数の経験値は[CPUコア数+ 1、CPUコア数* 2]の間にあります。

単一のサーバーが100万の長い接続をサポートし、サーバーコアの数が32の場合、単一のI / Oスレッドによって処理される接続の数はL = 100 /(32 * 2)= 15625です。5Sごとにメッセージ交換(新しいメッセージプッシュ、ハートビートメッセージ、およびその他の管理メッセージ)がある場合、平均CAPS = 15625/5 = 1秒あたり3125メッセージ。Nettyの処理パフォーマンスと比較すると、この値に圧力はかかりませんが、実際のビジネス処理では、パフォーマンス統計、インターフェイスログの記録など、追加の複雑なロジック処理が必要になることが多く、これらのビジネス操作のパフォーマンスオーバーヘッドも比較的大きくなります。ビジネスロジックがI / Oスレッドで直接処理されると、I / Oスレッドがブロックされ、他のリンクの読み取りおよび書き込み操作が影響を受ける可能性があります。これにより、パッシブに閉じられたリンクが時間内に閉じられなくなり、close_waitが累積されます。

設計ポイント2:I / Oスレッドでカスタムタスクを実行するときは注意してください。NettyのI / O処理スレッドNioEventLoopは、次の2つのカスタムタスクの実行をサポートします。

  1. 通常の実行可能:NioEventLoopのexecute(実行可能タスク)メソッドを呼び出して実行します。

  2. ScheduledFutureTask:NioEventLoopのschedule(Runnableコマンド、長い遅延、TimeUnitユニット)インターフェイスを呼び出すことによって実行されます。

NioEventLoopがユーザー定義のRunnableおよびScheduledFutureTaskの実行をサポートする必要がある理由は、この記事の焦点では​​ありません。後で紹介する特別な記事があります。この記事では、それらの影響の分析に焦点を当てています。

NioEventLoopでRunnableおよびScheduledFutureTaskを実行すると、ユーザーはNioEventLoopで非I / O操作ビジネスロジックを実行できます。これらのビジネスロジックは通常、メッセージ処理とプロトコル管理に関連しています。それらの実行は、NioEventLoop I / O読み取りおよび書き込みのCPU時間を優先します。ユーザー定義タスクが多すぎる場合、または単一タスクの実行サイクルが長すぎる場合、I / O読み取りおよび書き込み操作がブロックされ、間接的にclose_waitの累積につながります。

したがって、ユーザーがコードでRunnableおよびScheduledFutureTaskを使用する場合は、ioRatio比率を適切に設定してください。値はNioEventLoopのsetIoRatio(int ioRatio)メソッドを使用して設定できます。デフォルト値は50で、I / O操作とユーザー定義タスクの値です。実行時間の比率は1:1です。

私の提案は、サーバーが大規模なクライアント接続を処理しているときに、NioEventLoopでカスタムタスクまたはハートビート以外のタイミングタスクを実行しないことです。

設計ポイント3:IdleStateHandlerを使用するときは注意してください。多くのユーザーは、ハートビートの送信と検出にIdleStateHandlerを使用します。この使用法は、宣伝する価値があります。この方法は、タイミングタスクを開始して自分でハートビートを送信するよりも効率的です。ただし、実際の開発では、ハートビートのビジネスロジック処理では、正常なシナリオであろうと異常なシナリオであろうと、制御不能な遅延によってNioEventLoopが誤ってブロックされないように、処理遅延を制御可能にする必要があることに注意してください。たとえば、ハートビートタイムアウトまたはI / O例外が発生すると、ビジネスは電子メール送信インターフェイスを呼び出してアラートを出します。電子メールサーバーの処理タイムアウトにより、メール送信クライアントがブロックされ、カスケードによってIdleStateHandlerのAllIdleTimeoutTaskタスクがブロックされ、最後にNioEventLoop多重化が行われます。デバイス上の他のリンクの読み取りと書き込みはブロックされます。

ReadTimeoutHandlerおよびWriteTimeoutHandlerの場合、制約も存在します。

3.3。合理的なハートビートサイクル

百万レベルのプッシュサービスは、何百万もの長い接続が存在することを意味し、各長い接続は、リンクを維持するためにアプリ間のハートビートに依存する必要があります。ハートビートサイクルを合理的に設定することは非常に重要です。プッシュサービスのハートビートサイクル設定では、モバイルワイヤレスネットワークの特性を考慮する必要があります。

スマートフォンがモバイルネットワークに接続されている場合、実際にはインターネットに接続されていません。オペレーターによって携帯電話に割り当てられたIPは、実際にはオペレーターのイントラネットIPです。モバイル端末をインターネットに接続するには、オペレーターのIPゲートウェイを通過する必要があります。アドレス変換、このゲートウェイは略してNAT(ネットワークアドレス変換)と呼ばれます。簡単に言えば、モバイル端末はインターネットに接続します。実際には、モバイル内部ネットワークIP、ポート、および外部ネットワークIP間のマッピングです。

GGSN(GateWay GPRS Support Note)モジュールはNAT機能を実装します。ゲートウェイNATマッピングテーブルの負荷を軽減するために、ほとんどのモバイルワイヤレスネットワークオペレータは、一定期間通信がない場合、対応するテーブルを削除します。リンクの中断は、まさにこれがアイドル接続の解放タイムアウトを意図的に短縮することです。これは元々、チャネルリソースを節約することを目的としていました。インターネットアプリケーションが、長いプッシュ接続を維持するために通常よりもはるかに高い頻度でハートビートを送信するべきではないと思いました。チャイナモバイルの2.5Gネットワ​​ークを例にとると、ベースバンドが約5分間アイドル状態になると、接続が解放されます。

モバイルワイヤレスネットワークの特性により、プッシュサービスのハートビートサイクルを長く設定することはできません。そうしないと、長い接続が解放されてクライアントの再接続が頻繁に発生しますが、短く設定しすぎると、統一されたハートビートフレームワークメカニズムが不足します。ダウンロードすると、シグナリングストーム(マイクロコンフィデンスジャンプシグナリングストームなど)が発生しやすくなります。特定のハートビートサイクルに統一された基準はありません。180Sが適切な選択であり、WeChatは300Sです。

Nettyでは、IdleStateHandlerをChannelPipelineに追加し、コンストラクターでリンクアイドル時間を指定し、アイドルコールバックインターフェイスを実装してハートビートの送信と検出を実現することで、ハートビートの検出を実現できます。コードは次のとおりです。

public void initChannel({@ link Channel} channel){ 
channel.pipeline()。addLast( "idleStateHandler"、new {@link IdleStateHandler}(0、0、180)); 
channel.pipeline()。addLast( "myHandler"、new MyHandler()); 
}
拦截链路空闲事件并处理心跳:
パブリッククラスMyHandlerというが延びる{@link ChannelHandlerAdapter} { 
    {@code @Override}
     公共ボイドuserEventTriggered(CTX {リンクChannelHandlerContext @}、{@linkオブジェクト} EVT)はスロー{@link例外} { 
         if(evt instanceof {@link IdleStateEvent}} { 
             //心跳にする理
         } 
     } 
 }

3.4。受信および送信バッファ容量を合理的に設定する

長いリンクの場合、各リンクは独自のメッセージ受信および送信バッファーを維持する必要があります。JDKネイティブNIOクラスライブラリは、実際には固定長のByte配列であるjava.nio.ByteBufferを使用します。動的拡張、ByteBufferにもこの制限があり、関連するコードは次のとおりです。

public abstract class ByteBuffer 
   extends 
   Bufferimplements Comparable <bytebuffer> 
{ 
   final byte [] hb ; //ヒープバッファの場合のみnull以外
   finalint offset; 
   ブールisReadOnly; </ bytebuffer>

容量を動的に拡張できないと、ユーザーに問題が発生します。たとえば、各メッセージの長さを予測できないため、比較的大きなByteBufferを事前に割り当てる必要がありますが、通常は問題ありません。しかし、マスプッシュサービスシステムでは、これはサーバーに大きなメモリ負荷をもたらします。1つのプッシュメッセージの最大上限が10Kで、平均メッセージサイズが5Kであるとすると、10Kメッセージの処理を満たすために、ByteBufferの容量は10Kに設定され、各リンクは実際に5K多くのメモリを消費します。長いリンクの数が100万、各リンクは個別にByteBuffer受信バッファーを保持し、追加損失の合計メモリTotal(M)= 1000000 * 5K = 4882M。過度のメモリ消費はハードウェアコストを増加させるだけでなく、メモリが大きいとフルGCが長くなり、システムの安定性に比較的大きな影響を与えます。

実際、最も柔軟な処理方法は、メモリを動的に調整することです。つまり、受信バッファは、過去に受信​​したメッセージに基づいて計算し、メモリを動的に調整し、CPUリソースを使用してメモリリソースを調達できます。具体的な戦略は次のとおりです。

  1. ByteBufferは容量の拡大と縮小をサポートし、必要に応じて柔軟に調整してメモリを節約できます。

  2. メッセージを受信すると、指定したアルゴリズムに従って以前に受信したメッセージのサイズを分析し、将来のメッセージのサイズを予測し、予測値に従ってバッファ容量を柔軟に調整して、プログラムの通常の機能を満たす最小のリソース損失を実現できます。

幸い、Nettyが提供するByteBufは、動的な容量調整をサポートしています。受信バッファのメモリアロケータに対して、Nettyは次の2つのタイプを提供します。

  1. FixedRecvByteBufAllocator:固定長の受信バッファーアロケーター。割り当てられるByteBufの長さは固定サイズであり、実際のデータグラムサイズに応じて動的に縮小することはありません。ただし、容量が不足している場合は、動的拡張がサポートされます。動的拡張はNettyByteBufの基本機能であり、ByteBufアロケーターの実現とは関係ありません。

  2. AdaptiveRecvByteBufAllocator:動的な容量調整を備えた受信バッファーアロケーター。前のチャネルで受信したデータグラムサイズに従って計算されます。受信バッファーの書き込み可能スペースが継続的にいっぱいになると、容量が動的に拡張されます。2回受信したデータグラムが指定値よりも小さい場合、メモリを節約するために現在の容量が削減されます。

FixedRecvByteBufAllocatorと比較して、AdaptiveRecvByteBufAllocatorを使用する方が合理的です。クライアントまたはサーバーの作成時にRecvByteBufAllocatorを指定できます。コードは次のとおりです。

Bootstrap b = new Bootstrap(); 
           b.group(group).channel(
            NioSocketChannel.class)
            .option(ChannelOption.TCP_NODELAY、true)
            .option(ChannelOption.RCVBUF_ALLOCATOR、AdaptiveRecvByteBufAllocator.DEFAULT)

デフォルトで設定されていない場合は、AdaptiveRecvByteBufAllocatorが使用されます。

また、受信バッファであろうと送信バッファであろうと、バッファのサイズは、最大のメッセージの上限ではなく、メッセージの平均サイズに設定することをお勧めします。これにより、追加のメモリが無駄になります。受信バッファの初期サイズは次のように設定できます。

/ ***
    指定されたパラメーターを使用して新しい予測子を作成します。
    *  
    * @param minimum 
    *予想されるバッファサイズの包括的下限
    * @ param initial 
    *フィードバックが受信されなかったときの初期バッファサイズ
    * @ param maximum 
    *予想されるバッファサイズの包括的上限
    * / 
   public AdaptiveRecvByteBufAllocator(int最小、int初期、int最大)

メッセージ送信の場合、通常、ユーザーは自分でByteBufを作成してエンコードする必要があります。たとえば、次のツールを使用してメッセージ送信バッファーを作成します。

542401a7ddd74912b274a5e6ef1daf41

図3-2指定された容量のバッファの構築

3.5。メモリプール

プッシュサーバーは多数の長いリンクを伝送し、各長いリンクは実際にはセッションです。各セッションがハートビートデータ、受信バッファー、命令セットなどのデータ構造を保持し、これらのインスタンスがメッセージの処理に伴って出入りする場合、サーバーに大きなGCプレッシャーがかかり、大量のメモリを消費します。

最も効果的なソリューション戦略は、メモリプールを使用することです。各NioEventLoopスレッドはN個のリンクを処理します。スレッド内では、リンク処理はシリアルです。Aリンクを先に処理すると、受信バッファなどが作成されます。デコードが完了すると、構築されたPOJOオブジェクトがタスクとしてカプセル化され、バックグラウンドスレッドプールに配信されて実行され、受信バッファが解放されます。メッセージの受信と処理は、受信バッファーの作成と解放を繰り返します。メモリプールが使用されている場合、Aリンクが新しいデータグラムを受信すると、NioEventLoopのメモリプールからの空きByteBufが適用されます。デコードが完了したら、releaseを呼び出して、ByteBufをメモリプールに解放し、Bリンクで後で使用できるようにします。 。

メモリプールの最適化を使用した後、単一のNioEventLoopのByteBufアプリケーションの数とGC時間は、元のN = 1000000/64 = 15625回から少なくとも0回に減少します(各アプリケーションに使用可能なメモリがあると仮定)。

以下では、Netty4 PooledByteBufAllocatorを使用して、メモリプールの効果を評価するケースとしてGCを最適化します。結果は次のとおりです。

ゴミの発生速度は元の1/5で、ゴミの洗浄速度は5倍です。新しいメモリプールメカニズムを使用すると、ネットワーク帯域幅をほぼ埋めることができます。

以前のバージョンのNetty4の問題は、次のとおりです。新しいメッセージを受信するか、ユーザーがリモートエンドにメッセージを送信するたびに、Netty3は新しいヒープバッファーを作成します。これは、新しいバッファごとに、新しいバイト[容量]が存在することを意味します。これらのバッファはGCの圧力を引き起こし、メモリ帯域幅を消費する可能性があります。安全のために、新しいバイト配列は割り当て時にゼロで埋められ、メモリ帯域幅を消費します。ただし、ゼロで埋められたデータは実際のデータで再び埋められる可能性が高く、その結果、同じメモリ帯域幅が消費されます。Java仮想マシン(JVM)が、ゼロで埋めずに新しいバイト配列を作成する方法を提供した場合、メモリ帯域幅の消費を50%削減できたはずですが、現在そのような方法はありません。

新しいByteBufメモリプールがNetty4に実装されています。これは、jemallocの純粋なJavaバージョンです(Facebookでも使用されています)。これで、Nettyはバッファをゼロで埋めることによってメモリ帯域幅を浪費することはなくなりました。ただし、GCに依存しないため、開発者はメモリリークに注意する必要があります。ハンドラーでバッファーを解放するのを忘れると、メモリー使用量が無期限に増加します。

Nettyはデフォルトではメモリプールを使用しません。クライアントまたはサーバーを作成するときに指定する必要があります。コードは次のとおりです。

Bootstrap b = new Bootstrap(); 
           b.group(group).channel(
            NioSocketChannel.class)
            .option(ChannelOption.TCP_NODELAY、true)
            .option(ChannelOption.ALLOCATOR、PooledByteBufAllocator.DEFAULT)

メモリプールを使用した後、メモリアプリケーションとリリースはペアで表示される必要があります。つまり、retain()とrelease()はペアで表示される必要があります。そうしないと、メモリリークが発生します。

メモリプールを使用する場合は、ByteBufのデコードが完了した後、ReferenceCountUtil.release(msg)を明示的に呼び出して、受信バッファByteBufのメモリを解放する必要があります。そうしないと、まだ使用中であると見なされ、メモリが発生します。道を譲る。

3.6。「目に見えない殺人者の記録」に注意してください

通常の状況では、データベースへのアクセスや電子メールの送信など、実行時間が制御できない操作をNettyのI / Oスレッドで実行できないことは誰もが知っています。しかし、一般的に使用されているが非常に危険な操作があり、見落とされがちです。つまり、ロギングです。

通常、実稼働環境では、インターフェースログをリアルタイムで出力する必要があり、その他のログはERRORレベルです。プッシュサービスでI / O例外が発生すると、例外ログが記録されます。現在のディスクのWIOが比較的高い場合、ログファイルの書き込み操作が同期によってブロックされ、ブロック時間が予測できない可能性があります。これにより、NettyのNioEventLoopスレッドがブロックされ、Socketリンクを時間内に閉じることができず、他のリンクが読み取りおよび書き込み操作を実行できなくなります。

最も一般的に使用されるlog4jを例にとると、非同期ログ書き込み(AsyncAppender)をサポートしていますが、ログキューがいっぱいになると、ログキューが空くまでビジネススレッドを同期的にブロックします。関連するコードは次のとおりです。

同期(this.buffer){ 
     while(true){ 
       int previousSize = this.buffer.size(); 
       if(previousSize <this.bufferSize){ 
         this.buffer.add(event); 
         if(previousSize!= 0)break; 
         this.buffer.notifyAll(); ブレーク; 
       }
       ブール値破棄= true; 
       if((this.blocking)&&(!Thread.interrupted())&&(Thread.currentThread()!= this.dispatcher))//復是业务線程
       { 
         try 
         { 
           this.buffer.wait(); //減衰业务ワイヤー程ディスカード
           = false; 
         } 
         catch(InterruptedException e) 
         {
           Thread.currentThread()。interrupt();
         } 
 
       }

このタイプのBUGは非常に隠されており、多くの場合、WIOの高さは非常に短いか、場合によっては持続します。テスト環境でこのような障害をシミュレートすることは困難であり、問​​題を特定することは非常に困難です。これには、読者がコードを書くときに注意し、隠された地雷に注意を払う必要があります。

3.7.TCPパラメーターの最適化

TCPレベルでの受信および送信バッファサイズ設定など、一般的に使用されるTCPパラメータは、NettyのChannelOptionのSO_SNDBUFおよびSO_RCVBUFに対応し、プッシュメッセージのサイズに応じて適切に設定する必要があります。大規模で長い接続の場合、通常は32Kが適切です。

図に示すように、もう1つの一般的に使用される最適化方法はソフト割り込みです。すべてのソフト割り込みがCPU0の対応するネットワークカードのハードウェア割り込みで実行されている場合、cpu0は常にソフト割り込みを処理し、他のCPUリソースは複数のソフト割り込みを並行して実行できないため、無駄になります。

e4b6aa373d124eeb8d480853641649c2

図3-3割り込み情報

バージョン2.6.35以上のLinuxカーネルとRPSを有効にすると、ネットワーク通信のパフォーマンスが20%以上向上します。RPSの基本原理:データパケットの送信元アドレス、宛先アドレス、宛先ポートと送信元ポートに応じてハッシュ値を計算し、ハッシュ値に応じてソフト割り込み動作のcpuを選択します。上位から見ると、各接続がCPUにバインドされ、ハッシュ値を使用して複数のCPUで実行されているソフト割り込みのバランスを取り、通信パフォーマンスを向上させます。

3.8.JVMパラメータ

2つの最も重要なパラメータ調整があります。

  • -Xmx:JVMの最大メモリは、メモリモデルに従って計算する必要があり、比較的妥当な値が取得されます。

  • GC関連のパラメータ:新世代と旧世代の比率、永続的な世代、GC戦略、新世代の各領域の比率など、特定のシナリオに従って設定およびテストし、フルGCの頻度を減らすために可能な限り継続的に最適化する必要があります最低に。


おすすめ

転載: blog.51cto.com/14455981/2541785