HLSでサーバーレス動画ストリーミング配信をする

家にある動画を再生するのに、いちいちデスクトップパソコンを立ち上げたり、NASっぽいものを構築するのもめんどくさくて
URLを生成してiPhoneとかのブラウザでシュッと見られるようにしたかった。

長い動画を再生するためにストリーミング配信したくて
手持ちのMaciOS系デバイスで再生するのだと、どうやらHLSが向いてそうだという結論に落ち着いた。

自宅にサーバーを置きたくないので (でかいし、うるさいしで邪魔)
クラウド環境でいろいろやってみることにした。

結果できたのが以下のようなもの。

f:id:takanamito:20161022175237p:plain:w350

動画視聴までのワークフロー

  • 手元にある動画ファイルをS3にアップロード
  • S3にファイルが置かれたのを検知してLambdaを起動
  • LambdaでElasticTranscoderにエンコードジョブを作成してアップした動画をエンコード
  • エンコードが完了したらファイルをS3に出力
  • S3のファイルのURLをSafariで開いて再生

必要な作業は以下の通り

  • ElasticTranscoder側の設定 (Pipeline, Preset)
  • Lambda Functionの作成

S3のPUTイベントを検知してElasticTranscoderにジョブを投げるLambda Functionを作ります。
基本的には以下の記事を参考に進めました。

qiita.com

ElasticTranscoderの設定

Presetの作成

動画をエンコードする際の設定を決めます。
今回はさほど画質にこだわっていなかったのでデフォルトで用意されている設定を使うことにしました。
1M ~ 1.5MくらいのPresetがHD周辺の解像度でエンコードしてくれたので、そのまま採用しています。
この際、IDというカラムに表示されているPreset IDをメモしておきます。

f:id:takanamito:20161022181254p:plain

Pipelineの作成

ElasticTranscoderの作業フローを設定していきます。
S3上のどのBucketにある動画を、どのBucketにエンコードして出力するのかを決めます。

基本的には2つのBucketを用意し、仮にA, BというBucketがあるとすると

Aに動画ファイルをアップロード -> Lamba Functionがジョブを投げる -> ElasticTranscoderがエンコード -> Bにエンコード結果のファイルを保存

という流れになります。

こちらも生成後のPipeline IDをメモしておきましょう。

Lambda Functionの作成

先程のQiitaの記事を参考に、Lambda Functionを作っていきます。

メモしていた2つのIDを変数に埋めてください。

var aws = require('aws-sdk');
var s3 = new aws.S3({apiVersion: '2006-03-01'});
var ets = new aws.ElasticTranscoder({apiVersion: '2012-09-25', region: 'ap-northeast-1'});

exports.handler = function(event, context) {
   console.log('Received event:');
   console.log(JSON.stringify(event, null, '  '));
   
   var bucket = event.Records[0].s3.bucket.name;
   var key = event.Records[0].s3.object.key;
   
   var pipelineId = 'xxxxxxx';
   var presetId = 'xxxxxx';
  
   var fileName = key.split('.')[0];
   
   ets.createJob({
     PipelineId: pipelineId,
     OutputKeyPrefix: fileName + '/',
     Input: {
       Key: key,
       FrameRate: 'auto',
       Resolution: 'auto',
       AspectRatio: 'auto',
       Interlaced: 'auto',
       Container: 'auto',
     },
     Outputs: [
       {
         Key: 'ts-720p',
         PresetId: presetId,
         Rotate: 'auto',
         SegmentDuration: "10"
       },
     ],
     Playlists: [
       {
         Format: 'HLSv3',
         Name: 'master',
         OutputKeys: [
           'ts-720p',
         ],
       },
     ],
   }, function(error, data) {
        if(error) {
            console.log(error);
            context.done('error',error);
        } else {
            console.log('Job submitted');
            context.done(null,'');
        }
    });
};

またTriggersも設定します。
今回はS3上の.mp4というSuffixのファイルのObjectCreatedByPutObjectCreatedByCompleteMultipartUploadイベントを検知して起動するように設定しています。

