ニフクラ ブログ

ニフクラ/FJ Cloud-Vやクラウドの技術について、エンジニアが語るブログです。

ニフクラスクリプトでマルチロードバランサーを監視/自動スケールアップ

こんにちは、CRE部 技術支援チームです。

ニフクラのロードバランサー(L4)マルチロードバランサーは、様々な場面でご活用いただいており、サーバー冗長化、可用性の向上、負荷分散等に欠かせないコンポーネントです。

そんなロードバランサーですが、どれくらいのスペックで構築を進めたらよいか、悩まれたことはないでしょうか?(ロードバランサーに限った話ではないですが。。)
オンプレミス環境からの移行であれば、既存環境のトラフィックや負荷状況を把握でき、新環境でのサイジングに活かすことができますが、新規サービス立ち上げ等なかなか推測の難しいケースがあります。 最初からスペック不足だとサービスの利用に支障が出てしまいますので、たいていのお客様は、余裕を持ったスペックを選定しますが、高いスペックのものを用意することは当然のことながらコストが高くなるというデメリットにつながります。

ニフクラのロードバランサーは、「最大ネットワーク流量」でスペックを選択し、その流量で料金が変動します。流量に関しては注意点があり、契約している最大ネットワーク流量を超過した際、設定した流量を超えた通信はドロップされる可能性があります。これはロードバランサーがボトルネックになってしまうことを意味します。なるべくコストは抑えつつ、ボトルネックとならないような仕組みを実装できれば課題が解決できそうです。

今回は、これらのような課題を解決するための方法として、ニフクラスクリプトを活用したマルチロードバランサーの自動スケールアップをテーマに、省コストかつ安定運用ができるか、検証してみたいと思います。

構成イメージ

検証構成

本検証の概要は以下の通りです。

  • マルチロードバランサーのネットワーク流量を定期的に確認し、しきい値を超えた場合に流量をアップグレードするスクリプトを実装する(自動スケールアップ)
  • 流量が下がった場合、元のスペックに戻る仕組みもあわせて実装する(自動スケールダウン)

マルチロードバランサーの自動スケールアップ

前提条件

本記事は、以下の前提知識がある方を想定しています。

  • ニフクラの基本的なコントロールパネルの操作、サービス利用に関する知識
  • Linuxの基本的な操作、設定に関する知識
  • Windows Serverの基本的な操作、設定に関する知識

利用リソース

east-14にサーバーを5台作成し、プライベートLANで接続します。

リソース 数量
仮想サーバー (OS:Rocky Linux 8.6) 4
仮想サーバー (OS:Windows Server 2019 Standard) 1
マルチロードバランサー 1
プライベートLAN 2

環境構築

1. プライベートLANの作成

各サーバー間通信の経路として、プライベートLANを作成します。
本検証ではプライベートLANに付与する役割、IPアドレス帯を以下の通りとしています。

プライベートLAN名 CIDR 用途
PLAN100 192.168.100.0/24 ジョブ管理サーバー/クライアント用
PLAN200 192.168.200.0/24 マルチロードバランサー/トラフィックジェネレータ-用

※Webサーバー ~ マルチロードバランサー間は、共通プライベートLANを使用しています。

作成方法の詳細は以下をご参照ください。
クラウドヘルプ(プライベートLAN:作成)

2. 仮想サーバーの作成

各サーバーのホスト名と役割は以下の通りです。

ホスト名 役割 OS アプリケーション プライベートIP
ASCOpeM ジョブ管理実行サーバー Rocky Linux 8.6 Systemwalker Operation Manager Server V17.0.1 192.168.100.130
ASCOpeMNG ジョブ管理実行クライアント Windows Server 2019 Standard Systemwalker Operation Manager Client V17.0.1 192.168.100.100
ASCWeb01 Webサーバー1 Rocky Linux 8.6 Apache HTTP Server 2.4.37 共通プライベートIPを利用
ASCWeb02 Webサーバー2 Rocky Linux 8.6 Apache HTTP Server 2.4.37 共通プライベートIPを利用
ASCtestSV トラフィックジェネレータ― Rocky Linux 8.6 Taurus (JMeter) 1.16.19 192.168.200.100

