2015/09 最近の録画サーバー事情 (ZFS, 安鯖, Cloud Backupなど)
自分はネットラジオなんかを録画するシステムを開発してるわけなんですが、
これをどういうサーバーで運用しているかというのをメモしておこうと思います。
- 前提
- サーバー(ハードウェア)
- ストレージ代
- いわゆる安鯖について
- OS, FileSystem
- Backupについて
- CrashPlanについて
- ニコ生について
- Google Photosについて
- Virtual Box + Windows
- 監視・アラート
- 課題・今後の方向性
前提
- ストリーミングのインターネットラジオ(radikoを2チャンネルほどやA&G+など)をリアルタイムで録音・録画している
- その他毎週更新だったりするやつもとっている
- ニコ生も1日15本ほど保存している(net-radio-archiveの新機能として開発中)
- テレビ放送のTSはキャプチャしてない
サーバー(ハードウェア)
- 自宅サーバー
- 安鯖: Fujitsu TX100
- PentiumG 2Core 2.6GHz
- Unbuffered ECC Memory 2GB → 8GB に増設
- System HDD 250GB
- 3TB HDD x3
ストレージ代
自宅サーバーにしているのはクラウドストレージはなにをどうやっても料金が高いため。S3に月数万円も払うよりは自分でHDD買ってきてRAID組んだほうが経済的な気がした。
最近はBackblaze B2という破格のS3互換ストレージが発表されたのでこいつで動くようにシステムを改修してもいいかもしれないと考え始めている。
ただS3系のやつはディレクトリ構造というのを持つことができずいわゆるファイルシステムとしては使えないのでファイルのリストなんかは別にDBで持っておいて、リンクを表示するWebページを生成する必要があったり、sftpなんかで外部から繋いでファイルを設置したりできなかったりと不便なのでnfsなんかでファイルシステムとしてmountできてなおかつ安いクラウドストレージとか出てきてくれないかなーと願ってる
いわゆる安鯖について
安鯖は自分でPC自作するよりもだいぶ安い見積もりとなったし、どんな破格でも業務用サーバーなので安定しているだろうと思って買った。実際安定している。 ML110とかじゃなのはもう少し強いCPUが欲しかったからで、まあ現状そこそこ (CPU Usage Avg は 50/200% くらい)。純粋なNAS用じゃなくてアプリケーション色々動かす場合は4Coreのやつ買っても良かったかもしれない
ECCメモリしか使用できないものを買ったのは完全に失敗でNon-ECCが動いたほうが良い。自分は仕方ないのでヤフオクで安いのを6GB分買い集めた。多分全部で6000円くらいなので安いっちゃ安いがECCメモリでも色々種類とかあるし供給が少ないのは面倒。
OS, FileSystem
自宅でNASっぽいシステムを組むからにはデータをふっとばさないことは極めて重要であり、RAIDだったりSnapshotだったりの機能はあったほうが良い。 FreeNASなんかを入れれば全部揃うんですが、自分はこのサーバーをアプリケーションサーバーとしても使うし、FreeBSDを使えるほど豪の者ではないのでUbuntuにした。 LinuxはFreeBSDほどZFSとの親和性が高くないので大丈夫か不安だったが今のところ問題はない。(インストールは面倒だった)
Linuxを使う上でRAIDとSnapshotを使える組み合わせは以下の通りだが
1つめはトラブった時地獄っぽいし、Btrfsは不安定かつRAID5,6についてはStableじゃない。 ZFSはdkmsでkernelのbuildが必要だったりと面倒で、かつapt-get upgradeなんかでkernelが更新されるとZFSがロードされなくなったり再インストールが必要になったりする。
全部微妙なわけなのですがZFS使ってます。動くようになってzfs_arc_maxを1GBくらいに抑えこんだ後は快適。
Backupについて
自宅ストレージやってる知り合いいっぱいいるんですが、驚くべきことにBackupをとってない人が多い。つまりストレージが複数台壊れたらそれでデータはおしまいという。実際飛ばしてる人も多いしRAIDしたって故障率が下がるだけで壊れるときは壊れるし雷とか水害とか食らったらRAIDに意味ないわけだ。
というわけでどっか別の場所にデータを保存する必要があるわけで CrashPlanというサービスを使っている。
競合サービスにBackblazeがあるがこちらは「NASで使うな」と公式に書いてあってサーバーに優しくない。
Dropbox系の容量無制限サービスなんかはTS送るおじさんとか/dev/urandom送るおじさんなどの鬼畜によってガンガン殺されていく (Bitcasaとか) 一方でそのだいぶ前からあるバックアップ系のBackblazeとCrashPlanはいまだ問題なく健在で安定感がある。
CrashPlanについて
バックアップサービス。 $5/monthで容量無制限のバックアップができる。 headless(GUIのない)なLinuxでの使用方法なども丁寧に解説されている。 まあ実際復元するときの速度が現実的かみたいな問題はあるが、必要なデータだけピンポイントで復元することもできるし、自分の用途だと多分なんとかなるだろう。
アップロード速度は結構遅いがニコ生除いて20GB/monthくらいの増加量の自分だとなんとか追いついている。それ以上だと厳しくて工夫したり諦めたりする必要があるかもしれない。
バックアップ用のdaemonがJavaでわりとメモリを消費する。ファイル数にもよるがデフォルトで1GB消費するし、容量やファイル数によってはそれ以上のメモリの割当が必要になる (詳細はこの辺のドキュメント に書いてある)
ニコ生について
ニコ生は録画自体にも苦労するがその容量にも苦労する。30分程度の番組でも300MB程度の容量を消費しうっかりと一挙放送だったり野球中継だったりといった長時間番組を録画してしまうと1ファイル3GBくらいのブツになる。
ニコ生を録画し始めたらストレージに保存しておくにもキツイしCrashPlanでのバックアップも追いつかなくなってきてしまうようになった。
そんななか救世主として現れたのがGoogle Photosである。このサービスは再エンコードを許容すればなんと容量無制限で動画を保存できる!!アップロードも高速!!
ということでニコ生についてはGoogle Photosにアップロードした上で自宅サーバーからは順次消去していく運用をとることにした。
Google Photosについて
Google Photosとは画像と動画の無限ストレージ(検索機能付き)である。すばらしい。
アップロードも高速で一応QuotaはあるようだがCrashPlanとは比較にならないほどのスループットが出る。
とはいえLinux Serverで使用するにはいくつか問題がある。
- UploadにAPIが使えるのかよくわからない。古いPicasaのやつが使えるような使えないような(試していない)
- 自動バックアップソフトのGoogle Photos BackupはWindowsとMac版しかなくLinux版が用意される気配はない
というわけでファイルの更新を検知してAPI叩くものを作った上で謎APIを叩けないか試行錯誤するのはつらそうだったので。なんとかGoogle Photos BackupをLinuxで動かないか試行錯誤してみた。
- Wine → OAuthっぽいTokenをWindowsで取得してレジストリの内容をWineにコピーしてくると動くことは動く。ただしとても不安定で30分ほどで勝手に死んだりして、Wineの設定を試行錯誤するごとに振るまいが変わったりしてダメっぽいので諦めた。
- Virtual Box + Windows(正規) + Synced Folder → 概ね問題なく動く
なお、閲覧時に動画にファイル名などがでないので一覧性低そうだがGoogle Driveと連携してそちらから見ればファイル名で一覧表示できることがわかった。Googleサービス間の謎っぽい連携最高!
Virtual Box + Windows
VM上のWindowsでGoogle Photos Backupという一応GUIなアプリを動かすためUbuntuにGUI環境を入れた。
デスクトップ環境を丸ごと入れると重そうだったので X + fluxbox + tight vnc server という構成にした。環境構築はとても面倒だったがCPUもメモリもほとんど消費しておらず優秀
Google Photos Backupはだいたい問題ないものの、アップロードが十数個のファイルを残して止まり、新しいファリウを探索しなくなるという問題があったため、Windowsのタスクのスケジュールでアプリを再起動するbatファイルを登録して、一日に一度強制再起動を書けるようにした。アップロードが止まる根本原因は不明。
rem google-photos-backup-restart.bat taskkill \IM "Google Photos Backup.exe" "C:\......Google Photos Backup.exe"
それから、メモリは消費しないもののやたらにCPUを消費するのでVirtual Boxの設定で1Coreの25%などに制限しておくといい。
監視・アラート
メトリクス監視はmackerel。使いやすいし無料だし最高!
ログ監視してのアラートは手が回っておらず、一応cronのstdout, stderrの内容をメールするようにしている。日本語文字化けが直らないしmailtoをどこのファイルにも書いていない気がするのにメールが飛んでくるなど謎挙動で動作している状態なのでもっといい方法に切り替えていきたいが、このあたりは知見がない。
課題・今後の方向性
Twitter Great RSS で複数画像の表示に対応しました。
いまさらなのですが1ツイートに複数の画像を投稿してるやつに対応しました。
Twitter Greate RSS とは何なのかについてはこちら↓
TwitterのRSSを生成する Twitter Great RSS をつくった - 方向
ところでHerokuが24時間起動できなくなって若干困ってます。6時間くらい死んでる。 ユーザーで誰かHobby Dyno ($7 / month)分くらいの金払ってくれる人いたら連絡ください
アニたまを録音できるようになりました
インターネットラジオを全録音するソフトつくった
Net Radio Archive (https://github.com/yayugu/net-radio-archive) というソフトを作りました
特徴
番組表をいい感じにスクレイピングして、番組名などが付いた良い感じのファイルを出力してくれます。
... 2015_05_08_1200_村川梨衣の_a_りえしょんぷり~ず♡_村川梨衣.mp4 2015_05_08_1230_小澤亜李・長縄まりあのおざなり_小澤亜李、長縄まりあ.mp4 2015_05_08_1300_A&G_ARTIST_ZONE_THE_CATCH__AiRI_AiRI.mp4 2015_05_08_1430_A&G_NEXT_BREAKS_松田利冴のFIVE_STARS.mp4 2015_05_08_1500_金田朋子・保村真のエアラジオ_金田朋子、保村真.mp4 2015_05_08_1530_水島大宙・木村良平_←SIDE_BY_SIDE→_水島大宙、木村良平.mp4 2015_05_08_1600_超!A&G+_スペシャル.mp4 2015_05_08_1700_角元明日香の本気!アニラブ_角元明日香.mp4 2015_05_08_1730_高橋美佳子のマルごとぴ〜なっつ!_高橋美佳子.mp4 2015_05_08_1800_A&G_ARTIST_ZONE_THE_CATCH__鷲崎健_鷲崎健.mp4 2015_05_08_1930_A&G_NEXT_BREAKS_吉田有里のFIVE_STARS.mp4 2015_05_08_2000_三上枝織の_A&G_NEXT_GENERATION_Lady_Go!!_三上枝織.mp4 2015_05_08_2100_黒崎真音のRADIO_RONDO_ROBE~妄想王女の単独電波~_黒崎真音.mp4 ...
今までの似たソフトだと時刻を指定したりして録画・録音するものが多かったのですが、そうではなく全部とっておきたい、番組タイトルも自動で付けて欲しいと思いつくりました。
現在対応しているもの
動かし方
Linuxなどで動きます。
24時間起動するマシンが必要です。またRadikoのエリア判定がいい感じとなるIPのサーバーがいいでしょう。
自分ように作っているものなのでパッケージングが十分とは言いがたく、他環境で動かすと問題がでてくるかも知れませんのでgithub issueなりTwitter (@yayugu) なりでお気軽にご相談ください。
追記:
ライセンス付けました。MITです
超A&G+の新しい番組表(2015/04現在)をスクレイピングする
4月の番組改変に合わせて番組表が新しいやつになっていて悲鳴あげながら直しました。 前の番組表は1つの曜日が1つのtableにマッピングされていて楽だったのですが、 新しいやつは
- 全曜日で1テーブル
- 行(tr)は曜日ではなく時刻ごと。その中に全曜日の番組tdで入っている
- rowspanで行の結合があるため、それを考慮した2次元配列を作っておかないと曜日の判定がずれる
となかなか凶悪な仕様になっております。
自分が作っているラジオ録画ソフトではこんな感じで対応した。
こんかいの対応にかかったコーディング時間は2時間ほど。 こういうのは反射神経が重要なので、新しいやつが出たときにババっと書き捨てられる力をつけていきたいものです。
require 'net/http' require 'time' require 'chronic' require 'pp' require 'moji' module Ag class Program < Struct.new(:start_time, :minutes, :title) end class ProgramTime < Struct.new(:wday, :time) SAME_DAY_LINE_HOUR = 5 # convert human friendly time to computer friendly time def self.parse(wday, time_str) time = Time.parse(time_str) if time.hour < SAME_DAY_LINE_HOUR wday = (wday + 1) % 7 end self.new(wday, time) end def next_on_air time = chronic(wday_for_chronic_include_today(self[:wday])) if time > Time.now return time else chronic(wday_to_s(self[:wday])) end end def chronic(day_str) Chronic.parse( "#{day_str} #{self[:time].strftime("%H:%M")}", context: :future, ambiguous_time_range: :none, hours24: true, guess: :begin ) end def wday_for_chronic_include_today(wday) if Time.now.wday == wday return 'today' end wday_to_s(wday) end def wday_to_s(wday) %w(Sunday Monday Tuesday Wednesday Thursday Friday Saturday)[wday] end end class Scraping def main programs = scraping_page programs = validate_programs(programs) programs end def validate_programs(programs) if programs.size < 20 puts "Error: Number of programs is too few!" exit end programs.delete_if do |program| program.title == '放送休止' end end def scraping_page html = Net::HTTP.get(URI.parse('http://www.agqr.jp/timetable/streaming.php')) dom = Nokogiri::HTML.parse(html) trs = dom.css('.timetb-ag tbody tr') # may be 30minutes belt two_dim_array = table_to_two_dim_array(trs) two_dim_array.inject([]) do |programs, belt| programs + parse_belt_dom(belt) end end def parse_belt_dom(belt) belt.each_with_index.inject([]) do |programs, (td, index)| next programs unless td wday = (index + 1) % 7 # monday start programs << parse_td_dom(td, wday) end end def table_to_two_dim_array(trs) aa = [] span = {} trs.each_with_index do |tr, row_n| a = [] col_n = 0 tr.css('td').each do |td| while span[[row_n, col_n]] a.push(nil) col_n += 1 end a.push(td) cspan = 1 if td['colspan'] =~ /(\d+)/ cspan = $1.to_i end rspan = 1 if td['rowspan'] =~ /(\d+)/ rspan = $1.to_i end (row_n...(row_n + rspan)).each do |r| (col_n...(col_n + cspan)).each do |c| span[[r, c]] = true end end col_n += 1 end aa.push(a) end aa end def determine_wday(index, padded) wday = index - 1 % 7 # monday start end def padded?(td) end def parse_td_dom(td, wday) start_time = parse_start_time(td, wday) minutes = parse_minutes(td) title = parse_title(td) Program.new(start_time, minutes, title) end def parse_minutes(td) rowspan = td.attribute('rowspan') if !rowspan || rowspan.value.blank? 30 else td.attribute('rowspan').value.to_i * 30 end end def parse_start_time(td, wday) ProgramTime.parse(wday, td.css('.time')[0].text) end def parse_title(td) [td.css('.title-p')[0].text, td.css('.rp')[0].text].select do |text| !text.gsub(/\s/, '').empty? end.map do |text| Moji.normalize_zen_han(text).strip end.join(' ') end end end
元コード
悲しみのdiff
Follow the A&G+ new timetable!!!!!!!!!!!!!! · yayugu/net-radio-archive@67990ee · GitHub
響 HiBiki Radio Station をaacで録音する
2015/11/10追記
響がリニューアルしたんでこの記事の情報は全く役に立たなくなりました。 新しい仕様についての説明、コードはこちら↓
検索用キーワード: Mac, Linux, rtmp, rtmpdump, rtmpe, 保存
まえがき
響で再エンコードなしでaacを取得する方法がわかったんでメモ。
ググるとwmaやm3u8で取得する方法は見つかるんだが、wmaだとiOSとかで再生できないし、再エンコードは気分が悪い。m3u8 (HTTP Live Streaming)はなんかファイルがすごい分割されていてパッとググってもffmpegとかでいい感じにmp4などに復元する方法が見つからなかったので遠慮したさがあった。
コード
雑なコードとしては下の2ファイルを見て欲しい
手順
http://hibiki-radio.jp/get_program/$(WEEKDAY)
にアクセスすると曜日ごとの番組情報が取得できる。$WEEKDAYは数字で1-6がそれぞれ月〜金と土日に対応している。
Aタグのonclickが onclick="AttachVideo('garo','1637','1','0')"
みたいになっているのでこの関数の1つめと2つめを取り出す。
m = /AttachVideo\('(.+?)','(.+?)','.+?','.+?'\)/.match(onclick_text) short_name = m[1] channel_id = m[2]
その情報からURLを生成してアクセスする。
uri = URI.parse("http://image.hibiki-radio.jp/uploads/data/channel/#{base.short_name}/#{base.channel_id}.xml") res = Net::HTTP.get_response(uri) unless res.is_a?(Net::HTTPSuccess) return nil end dom = Nokogiri::HTML.parse(res.body) protocol = dom.css('protocol').text domain = dom.css('domain').text dir = dom.css('dir').text flv = dom.css('flv').text if protocol.blank? || domain.blank? || dir.blank? || flv.blank? return nil end m = /^.+?\:(.+)$/.match(flv) filename_query = m[1] rtmp_url = "#{protocol}://#{domain}/#{dir}/#{filename_query}"
xmlのような雰囲気のものが取り出せる。404だったり、情報が空のこともある。その場合はその番組はおそらく配信していない。 無事取得できて、以下のようになってたら成功。この情報を組み立てるとrtmpのurlができる。
<data> <protocol>rtmpe</protocol> <domain>cp209391.edgefcs.net</domain> <dir>ondemand</dir> <channel type="main"> <flv>mp4:150101_lovelive_ms_150101_lovelive_ms.mp4?di=910&si=609&pi=2806&gi=6494&gc=3&bi=34236&bc=lovelive_ms&ei=921385&ec=150101_lovelive_ms&vi=4989926&vc=150101_lovelive_ms&msi=516&mc=&ni=1625</flv> <thumbnail>http://image.hibiki-radio.jp/uploads/radio_program/flash_image/c7562a9a9a67e099402d472585bcdc5068da0d24.jpg</thumbnail> </channel> </data>
組み立てたurlをrtmpdumpに渡す
`rtmpdump -q -r #{Shellwords.escape(rtmp_url)} -o #{Shellwords.escape(flv_path)}`
うまくいっていればflvファイルがあるはずである。中身はh264の映像とaacの音声である。h264の映像はダミーであるため、ffmpeg(or avconv)でaacだけを取り出す。
`avconv -loglevel error -y -i #{Shellwords.escape(flv_path)} -acodec copy #{Shellwords.escape(aac_path)}`
あとがき
ちなみにrtmpもhls(HTTP Live Streaming)もデータを転送するための方法であり、中身は大抵の場合h264とaacである。 なのでどちらでもmp4が取り出せるはず(方法は知らないけど)
VMで開発してるんだけど IntelliJ / PHPStorm とかを使いたい
xxxa: dockerコンテナ側のディレクトリをローカルmacにマウントしたいんだけど xxxa: -v, --volume=[] Bind mount a volume (e.g., from the host: -v /host:/container, from Docker: -v /container) xxxa: -v /container してもローカルmacに表われないんですけどどうしたらいんですか xxxb: boot2docker? xxxa: はい yayugu: 多分出来ない xxxa: そうなんですね yayugu: boot2dockerに多くを期待してはいけません xxxa: intelliJ使いたいんだけどなあ・・・ yayugu: っsshfs yayugu: sshfsクソ遅いということは知りながら書いてる yayugu: この辺についてはおおいに問題なんですけど yayugu: まあ現実解としては4つくらいしかないくて yayugu: 1. Linuxをマシン直で使う yayugu: 2. sshfsで転送遅いのを我慢する yayugu: 3. ローカルにgit置いてIntelliJの機能(SFTP)でlinuxマシンに転送する。ラグいのは我慢する。PHPUnitなどの連携がクソるのも我慢する yayugu: 4. Macで動くようにする xxxc: 3がオススメだ yayugu: 1ができると最高なんだが、俺はムリです。X window systemキツイ xxxc: 無理して1にする必要も感じないな。開発用のライブラリとか突っ込んでいくとサーバーと同じ環境じゃなくなっていく。 xxxa: 1、江添さんっぽい xxxc: 1でやったとしても VM 立てたくなる。 xxxb: というかローカル開発じゃなくてローカルVM開発がもう基本なのか。4だと思ってた xxxc: ほら、ローカルにいろいろインストールしたくないじゃない。 xxxb: はい xxxc: プロジェクトごとにまっさらな環境でやりたいじゃない。 xxxb: はい yayugu: 1でdocker使うとVM立てなくてもxxxaが最初にやろうとしてたことできるよ