Un nouveau monde parfumé

香り立つ備忘録

AWS で爆速エンコード -自動化編-

続きます。

pikachuism.hatenablog.com

ffmpeg から nvenc を使ったエンコードができたので、これを自動でやっていくことになります。

再度様子を説明します。

f:id:pikatenor:20170517052739p:plain

S3 のフォルダ内にデータがアップロードされる

イベントが発火し、Lambda関数が呼び出される

Lambda が EC2 インスタンスを起動する

EC2 がエンコードする

エンコード結果を S3 に保存していって、無くなったら自動でシャットダウン

までやれると、適当に S3 にアップロードして家で寝ておけばいつの間にかエンコードが終わっており、起きたら適当にデータを取りに行けばいいという感じになりそうだな、と思ったので作りました。

S3 バケットを生やす

S3 バケットを適当な名前で適当に作ります。 リージョンは EC2 と同じにしておくと転送料金がかかりません。 作ったらば queue/, completed/, failed/ と3つフォルダを作ります。
queue/ フォルダにソースファイルをアップロードしていって、エンコード済みファイルを completed/ に保存するようにしたためです。 failed/ はエンコ失敗したソースと残骸を入れる用です。

f:id:pikatenor:20170517072538p:plain:w500

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 関数が起動します。

f:id:pikatenor:20170517083455p:plain:w300

コードは 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 燃やしきらないでしょ

これはフラグです。以上です。