言語処理100本ノックを敢えてRubyで (2)

10. 行数のカウント

p $<.count

確認

wc hightemp.txt

$< の仕様はまさにこういう処理を書くために考えられている。 が、今回に関しては普通ならわざわざ書かずにwcを使う。

11. タブをスペースに置換

$<.each_line do |line|
  puts line.gsub("\t", ' ')
end

確認

sed "s/^I/ /g" hightemp.txt

12. 1列目をcol1.txtに,2列目をcol2.txtに保存

File.open("col1.txt", "w") {|f1|
  File.open("col2.txt", "w") {|f2|
    $<.each_line do |line|
      cols = line.split("\t")
      f1.puts cols[0]
      f2.puts cols[1]
    end
  }
}

確認

cut -f1 hightemp.txt
cut -f2 hightemp.txt

普通で面白くない。 今回はデータが小さいことが分かっているので、オンメモリに構築してから File::write で書いた方がインデントは少なくなって綺麗だろう。 複数のファイルを同じ寿命で開くことはままあるので、複数まとめて開いてまとめて閉じるラッパーメソッドを書いたことはある。

こうしてみるとgolangdefer がちょとうらやましいなぁ。 Object.instance_eval で似たようなのを構築するのは易しいが、 self がすり替わってしまうし完全ではない。

13. col1.txtとcol2.txtをマージ

File.open("col1.txt") {|f1|
  File.open("col2.txt") {|f2|
    f1.zip(f2).each do |col1, col2|
      puts "#{col1.chomp}\t#{col2}"
    end
  }
}

性能的に見ると Enumerable.zipArray を返すのでメモリを食ってよろしくない。今回はデータが小さいから良いものの、 lazy を使っても全般的にRubyはこの手の処理が苦手である。 この辺の失敗体験がStreemにつながっているんだろうか。

paste col1.txt col2.txt

14. 先頭からN行を出力

n = ARGV.shift.to_i
$<.each_with_index do |line, i|
  break if i >= n
  puts line
end

別解。こちらのほうが今風だ。

n = ARGV.shift.to_i
$<.lazy.take(n).each{|line| puts line }

確認

head -10 hightemp.txt

15. 末尾のN行を出力

class Ring
  include Enumerable
  ABSENT = Object.new.freeze
  def initialize(n)
    @buf = Array.new(n, ABSENT)
    @pos = 0
  end

  def add(item)
    @buf[@pos] = item
    @pos += 1
    @pos %= @buf.size
  end

  def each
    (0...@buf.size).each do |i|
      item = @buf[(i + @pos) % @buf.size]
      next if item == ABSENT
      yield item
    end
  end
end

n = ARGV.shift.to_i
ring = Ring.new(n)
$<.each(&ring.method(:add))
ring.each do |line|
  puts line
end

まじめに後方からバッファリングしつつEOLを探索すると面倒いので先頭からなめた。 tail (1)の実装は結構うまくできていて、まねしようとするとそれなりに難しい。

あと、 &obj.method(:mid) は見慣れない割には簡潔でないので実務では避けるべき。

Ring もあまりまじめな実装ではない。

そういえば、Ring bufferが標準ライブラリに入っていて気軽に使えると割と便利なことがgolangの経験で分かった。 しかし、今からコンテナをRubyの標準添付ライブラリに足す線はなかなか無いだろうなぁ。

tail -10 hightemp.txt

16. ファイルをN分割する

n = ARGV.shift.to_i
lines = $<.to_a
per_chunk = if lines.size % n == 0
              lines.size / n
            else
              lines.size / n + 1
            end

suffix = 'aa'
lines.each_slice(per_chunk) do |chunk|
  File.open("x#{suffix}", "w") {|f|
    chunk.each do |line|
      f.puts line
    end
  }
  suffix.succ!
end

出力ファイル名は split (1)のデフォルトに合わせた。

split (1)で実現せよ」という設問については、BSDsplit には -n が無いから難しいんだが。

gsplit -n 5 hightemp.txt

17. 1列目の文字列の異なり

puts $<.map {|line| line.split("\t")[0] }.uniq

確認

cut -f1 hightemp.txt | sort | uniq

18. 各行を3コラム目の数値の降順にソート

$<.map {|line| line.split("\t") }.sort_by {|_, _, temp, _|
  -temp.to_f
}.each do |data|
  puts data.join("\t")
end

確認

sort -rnk3 hightemp.txt