今回はRocky Linuxを使用しているため、SSHキーの作成が必須となります。 また、本検証ではサーバーに適用するファイアウォールには以下の通信を許可するよう設定しています。

ファイアウォール設定:ASCOpeM(ジョブ管理実行サーバー)

INルール
プロトコル ポート 接続元 用途
TCP any 192.168.100.0/24 サーバー間接続

ファイアウォール設定:ASCOpeMNG (ジョブ管理実行クライアント)

INルール
プロトコル ポート 接続元 用途
TCP 3389 作業端末のグローバルIPアドレス RDP接続
TCP any 192.168.100.0/24 サーバー間接続

ファイアウォール設定:ASCWeb01,02 (Webサーバー1,2)

INルール
プロトコル ポート 接続元 用途
TCP 80 マルチロードバランサーの送信元IPアドレス マルチロードバランサーとの接続

ファイアウォール設定:ASCtestSV (トラフィックジェネレータ―)

INルール
プロトコル ポート 接続元 用途
TCP 22 作業端末のグローバルIPアドレス SSH接続

作成方法の詳細は以下をご参照ください。 
クラウドヘルプ(SSHキー)
クラウドヘルプ(サーバーの作成)
クラウドヘルプ(ファイアウォールグループの新規作成)

3. Webサーバーの準備

Webサーバーを用意します。 本検証では Webサーバー(ASCWeb01,02)にてApache HTTP Serverを起動し、テスト用のhtmlファイルを配置しています。

ここでは設定項目や設定値は省略します。

4. マルチロードバランサーの準備

テストトラフィック受付用のマルチロードバランサーを用意します。 最大ネットワーク流量を最小の10Mbpsとし、http(port:80)で受け付けたトラフィックを、Webサーバーへ転送する設定としています。

ここでは設定項目や設定値の詳細は省略します。

作成方法の詳細は以下をご参照ください。 

クラウドヘルプ(マルチロードバランサーの作成)

5. トラフィックジェネレータ―の準備

本検証では トラフィックジェネレータ―(ASCtestSV)にてTaurusをインストールし、テスト用のシナリオを設定しています。 YAMLまたはJSONで自在にシナリオを作成できますが、今回はシンプルに以下の設定としました。

tgen.yml

execution:
- concurrency: 100     // 同時接続仮想ユーザー数
  ramp-up: 1m         // 設定したconcurrencyに到達するまでの時間
  hold-for: 120m        // 設定したconcurrencyの継続時間
  scenario: quick-test  // シナリオ名

scenarios:
  quick-test:
    requests:
    - http://192.168.200.222  // リクエストの送信先(マルチロードバランサーのVIP宛)

Taurusのインストールや操作方法については、以下をご参照ください。 https://gettaurus.org/install/Installation/ (外部サイトのため、リンク切れの際はご容赦ください)

設定完了後、マルチロードバランサー宛にテストトラフィックを送信し、Webサーバーまで到達できることと、テストシナリオ通りのトラフィックが印加できていることを確認しておきます。

6. 自動スケールアップスクリプトの準備

今回の検証では、マルチロードバランサーの10分間の平均流量が設定されている流量の80%を超過していた場合に、マルチロードバランサーの帯域を1ステップ増やす(またはその逆)という動作を、ニフクラスクリプトにて実装します。

本検証での登録内容はRubyで作成しております。

スクリプト登録

検証で使用したスクリプトサンプルはこちらです。

スクリプトサンプル(クリックすると展開されます) update_mlb_scale.rb

require 'rack'
require 'httparty'
require 'base64'
require 'rexml/document'
require 'uri'
require 'pp'
require 'open-uri'
require 'time'

ENDPOINT='https://jp-east-1.computing.api.nifcloud.com/api/'
TRAFFIC_LIST=[10,20,30,40,100,200,300,400,500]
ELBNAME='ASCMLB'
ELBRESOURCE='ASCMLB:TCP:80'
THRESHOLD_MIN=20 # 20min

def openURL(url)
  return HTTParty.get(url).body
end

def getURISortquery(query:)
  sort_query_str=""
  for key in query.keys.sort
    value = URI.encode_www_form_component(query[key])
    sort_query_str += "#{key}=#{value}&"
  end
  sort_query_str.delete_suffix!('&')
  return sort_query_str
end

