Amazon EBSのボリュームサイズをElasticにしたい
2行でまとめると
- EBSボリュームのサイズを使用中に増やす方法を検討した
- LVMを使用すればボリュームを継ぎ足してサイズを増やせる
課題
動画のエンコードをAmazon EC2上で行う場合、ファイルの置き場所には通常Amazon EBSが使用されます。
EBSは原則としてボリューム作成時に指定したサイズまでしか使えないのですが、処理によって必要となるボリュームサイズが大きく異なる場合、ボリュームを必要とされる最大サイズで作成すると、主に費用の面で無駄が大きくなってしまう課題があります。
例えばあるエンコード処理では5GB必要で、別のエンコード処理では500GB必要となるような場合、小さいサイズ(5GB+)でボリュームを作成すると500GBの処理では容量が不足し、大きいサイズ(500GB+)で作成すると、5GBの処理では使われなかった分(495GB)の費用が無駄になります。
解決案の検討
金額を気にしない以外の解決案として次の案が考えられます。
Elastic Volumesを使用してボリュームサイズを増やす
Elastic Volumesを使用すると対象のEBSボリュームサイズを拡張することができます。この機能を使用して、ボリュームサイズが不足しそうになったらサイズ拡張を行う方法です。
ドキュメントを読むとサイズ拡張には数分〜数時間掛かり、拡張後は同じボリュームに対して再度サイズ拡張を行うには最低6時間待つように書かれています。
- After modifying a volume, you must wait at least six hours and ensure that the volume is in the
in-use
oravailable
state before you can modify the same volume.- Modifying an EBS volume can take from a few minutes to a few hours, depending on the configuration changes being applied. ...
Modify an Amazon EBS volume using Elastic Volumes operations
動画エンコード途中でボリュームサイズが不足した時に最大数時間処理を止める前提が必要となると、時間と費用の面で無駄が多くなり、今回の課題を解決するための案としては適切ではありません。
処理開始前に必要となるサイズを計算してEBSボリュームを作成する
動画のエンコードを始める前に、
- 入力ファイルのサイズ
- 入力(出力)ファイルの再生時間
- エンコード出力の合計ビットレート
- 中間ファイルのサイズ
から必要となるサイズを計算してEBSボリュームを作成する方法です。
この方法は、毎回同じ設定でEC2インスタンスが起動できない点、入力ファイルをダウンロードする前に再生時間を知ることが難しい[1]点や、EC2インスタンスを他の(必要サイズの異なる)動画エンコードには使えないため、都度インスタンスの起動と終了が必要となる点がいまいちな案です。
LVMを使用してEBSボリュームを継ぎ足す
適当なサイズのEBSボリュームをアタッチしたEC2インスタンスを起動し、処理中にボリュームサイズが不足しそうになったら追加のボリュームを作成しLVM (Logical Volume Manager)を使用して既存のボリュームに継ぎ足す方法です。
継ぎ足す際に実行中の動画エンコード等の処理に影響が出なければ今回の課題を解決できそうですので、この案を試してみます。
実装
環境
OSとしてAmazon Linux 2 (x86_64)を使用します。
権限
EBSのボリューム作成とEC2へのアタッチに以下の権限が必要になります。
ec2:CreateVolume
ec2:AttachVolume
ec2:ModifyInstanceAttribute
EC2のIAMロールにこれら権限の許可を追加します。
起動時の処理
ユーザーデータを使用してEC2インスタンスの起動時にボリュームの初期化を行います。
Name | Value |
---|---|
Region | ap-northeast-1 |
最初のデバイス名 | /dev/sdf |
EBS ボリュームサイズ | 40GB |
EBS ボリュームタイプ | gp3 |
LVM ボリュームグループ名 | evg |
LVM 論理ボリューム名 | elv |
ファイルシステム | XFS |
マウントポイント | /data |
USER_DATA=`cat << _EOL_ | base64
#!/bin/bash
pvcreate /dev/sdf
vgcreate evg /dev/sdf
lvcreate -n elv -l 100%FREE evg
mkfs -t xfs /dev/evg/elv
mkdir /data
mount /dev/evg/elv /data
chown username:groupname /data
_EOL_
`
aws ec2 run-instances \
--image-id "ami-XXXXXXXXXXXXXX" \
--instance-type "c6i.4xlarge" \
--block-device-mappings "DeviceName=/dev/sdf,Ebs={DeleteOnTermination=True,VolumeSize=40,VolumeType=gp3}" \
--key-name "XXXXXXXXXXXXXX" \
--security-group-ids "sg-XXXXXXXXXXXXXX" \
--subnet-id "subnet-XXXXXXXXXXXXXX" \
--iam-instance-profile "EC2Role-XXXXXXXXXXXXXX" \
--instance-initiated-shutdown-behavior terminate \
--instance-market-options "MarketType=spot" \
--user-data "${USER_DATA}" \
--region "ap-northeast-1"
ボリュームサイズ拡張処理
ボリュームサイズの拡張処理は以下の順に行います。処理はボリュームの空きが不足したEC2上で行います。
EBSのボリューム作成
EBSのボリュームを作成します。成功するとVolumeId
を含む結果が返ってきます。
aws ec2 create-volume \
--region "ap-northeast-1" \
--availability-zone "ap-northeast-1a" \
--volume-type gp3 \
--size 40
作成されたボリュームをEC2にアタッチ
作成されたEBSボリュームをEC2インスタンスにアタッチします。デバイス名には/dev/sd[f-p]
が推奨されていますので、使っていない次の名前を使用します。(/dev/sdf
→/dev/sdg
→/dev/sdh
)
aws ec2 attach-volume \
--region "ap-northeast-1" \
--device "/dev/sdg" \
--instance-id "i-XXXXXXXXXXXXXX" \
--volume-id "vol-XXXXXXXXXXXXXX"
DeleteOnTermination
を有効に設定
アタッチされたボリュームのアタッチされたEBSボリュームがEC2の終了時に削除されるように設定します。この設定を行わないとEC2終了後もボリュームが残り(費用が発生し)続けます。
MAPPINGS=`cat << _EOL_
{
"DeviceName":"/dev/sdg",
"Ebs":{
"DeleteOnTermination":true
}
}
_EOL_
`
aws ec2 modify-instance-attribute \
--region "ap-northeast-1" \
--instance-id "i-XXXXXXXXXXXXXX" \
--block-device-mappings "${MAPPINGS}"
論理ボリュームに追加
アタッチされたボリュームの全領域を、起動時に作成したLVMの論理ボリュームに追加します。
/usr/sbin/pvcreate /dev/sdg
/usr/sbin/vgextend evg /dev/sdg
/usr/sbin/lvextend -l +100%FREE "/dev/evg/elv"
ファイルシステムの拡張
最後にファイルシステムの拡張を行います。
/usr/sbin/xfs_growfs /data
全体
ボリュームサイズ拡張処理全体のスクリプトはこちらになります。
ボリュームサイズ拡張スクリプト
#!/bin/bash
# jq コマンドが必要です
# EC2インスタンスメタデータからRegion, AvailabilityZone, InstanceIdを取得します
TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`
AZ=`curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/availability-zone`
REGION=${AZ:0:-1}
INSTANCE_ID=`curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id`
# 設定値
SIZE="40"
VG_NAME="evg"
LV_NAME="elv"
MOUNT_POINT="/data"
# "次"のデバイス名を生成します
# /dev/sdf がある場合は /dev/sdg (f→g)
LAST_DEVICE=`find /dev -name "sd*" | sort | tail -n 1`
LAST_DEVICE_SUFFIX=${LAST_DEVICE: -1}
NEXT_DEVICE_SUFFIX=$(printf "\x$((`printf "%x" "'$LAST_DEVICE_SUFFIX"`+1))")
NEXT_DEVICE_NAME="/dev/sd${NEXT_DEVICE_SUFFIX}"
# ボリュームの作成
RESULT_CV=`aws ec2 create-volume --region "${REGION}" --availability-zone "${AZ}" --volume-type gp3 --size ${SIZE}`
VOLUME_ID=`echo $RESULT_CV | jq -r .VolumeId`
# ボリューム作成後すぐにアタッチを行うと失敗する場合があるため3秒待機します("3秒"に深い意味はありません)
sleep 3
# ボリュームをEC2にアタッチ
aws ec2 attach-volume \
--region "${REGION}" \
--device "${NEXT_DEVICE_NAME}" \
--instance-id "${INSTANCE_ID}" \
--volume-id "${VOLUME_ID}"
# DeleteOnTerminationを有効に設定
MAPPINGS=`cat << _EOL_
{
"DeviceName":"${NEXT_DEVICE_NAME}",
"Ebs":{
"DeleteOnTermination":true
}
}
_EOL_
`
aws ec2 modify-instance-attribute \
--region "${REGION}" \
--instance-id "${INSTANCE_ID}" \
--block-device-mappings "${MAPPINGS}"
# 論理ボリュームの拡張
/usr/sbin/pvcreate "${NEXT_DEVICE_NAME}"
/usr/sbin/vgextend "${VG_NAME}" "${NEXT_DEVICE_NAME}"
/usr/sbin/lvextend -l +100%FREE "/dev/${VG_NAME}/${LV_NAME}"
# ファイルシステムの拡張
/usr/sbin/xfs_growfs "${MOUNT_POINT}"
空き容量の監視
ボリュームの空き容量監視はcronを使用して1分ごとに行います。
#!/bin/bash
set -o pipefail
THRESHOLD_IN_MB=20000
TARGET="/data"
declare -i AVAIL
if AVAIL=$(df --block-size=1M --output=avail $TARGET | tail -n 1); then
if [ $AVAIL -lt $THRESHOLD_IN_MB ]; then
echo "Extend Volume"
# ボリューム拡張処理呼び出し
/path/to/extend-volume.sh
fi
else
echo "Failed to retrieve available volume space."
exit 1
fi
このスクリプトをcronから1分ごとに呼び出します。
cronへの登録処理はユーザーデータに追加してEC2起動時に行います。
echo "* * * * * root /path/to/watch-volume-space.sh" > /etc/cron.d/volume-space
chmod 644 /etc/cron.d/volume-space
watch-volume-space.sh
スクリプトは、ロックファイルを作るなどして重複実行されないようにした方が安全です。
空き容量が足りないと判断する閾値は上記サンプルではTHRESHOLD_IN_MB=20000
と約20GBにしています。cronの実行間隔が60秒で、拡張処理extend-volume.sh
は通常10秒以内で完了しますので、最大スループットの70秒でも消費し切れないサイズを指定します。
Type | Size | Throughput |
---|---|---|
gp2 |
≦ 170GiB | 128MiB |
gp2 |
≧ 334GiB | 250MiB |
gp3 [2]
|
- | 125MiB |
計算すると、最も低いスループットのgp3
の125MiBで約8.5GiB、最も高いgp2
の250MiBでは約17GiBが70秒で消費し得る最大サイズになります。
テスト
ボリュームサイズの拡張処理中に著しいパフォーマンスの低下やエラーが起きないかのテストを行いました。
fioでの通常時と拡張処理時の簡易的なパフォーマンステストでは、読み込みの遅延が若干大きい傾向があります[3]が、全体として大きなパフォーマンスの低下はありませんでした。(t3.medium
[4], gp2
128MiB, blocksize=32M
)
通常時 | 拡張時 | |
---|---|---|
Throughput (Read) | 96182KB/s | 92231KB/s |
Throughput (Write) | 122526KB/s | 124964KB/s |
Latency (avg Read) | 336.58msec | 351.04msec |
Latency (max Read) | 447msec | 830msec |
Latency (avg Write) | 263.59msec | 258.25msec |
Latency (max Write) | 847msec | 744msec |
実際の使用方法に近いファイルダウンロード→エンコードのテストでは、EBSボリューム単独使用と比較してわずかに処理時間が増加しましたが1%未満[5]の差でした。
まとめ
稼働中のEC2インスタンスでEBSのボリュームサイズ不足が起きた場合の対策として、LVMを使用してボリュームを継ぎ足す方法を検討しました。
インスタンス起動時や稼働中に定期的な処理を行う必要がありますが、一度環境を作ってしまえば通常運用時はボリュームサイズの空きをほとんど意識する必要がなくなります。
EC2インスタンスから孤立したEBSボリュームが残り続けてしまう可能性があるため、何らかの対策が必要となります。
gp2
を使用する場合、ボリュームサイズの設定によってはスループットパフォーマンス上昇の恩恵が受けられなくなります。
動画エンコードのような短期間でEBSを使い捨てする用途以外では、例えばEBSスナップショットが使えないなどでこの拡張方法が向いていないケースもあります。
Discussion