19. 各行の1コラム目の文字列の出現頻度を求め,出現頻度の高い順に並べる

freq = Hash.new(0)
$<.map {|line|
  pref, *rest = line.split("\t")
  freq[pref] += 1
  [pref, *rest]
}.sort_by {|pref,| [-freq[pref], pref] }.each do |data|
  puts data.join("\t")
end

同じ出現頻度が多くて分かりづらい。あと、都道府県でグルーピングもしてみた。

shellコマンドでの確認指示については、こういう意図かなぁ?

cut -f1 hightemp.txt | sort | uniq -c | sort -rnk1 -t' '

言語処理100本ノックを敢えてRubyで (1)

言語処理100本ノック を(飽きるまで)やってみるにあたり、敢えてRubyで書いてみる。

基本的にはPythonを想定しているらしいし、そもそもNLPライブラリの充実度から言ってもPythonを使うのが極めて妥当な選択といえるだろう。そこを敢えてRubyで。

00. 文字列の逆順

puts "stressed".reverse

RubyでもPythonでも大差ない。

01. 「パタトクカシーー」

str = "パタトクカシーー"
puts 1.step(7, 2).map{|i| str[i]}.join

やはりスライスにstepを指定できるPythonのほうが楽である。

02. 「パトカー」+「タクシー」=「パタトクカシーー」

strs = %w[パトカー タクシー]
puts strs.map(&:chars).inject(&:zip).flatten.join

別解

strs = %w[パトカー タクシー]
puts strs.map(&:chars).transpose.flatten.join

ziptransposeflatten を基本装備しているRubyに死角はなかった。

しかし、interleave操作ってのは割と使うのでArrayにあっても良い気はするけど、そう思う人は自分で定義できるのがRubyの良いところである。 ActiveSupportみたいな外部ライブラリを通じて「そう思う人」が一般的であることやライブラリデザインの善し悪しが実証されれば、Ruby本体に取り込まれることもある。ここでも登場している Symbol.to_proc がその好例である。

ただ、 RubyのStringが「文字の列」でない のが地味に辛い。 StringがEnumerableでないのは、「何を単位としてiterateしたいのかは場合によるからcodepoints, chars, linesなどを明示的に使おう」「昔はlinesがデフォルトだったけど、少なくともこれは違うんでは?」「文字列を文字の列として扱うのが基本であるかのような風潮を助長するのは良くない」などが複合していると思われる。

03. 円周率

sentence = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
p sentence.split(/\W+/).map(&:size)

04. 元素記号

sentence = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
words = sentence.split(/\W+/)
result = {}
len = ->(i){ [0, 4, 5, 6, 7, 8, 14, 15, 18].include?(i) ? 1 : 2 }
words.each_with_index do |word, i|
  key = word[0, len[i]]
  result[key] = i+1
end
p result

リスト内包表記がある分Pythonのほうが綺麗にいくだろうか。 あと、出題通りだと"Might"から"Mi"を切り出すことになるけど良いの?

05. n-gram

module Enumerable
  def ngram(n)
    each_cons(n).to_a
  end
end

if $0 == __FILE__
  str = "I am an NLPer"
  p str.chars.ngram(2)
  p str.split(/\W+/).ngram(2)
end

文字bi-gramのほうは空白は無視した方が良かったんだろうか。

06. 集合

require_relative "05.rb"
strs = %w[paraparaparadise paragraph]
x = strs[0].chars.ngram(2)
y = strs[1].chars.ngram(2)

puts "union:\t#{x | y}"
puts "intersection:\t#{x & y}"
puts "diff:\t#{x - y}"
puts "X includes 'se'?: #{x.include?(%w[s e])}"
puts "Y includes 'se'?: #{y.include?(%w[s e])}"

Arrayクラスが集合演算を持っているのでそのままである。実用的にはSetを使うべきかもしれない。

07. テンプレートによる文生成

def run_template(x, y, z)
  "#{x}時の#{y}#{z}"
end

puts run_template(12, "気温", 22.4)

やっぱり引数が短い場合は文字列interpolationのほうが String::% より便利である。

ちなみにRubyString::%Pythonが元ネタだったはず。

08. 暗号文

CIPHER_TABLE = (?a .. ?z).map{|c| (219 - c.ord).chr }.join.freeze
def cipher(str)
  str.tr('a-z', CIPHER_TABLE)
end

if $0 == __FILE__
  sentence= <<-EOS