def createQuery(method:, action:, endpoint:, query:, access_key:, secret_access_key:)
  return "" if !query.is_a?(Hash)

  host=URI.parse(endpoint).host
  path=URI.parse(endpoint).path

  t = Time.now
  t.localtime("+09:00")
  date=t.strftime("%FT%H:%M:%S")
  query_array = {
                  "Action" => action,
                  "SignatureVersion" => 2,
                  "SignatureMethod" => 'HmacSHA256',
                  "AccessKeyId" => access_key,
                  "Timestamp" => date,
                }
  query_array.merge!(query)
  query_str = getURISortquery( query: query_array )

  string_to_sign = "#{method}\n#{host}\n#{path}\n"
  string_to_sign += query_str

  hash = Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', secret_access_key, string_to_sign))
  query_str += "&Signature=#{URI.encode_www_form_component(hash)}"
  return query_str
end

def getResultLastMinutes(access_key,secret_key,threshold_minutes:)
  action='NiftyDescribePerformanceChart'
  query={}
  query['FunctionName'] = 'ElasticLoadBalancer'
  query['ResourceName.1'] = ELBRESOURCE
  query['DataType.1'] = 'network'
  query['ValueType'] = 1


  query_str = createQuery(
                :method=>'GET',
                :endpoint=>ENDPOINT,
                :action=>action,
                :query=>query,
                :access_key=>access_key,
                :secret_access_key=>secret_key
              )

  result = REXML::Document.new(openURL(ENDPOINT+'?'+query_str))
  diff_list={"in_traffic"=>[],"out_traffic"=>[]}
  t = Time
  nowtime = Time.now
  result.elements.each('NiftyDescribePerformanceChartResponse/performanceChartSet/item') do |resource|
    key = ""
    if resource.elements['dataType'].text == "network(in)"
      key = "in_traffic"
    else
      key = "out_traffic"
    end
    resource.elements.each('dataSet/item') do |item|
      log_utctime = t.parse(item.elements['dateTime'].text)-32400#JCT->UTC(-9h)
      diff_time =  nowtime - log_utctime
      # Record if time in check range
      if diff_time <= (60*threshold_minutes)#conver second
        traffic = item.elements['value'].text.to_f/1000000#bps->Mbps
        puts traffic
        diff_list[key].push(traffic)
      end
    end
  end
  return diff_list
end

def getNowMLBLimitTraffic(access_key,secret_key)
  action='NiftyDescribeElasticLoadBalancers'
  query={}
  query['ElasticLoadBalancers.ElasticLoadBalancerName.1'] = ELBNAME
  query['ElasticLoadBalancers.Protocol.1'] = 'TCP'
  query['ElasticLoadBalancers.ElasticLoadBalancerPort.1'] = '80'


  query_str = createQuery(
                :method=>'GET',
                :endpoint=>ENDPOINT,
                :action=>action,
                :query=>query,
                :access_key=>access_key,
                :secret_access_key=>secret_key
              )
  result = ""
  result = REXML::Document.new(openURL(ENDPOINT+'?'+query_str))
  limit_traffic = result.elements['//NetworkVolume'].text
  return limit_traffic.to_i
end

def calcUpNextTraffic(traffic)
  # Calc Next Traffic Step
  return TRAFFIC_LIST[TRAFFIC_LIST.index(traffic)+1]
end

def calcDownNextTraffic(traffic)
  # Calc Next Traffic Step
  return TRAFFIC_LIST[TRAFFIC_LIST.index(traffic)-1]
end

def isScaleUp(traffic_list,limit_traffic)
  return false if traffic_list.all?{|k,v| v.size <= 0 }
  # all metric check loop
  traffic_list.each_value do|value|
    avg_traffic = value.sum(0.0)/value.size
    # up/down metric over 80% of limit capacity?
    if avg_traffic > (limit_traffic * 0.8)
      return true
    end
  end
  return false
end

def isScaleDown(traffic_list,limit_traffic)
  return false if traffic_list.all?{|k,v| v.size <= 0 }
  return false if limit_traffic <= TRAFFIC_LIST.min
  result = true
  next_down_traffic = calcDownNextTraffic(limit_traffic)
  # all metric check loop
  traffic_list.each_value do|value|
    traffic_sum = 0
    value.each_with_index do |x,i|
      # if traffic is zero.use before traffic
      x = value[i-1] if x <= 1.0
      traffic_sum += x
    end
    avg_traffic = traffic_sum/value.size
    # up/down metric under 80% of limit capacity?
    if avg_traffic > (next_down_traffic * 0.8)
      result = false
    end
  end
  return result
