プロトコル拡張 - ユースケース

プロトコル拡張は、次のユースケースに使用できます。

  • メッセージベースの負荷分散 (MBLB)
  • ストリーミング
  • トークンベースの負荷分散
  • ロード・バランシング・パーシステンス
  • TCP 接続ベースのロードバランシング
  • コンテンツベースの負荷分散
  • SSL
  • トラフィックを変更
  • クライアントまたはサーバーへのトラフィックの発信
  • 接続確立時のデータ処理

メッセージベースの負荷分散

プロトコル拡張はメッセージベースの負荷分散(MBLB)をサポートしており、NetScalerアプライアンス上の任意のプロトコルを解析し、1つのクライアント接続で届くプロトコルメッセージの負荷分散、つまりメッセージを複数のサーバー接続に分散できます。MBLB は、クライアントの TCP データストリームを解析するユーザーコードによって実現されます。

TCP データストリームは、クライアントとサーバーの動作の on_data コールバックに渡されます。TCPデータストリームは、Lua文字列のようなインターフェイスを介して拡張機能で使用できます。Lua 文字列 API に似た API を使用して TCP データストリームを解析できます。

便利な API には以下が含まれます。

data:len()

data:find()

data:byte()

data:sub()

data:split()

TCP データストリームが解析されてプロトコルメッセージになったら、ユーザーコードは、クライアントの on_data コールバックに渡されたコンテキストから利用可能な次のコンテキストにプロトコルメッセージを送信するだけで、負荷分散を実現します。

ns.send () API は、他の処理モジュールにメッセージを送信するために使用されます。送信先コンテキストに加えて、送信APIはイベント名とオプションのペイロードを引数として取ります。イベント名と動作のコールバック関数名は 1 対 1 で対応しています。<event_name>イベントのコールバックは on_ と呼ばれます。コールバック名は小文字のみを使用します。

たとえば、TCP クライアントとサーバーの on_data コールバックは、「DATA」という名前のイベントのユーザー定義ハンドラーです。1 回の送信呼び出しでプロトコルメッセージ全体を送信するには、EOM イベントが使用されます。EOM は end of message の略で、LB コンテキストのダウンストリームへのプロトコルメッセージの終了を意味します。そのため、このメッセージに続くデータについては、新たに負荷分散の決定が行われます。

拡張コードが on_data イベントでプロトコルメッセージ全体を受け取らないことがあります。このような場合、ctx: hold () API を使用してデータを保持できます。hold API は、TCP クライアントコンテキストとサーバコールバックコンテキストの両方で使用できます。「hold with data」が呼び出されると、データはコンテキストに保存されます。同じコンテキストでさらにデータを受信すると、新しく受信したデータが以前に保存されたデータに追加され、結合されたデータを使用して on_data コールバック関数が再度呼び出されます。

注: 使用される負荷分散方法は、負荷分散コンテキストに対応する負荷分散仮想サーバーの構成によって異なります。

次のコードスニペットは、send API を使用して解析されたプロトコルメッセージを送信する方法を示しています。

例:

    function client.on_data(ctxt, payload)
        --
        -- code to parse payload.data into protocol message comes here
        --
        -- sending the message to lb
        ns.send(ctxt.output, "EOM", {data = message})
    end -- client.on_data

    function server.on_data(ctxt, payload)
        --
        -- code to parse payload.data into protocol message comes here
        --
        -- sending the message to client
        ns.send(ctxt.output, "EOM", {data = message})

    end -- server.on_data
<!--NeedCopy-->

ストリーミング

シナリオによっては、プロトコルメッセージ全体が収集されるまで TCP データストリームを保持する必要がない場合があります。実際、必要な場合以外はお勧めしません。データを保持すると、NetScalerアプライアンスのメモリ使用量が増加し、多くの接続で不完全なプロトコルメッセージでNetScalerアプライアンスのメモリが使い果たされ、アプライアンスがDDoS攻撃を受けやすくなります。