He then led me to the frame, about the sides, whereof all his pupils stood in ranks.  It was twenty feet square, placed in the middle of the room.  The superfices was composed of several bits of wood, about the bigness of a die, but some larger than others.  They were all linked together by slender wires.  These bits of wood were covered, on every square, with paper pasted on them; and on these papers were written all the words of their language, in their several moods, tenses, and declensions; but without any order.  The professor then desired me “to observe; for he was going to set his engine at work.”  The pupils, at his command, took each of them hold of an iron handle, whereof there were forty fixed round the edges of the frame; and giving them a sudden turn, the whole disposition of the words was entirely changed.  He then commanded six-and-thirty of the lads, to read the several lines softly, as they appeared upon the frame; and where they found three or four words together that might make part of a sentence, they dictated to the four remaining boys, who were scribes.  This work was repeated three or four times, and at every turn, the engine was so contrived, that the words shifted into new places, as the square bits of wood moved upside down.
  EOS

  puts cipher(sentence)
  raise unless cipher(cipher(sentence)) == sentence
end

09. Typoglycemia

def randomize(str)
  str.split(/ /).map {|word|
    if word.size <= 4
      word
    else
      [word[0], word[1..-2].chars.shuffle, word[-1]].flatten.join
    end
  }.join(' ')
end

if $0 == __FILE__
  puts randomize("I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind .")
end

アンダースタンディング・コンピュテーション

アンダースタンディング コンピュテーション ―単純な機械から不可能なプログラムまで 』を監訳者の笹田さんから頂戴したので、読んだ感想をまとめてみる。

この本はRubyを用いて計算理論を紹介しようというものだ。具体的には次のようなトピックを扱っている。

サンプルが丁寧に書かれていて簡単に動作させられること。これが本書の強みだと言える。 正直なところ今までオートマトンはともかく形式的意味論は他の本を何度を読んでもいまいちピンと来なかったのだが、本書のサンプルプログラムを動かして初めて理解が進んだ気がする。

そもそも本文にも書かれているように、ミニ言語の形式的意味論をより強く(曖昧な)言語であるRubyで書いたところで理論的には意味は無い。 意味の定義がRubyの形式的意味論に依存するようになるだけで、問題をより難しくしているに過ぎないことになる *1 。 それでも、読みやすく処理系の入手が容易な言語でサンプルを動かすのは学習上の利点は大きいと感じた。

いままで理解がいまいちだった理由にも思い当たった。 意味論にせよオートマトン、ラムダ計算いずれにしても、きちんと理解しようと思ったら結局は実際にステップを追って意味の評価を進めてみるしかないのだろう。 顧みれば、実務でオートマトンを書くのはたまにあることだし、そもそも私は正規表現エンジンを書きたくてプログラミングを覚えたようなものなのでその辺の経験はいくらかあった。 これに対してスモールステップ意味論を意識しながら構文解析することは少ないし、Yaccのような具体的なパーザージェネレータと教科書に出てくる数学的な表記の乖離はオートマトンのそれよりも大きい。 これが理解が進まない原因だったに違いない。

その点、本書はある程度数学的厳密さを放棄してでも読者が手を動かすことを促している。 定義を一つ一つ追って評価してみる手順を紙と鉛筆でやるのはなかなか億劫だが、幸いなことに我々の前には高性能な計算機械があるのでこれを補助に使えばそんなに大変でもない。 そこでRubyを用いた実装なのだ。いろんな入力に反応して実際に意味論が評価され抽象機械が動作するのは見ていて楽しい。

Rubyという言語の選択も悪くない。余分な構文要素や非本質的な処理なしに簡潔にプログラムの意図を書き下せるというRubyの利点が良く活かされている。 ラムダ計算そのものを出自とするいくつかの関数型言語とどちらが良いかは議論の余地があるが、Rubyの良い点を見いだすとすれば「ラムダ計算と異質な表記」であることが挙げられる。 つまり、記述されているものと記述しているものの区別が容易である。

理解をさらに進めるとすればこの後改めて、数学的により厳密な教科書を当たるべきなのだろう。ただ、初心者がまずは手軽に試して概要をつかむための本として良い本だと思った。 本書を読むならば是非サンプルプログラムはすべて実際に動かしてみるべきである。私もまだ動かしていない残りの部分をぼちぼち試してみようと思う。

*1:一応Rubyの操作的意味論はISO/IEC 30170に定義されてはいるが、本書のサンプル言語の意味論より明らかに大きく複雑だ