end

def updateMLBTraffic(access_key,secret_key,set_traffic:)
  action='NiftyUpdateElasticLoadBalancer'
  query={}
  query['ElasticLoadBalancerName'] = ELBNAME
  query['NetworkVolumeUpdate'] = set_traffic
  puts "Update #{set_traffic}Mbps"


  query_str = createQuery(
                :method=>'GET',
                :endpoint=>ENDPOINT,
                :action=>action,
                :query=>query,
                :access_key=>access_key,
                :secret_access_key=>secret_key
              )
  result = ""
  openURL(ENDPOINT+'?'+query_str)
end

def configMLBLimitUp(access_key,secret_key,limit_traffic:)
  return false if limit_traffic >= TRAFFIC_LIST.max
  config_traffic = calcUpNextTraffic(limit_traffic)
  updateMLBTraffic(access_key, secret_key,
                   set_traffic: config_traffic)
  return true
end

def configMLBLimitDown(access_key,secret_key,limit_traffic:)
  return false if limit_traffic <= TRAFFIC_LIST.min
  config_traffic = calcDownNextTraffic(limit_traffic)
  updateMLBTraffic(access_key, secret_key,
                   set_traffic: config_traffic)
  return true
end

def call(env)
  retrun_status = 200
  result = ["Not Config\n"]
  headers = env.select{ |k, v| k.start_with?('HTTP_') }
  traffic_list = getResultLastMinutes(headers['HTTP_X_NIFCLOUD_ACCESS_KEY_ID'],
                                     headers['HTTP_X_NIFCLOUD_SECRET_ACCESS_KEY'],
                                     threshold_minutes:THRESHOLD_MIN)
  limit_traffic = getNowMLBLimitTraffic(headers['HTTP_X_NIFCLOUD_ACCESS_KEY_ID'],
                                        headers['HTTP_X_NIFCLOUD_SECRET_ACCESS_KEY'])
  if isScaleUp(traffic_list,limit_traffic)
    state = configMLBLimitUp(headers['HTTP_X_NIFCLOUD_ACCESS_KEY_ID'],
                             headers['HTTP_X_NIFCLOUD_SECRET_ACCESS_KEY'],
                             limit_traffic: limit_traffic)
    #state = true
    result = ["Scale Up\n"] if state
    retrun_status= 291 if state
  elsif isScaleDown(traffic_list,limit_traffic)
    state = configMLBLimitDown(headers['HTTP_X_NIFCLOUD_ACCESS_KEY_ID'],
                               headers['HTTP_X_NIFCLOUD_SECRET_ACCESS_KEY'],
                               limit_traffic: limit_traffic)
    #state = true
    result = ["Scale Down\n"] if state
    retrun_status= 292 if state
  end
  result.concat traffic_list["in_traffic"].map{|v| "in "+v.to_s+"\n"}
  result.concat traffic_list["out_traffic"].map{|v| "out "+v.to_s+"\n"}
  [retrun_status, {"Content-Type" => "text/plain"}, result]
end

スクリプトの内容で、環境によって変更すべき主な部分(変数)は以下の通りです。

変数名 設定内容
ENDPOINT スクリプト用エンドポイントのURLを入力
ELBNAME マルチロードバランサー名を入力
ELBRESOURCE マルチロードバランサー名、プロトコル、ポート番号を入力
THRESHOLD_MIN 何分平均を判断基準にするかを入力

作成方法の詳細は以下をご参照ください。 
スクリプト:スクリプトを作成する

7. ジョブ管理実行サーバー、クライアントの準備

ジョブ管理実行環境として、Systemwalker Operation Manager をインストールします。 今回はスクリプト実装の都合上、ジョブ管理実行サーバーにはLinux版、ジョブ管理実行クライアントにはWindows版を それぞれ導入しています。

ジョブ管理実行サーバーの準備(ASCOpeM)

ニフクラコマンドラインツールの設定