ユーザーは、send API を使用して拡張コールバックハンドラーで TCP データのストリーミングを実現できます。メッセージ全体が収集されるまでデータを保持する代わりに、データをまとめて送信できます。DATA イベントを使用して ctxt.output にデータを送信すると、プロトコルメッセージの一部が送信されます。その後にさらに多くの DATA イベントが続くことがあります。プロトコルメッセージの終了を知らせるには、EOM イベントを送信する必要があります。下流の負荷分散コンテキストは、最初に受信したデータに対して負荷分散の決定を行います。EOM メッセージの受信後に、新しい負荷分散の決定が行われます。

プロトコルメッセージデータをストリーミングするには、複数の DATA イベントの後に EOM イベントを送信します。連続した DATA イベントと次の EOM イベントは、シーケンスの最初の DATA イベントの負荷分散決定によって選択された同じサーバー接続に送信されます。

クライアントへの送信コンテキストでは、下流のクライアントコンテキストによる EOM イベントの特別な処理がないため、EOM イベントと DATA イベントは実質的に同じです。

トークンベースの負荷分散

ネイティブにサポートされているプロトコルについては、NetScalerアプライアンスはPI式を使用してトークンを作成するトークンベースの負荷分散方法をサポートしています。拡張機能の場合、プロトコルが事前にわからないため、PI 表現は使用できません。トークンベースの負荷分散では、USER_TOKEN 負荷分散メソッドを使用するようにデフォルトの負荷分散仮想サーバーを設定し、user_token フィールドを指定して send API を呼び出して拡張コードからトークン値を指定する必要があります。トークン値が送信APIから送信され、USER_TOKEN負荷分散方法がデフォルトの負荷分散仮想サーバーで構成されている場合、負荷分散の決定はトークン値に基づいてハッシュを計算することによって行われます。トークン値の最大長は 64 バイトです。

add lb vserver v\_mqttlb USER\_TCP –lbMethod USER\_TOKEN

次の例のコードスニペットは、送信 API を使用して LB トークン値を送信します。

例:

        -- send the message to lb




        -- user_token is set to do LB based on clientID




        ns.send(ctxt.output, "EOM", {data = message,

                                 user_token = token_info})
<!--NeedCopy-->

ロード・バランシング・パーシステンス

負荷分散の永続性は、トークンベースの負荷分散と密接に関係しています。ユーザーは、永続性セッションの値をプログラムで計算し、それを負荷分散の持続性に使用できなければなりません。send API はパーシスタンスパラメータの送信に使用されます。負荷分散永続性を使用するには、デフォルトの負荷分散仮想サーバーで USERSESSION 永続性タイプを設定し、user_session フィールドを指定して send API を呼び出して拡張コードから永続性パラメーターを指定する必要があります。パーシスタンスパラメータ値の最大長は 64 バイトです。

カスタムプロトコルに複数のタイプのパーシステンスが必要な場合は、ユーザーパーシステンスタイプを定義して設定する必要があります。仮想サーバーの構成に使用されるパラメータの名前は、プロトコル実装者によって決定されます。パラメータの設定値は、拡張コードでも使用できます。

次の CLI とコードスニペットは、ロードバランシングの永続性をサポートする送信 API の使用方法を示しています。 mqtt.lua のコードリストのセクションにあるコードリストでは 、user_session フィールドの使用方法も示しています。

永続性については、負荷分散仮想サーバーで USERSESSION 永続性タイプを指定し、ns.send API から user_session 値を渡す必要があります。

add lb vserver v\_mqttlb USER\_TCP –persistencetype USERSESSION

ペイロードの user_session フィールドを ClientID に設定して、MQTT メッセージをロードバランサーに送信します。

例:

-- send the data so far to lb

-- user_session is set to clientID as well (it will be used to persist session)

ns.send(ctxt.output, “DATA”, {data = data, user_session = clientID})
<!--NeedCopy-->

TCP 接続ベースのロードバランシング