Dockerイメージのフォーマット

  • 問: Dockerイメージはどういうフォーマットなのか
  • 答: 特定のフォーマットはないが転送時はtar

以前の記事 では、Dockerはアプリケーション動作環境のファイルステムをまとめて差分転送できると書いた。 ではこのディスクイメージはどういうフォーマットなのだろう。

実装こそ違うが実態としては仮想マシン環境に似ているという点を鑑みると、qcowとか何かその類いのファイルフォーマットなのかもしれないという想像も成り立つ。 しかし実際には特定のフォーマットというものはない。

Dockerホスト環境では、ディスクイメージは動作に適した形で保存されている。このため保存形式はdocker daemon-storage-driver オプションによって異なる。 つまり --storage-driver=aufs で走っているならローカルファイルシステムのサブディレクトリ内に展開されているし、 --storage-driver=devicemapper ならblock deviceとして格納されている。 このためdockerコンテナを走らせるときにはただaufsなり何なりをmountするだけで即座にコンテナのファイルシステムを用意できる。イメージを展開したりそのコンテナ用に複製したりする作業は不要なわけだ。

一方、イメージを転送する際には格納方法がまちまちでは困る。そこで転送時は単にファイルシステム内のエントリを(正確に言うと親イメージからの差分を)tarで固めて転送する。 なおdockerイメージには親イメージのIDやコンテナ実行時のデフォルトのコマンドラインなどメタデータも含まれるので、これは別途JSON形式で転送する。 詳しいことは Docker Registry API reference に書いてある。

Heroku本を読んだ

『プロフェッショナルのための実践Heroku入門』をざっと読んだ。

コンパクトにまとまっていて良い本だと思う。 私が以前1日がかりで調べて回ったこと+αぐらいが書かれているので、読んでおけばそういう調べ回る時間を節約してくれるし。

インストール手順に結構な紙面を割いたりするあたりはあまり好みの構成ではないが、薄い割に触れているトピックが豊富なのは良い。 Herokuの概要、各言語の開発環境セットアップ、Herokuを用いたアプリケーションの開発サイクル、デプロイの仕組み、Addon, Herokuアーキテクチャの外観などに順に触れている。 web開発者ならこれだけ一冊読めばとりあえずHerokuにアプリケーションを立ち上げられるようになるだろう。 ベストプラクティスやアンチパターンについても折々触れているので、読んでおけば先々楽にもなる。

残念なのは、workerプロセスについては名前ぐらいであまり触れていないことだろうか。workerがあればmicroservicesも可能だ思うのだけれども、web dynoからworker dynoにアクセスするにはどうしたら良いのかわからなかった。これは前にHerokuのhelpを調べたときも良くわからなかった。ambasadorパターンしてくれてたりするのだろうか。

これは愚痴だけれども、冒頭の概説におけるGoogle Cloud Platformの扱いが悲しい。

  • IaaSの例としてはGoogle Compute Engineは無視された。紙面には限りもあるので落とされるものがあるのは仕方がないが、ごく個人的な希望としては入れてくれたら嬉しかった。
  • Google AppEngineではRDBが使えない、ということはない。 Cloud SQL というのがあって、これは普通のMySQLとして使える。

    • AppEngineではBigDataというKVSしか使えないと書いてあるのは Blobstore (もしくは Google Cloud Storage )と Datastore が混ざってないだろうか。BigDataという製品は今のところ無い。
    • Blobstore/Cloud Storageは要するに S3 みたいなものなのでKVSといえばKVSだし、特にBlobstoreのAPIはKVSっぽい。DatastoreはEventually Consistentじゃなくて厳密にtransactionalな HBase みたいなものなのでKVSとはちょっと違うNoSQL DBだ。

libchanを読んだ

libchan を読んだのでまとめてみる。

libchanとは

libchanはdockerに使われているライブラリの1つで、先月の DockerCon で発表された。 非同期かつ一方向の通信チャネルをインプロセスでもネットワーク越しでも扱えるというGoライブラリである。 一方向とはいうものの、チャネル自体をデータに添えて他のチャネル越しに送れる。なので、返信や待ち合わせが必要ならば自分宛のチャネルを送って相手に使ってもらい、自分はそのチャネルの上で待機していれば良い。

早い話がGo言語の機能であるチャネルをネットワーク対応したようなものだ、と書いてある。

