AWS で爆速エンコード -自動化編-
続きます。
ffmpeg から nvenc を使ったエンコードができたので、これを自動でやっていくことになります。
再度様子を説明します。
S3 のフォルダ内にデータがアップロードされる
↓
イベントが発火し、Lambda関数が呼び出される
↓
Lambda が EC2 インスタンスを起動する
↓
EC2 がエンコードする
↓
エンコード結果を S3 に保存していって、無くなったら自動でシャットダウン
までやれると、適当に S3 にアップロードして家で寝ておけばいつの間にかエンコードが終わっており、起きたら適当にデータを取りに行けばいいという感じになりそうだな、と思ったので作りました。
S3 バケットを生やす
S3 バケットを適当な名前で適当に作ります。 リージョンは EC2 と同じにしておくと転送料金がかかりません。
作ったらば queue/
, completed/
, failed/
と3つフォルダを作ります。
queue/
フォルダにソースファイルをアップロードしていって、エンコード済みファイルを completed/
に保存するようにしたためです。
failed/
はエンコ失敗したソースと残骸を入れる用です。
EC2 の準備
S3 のマウント
「EC2 インスタンスを生やす」の項でサラッと書きましたが、僕の場合 S3 を読み書きできるロールを作って、インスタンスに付与しました。
IAM いろんな概念があって正直よくわかってない。
本当はユーザーを別に作ってエンコ用バケットだけ読めるようにするのが良いんだろうと思います。ちょっと詳細に書くのは遠慮しておきます。
S3 を EC2 から手軽に見る方法として、 s3fs-fuse を使って fuse でマウントするなどがあります。
s3fs は遅いことで有名で、同じ fuse を使ったアプローチで速さを求めるなら goofys というのもあります。
今回は無難に s3fs を選択しましたが、確かに遅かったです。まあ一人でやっていく用なので、妥協しています。
s3fs のインストール
こんな感じでした。
$ sudo yum -y install fuse fuse-devel libcurl-devel libxml2-devel openssl-devel $ wget https://github.com/s3fs-fuse/s3fs-fuse/archive/v1.82.tar.gz $ tar xvf v1.82.tar.gz $ cd s3fs-fuse-1.82/ $ ./autogen.sh $ ./configure $ make $ sudo make install
/etc/fstab
に追記して、/mnt
に作ったバケットをマウントしました
s3fs#<バケット名> /mnt fuse auto,rw,allow_other,uid=<普段使いのUID>,gid=<普段使いのGID>,iam_role=<EC2に設定したロール名>,use_cache=/tmp,_netdev 0 0
s3fs はなにもしないと owner が root になってしまうので、allow_other や uid をオプションに付けて良い感じに読み書きできるようにしておきます。
自動起動スクリプトの設定
起動すると /mnt/queue
を見にいって、ソースファイルを順にエンコードしてシャットダウンするスクリプトを書き、自動起動するようにしました。
ファイルが無かった場合即シャットダウンされては ssh すらできないので、ファイルが無い場合はそのまま待機する感じです。
何回も ls
しているのは S3 に逐次的にファイルが投入されることもあるだろうと思ったからです。
#!/bin/bash QUEUE_FOLDER="/mnt/queue" COMPLETED_FOLDER="/mnt/completed" FAILED_FOLDER="/mnt/failed" TEMP="/tmp" do_ffmpeg() { ffmpeg -c:v mpeg2_cuvid -deint adaptive -i "$1.m2ts" -c:v h264_nvenc -vf hwupload_cuda,scale_npp=-1:720 -preset slow -rc vbr -cq 10 -b:v 2M -minrate 500k -maxrate 5M "$TEMP/$1.mp4" } cd $QUEUE_FOLDER if [ `ls -1 *.m2ts | wc -l` -eq 0 ] then echo 'no files to encode. exiting.' exit fi while true do SRC=`ls -tr1 *.m2ts | head -n 1` if [ $SRC ] then echo "starting:" $SRC BASENAME=`basename $SRC .m2ts` if do_ffmpeg $BASENAME then mv $TEMP/$BASENAME.mp4 $COMPLETED_FOLDER rm $BASENAME.m2ts $COMPLETED_FOLDER echo "completed:" $SRC else mv $TEMP/$BASENAME.mp4 $FAILED_FOLDER mv $BASENAME.m2ts $FAILED_FOLDER echo "failed:" $SRC fi else echo 'no files to encode. powering off.' break fi done poweroff
これを例によって systemd で自動起動させました。
長くなってしまうので割愛しますが、今回はなんとなく最終段に起動してほしかったので multi-user.target
の先に encode.target
を作ってデフォルトにしてやって、encode.service
から指している感じにしてやっています。
Lambda 関数の作成
本当はここからコマンドを叩いたり、欲を言えばブートパラメーターをいじって encode.target
を起動してやりたい… と思ったのですがそういうことはできない。
コマンドの実行は SSM を使うとできそうだったんですが起動のタイミングが悪いのかうまくいかなかった。のでシンプルに EC2 を起動するのみです。
ブランク関数を作り、トリガーをこういう風に設定すると、queue/
に TS ファイルが入った段階で Lambda 関数が起動します。
コードは Python でこんな感じに。
import boto3 region = '<リージョン>' instance_id = '<インスタンスID>' def lambda_handler(event, context): ec2 = boto3.resource('ec2', region_name=region) instance = ec2.Instance(instance_id) status = instance.start(); print event['Records'][0]['eventTime'], event['Records'][0]['s3']['object']['key'] if status['StartingInstances'][0]['PreviousState']['Code'] == 16: print "The instance has already been started."
既にインスタンスが起動している場合はなにもしないというやつですね。
芸がないので実行用ロールに AmazonEC2FullAccess を割り当てています。
本当はカスタムポリシー作って ec2:Start
を許可するのがスジっぽい。
queue/
に .m2ts ファイルをぶち込んで、CloudWatch ログに何事か出ていれば成功。
結果
こうして AWS-powered エンコ鯖ができたというわけです。
エンコード速度は前記事で書いたとおり一つ9分くらいで、状況によりけりですが S3 へのアップロードよりちょっと速い。
今は家の録画サーバーから aws cli でガバッとアップロードしているので、マルチパートアップロードが働いて 4~5 個一気にアップロード完了してエンコードしてちょっと終了してまた再開… という動きになりました。
アップロード/エンコード/ダウンロード トータルで30分アニメ1本あたりちょうど30分弱といったところでしょうかね。
正直家で QSV とか使っても大差ない気がしてきた……
エンコ中の CPU 使用率が 5%、GPU使用率が50%ぐらいで、そもそもデコード/エンコード部分はCUDAコアから独立していて本来ならもっとやれるはずなので、並列に走らせれば少なくとも課金時間は減らせそうですね。(起動・終了のオーバーヘッドはまた増えるけど…)
そうそう、気になるお値段なんですが、運用し始めて間もないのでまだよく分かっていません。正確な額が判明したら書きます。
実はそもそもの動機が GitHub Student Developer Pack で降ってくる AWS のクレジットを無駄遣いしたかったというのがあるので、超えない範囲でやっていこうと思います。流石に $150 燃やしきらないでしょ
これはフラグです。以上です。