一部のプロトコルでは、MBLB が必要ない場合があります。代わりに、TCP 接続ベースの負荷分散が必要な場合があります。たとえば、MQTT プロトコルは TCP ストリームの最初の部分を解析して、負荷分散用のトークンを決定する必要があります。また、同じ TCP 接続上のすべての MQTT メッセージは、同じサーバー接続に送信する必要があります。

TCP 接続ベースの負荷分散は、DATA イベントのみで EOM を送信せずに send API を使用することで実現できます。これにより、ダウンストリームのロードバランシングコンテキストは、最初に受信したデータに基づいてロードバランシングを決定し、後続のすべてのデータをロードバランシング決定によって選択された同じサーバー接続に送信します。

また、ユースケースによっては、負荷分散の決定後に拡張処理をバイパスする機能が必要になる場合があります。拡張呼び出しをバイパスすると、トラフィックがネイティブコードだけで処理されるため、パフォーマンスが向上します。バイパスは ns.pipe () API を使用して実行できます。pipe () API 拡張コードを呼び出すと、入力コンテキストを出力コンテキストに接続できます。pipe () の呼び出し後、入力コンテキストからのすべてのイベントは直接出力コンテキストに送られます。事実上、pipe () 呼び出しが行われたモジュールはパイプラインから削除されます。

次のコードスニペットは、ストリーミングと pipe () API を使用してモジュールをバイパスする方法を示しています。 mqtt.lua のコードリストのセクションにあるコードリストは 、ストリーミングを行う方法と pipe () API を使用して接続上の残りのトラフィックに対してモジュールをバイパスする方法も示しています。

例:

        -- send the data so far to lb
        ns.send(ctxt.output, "DATA", {data = data,
                                       user_token = clientID})
        -- pipe the subsequent traffic to the lb - to bypass the client on_data handler
        ns.pipe(ctxt.input, ctxt.output)
<!--NeedCopy-->

コンテンツベースの負荷分散

ネイティブプロトコルでは、プロトコル拡張用のコンテンツスイッチング機能のような機能がサポートされています。この機能を使用すると、データをデフォルトのロードバランサーに送信する代わりに、選択したロードバランサーにデータを送信できます。

プロトコル拡張用のコンテンツスイッチング機能は、ctxt: lb_connect () API を使用することで実現されます。<lbname> この API は TCP クライアントコンテキストで使用できます。この API を使用すると、拡張コードは、すでに構成されている負荷分散仮想サーバーに対応する負荷分散コンテキストを取得できます。その後、取得した負荷分散コンテキストで送信 API を使用できます。

lb コンテキストは NULL になることがあります。

  • 仮想サーバーは存在しません
  • 仮想サーバーはユーザープロトコルタイプではありません
  • 仮想サーバーの状態は UP ではありません
  • 仮想サーバーはユーザー仮想サーバーであり、負荷分散仮想サーバーではありません

使用中にターゲットの負荷分散仮想サーバーを削除すると、その負荷分散仮想サーバーに関連するすべての接続がリセットされます。

次のコードスニペットは、lb_connect () API の使用方法を示しています。このコードは、Lua テーブル lb_map を使用してクライアント ID を負荷分散仮想サーバー名 (lbname) にマッピングし、次に lb_connect () を使用して lbname の LB コンテキストを取得します。そして最後に send API を使用して LB コンテキストに送信します。

    local lb_map = {
       ["client1*"] = "lb_1",
       ["client2*"] = "lb_2",
       ["client3*"] = "lb_3",
       ["client4*"] = "lb_4"
    }

    -- map the clientID to the corresponding LB vserver and connect to it
    for client_pattern, lbname in pairs(lb_map) do
       local match_idx = string.find(clientID, client_pattern)
       if (match_idx == 1) then
      lb_ctxt = ctxt:lb_connect(lbname)
      if (lb_ctxt == nil) then
         error("Failed to connect to LB vserver: " .. lbname)
      end
      break
       end
    end
    if (lb_ctxt == nil) then
    -- If lb context is NULL, the user can raise an error or send data to default LB
       error("Failed to map LB vserver for client: " .. clientID)
    end