ジョブ管理実行サーバーにてスクリプト実行APIを定期実行するため、サーバーのローカルにリクエスト用のスクリプトを保存します。 今回はニフクラコマンドラインツールにて作成しております。

コマンドラインツールは別途インストール作業が必要となります。詳細は下記をご参照ください。 
クラウド CLI(コマンドラインツールについて) | ニフクラ

検証で使用したコマンドサンプルはこちらです。

コマンドサンプル(クリックすると展開されます) req_scripts.sh

nifcloud --endpoint-url https://script.api.nifcloud.com --region jp-east-1 script execute-script --script-identifier update_mlb_scale.rb --method GET --header "{\"X_NIFCLOUD_ACCESS_KEY_ID\":\"$ACCESS_KEY\",\"X_NIFCLOUD_SECRET_ACCESS_KEY\":\"$SECRET_ACCESS_KEY\"}"

上記コマンドサンプル内では、自動スケールアップスクリプト内部で利用しているアクセスキーおよびシークレットアクセスキーを、コマンドラインツール実行時に環境変数から取得し、headerとして引き渡しています。

ジョブ管理実行サーバーのインストール

インストールメディアをOSにマウント後、インストールコマンドを実行し、インストールパラメーターを選択します。

# /media/iso/Linux/unx/swsetup 

インストールの準備中です。
しばらくお待ちください。
...
--- 省略 ---

詳細の説明は省略しますが、システムのインストール要件、手順等はマニュアルをご参照ください。

システム再起動後、ジョブ管理実行サーバーのインストールは完了です。

ジョブ管理クライアントの準備(ASCOpeMNG)

ジョブ管理クライアントのインストール

クライアントからサーバーへの接続には、ホスト名を使用しますので、予めクライアント環境の hosts に サーバーのホスト名、IPアドレスを登録しておきます。

hosts

192.168.100.130  ASCOpeM

インストールメディアをOSにマウント後、インストールプログラムを実行します。 あとは流れに従ってクライアント機能のインストールを実施していきます。

詳細の説明は省略します。 システムのインストール要件、手順等はマニュアルをご参照ください。

ジョブ管理クライアントにて定期ジョブ登録(ASCOpeMNG)

ジョブ管理クライアントのインストール完了後、実際に動作させるジョブの内容を登録していきます。

  • スタートメニューから、「Systemwalker Operation Manager」を選択し、起動します。

ジョブの登録

  • ホスト名、ユーザーID、パスワードを入力後、ジョブ管理サーバーにログインします。ログインユーザーID、パスワードは、ジョブ管理サーバーのユーザー情報となります。
    • 本検証では動作確認のみのため、ルートアカウントで検証しています。

ジョブの登録

  • 管理画面起動後、ジョブ管理のためのプロジェクトを作成します。
    • 「ジョブスケジューラ」→ 「新規作成」→「プロジェクト」

ジョブの登録

  • 任意のプロジェクト名を設定します。

ジョブの登録

  • 作成したプロジェクトを右クリックし、ジョブ実行制御を定義するジョブネットというオブジェクトを作成します。
    • 「新規作成」→「ジョブネット」→「ジョブ実行制御」

ジョブの登録

  • ジョブネットの新規作成画面より、コマンド実行アイコン(赤枠)を選択します。

ジョブの登録

  • コマンド実行のジョブ詳細設定画面が開きますので、今回使用するスクリプト、場所を指定します。

ジョブの登録

  • 「詳細情報」タブにて、環境変数を設定します。

ジョブの登録

  • 今回使用するニフクラコマンドライン内の変数に対応するアクセスキー、シークレットアクセスキーを以下の通り設定しています。
変数名 変数値
ACCESS_KEY アクセスキーの値を入力
SECRET_ACCESS_KEY シークレットアクセスキーの値を入力

各アクセスキーの取得方法については、 クラウドヘルプ(アカウントメニュー:アカウント管理:アクセスキー) | ニフクラ をご参照ください。

  • これまで設定した内容を保存します。

ジョブの登録

  • 作成したジョブネットを右クリックし、プロパティを選択します。

ジョブの登録

  • 指定したジョブ(スクリプト)を起動する間隔を定義します。本検証では、9時~17時の間で10分間隔としました。

ジョブの登録

  • ジョブネットを右クリック、「起動日」を選択。