これでS3に動画がアップされると自動でエンコードが開始されるようになります。

動画の再生

エンコードが完了すると.m3u8ファイルが2種類と.tsファイルが大量にbucket上に生成されます。

f:id:takanamito:20161022182541p:plain

.m3u8ファイルは動画のプレイリストとしてのテキストファイルです。
HLSはAppleが仕様を作っているプロトコルということもあり、Safariであればこの.m3u8ファイルのURLを開くだけで動画配信が始まります。

S3のPolicyを編集してIP絞るなど
インターネットに広く公開しないような設定をお忘れなく。

所感

ほぼプログラムを書かずにストリーミング配信を実現することができるので非常に楽。

ただElasticTranscoderの料金がバツグンに高いので、気になる方は手元のマシンでffmpeg使うなりしてHLS形式にするところまでやってS3に上げるのがよさそう。

HLSがHTTP/1.1の上で動作するため考えることも少なく、基本的にS3に動画ファイルとプレイリストを置いて静的に配信できれば
あとはプレーヤー側がうまくつなげて再生してくれるため、とても楽にストリーミング再生が実現できます。

またRTMPなどのプロトコルと違い、配信用サーバーを置かずにすんだり
CDNを使うことで容易に配信負荷分散ができるので、そのうち仕事で使う場面もでてきそう。

実際こないだのAbemaTVのカンファレンスでも現場のおもしろい話が聞けたので今後もHLS注目していこうと思います。

AbemaTV Developer Conference 2016

iPhone7をお風呂に落として水没させるも、乾燥剤のシリカゲルで復活した ※iPhone12もいけるはず

f:id:takanamito:20161017200027j:plain:w350

お風呂に浸かりながらiPhone7触ってたら、見事に浴槽に落としました。

浴槽のふちに乾いたタオルを敷いて、iPhoneをその上に置いていたのですが
ふとした拍子に腕が当たってしまい「ボチャッ」という音で気がつく。

慌てて救出して(約1秒) 起動すると、さすが防水端末。余裕で起動したのでひと安心。

15~20分ほどお風呂でiPhoneを操作していて、のぼせてもいけないのでそろそろお風呂からあがることに。

しかし様子がおかしい

着替えてから再度iPhoneを触ると何かおかしい。
具体的には以下のような症状

  • 液晶上部にシミ
  • 電源、ホームボタン接触不良。勝手に押されたり、押しても反応しなかったり
  • レンズに水滴侵入
  • SIMスロット内部に水滴

先日話題になった記事
iPhone 7 Plusを、シャワー中に使ったり水ですすいだりしてたら、水没判定で有償交換となってしまった話 – mono-gadget – Medium

読んだばかりだったので一気に血の気が引く。

水没判定を食らうとアウトとのことだったので、どうやったら水没判定アウトなのか調べる。

f:id:takanamito:20161018182944p:plain:w350

iPhone や iPod が水濡れにより損傷した場合は保証対象外 - Apple サポート

僕の場合はSIMスロット内部に水滴がついていたので、半分あきらめてましたが
もちろんシールも赤で、完全にアウトでした。

修理費用も3万円を超えるとのことでかなり絶望。

シリカゲルを試す

スマートフォンが水没した場合、電源OFF状態でシリカゲルとともに密封作戦は何度か聞いたことがあったので
Amazonシリカゲルを買って試してみることに。

あわせ買い不要で、オーソドックスなやつを探しました。
お急ぎ便ですぐ届いてよかった。ありがとうAmazonプライム

復活ツールと称した謎セットも売られていて驚く

手頃なサイズのタッパーを用意して、シリカゲルを敷き詰め
その上に電源を落としてSIMスロットを抜いたiPhone7を寝かせます。
※SIMスロットを抜いたのは、端末内部の通気性をより増すためです。効くのかわかりません。

f:id:takanamito:20161017200203j:plain:w350

さらに上からシリカゲルをかぶせます。

f:id:takanamito:20161017200225j:plain:w350