-- send the data so far to lb
ns.send(lb_ctxt, "DATA", {data = data}
<!--NeedCopy-->

SSL

拡張機能を使用するプロトコルの SSL は、ネイティブプロトコルの SSL のサポートと同様の方法でサポートされます。同じ解析コードを使用してカスタムプロトコルを作成すると、TCP または SSL を介してプロトコルインスタンスを作成し、それを使用して仮想サーバーを構成できます。同様に、TCP または SSL 経由でユーザーサービスを追加できます。

詳細については、 MQTT の SSL オフロードの設定およびエンドツーエンド暗号化を使用したMQTT の SSL オフロードの設定を参照してください

サーバー接続の多重化

クライアントは一度に 1 つの要求を送信し、最初の要求に対する応答がサーバーから受信された後にのみ次の要求を送信することがあります。このような場合、サーバー接続を他のクライアント接続に再利用できます。また、応答がクライアントに送信された後の同じ接続上の次のメッセージにも再利用できます。他のクライアント接続によるサーバー接続の再利用を許可するには、サーバー側コンテキストで ctxt: reuse_server_connection () API を使用する必要があります。

:このAPIはNetScaler 12.1ビルド49.xx以降で使用できます。

トラフィックを変更

リクエストまたはレスポンス内のデータを変更するには、高度なポリシー PI 表現を使用するネイティブの書き換え機能を使用する必要があります。拡張で PI 式を使用できないため、次の API を使用して TCP ストリームデータを変更できます。

data:replace(offset, length, new_string)
data:insert(offset, new_string)
data:delete(offset, length)
data:gsub(pattern, replace [,n]))

次のコードスニペットは、replace () API の使用方法を示しています。

-- Get the offset of the pattern, we want to replace
   local old_pattern = “pattern to repalace”
local old_pattern_length = old_pattern:len()
   local pat_off, pat_end = data:find(old_pattern)
   -- pattern is not present
if (not pat_off) then
    goto send_data
   end
  -- If the data we want to modify is not completely present, then
  -- wait for more data
  if (not pat_end) then
        ctxt:hold(data)
        data = nil
     goto done
  end
data:replace(pat_off, old_pattern_length, “new pattern”)
::send_data::
ns.send(ctxt.output, “EOM”, {data = data})
::done::

次のコードスニペットは、insert () API の使用方法を示しています。

data:insert(5, “pattern to insert”)

次のコードスニペットは、いくつかのパターンの前または後に挿入したいときに、insert()APIの使用を示しています。

-- Get the offset of the pattern, after or before which we want to insert
   local pattern = “pattern after/before which we need to insert”
local pattern_length = pattern:len()
   local pat_off, pat_end = data:find(pattern)
-- pattern is not present
   if (not pat_off) then
    goto send_data
   end
  -- If the pattern after which we want to insert is not
  -- completely present, then wait for more data
  if (not pat_end) then
        ctxt:hold(data)
        data = nil
     goto done
  end
-- Insert after the pattern
data:insert(pat_end + 1, “pattern to insert”)
   -- Insert before the pattern
data:insert(pat_off, “pattern to insert”)
::send_data::
    ns.send(ctxt.output, “EOM”, {data = data})
::done::

次のコードスニペットは、delete () API の使用方法を示しています。

-- Get the offset of the pattern, we want to delete
   local delete_pattern = “pattern to delete”
local delete_pattern_length = delete_pattern:len()
   local pat_off, pat_end = data:find(old_pattern)
   -- pattern is not present
if (not pat_off) then
          goto send_data
   end
  -- If the data we want to delete is not completely present,
  -- then wait for more data
  if (not pat_end) then
        ctxt:hold(data)
        data = nil
    goto done
  end
data:delete(pat_off, delete_pattern_length)
::send_data::
ns.send(ctxt.output, “EOM”, {data = data})
::done::

次のコードスニペットは、gsub()APIの使用を示しています。

    -- Replace all the instances of the pattern with the new string