DockerはこのDockerConではDocker 1.0に加えてlibcontainer, libchan, libswarm, Docker Hubを発表していて一応キーノートの話題の1つではあったものの、 個人的にはlibswarmやKubernetesに比べるとインパクトは小さいなという感想であった。

実装

何らかのトランスポート機構の上にシリアライズされたメッセージを送る仕組みで、割とシンプルな造りである。

トランスポートとしては下記をサポートするとドキュメントに書いてある。

  • In-memory Go channel
  • Unix socket
  • Raw TCP
  • TLS
  • HTTP2/SPDY
  • Websocket

ただし、現在実装されているのは下記のみに見える。

  • In-memory Go channel
  • Unix socket
  • SPDY

また"In-memory Go channel"とはいうものの、実際にはmutexと変数共有で実装されていてchanは使っていない。

送信可能なメッセージの定義は次のようになっている。

type Message struct {
    Data    []byte
    Fd  *os.File
    Ret  Sender
}

Data は任意のペイロードRet は返信用に相手に送るチャネルである。

Ret には特別な値である libchan.RetPipe を指定できる。これを使うとチャネルの実装が勝手に返信用の逆方向チャネルを作って送ってくれるので、位置透過性を保ったまま「送信元に返信してね」と指定できる。

Fd はよく分からない。一見するとUnixソケットのFD passingみたいなものを実現するつもりなのかとも思うが、"file attachment not yet implemented in unix transport"とか書いてあるし、"file attachment"というのは何か別のものなのかもしれない。

unix.Receiver の定義が下記のようになっているあたり、やはりFD passingか、とも思うのだが。

type Receiver interface {
    Receive() ([]byte, *os.File, error)
}

感想

デザインは魅力的だ。ただし、位置づけがまだ不安定であるし、実装も開発途上で未実装機能が多い。

チャネル上でチャネルを送れるということの強力さはGoでchanを使っているとよく分かる。 それをネットワーク分散システムで利用できるのはとても楽しみだ。ただしSPDYトランスポートでのチャネル転送が libchan.RetPipe を除いて未実装なので、その魅力は現時点では半減する。

チャネル再転送の困難

実際のところリモートマシン宛の任意のチャネル転送をサポートするとなると厄介な問題に出会うだろう。 転送されてきたチャネルを再転送できるというのがこのパラダイムを強力たらしめるのだが、そのトランスポートレベルでの実装は面倒になり得る。

ノードA, B, Cがあって、Aが生成したA宛のチャネルをBに送り、BはそれをCに再転送し、Cがそのチャネルに書き込んだとしよう。素朴には2通りの実装が考えられる。

  • CはAと直接通信する。Cはチャネルを受け取った時点でAへの通信を確立し、その上に受け取ったチャネルを再現する
  • BがCとAの通信を中継する。

複雑なシステムでチャネルがあちこちでやり取りされると、中継モデルは破綻することが予想される。 チャネルの流通経路を逆に辿ってシステム内のノードをたらい回しされ、無駄に転送コストを支払うメッセージが見えるようだ。

一方で直接通信モデルはではAとCが直接通信可能とは限らない。firewall越えやNAT越えの問題があるし、Raw TCP経由でWebsocketチャネルを送った場合どっちのプロトコルが妥当とも言えない。

結局は直接通信と中継のハイブリッドが妥当であろうと考えられるが、どうハイブリッドするのか、通信パスをどう最適化するのかというルーティング問題が残る。

将来性

Dockerを実装するには便利なんだろうと思う。問題はどこまで汎用化してくれるだろうかというところにある。

一応は任意の言語でライブラリを実装可能と書いてあるので、Docker以外が使うことも想定はしているんだろう。それでも、NAT越えとかルーティングとかをどこまで真剣にサポートしてくれるのか。 その辺のDockerには必要かどうか分からない機能のためにパッチを継続的に受け付けてくれるのか。誰がそういうパッチを書くのか。

競合

多様なトランスポートをサポートした通信ライブラリという点でlibchanはあからさまにZeroMQと競合する。

中の人のコメント では「ZeroMQは機能過剰だし、ローレベル過ぎるし、要らない機能を外すのも大変」だからlibchanを作ったとある。

個人的にはこれに賛同する。チャンネル自体の転送ができるのは圧倒的な魅力だし、これで十分に強力だ。 ZeroMQがサポートするPublisher-Subscriberモデルや同期通信, 双方向通信はlibchanに欠けているが特に問題とも思わない。 バイトストリーム上にチャネル転送を構築することに比べると、PubSubや同期、双方向通信を非同期一方向+チャネル転送で実装するほうが楽だし、抽象化としてまともに思える。