この状態で、蓋を閉め密封して一晩放置。

翌朝

タッパーから取り出し起動してみると、ほぼ全ての症状が改善していました。
よかったよかった。

所感

お風呂で使うやつが悪いけど、もうちょい頑張って水を防いでくれると助かります。

あとシリカゲル常備はマジでしといたほうがいいと思った。
僕の場合は時間経過とともに症状が重くなっていったので
水没 → 電源OFF → シリカゲルの対応スピードで被害範囲がけっこう変わってきそう。

Terraformベストプラクティス解釈

最近、自分のチームでTerraformを使ってAWSオーケストレーションをやろうという話になった。

サーバー台数も10~20台程度、今まではAWSコンソールをポチポチしてインスタンスを用意していたが
サービス規模の拡大に伴って、手動オペレーションによるミスを防いだり、インフラ説明コストなどを省くこと、インフラ整理の意味合いで導入している最中。

Terraformに触るのは初めてだったので、ドキュメントを読みつつ基本的な機能について勉強しつつ
どのようなファイル構成で管理をするのがいいのか、経験者の同僚にアドバイスをもらいにいったところ
Hashcorpが公開しているベストプラクティスのリポジトリを教えてくれた。

github.com

ところがこの構成、けっこう複雑で初心者がいきなり読み始めるのは辛かったので
復習も兼ねてベストプラクティスを読み解いていく。

理解するために必要な要素

  • AWSの基本的な知識
  • 基本的なTerraformの操作
  • tfvarsファイルを使った変数管理
  • module機能

おおむねこの4つを理解すれば、その知識を使って読み解くことができるはず。
最初の2つは今回は解説を省きます。

tfvarsファイルを使った変数管理

Terraformではtfファイル内に変数を記述することができますが、変数に値を入力するには

  • tfファイル内に記述
  • 実行時にオプションで指定
  • 環境変数に埋めておく
  • tfvarsファイルに記述

の4種類の方法があります。

Input Variables - Terraform by HashiCorp

ベストプラクティスではtfvarsの手法が採用されており
以下のようなファイルが存在します。

best-practices/terraform.tfvars at master · hashicorp/best-practices

ここで定義した変数はtfファイル内で参照することができます。
ベストプラクティスでは環境ごとに利用する変数を全て1つのtfvarsファイルで管理しています。

module機能

Terraformにおけるmoduleは各resourceを抽象化するためのものです。

例えばベストプラクティスの中では

AWSのコンピューティングリソース(要するにEC2)群を管理する compute module
HAProxyが動くであろうインスタンスのresourceを抽象化した haproxy moduleが存在します。

ベストプラクティスでは、上に示したように

  • 各resource自体の抽象化 (haproxy module)
  • AWSのリソース群を示す抽象化 (compute module)

の2段階の抽象化が行われています。

さらにmoduleには、自分自身の変数をmodule外から出力するための outputという構文も用意されており
これを使うことで、例えばTerraformで作成したVPCのidをmoduleの外からも参照することが可能です。
https://github.com/hashicorp/best-practices/blob/master/terraform/modules/aws/network/vpc/vpc.tf#L17

ベストプラクティスでは、このmodule機能を使って
各resourceを抽象化、そして作成したresourceのidなど動的な情報をoutputすることで
module間の値の受け渡しを実現しています。

具体的にはEC2インスタンスを立ち上げる際に
先にネットワークを構築し、VPC, subnetなどのidをoutputしてEC2の設定に利用するなどの使い方をしています。

ベストプラクティスの構成

ここまでで、必要な知識は揃ったので
改めてベストプラクティスの構成を見ていきます。

module/aws
├── compute
│   ├── compute.tf
│   └── haproxy
│          ├── haproxy.sh.tpl
│          └── haproxy.tf
└── network
    ├── bastion
    │   └── bastion.tf
    └── vpc
     └── vpc.tf

providers/aws
├── README.md
├── global
│   ├── global.tf
│   └── terraform.tfvars
├── us_east_1_prod
│   ├── terraform.tfvars
│   └── us_east_1_prod.tf
└── us_east_1_staging
    ├── terraform.tfvars
    └── us_east_1_staging.tf