data:gsub(“old pattern”, “new string”)
-- Replace only 2 instances of “old pattern”
data:gsub(“old pattern”, “new string”, 2)
-- Insert new_string before all instances of “http”
data:gsub(“input data”, “(http)”, “new_string%1”)
-- Insert new_string after all instances of “http”
data:gsub(“input data”, “(http)”, “%1new_string”)
-- Insert new_string before only 2 instances of “http”
data:gsub(“input data”, “(http)”, “new_string%1”, 2)

:このAPIは、NetScaler 12.1ビルド50.xx以降で使用できます。

クライアントまたはサーバーへのトラフィックの発信

ns.send () API を使用して、拡張コードから生成されたデータをクライアントとバックエンドサーバーに送信できます。クライアントコンテキストからクライアントと直接応答を送受信するには、ターゲットとして ctxt.client を使用する必要があります。サーバーコンテキストからバックエンドサーバーと直接応答を送受信するには、ctxt.server をターゲットとして使用する必要があります。ペイロード内のデータは、TCP ストリームデータでも Lua 文字列でもかまいません。

接続上のトラフィック処理を停止するには、クライアントまたはサーバーコンテキストのいずれかから ctxt: close () API を使用できます。この API は、クライアント側の接続またはそれにリンクされているサーバー接続を閉じます。

ctx: close () APIを呼び出すと、拡張コードはクライアントとサーバーの接続にTCP FINパケットを送信し、この接続でクライアントまたはサーバーからさらにデータを受信すると、アプライアンスは接続をリセットします。

次のコードスニペットは、ctxt.clientとctxtの使用方法を示しています。クローズ()API。

    -- If the input packet is not MQTT CONNECT type, then
-- send some error response to the client.
function client.on_data(ctxt, payload)
    local data = payload.data
    local offset = 1
    local msg_type = 0
    local error_response = “Missing MQTT Connect packet.”
    byte = data:byte(offset)
msg_type = bit32.rshift(byte, 4)
if (msg_type ~= 1) then
-- Send the error response
   ns.send(ctxt.client, “DATA”, {data = error_response})
-- Since error response has been sent, so now close the connection
    ctxt:close()
end

次のコードスニペットは、ユーザーが通常のトラフィックフローにデータを注入できる例を示しています。

-- After sending request, send some log message to the server.
function client.on_data(ctxt, payload)
local data = payload.data
local log_message = “client id : “..data:sub(3, 7)..” user name : “ data:sub(9, 15)
-- Send the request we get from the client to backend server
ns.send(ctxt.output, “DATA”, {data = data})
After sending the request, also send the log message
ns.send(ctxt.output, “DATA”, {data = log_message”})
end

次のコードスニペットは、ctxt.to_server APIの使用方法を示しています。

-- If the HTTP response status message is “Not Found”,
-- then send another request to the server.
function server.on_data(ctxt, payload)
    local data = payload.data
    local request “GET /default.html HTTP/1.1\r\n\r\n”ss
    local start, end = data:find(“Not Found”)
    if (start) then
    -- Send the another request to server
        ns.send(ctxt.server, “DATA”, {data = request})
end

:このAPIは、NetScaler 12.1ビルド50.xx以降で使用できます。

接続確立時のデータ処理

接続確立時(最終ACKを受信したとき)にデータを送信したいユースケースがあるかもしれません。たとえば、プロキシプロトコルでは、接続確立時にクライアントの送信元と宛先のIPアドレスとポートをバックエンドサーバーに送信したい場合があります。この場合、client.init () コールバックハンドラを使用して、接続確立時にデータを送信できます。

次のコードスニペットは、client.init () コールバックの使用方法を示しています。

-- Send a request to the next processing context
-- on the connection establishment.
function client.init(ctxt)
   local request “PROXY TCP4” + ctxt.client.ip.src.to_s + “ “ + ctxt.client.ip.dst.to_s + “ “ + ctxt.client.tcp.srcport + “ “ +     ctxt.client.tcp.dstport
-- Send the another request to server
   ns.send(ctxt.output, “DATA”, {data = request})
  end

:このAPIは、NetScaler 13.0ビルドxx.xx以降で使用できます。