ジョブの登録

  • 起動する日程を指定します。

ジョブの登録

  • 最後に、作成したジョブネットを右クリックし、「起動」を選択します。

ジョブの登録

  • 実行結果が正常終了となっており、次の起動予定日時が表示されていれば定期ジョブ実行の準備は完了です。

負荷テストを実施してみる

それでは実際にトラフィックを流して動作確認してみたいと思います。

自動帯域UPの確認

  • ジョブ管理実行サーバーで定期ジョブが動作していることを確認後、トラフィックジェネレータ―からテストシナリオを開始します。

  • トラフィックの確認、スクリプトの発動は10分毎となりますので、少し状況を監視します。

①しばらくしてから状況を確認すると、スクリプトの確認結果でトラフィックの増量を検知し、マルチロードバランサーの最大帯域が 10Mbps から 20Mbps へ自動的に変わっていました。

アクティビティログから帯域UPを確認 10M -> 20M

②さらに時間をおいて、同じくトラフィック増量検知と、最大帯域UPを確認しました。(20Mbps から 30Mbpsへ)

アクティビティログから帯域UPを確認 20M -> 30M

③その後 30Mbps から 40Mbpsへ

アクティビティログから帯域UPを確認 30M -> 40M

一連の動作確認の結果、マルチロードバランサー側でのトラフィック量推移は以下の通りとなりました。

監視 - パフォーマンスチャート(マルチロードバランサー)

  • ※帯域変更のタイミングでトラフィックが落ち込んでいるように見えておりますが、パフォーマンスチャート表示上の仕様となります。実際には指定帯域でトラフィックを継続処理しています。

トラフィックジェネレータ―側の送信トラフィック量推移は以下です。

送信トラフィック量(トラフィックジェネレータ―)

マルチロードバランサー、トラフィックジェネレータ―共に、帯域変更のタイミング①②③にて前後の挙動がわかります。 それぞれ、帯域UPするまでは設定上限あたりで頭打ちとなり、自動帯域UP後に次の設定限度までトラフィックが増加していきます。

流入するトラフィック量に応じて、最大帯域の設定を変更することにより、マルチロードバランサーのボトルネック化を回避できていることを確認できました。

自動帯域DOWNの確認

  • 自動帯域UPの動作を確認できたので、トラフィックを停止して様子を見ます。

スクリプトの確認結果でトラフィック減を検知し、マルチロードバランサーの最大帯域が 40Mbps から 30Mbps へ自動的に下がりました。

アクティビティログから帯域DOWNを確認 40M -> 30M

その後、30Mbps から 20Mbps、10Mbpsまで戻ることを確認しました。

アクティビティログから帯域DOWNを確認 30M -> 20M

アクティビティログから帯域DOWNを確認 20M -> 10M

一旦UPした帯域が、トラフィック減を検知して段階的に下がり、最終的には最小の10Mbpsに戻りました。

まとめ

今回はマルチロードバランサーを対象に、ニフクラスクリプトによる流量監視と自動スケールアップ/ダウン について検証しました。 流入トラフィックの需要量に応じてマルチロードバランサーの対応帯域を上げ下げすることにより、ボトルネックが発生することなく、かかるコストも最小化できました。 動作の要はスクリプトと実行APIとなりますが、ニフクラではそれらを利用可能なプラットフォームを提供しております。本検証では比較的シンプルなスクリプトでしたが、より複雑な処理等を組み合わせることにより、様々なお客様要件にご活用いただけるのではないかと思いました。

ここまで読んでいただきありがとうございました!

注意事項

  • 本記事については検証結果の1つとなります。実際に検討される場合は、事前にそれぞれの要件を確認の上、実装してください。
  • 本記事ではOS上の操作についても記載していますが、ニフクラではOS以上はご利用者様の責任範囲となりますのでご留意ください。
  • 本記事での各ソフトウェアの設定パラメータはテスト用となります。実際に構築される場合は、サイトのSSL化や、アクセスを許可するネットワークの限定など、要件に応じたセキュリティ設定を検討してください。
  • 本記事で記載した各サービス/ニフクラの機能等は、2023年2月時点の情報です。利用時には各サービス/ニフクラの機能の最新情報をご確認いただきご利用ください。