まず、1番上の階層には moduleとprovidersの2つのディレクトリが存在します。

前者はそのままmoduleを、後者は環境ごとのtfvarsやリソース群のmoduleを管理するtfファイルを含みます。

どんなリソースを作成しているか追うためにproduction配下のtfファイルを見ていきます。

production.tfでは、利用するリソース群のmoduleを定義しています。 以下はEC2リソース群を管理するcompute moduleです。

# production.tf
module "compute" {
  source = "../../../modules/aws/compute"

  name               = "${var.name}"
  region             = "${var.region}"
  vpc_id             = "${module.network.vpc_id}"
  vpc_cidr           = "${var.vpc_cidr}"
  key_name           = "${aws_key_pair.site_key.key_name}"
  azs                = "${var.azs}"
  private_subnet_ids = "${module.network.private_subnet_ids}"
  public_subnet_ids  = "${module.network.public_subnet_ids}"
  ~以下略~
}

compute moduleで読み込んでいるcompute.tf内には
各resourceを抽象化したmoduleが定義されています。
このmoduleで渡した変数が最終的にresourceとして使用されます。

# compute.tf
module "haproxy" {
  source = "./haproxy"

  name               = "${var.name}-haproxy"
  vpc_id             = "${var.vpc_id}"
  vpc_cidr           = "${var.vpc_cidr}"
  key_name           = "${var.key_name}"
  subnet_ids         = "${var.public_subnet_ids}"
  atlas_username     = "${var.atlas_username}"
  atlas_environment  = "${var.atlas_environment}"
  atlas_token        = "${var.atlas_token}"
  amis               = "${var.haproxy_amis}"
  nodes              = "${var.haproxy_node_count}"
  instance_type      = "${var.haproxy_instance_type}"
  sub_domain         = "${var.sub_domain}"
  route_zone_id      = "${var.route_zone_id}"
}

順番に見ていったように

ファイル 記述内容
production.tf どういったリソース群を利用しているのか
compute.tf どういうresourceを利用しようとしているのか
haproxy.tf どういう設定でリソースを利用しようとしているのか

が、それぞれわかるようになっており
大きく3階層の構造で ファイル構成が成り立っています。

ここにEC2のインスタンスを追加したければ最下層のmoduleを
新しいリソース群を追加したければ、中段以降のmoduleを追加していくような形です。

所感

ひと通り触ってみましたが

  • 階層の深さ
  • 変数定義の非DRYな感じ

は最後まで辛かったです。

階層に関しては、初見時にコードを追いかけていくのが大変だったので
中段のリソース群をまとめているcompute.tfなどのファイルを減らせないかと考えたのですが
そうするとproduction.tfなどの大元のファイルに各moduleを定義することになり、行数が肥大化してしまい
その環境にどういった種類のリソースが存在するのか、見通しが悪くなってしまうので、今の構成を受け入れるしかないという結論に落ち着きました。

2つ目の変数定義なのですが、各tfファイルに何度も variable hoge { } と書かされるのが本当にしんどいです。

今回は tfvars -> environment.tf -> resource_group.tf -> resource.tf といった具合に計3階層に渡って変数の値を受け渡す必要があったため
同じ変数の定義を3回書いています。

めちゃくちゃ面倒です。
この辺はTerraform側の対応を待つしかないようです。

しかし、それら不便な点を差し引いても Terraformは非常に便利な点が多いため、これからも積極的に利用していこうと思います。

ISUCON6 3万点台で予選敗退

会社の同僚と一緒に「尿酸値E」としてISUCON6出ました。
Ruby実装を選んで、最終的に「32660」というスコアで予選敗退。今年こそは本戦出るぞと意気込んでいたのでとても悔しい。

事前準備

前回出場時に、序盤の解析系作業で手こずったため
今回はかなり念入りに解析ツールの準備を行っていました。

