こんにちは、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を確認しました。(20Mbps から 30Mbpsへ)

③その後 30Mbps から 40Mbpsへ

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

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

マルチロードバランサー、トラフィックジェネレータ―共に、帯域変更のタイミング①②③にて前後の挙動がわかります。 それぞれ、帯域UPするまでは設定上限あたりで頭打ちとなり、自動帯域UP後に次の設定限度までトラフィックが増加していきます。
流入するトラフィック量に応じて、最大帯域の設定を変更することにより、マルチロードバランサーのボトルネック化を回避できていることを確認できました。
自動帯域DOWNの確認
- 自動帯域UPの動作を確認できたので、トラフィックを停止して様子を見ます。
スクリプトの確認結果でトラフィック減を検知し、マルチロードバランサーの最大帯域が 40Mbps から 30Mbps へ自動的に下がりました。

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


一旦UPした帯域が、トラフィック減を検知して段階的に下がり、最終的には最小の10Mbpsに戻りました。
まとめ
今回はマルチロードバランサーを対象に、ニフクラスクリプトによる流量監視と自動スケールアップ/ダウン について検証しました。 流入トラフィックの需要量に応じてマルチロードバランサーの対応帯域を上げ下げすることにより、ボトルネックが発生することなく、かかるコストも最小化できました。 動作の要はスクリプトと実行APIとなりますが、ニフクラではそれらを利用可能なプラットフォームを提供しております。本検証では比較的シンプルなスクリプトでしたが、より複雑な処理等を組み合わせることにより、様々なお客様要件にご活用いただけるのではないかと思いました。
ここまで読んでいただきありがとうございました!
注意事項
- 本記事については検証結果の1つとなります。実際に検討される場合は、事前にそれぞれの要件を確認の上、実装してください。
- 本記事ではOS上の操作についても記載していますが、ニフクラではOS以上はご利用者様の責任範囲となりますのでご留意ください。
- 本記事での各ソフトウェアの設定パラメータはテスト用となります。実際に構築される場合は、サイトのSSL化や、アクセスを許可するネットワークの限定など、要件に応じたセキュリティ設定を検討してください。
- 本記事で記載した各サービス/ニフクラの機能等は、2023年2月時点の情報です。利用時には各サービス/ニフクラの機能の最新情報をご確認いただきご利用ください。
