OpenResty FAQ | 動的ロード、NYI、およびShared Dictのキャッシュ

API7.ai

January 19, 2023

OpenResty (NGINX + Lua)

OpenRestyの記事シリーズはこれまでに更新され、パフォーマンス最適化に関する部分は私たちが学んだすべてです。遅れずに積極的に学び、実践し、熱心に意見を残しているあなたにおめでとうございます。

私たちは多くの典型的で興味深い質問を集めましたが、その中から5つを紹介します。

質問1: Luaモジュールの動的ロードをどのように実現するか?

説明: OpenRestyで実装されている動的ロードについて質問があります。ファイルが置き換えられた後、新しいファイルをロードするためにloadstring関数を使用するにはどうすればよいですか? loadstringは文字列しかロードできないと理解していますが、Luaファイル/モジュールを再ロードしたい場合、OpenRestyでどのように行うことができますか?

ご存知の通り、loadstringは文字列をロードするために使用され、loadfileは指定されたファイルをロードできます。例えば、loadfile("foo.lua")です。これら2つのコマンドは同じ結果を達成します。Luaモジュールをロードする方法については、以下の例を参照してください:

resty -e 'local s = [[
local ngx = ngx
local _M = {}
function _M.f()
    ngx.say("hello world")
end
return _M
]]
local lua = loadstring(s)
local ret, func = pcall(lua)
func.f()'

文字列sの内容は完全なLuaモジュールです。したがって、このモジュールのコードに変更があった場合、loadstringまたはloadfileで再ロードすることができます。これにより、その中の関数や変数が更新されます。

さらに進んで、変更の取得と再ロードをcode_loader関数と呼ばれる層でラップすることもできます。

local func = code_loader(name)

これにより、コードの更新が非常に簡潔になります。同時に、code_loaderは一般的にlru cacheを使用してsをキャッシュし、毎回loadstringを呼び出さないようにします。

質問2: OpenRestyはなぜブロッキング操作を禁止しないのか?

説明: 長年、これらのブロッキング呼び出しが公式に推奨されていないのになぜ無効にしないのか、またはユーザーが無効にすることを選択できるフラグを追加しないのか疑問に思っています。

私の個人的な意見です。まず、OpenRestyの周りのエコシステムが完璧ではないため、時にはブロッキングライブラリを呼び出して機能を実装する必要があります。例えば、バージョン1.15.8以前では、外部コマンドを呼び出すためにlua-resty-shellの代わりにLuaライブラリos.executeを使用する必要がありました。また、OpenRestyではファイルの読み書きはまだLua I/Oライブラリでのみ可能で、非ブロッキングの代替手段はありません。

次に、OpenRestyはこのような最適化に非常に慎重です。例えば、lua-resty-coreは長い間開発されてきましたが、デフォルトでは有効になっておらず、手動でrequire 'resty.core'を呼び出す必要がありました。最新の1.15.8リリースまで有効になりませんでした。

最後に、OpenRestyのメンテナは、コンパイラとDSLを通じて高度に最適化されたLuaコードを自動生成することでブロッキング呼び出しを標準化することを好んでいます。したがって、OpenRestyプラットフォーム自体でフラグオプションのようなものを実装する努力はありません。もちろん、この方向性が問題を解決できるかどうかはわかりません。

外部開発者の視点から見ると、より実践的な問題は、このようなブロッキングをどのように避けるかです。luacheckなどのLuaコード検出ツールを拡張して、一般的なブロッキング操作を見つけて警告することができます。または、_Gを書き換えて特定の関数を直接無効にしたり、書き換えたりすることもできます。例えば:

resty -e '_G.ngx.print = function()
ngx.say("hello")
end
ngx.print()'

# hello

このサンプルコードを使用すると、ngx.print関数を直接書き換えることができます。

質問3: LuaJITのNYI操作はパフォーマンスに大きな影響を与えるか?

説明: loadstringはLuaJITのNYIリストでneverと表示されています。これはパフォーマンスに大きな影響を与えますか?

LuaJITのNYIについては、あまり厳密になる必要はありません。JIT可能な操作については、JITアプローチが自然に最適です。しかし、まだJITできない操作については、引き続き使用できます。