事前に立てていた作戦

  • 午前
    • アプリケーションをサーバー上で動かしてベンチマーク流す
    • 同様にローカルで動かす
    • コマンド一発でデプロイできるスクリプトを置く
    • kataribe, pt-query-digest, stack-profなどで各種解析データを揃える
    • 解析データを元に作戦を立てて、各自作業していく
  • 午後
    • まずはベタにまずいクエリをつぶす (index効いてない, n+1)
    • 同様にミドルウェアもチューニングする (普段の業務で使っているnginx, unicorn, MySQL, sysctl.confを用意)
    • ↑やりきってからキャッシュ戦略考えよう

今回は午後の作戦を事前に話してしまったことで、キャッシュ戦略への切り替えが遅くなったのが敗因ぽい。
htmlifyボトルネックだということに午前の段階で気づけていたものの、ロジック改善を頑張ってしまい
結果スコアが伸びずキャッシュに切り替える時間をロスした。

当日タイムライン

9:30

無事に集合。前回に引き続き、会社のオフィスで作業させてもらうことに。
普段なれてる環境で作業するのが1番でした。

10:00

Azureにデプロイ。
最初なぜかインスタンス起動でエラーが出て、30分ほどリソースグループごと作り直したりしてた。

  • とりあえず1度ベンチマークを流す
  • 「isudaってなんだ? 増田か!!」
  • 「メモリ7GB近くあって結構いろいろのせられそうだね」
  • nginx.conf見て「isudaとisutarがマイクロサービスっぽくなってる」
  • githubにコードを上げておく
  • dumpを取る
  • ローカルで動かす

最初のインスタンス起動トラブルで焦ったものの、アプリケーションが動き出してからは比較的スムーズにいった。
この時点では作戦通り、まだアプリケーションを読み始めていない。

11:00

事前に準備をしていた解析ツールを入れて導入検証を進める

この辺の作業は

ISUCON予選突破を支えたオペレーション技術 - ゆううきブログ

ゆううきさんのブログを参考に用意していた。
特にデプロイスクリプトミドルウェアごと再起動するあたりは、忘れがちなので用意しておいてよかったと思う。

作戦会議の結果、特に事前に話していた作戦とは差分なく

みたいな配分で作業していくことになった。
ここまでは予定通り。

12:00 - 15:00

各自の改善を入れて少しずつスコアが上がる。しかしこの時間帯はスコアが1万にものらない。 以下のようなことを試した。

  • nginx -> unicornunixドメインソケット化
  • staticなファイルをnginxで返す
  • unicornのworkerを増やす 5 -> 10程度
  • istarをisudaに統合
  • MySQLにインデックス追加
  • keyword_pattern, htmlifyのロジック改善
  • ispamは解析始めると時間がかかりそうなのでいったん放置

ロジック改善以外はそこそこ効果があった。 正規表現部分だけどうしようもなくて、徐々にキャッシュの方に話がいきはじめる。

15:00 - 18:00

15:00すぎから、少し作業を止めて話し合い、キャッシュ導入を検討しはじめる。

最初は htmlifyまるごとredisにキャッシュ。たしかこれでスコアが1万を超えた。

そこからまた少しスコアが伸び悩み、再度ロジック改善に走る。
その間に htmlifyの処理を細分化し、さらに細かくキャッシュしていく。これでスコアが2万を超える。

また17:00の時点で一度インスタンスごと再起動してベンチマークが走りきるか確認。

キャッシュを進めていくと今度はまたstaticなファイルを返すのに詰まりはじめたり
ファイルディスクリプタを使い切ったりしたのでカーネルチューニング。

最終的に終了直前に keyword_patternの結果をredisにキャッシュしたあたりで3万オーバーのスコアが出て終了。

終了後

近所のすしざんまいに行って反省会

個人的には

  • キャッシュ戦略の引き出しが足りなかった
  • 正規表現周辺のロジック改善にこだわりすぎた
  • /initializeの5秒を使えず

あたりに後悔が残ってます。

今年けっこう気合い入ってただけに悔しい。本戦出場の壁高い。