しかし、皆がそう思わなければ使われないだろう。どれだけユーザー層が成熟していくのか未知数である。

結論

流行ったらいいなと思うし、ネットワーク越しのチャネル転送の仕様が固まってきたらRubyC++で実装するのもの面白いかもしれない。 でも今は使うときではないと思う。

Dockerで何が変わるのか

DockerCon 2014 に行ってきた。

この会期中には各社からいくつもの製品が紹介/発表された。そして、それによってクラウドという技術は次のステージに移行したと言っても過言ではないだろう。

より自由にユーザーがクラウドベンダーを選べる時代へ。どうやってクラウドにうまくデプロイするかではなく、アプリケーションそのものに注力できる時代へ。

Dockerとは

Docker とはいわゆるコンテナ技術の1つで、Linuxホスト環境の中に隔離された別のLinux環境を作ってくれる技術だ。

軽量仮想マシンと呼ばれたりもする。 Solaris Container とも似ている。

新しくないDocker

1つ述べておくとDockerは技術的には新しくない。Dockerの価値は技術以外にある(とDockerのCEOもDockerConで言ってた)。

技術的にはSolarisにはSolaris 10の頃からあるし(ruby-lang.orgも使ってた)、LinuxにだってLXCがKernel 2.6の頃からある。そもそもの話Dockerの初期のバージョンはLXCのラッパーみたいなもんだった。

仮想マシンに似ているという観点から述べるなら、コンテナ技術というのはマシンやOSそのものを仮想的に実現してその上でアプリケーションを動かす代わりにマシンとカーネルはホストのものをそのまま使って、ユーザーランド+αだけを仮想化する、みたいなもんだ。

別の視点から述べると、これって要するにchroot+αだ。「ちょっとすごいJail」と言ってもいいだろう。Chrootだとファイルシステムだけが隔離されるけど、その他にDockerではプロセスIDのネームスペースや、ユーザーIDやネットワークポートの空間も隔離される。Dockerの中のrootはホスト環境のrootとは違うユーザーだし、プロセス番号は同じでも違うプロセスだし、TCP portも同様。要は、これぐらい一通りホスト環境から隔離してやればユーザーランドから見ると仮想マシンに隔離されてるも同然だよね、という話。実際にはカーネルはホスト環境のやつを使ってるけど、ユーザーランドからそれを知るすべはほとんどない筈。

新しいDocker

Dockerが新しいのは、コンテナ環境で走らせるアプリケーションを簡単にパッケージする標準的な方法を確立したことにある。

Dockerは、ユーザーランドで使われるGNU/Linuxディストリビューションファイルシステムひと揃いをメタデータと一緒に固めた形式を定義している。要するに仮想ディスクイメージなんだけど。

また、イメージはしばしば他のイメージから派生する。その場合は元のイメージからの差分だけが保存されるので軽量である。

更に、イメージはgitのコミットと同じ要領で内容のハッシュで識別されることになっている。

これらが合わさると、こういうことになる。アプリケーションを含まないOSだけのbaseイメージ(例えばUbuntu preciseイメージ)はみんなが大抵手元に持っているし、必要であれば公式のリポジトリから簡単にダウンロードできる。

そのベースイメージから派生した自分のアプリケーションインスール済み環境を配布したいとする。

受け取る相手は、今自分が受け取ろうとしているイメージの派生元を辿って行く。既に持っている分は改めて取得する必要はなくて、そこからの差分だけを転送すればよい。つまり、ディスクイメージの転送が軽量にできる。

同じ内容のイメージは必ず同じハッシュを持つので、既に持っているかどうかは簡単に判別できる。この辺もgitと同じだ。

更に、こうして作成された差分ディスクイメージを走らせるときもホスト環境のディスクを無駄に使わず、起動時にマージなんてアホなことをすることもなく、効率的にインスタンス化できるようになっている。

こういう便利なパッケージの仕組みをオープンソースで作った。そしてこの仕組みをみんなが使い始めた。みんなというのはGoogle, Amazon, IBM, Microsoft, Rackspace, ...を含む。

新しいクラウドの話

Dockerはそれ単体で使っても便利なものだ。プロセスを隔離したいときのchrootの不便を解消できる。