パフォーマンス最適化については、統計に基づいた科学的なアプローチを取る必要があります。これがフレームグラフサンプリングの目的です。早すぎる最適化はすべての悪の根源です。多くの呼び出しを行い、多くのCPUを消費するホットコードのみを最適化する必要があります。

loadstringに戻ると、コードが変更されたときにのみ再ロードするために呼び出し、リクエスト時に呼び出すわけではないため、頻繁な操作ではありません。この時点では、システム全体のパフォーマンスへの影響を心配する必要はありません。

2番目のブロッキング問題と関連して、OpenRestyではinitおよびinit workerフェーズでブロッキングファイルI/O操作を呼び出すこともあります。この操作はNYIよりもパフォーマンスに影響を与えますが、サービスが起動するときに一度だけ実行されるため、許容範囲内です。

いつものように、パフォーマンス最適化はマクロな視点から見る必要があります。そうでないと、特定の詳細にこだわることで、長い時間をかけて最適化しても良い効果が得られない可能性があります。

質問4: 動的なアップストリームを自分で実装できますか?

説明: 動的なアップストリームについて、私のアプローチは、サービスに対して2つのアップストリームを設定し、ルーティング条件に応じて異なるアップストリームを選択し、マシンのIPが変更されたときにアップストリームのIPを直接変更することです。このアプローチは、balancer_by_luaを直接使用する場合と比較して、どのような欠点や落とし穴がありますか?

balancer_by_luaの利点は、ユーザーがロードバランシングアルゴリズムを選択できることです。例えば、roundrobinchashを使用するか、ユーザーが実装した他のアルゴリズムを使用するか、柔軟で高性能です。

ルーティングルールの方法で行う場合、結果は同じです。ただし、アップストリームのヘルスチェックは自分で実装する必要があり、多くの追加作業が発生します。

この質問を拡張して、abtestの場合、異なるアップストリームが必要な場合にどのように実装すべきかを尋ねることもできます。

urihostparametersなどに基づいて、balancer_by_luaフェーズでどのアップストリームを使用するかを決定できます。また、APIゲートウェイを使用してこれらの判断をルーティングルールに変換し、初期のaccessフェーズでどのルートを使用するかを決定し、ルートとアップストリームのバインディング関係を通じて指定されたアップストリームを見つけることもできます。これはAPIゲートウェイの一般的なアプローチであり、後でハンズオンセクションで具体的に説明します。

質問5: shared dictのキャッシュは必須ですか?

説明:

実際の生産アプリケーションでは、共有ディクショナリ層のキャッシュは必須だと思います。誰もがlru cacheの良さだけを覚えているようで、データ形式に制限がなく、デシリアライズする必要がなく、k/vの量に基づいてメモリスペースを計算する必要がなく、ワーカー間の競争がなく、読み取り/書き込みロックがなく、高性能です。

しかし、その最も致命的な弱点の1つは、lru cacheのライフサイクルがWorkerに従うことです。NGINXがリロードするたびに、このキャッシュ部分は完全に失われ、この時点でshared dictがない場合、L3データソースはすぐにダウンします。

もちろん、これはより高い並行性の場合ですが、キャッシュが使用されている場合、ビジネス量は確かに小さくないため、前述の分析は依然として適用されます。この見方が正しい場合、共有ディクショナリは必須ですか?

いくつかのケースでは、あなたが言うように、shared dictはリロード中に失われないため、必要です。しかし、特定のケースでは、initフェーズまたはinit_workerフェーズでL3データソースからすべてのデータをアクティブに取得できる場合、lru cacheのみが許容されます。

例えば、オープンソースのAPIゲートウェイAPISIXは、データソースがetcdにある場合、etcdからのみデータを取得します。init_workerフェーズでlru cacheにキャッシュし、後でetcdwatchメカニズムを通じてキャッシュ更新をアクティブに取得します。これにより、NGINXがリロードしても、キャッシュスタンピードは発生しません。

したがって、技術を選択する際には好みを持つことができますが、絶対的に一般化しないでください。すべてのキャッシュシナリオに適合する銀の弾丸はありません。実際のシナリオのニーズに応じて最小限の利用可能なソリューションを構築し、それを徐々に増やしていくことが良い方法です。