しかしISUCON楽しい。出題チームの方々もおつかれさまでした。

表参道.rb #14 でLTしてきた

シャドウプロキシのkageを使って安心してFuelPHP -> Railsに移行した話をしました。

表参道.rb #14 ビアガーデン風編 - connpass http://omotesandorb.connpass.com/event/36622/

今回の会場はSansanさんのオフィスでした。
13Fにオフィスがあって、LT中におそらく神宮球場のであろう花火が見えたりしてよかったです。

今年は仕事で使った技術について色々話せるといいなと思います。 よろしくお願いします。

nginx実践入門を読んだ

nginx実践入門 (WEB+DB PRESS plus)

nginx実践入門 (WEB+DB PRESS plus)

発売を知って即予約した cubicdaiya さんの本。
タイトルの通り、実践のための内容でした。

特に良いと思ったのが

の4つの章で

3・4章では、普段webサービスを運営する上で使っている基本的な設定項目について丁寧に解説されていて
自分の中で歯抜けになっている知識の整理ができたり

基本的な設定やモジュールに関する解説中心ではあるものの、非常に丁寧で
たとえばngx_http_limit_req_moduleについてはleaky bucketアルゴリズムの図を交えて解説があり
モジュールに対する僕の理解が間違えていたことに気付くきっかけになりました。
感謝。

6・7章ではより実践的な内容を扱っていて

6章では自分も経験のあるphp-fpmやunicornなどの
アプリケーションサーバと連携して稼働するリバースプロキシの細かい解説

7章ではより大規模なトラフィックを扱う場合のキャッシュ戦略や負荷分散についてnginxででき得る対処が解説されていました。

特に7章ではwebアプリケーションサーバにおけるボトルネックとなりそうなハードウェアリソースとその対策の指針が解説されており
nginx実践入門というよりは、webサービス運用実践入門みたいな内容になっています。

普段本を読むのが遅く、読書自体に苦手意識が生まれてしまっているのですが
この本は全体構成としてwebアプリケーションを運用していく上で必ず遭遇するような問題と
それに対処するための方法として、nginxの設定やモジュール、構成が解説されているため
今までの自分の経験と照らしあわせながら読むことができ、ストレスなく読み終えることができました。

デスクに置いてリファレンスとしても活用できる非常にありがたい本です。おすすめ。

#CookpadTechConf 2016に行ってきた

クックパッドさん主催のCookpadTechConfに行ってきました。

techconf.cookpad.com

いくつか自分のチームで試したいことがあったのでメモ

技術力を事業の強みするために必要なこと

  • とにかくシンプルにする
  • 営業力でなんとかしない、技術で価値をつくる
  • 「とにかくがんばるぞ」という局面において技術は全く役に立たない
    • どの数字にフォーカスするか定義する必要がある
  • 議論や調整じゃなくて決定とテスト

開発した新技術から、新しい価値を作るためのクックパッド検索チームのプロダクト開発手法

  • 技術、解決策のことはいったん忘れる
    • 技術から発想してプロダクトをつくると失敗しがち
  • ユーザーの悩みや、欲求にフォーカス
    • 「あったらいいね」みたいなものは作らない。「ないと死ぬ」みたいな機能をつくる
  • 欲求から解決策を考え、そこに技術を使う
  • 技術→製品→価値ではなくて、価値→製品→技術の順で考える
  • 週に1~2h程度の時間を取って、技術を抜きにした価値に関する議論をする

「今日なに作ろう?」を支えるデザイン

  • 機能ではなくストーリーで考える
  • ユーザーのどんな問題を解決できるのか
  • プロトタイプで仮説検証
    • イデアの共有
    • エンジニア工数を取らない
  • こういうの作りたい→OK!→最高かよ!
  • デザインもIssue上で議論

確かめながらつくるユーザー体験

いったんサービスの作り方にフォーカスしてメモ。
Rails高速化の話や、DWHの話がくそおもしろかったので
次回あればもっと技術寄りの話も聞いてみたい。