Chrootだと、chroot環境のファイルシステムを作るのが結構面倒でみんな自作のスクリプトで頑張ったりしたものだった。Dockerなら簡単にUbuntuでもCentOSでも好きな環境のイメージを作れる。 *1

しかし、Dockerが本当に便利なのはクラウド環境との組み合わせで、イメージを他のマシンに転送するときだ。手元のマシンでDockerイメージを作り込んでプロダクション環境に転送、実行してやる。すると、手元で設定したアプケーションがそのままプロダクションで動く。ファイルを配備して、ユーザーを作成して、設定ファイルを弄って、みたいな成果物をそのままの状態で転送できる。もう苦労してプロダクション環境を適切にセットアップしてやる必要はない。つまり、ProvisioningやDeploymentの手順が劇的に簡略化される。

Dockerさえ設定しておけば、あとはイメージをポイッと配布するとそれがそのまま動くのだ。

最初にDockerを軽量仮想マシンぽいものだと言ったけれど、ここに至っては仮想化であるということは手段の1つでしかなくてあまり重要なことではない。重要なのは設定済みのアプケーション動作環境をユーザーランドまるごとパッケージして軽量に移動できるということのほうだ。

そして、これがクラウドの主要ベンダーたちによってサポートされた。

  • GoogleはManaged VMを発表した。ユーザーがアップロードしたDockerイメージがAppEngine相当のmanaged環境で実行される。AppEngine同様の「Googleが運用の面倒を見てくれる」手軽さがありながら、Dockerイメージには任意の言語で任意のアプケーションを入れられる。
  • AmazonはElastic BeanstalkでDockerをサポートした。話としてはManaged VMと同じだ。
  • ...

こうして各社がDockerをサポートした。つまり、クラウド環境で動かすためにアプケーションをパッケージングし転送する標準的な方法が生まれた。もうベンダーロックインを心配しなくていい。他社のサービスへ移りたくなったらそのDockerイメージを移動先に改めてアップロードすれば良い。

libswarm

今回のDockerConでは、もう1つベンダーロックインをなくすための製品が発表された。

なるほど、一個のサービスはDockerで簡単に配備できるし移動もできるかもしれない。しかし、今どきのクラウド上のアプケーションというのは1つのサービスだけで動く訳ではない。データベースに、キャッシュに、バックエンドサーバー、フロントエンドwebアプケーションサーバーなどなど沢山のroleを為すそれぞれの種類のサーバーインスタンスがあって、それぞれを適切に繋げてやらねばならない。しかもクラウド上の話だからしばしばサーバーのIPはクラウドベンダーに割り振られる。IP決めうちであらかじめ設定してからイメージを作るという訳にもいかない。つまり、依存する各サーバーが他のサービスを発見して繋ぎにいくというサービスディスカバリとオーケストレーションの問題が発生する。

この問題に対しては既にいくつものソリューションがある。fleet+etcdだとか、gearだとか。うまく使えばDNSも解になり得るだろう。クラウドベンダーも思い思いに解決法を提供している。

真の問題は、解決法が多すぎることだ。未だに標準的な方法はないし、どこかのベンダーの機能を使えば、サービスのオーケストレーションという要の部分を握られてロックインされる。

そこでDockerはこの度libswarmを発表した。libswarmはこの様々な実装を抽象化したAPIを提供するライブラリだ。

個人的にはlibvirtオーケストレーション版、という理解をしている。

libswarmによっていよいよクラウドベンダー間の移動は楽になる。もっと安いところ、もっとスケールするところ、などなど必要に応じて他のベンダーに自由に移動できる時代がやってくる。

Kubernetes

そしてKubernetesは更にその一歩先の話、だと思うけどその話は後で書く

Disclaimer このブログはYuguiの個人的なものです。ここで述べられていることはDockerConで聞いた話の個人的理解、および個人的な意見に基づくものであり、私の雇用者には一切の関係はありません。

上で間違ってdocker imageのidがcontent hashかのように書いたが、それは次期イメージフォーマットの話で現在はイメージ作成時にランダムに決まる256bitだった。イメージ生成時にはキャッシュが効くので続けざまに二回同じイメージをビルドすると同じIDが帰ってくるが、別のホストで独立に同じイメージをビルドしても別のIDになる。

*1:隔離されていて予測可能な状態にある環境というのはビルドやなんかにはとても便利だ。だからDockerのビルドスクリプトはDocker環境の中でDockerをビルドする