garbagetown

個人の日記です

象の虫を踏んだ話

大人の事情で古い CDH 5.1.2 を素の設定で使うと Reducer で OOME が出たり出なかったりする。

2016-02-12 00:58:44,134 WARN [main] org.apache.hadoop.mapred.YarnChild: Exception running child : org.apache.hadoop.mapreduce.task.reduce.Shuffle$ShuffleError: error in shuffle in fetcher#1
    at org.apache.hadoop.mapreduce.task.reduce.Shuffle.run(Shuffle.java:134)
    at org.apache.hadoop.mapred.ReduceTask.run(ReduceTask.java:376)
    at org.apache.hadoop.mapred.YarnChild$2.run(YarnChild.java:167)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAs(Subject.java:415)
    at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1554)
    at org.apache.hadoop.mapred.YarnChild.main(YarnChild.java:162)
Caused by: java.lang.OutOfMemoryError: Java heap space
    at org.apache.hadoop.io.BoundedByteArrayOutputStream.<init>(BoundedByteArrayOutputStream.java:56)
    at org.apache.hadoop.io.BoundedByteArrayOutputStream.<init>(BoundedByteArrayOutputStream.java:46)
    at org.apache.hadoop.mapreduce.task.reduce.InMemoryMapOutput.<init>(InMemoryMapOutput.java:63)
    at org.apache.hadoop.mapreduce.task.reduce.MergeManagerImpl.unconditionalReserve(MergeManagerImpl.java:297)
    at org.apache.hadoop.mapreduce.task.reduce.MergeManagerImpl.reserve(MergeManagerImpl.java:287)
    at org.apache.hadoop.mapreduce.task.reduce.Fetcher.copyMapOutput(Fetcher.java:411)
    at org.apache.hadoop.mapreduce.task.reduce.Fetcher.copyFromHost(Fetcher.java:341)
    at org.apache.hadoop.mapreduce.task.reduce.Fetcher.run(Fetcher.java:165)

OutOfMemoryError: Java heap space という文字を見て脊髄反射的に Reducer のヒープメモリを増強すると却って傷が広がるので要注意。

事象

MapReduce は以下のようなイメージで動く。象本に載っているのと同じ図がインターネットに転がっていたので拝借。

f:id:garbagetown:20160311203803p:plain

問題が起こっているのは reduce task"Sort" phase のところ。

mapper が分割したデータのうち、自分が集計するものを集めてマージするわけだけど、mapper が分割したデータは上図のように全部同じ大きさではなくて、あるものは小さいし、あるものは大きい。

そんな状況において限りあるリソースを最大限に活用してできるだけ速くマージするために、小さいデータはメモリに載せて、大きいデータはディスクに吐く。

このうち、ディスクに吐いたデータは当然メモリを使わないので今回の問題には関係なくて、小さいデータが積もりに積もってヒープを食い尽くしてしまったという話。

以下、原因と対応。

原因

結論から先に書くと、CDH 5.1.2 (に同梱されている Apache Hadoop 2.3.0) のデフォルト設定値と実装バグが原因。

設定値

マージに使えるメモリ量 (memoryLimit) は、下記の通りデフォルトでコンテナが使えるメモリの 90% に設定される。

一方で、データをメモリに載せるかディスクに吐くかの閾値 (maxSingleShuffleLimit) は、下記の通りデフォルトで memoryLimit の 25% に設定される。

仮にコンテナのヒープメモリを 1GB に設定した場合、225MB (=1,000 * 0.9 * 0.25) 以下のデータはメモリに載せて、それより大きいとディスクに吐く。

これだけだと小さいデータをどんどん拾ってきた場合に memoryLimit に到達してしまうので、ここにも閾値 (mergeThreshold) が設けられていて、デフォルトで memoryLimit の 90% に設定される。

つまり、メモリでマージするデータ量が 810MB (=1,000 * 0.9 * 0.9) を超えるとディスクに書き出してメモリを解放する。これはスピルレコードとしてレポートに出力されて、チューニングの指標になる。

実装

牧歌的な逐次実行脳で考えると上記の設定で問題ないように思えるけど、現実は甘くないので mapper からデータを拾う処理はマルチスレッドで動いていて、スピルレコードを書き出している最中にもデータがどんどん飛んでくる。

このため、最後の砦としてメモリ使用状況をチェックする処理が入っているんだけど、残念ながらこれがバグっている。

具体的には以下の箇所で、

if (usedMemory > memoryLimit) {

現状のメモリ使用量だけではなく、これから扱おうとしているデータ量も考慮して

if (usedMemory + requestedSize > memoryLimit) {

と判定しなければならない。

これができていないので、例えば

  • コンテナのヒープメモリが 1GB
  • すでにマージ用メモリに 890MB 載っている
  • スピルレコードの書き出しは終わっていない

という状態で、mapper から 200MB のデータを拾ってきたとすると、上記チェック処理は

  • メモリ使用量は memoryLimit に到達していない
  • 200MB は maxSingleShuffleLimit より小さい

と判定してデータをメモリに載せようとするので、メモリ使用量は 1090MB (=890 + 200) となり、ヒープメモリを食い尽くして Reducer が死亡する。

対応

実装はまだ直っていない。2.8.0 で直るらしい。

自分で実装を直してビルドするような豪傑でない場合は設定値をいじって対応する。最新の Hadoop では memoryLimit のデフォルト値が 70% に変更されているので、これに倣えばよい。

2016/04/20 修正

CDH 5.1.2 では設定ファイルで memoryLimit のデフォルト値を 70% に設定していた。Mapper の数に対して Reducer が少な過ぎたためにスピルレコードの書き出しが間に合わなかったと考えて、Reducer 数を増強した。Reducer 数は Mapper の総出力ファイルサイズをブロックサイズで割って算出すればい。

まとめ

以上のとおり、処理タイミングに依存した問題だったため再現性が低くて苦戦したが、原因が分かってすっきりした。

それから、(たぶんヒープサイズの問題ではないとぼくが止めるのも聞かずに) 脊髄反射的にヒープメモリを増強したら、これまではディスクでマージしていたサイズのデータもメモリに載るようになり、却って再現性が高まってウケた。

当たり前だけど、問題は論理的に原因を切り分けて対処しましょうと再認識。システム開発にオカルトは存在しない。

参考

Hadoop 第3版

Hadoop 第3版

cygwin で sbt

職場のウインドーズに sbt が入っていなかったので入れた。

インストール

以下を読みながら tar を落として開いてパスを通した。

# yu-umezawa at 64PC0254 in ~ [12:23:51]
$ cd /usr/local

# yu-umezawa at 64PC0254 in /usr/local [12:23:55]
$ wget https://dl.bintray.com/sbt/native-packages/sbt/0.13.9/sbt-0.13.9.tgz
--2016-02-05 12:24:10--  https://dl.bintray.com/sbt/native-packages/sbt/0.13.9/sbt-0.13.9.tgz
dl.bintray.com (dl.bintray.com) をDNSに問いあわせています... 108.168.243.150, 75.126.118.188
dl.bintray.com (dl.bintray.com)|108.168.243.150|:443 に接続しています... 接続しました。
(snip)
2016-02-05 12:24:13 (7.40 MB/s) - `sbt-0.13.9.tgz' へ保存完了 [1049367/1049367]


# yu-umezawa at 64PC0254 in /usr/local [12:24:14]
$ tar zxvf sbt-0.13.9.tgz
sbt/
sbt/conf/
sbt/conf/sbtconfig.txt
sbt/conf/sbtopts
sbt/bin/
sbt/bin/sbt.bat
sbt/bin/sbt
sbt/bin/sbt-launch.jar
sbt/bin/sbt-launch-lib.bash

# yu-umezawa at 64PC0254 in /usr/local [12:24:23]
$ rm sbt-0.13.9.tgz

# yu-umezawa at 64PC0254 in /usr/local [12:24:36]
$ vi ~/.zshrc

# yu-umezawa at 64PC0254 in /usr/local [12:25:13]
$ grep -i sbt ~/.zshrc
export SBT_HOME=/usr/local/sbt
export PATH=$PATH:$SBT_HOME/bin

# yu-umezawa at 64PC0254 in /usr/local [12:25:20]
$ source ~/.zshrc

バージョン確認

バージョンを確認して終わりと思ったら意外とむずかしかったのでググったら特攻ブログがヒットして助かった。

# yu-umezawa at 64PC0254 in /usr/local [12:25:48]
$ sbt --version
[error] Not a valid command: version (similar: session)
[error] version
[error]        ^


# yu-umezawa at 64PC0254 in /usr/local [12:26:14]
$ sbt
[info] Set current project to local (in build file:/C:/gnupack_devel-13.04-2015.06.27/app/cygwin/local/)
> sbt-version
[info] 0.13.9
>

参考

#ScalaMatsuri 2016 に参加してきた

scalamatsuri.org

一日目

友人の結婚式に招待して頂いたので残念でしたが欠席。二次会はデリリウムカフェで十種類のベルギービールが飲み放題でした。

二日目

前日に痛飲したため午後から参加。参加したセッションは以下。二日目はアンカファレンスとのことで、もっとだらっとした雰囲気になるかと思いきや、予想以上にがっつり話が聴けて楽しかったです。

ScalaとSparkによる日本語テキストマイニング

以前にコップ本をくださったゴンザレスさんのセッション。Spark と Kuromoji による日本語テキストマイニングの手順を丁寧に解説されていて非常に分かり易かったです。

最後にぽろっと言っていた「"ヤバい" がいい意味にも使われるので機械学習がむずかしい」に笑いましたw

Typesafeの人にリアクティブについて聞こう

Typesafe の皆さんによるガチリアクティブトーク。

リアクティブやストリームという言葉がいろいろな意味で使われているので混乱しがちですが、 岡本さん さんが質問しながらご自身のスライドを表示してくださったので分かり易かったです。

DDD+CQRS+EventSourcing

恥ずかしながら CQRS について何も知らなかったので、あまり感想がない...加藤さんがツイートされていた pdf を読んでみます。

Scala 転職・年収

正直ちょっとエンジニア目線過ぎる内容だなあと思いましたが、オフレコで生々しい話が聴けました。

懇親会

おえさん たちのご好意で懇親会にもお邪魔させていただきました。

tabelog.com

Play Framework のドキュメント翻訳がずっと止まってしまっていることを Christopher Hunt に謝ったところ、笑顔で次に会うときに Play Framework T シャツをあげるよと言ってくれました。そのときまでにもっと英語を話せるようにならないと...

あと Typesafe のみなさんが Play Framework Japan ステッカーを喜んでくれたのがうれしかったです。

ちなみに きの子さん が翻訳をやってみたいと言ってくれたので、一旦ストップしてしまった反省を踏まえて、無理せず継続できる仕組みを考えつつ、もう一度がんばってみようと思います。

まとめ

最後に、Scala 祭り運営スタッフのみなさん、本当にお疲れさまでした。二日目しか参加できませんでしたが、国境を越えた交流を感じられるすばらしいイベントだったと思います。

また、当日お話をさせて頂いたみなさん、本当にありがとうございました。またよろしくお願いいたします :-)

cygwin で rails する

職場のウインドーズrails したいんだけどコマンドプロンプトが大嫌いなのでイバラの道とは知りつつ cygwinrails してみた。

なお、過去の経験上、生の cygwinrails どころか ruby を入れるだけで二人月くらい掛かるので、昨年からもともと ruby が入っている gnupack に oh-my-zsh を入れて使っている。

以下、作業ログ。いつか誰か (三ヶ月後の自分を含む) がググるかもしれないので、長いけどエラーログもすべて貼る。

gem

ruby は 2.2.2 が入っていた。意識低めなので今のところ rbenv は使わない。

gem が入っていなかったので apt-cyg でさくっとインストールする。

$ ruby -v
ruby 2.2.2p95 (2015-04-13 revision 50295) [i386-cygwin]

$ gem -v
zsh: command not found: gem

$ apt-cyg install rubygems
(snip)
Package ruby-rdoc installed
Package ca-certificates is already installed, skipping
Package cygwin is already installed, skipping
Package rubygems installed

$ gem -v
2.4.8

rails

どうせどこかで失敗するんだろうなと予感しつつ rails を入れたら、お馴染みの nokogiri でコケた。

$ gem install rails -v 4.2.2
(snip)
Fetching: nokogiri-1.6.7.2.gem (100%)
Building native extensions.  This could take a while...
ERROR:  Error installing rails:
        ERROR: Failed to build gem native extension.

    /usr/bin/ruby.exe -r ./siteconf20160121-1180-1s3cu4.rb extconf.rb
checking if the C compiler accepts ... *** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
        --with-opt-dir
        --without-opt-dir
        --with-opt-include
        --without-opt-include=${opt-dir}/include
        --with-opt-lib
        --without-opt-lib=${opt-dir}/lib
        --with-make-prog
        --without-make-prog
        --srcdir=.
        --curdir
        --ruby=/usr/bin/$(RUBY_BASE_NAME)
        --help
        --clean
/usr/share/ruby/2.2.0/mkmf.rb:456:in `try_do': The compiler failed to generate an executable file. (RuntimeError)
You have to install development tools first.
        from /usr/share/ruby/2.2.0/mkmf.rb:571:in `block in try_compile'
        from /usr/share/ruby/2.2.0/mkmf.rb:522:in `with_werror'
        from /usr/share/ruby/2.2.0/mkmf.rb:571:in `try_compile'
        from extconf.rb:80:in `nokogiri_try_compile'
        from extconf.rb:87:in `block in add_cflags'
        from /usr/share/ruby/2.2.0/mkmf.rb:619:in `with_cflags'
        from extconf.rb:86:in `add_cflags'
        from extconf.rb:336:in `<main>'

extconf failed, exit code 1

Gem files will remain installed in /home/.gem/ruby/gems/nokogiri-1.6.7.2 for inspection.
Results logged to /home/.gem/ruby/extensions/x86-cygwin/nokogiri-1.6.7.2/gem_make.out

ざっと調べて libxml2, libxslt, libxml2-devel, libxslt-devel を入れてみても駄目。

$ apt-cyg install libxml2 libxslt libxml2-devel libxslt-devel
(snip)
Package libxml2-devel is already installed, skipping
Package libxslt is already installed, skipping
Package zlib-devel is already installed, skipping
Package libxslt-devel installed

$ gem install rails -v 4.2.2
(snip)
Building native extensions.  This could take a while...
ERROR:  Error installing rails:
        ERROR: Failed to build gem native extension.

    /usr/bin/ruby.exe -r ./siteconf20160121-4560-flmffc.rb extconf.rb
checking if the C compiler accepts ... *** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
        --with-opt-dir
        --without-opt-dir
        --with-opt-include
        --without-opt-include=${opt-dir}/include
        --with-opt-lib
        --without-opt-lib=${opt-dir}/lib
        --with-make-prog
        --without-make-prog
        --srcdir=.
        --curdir
        --ruby=/usr/bin/$(RUBY_BASE_NAME)
        --help
        --clean
/usr/share/ruby/2.2.0/mkmf.rb:456:in `try_do': The compiler failed to generate an executable file. (RuntimeError)
You have to install development tools first.
        from /usr/share/ruby/2.2.0/mkmf.rb:571:in `block in try_compile'
        from /usr/share/ruby/2.2.0/mkmf.rb:522:in `with_werror'
        from /usr/share/ruby/2.2.0/mkmf.rb:571:in `try_compile'
        from extconf.rb:80:in `nokogiri_try_compile'
        from extconf.rb:87:in `block in add_cflags'
        from /usr/share/ruby/2.2.0/mkmf.rb:619:in `with_cflags'
        from extconf.rb:86:in `add_cflags'
        from extconf.rb:336:in `<main>'

extconf failed, exit code 1

Gem files will remain installed in /home/.gem/ruby/gems/nokogiri-1.6.7.2 for inspection.
Results logged to /home/.gem/ruby/extensions/x86-cygwin/nokogiri-1.6.7.2/gem_make.out

ログを確認すると /usr/lib/gcc/i686-pc-cygwin/4.9.2/../../../../i686-pc-cygwin/bin/ld: -lgmp が見つかりません とのことだったので、libgmp-devel を入れたら無事に rails が入った。

$ apt-cyg install libgmp-devel
(snip)
Package gmp installed
Package cygwin is already installed, skipping
Package libgmp-devel installed

$ gem install rails -v 4.2.2
(snip)
Done installing documentation for rack, concurrent-ruby, sprockets, nokogiri, loofah, rails-html-sanitizer, rails-deprecated_sanitizer, rails-dom-testing, rack-test, actionview, actionpack, sprockets-rails, thor, railties, bundler, arel, activemodel, activerecord, globalid, activejob, mime-types, mail, actionmailer, rails after 397 seconds
24 gems installed

が、パスが通っていなかったので .zshrc に home/bin を追加。

$ rails -v
zsh: command not found: rails

$ /home/bin/rails -v
Rails 4.2.2

$ vi ~/.zshrc
$ tail -1 ~/.zshrc
export PATH=$PATH:/home/bin

$ source ~/.zshrc
$ rails -v
Rails 4.2.2

sqlite3

雑に作った rails アプリケーションに bundle install したら今度は sqlite3 で失敗。

$ bundle install --without production
(snip)
Installing sqlite3 1.3.9 with native extensions

Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    current directory: /home/.gem/ruby/gems/sqlite3-1.3.9/ext/sqlite3
/usr/bin/ruby.exe -r ./siteconf20160121-2500-vm72n8.rb extconf.rb
checking for sqlite3.h... no
sqlite3.h is missing. Try 'port install sqlite3 +universal',
'yum install sqlite-devel' or 'apt-get install libsqlite3-dev'
and check your shared library search path (the
location where your sqlite3 shared library is located).
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
        --with-opt-dir
        --without-opt-dir
        --with-opt-include
        --without-opt-include=${opt-dir}/include
        --with-opt-lib
        --without-opt-lib=${opt-dir}/lib
        --with-make-prog
        --without-make-prog
        --srcdir=.
        --curdir
        --ruby=/usr/bin/$(RUBY_BASE_NAME)
        --with-sqlite3-dir
        --without-sqlite3-dir
        --with-sqlite3-include
        --without-sqlite3-include=${sqlite3-dir}/include
        --with-sqlite3-lib
        --without-sqlite3-lib=${sqlite3-dir}/lib
        --enable-local
        --disable-local

To see why this extension failed to compile, please check the mkmf.log which can be found here:

  /home/.gem/ruby/extensions/x86-cygwin/sqlite3-1.3.9/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in /home/.gem/ruby/gems/sqlite3-1.3.9 for inspection.
Results logged to /home/.gem/ruby/extensions/x86-cygwin/sqlite3-1.3.9/gem_make.out

sqlite3 と libsqlite3-devel を入れたら動いた。

$ apt-cyg install sqlite3
(snip)
Package sqlite3-vfslog requires the following packages, installing:
cygwin libsqlite3_0
Package cygwin is already installed, skipping
Package libsqlite3_0 is already installed, skipping
Package sqlite3-vfslog installed
Package sqlite3 installed

$ apt-cyg install libsqlite3-devel
(snip)
Package libsqlite3-devel requires the following packages, installing:
libsqlite3_0 cygwin
Package libsqlite3_0 is already installed, skipping
Package cygwin is already installed, skipping
Package libsqlite3-devel installed

$ bundle install --without production
Fetching gem metadata from https://rubygems.org/..........
Fetching version metadata from https://rubygems.org/...
Fetching dependency metadata from https://rubygems.org/..
(snip)
Bundle complete! 17 Gemfile dependencies, 75 gems now installed.
Gems in the group production were not installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.

以上。

English conversation with 140-character limit is hard for Japanese

This post is an English translation of http://garbagetown.hatenablog.com/entries/2016/01/08 . *1

Background

While I was watching Building Microservices with Spring Boot LiveLessons (Video Training), I noticed that I can see the usage examples by executing curl start.spring.io without parameters.

f:id:garbagetown:20160108005810p:plain

I didn't know how to display the usage examples, although I knew that I can create an application using curl with parameters such as curl start.spring.io -d style=web .

I had tweeted about that because I thought it was very useful, then Stephane Nicoll(@snicoll) gave me a mention.

I couldn't express what I wanted to say in 140 chars because I'm not good at English, so I told him that I'll write that in this blog post.

What I wanted to say was...

I think it's better if how to use curl was on Spring Boot Reference Guide where Spring Boot beginners refer at first.

Having said that, however, I think it's not a big problem because I think most of users use Spring Initializr UI or STS rather than using curl to generate Spring Boot applications.

I'm sorry that I might have Stephane confused.

P.S.

  • Spring Boot makes Java fun again. Thanks!
  • I love Belgian beer, so I want to visit Belgium some day.
  • I need to study English :-(

Edit on 12 Jan, 2016

Spring Boot Reference Guide has been improved. Thanks Stéphane!

*1:My friend, @xxputaxx helps me to write this English post. Thanks!!

日本人に 140 文字の英会話は難しい

英語版http://garbagetown.hatenablog.com/entries/2016/01/10 に書きました。 *1

経緯

Building Microservices with Spring Boot LiveLessons (Video Training) を観ていたら、パラメータを付けずに curl start.spring.io と実行すると使い方が表示されることを知った。

f:id:garbagetown:20160108005810p:plain

curl start.spring.io -d style=web のように、パラメータを付けてアプリケーションを作成できることは知っていたけど、使い方が表示できることは知らなかった。

これは便利だなと思ってツイートしたら、 Stéphane Nicoll(@snicoll) が mention をくれた。

"usage の表示方法をどこかに書いておいた方がいい?" という内容だと思うんだけど、自分の英語が下手くそで話が噛み合わなかったので、"140 文字じゃ伝えられないのでブログに書く" と待ってもらうことにした。

言いたかったこと

  • Spring Initializr の Web UI や STS でも Spring Boot アプリケーションを作成できるので、ユーザの多くはこれらを使うと思う
  • curl を使うユーザは少ないので、ドキュメントに書いていなくても大きな問題ではない
  • curl を使ってアプリケーションを作成できることは素晴らしいので、Spring Boot を始めるユーザが最初に目を通す Spring Boot Reference Guide に書いてあると、より良いとも思う

二点目と三点目で逆のことを言ったのが良くなかった。混乱させて申し訳ない。

追伸

  • Spring Boot のおかげで Java がまた楽しくなった。ありがとう!
  • ベルギービールが大好きなので、いつかベルギーに行ってみたい
  • 英語を勉強します...

2016/01/12 追記

Spring Boot Reference Guide を改善してもらいました。ありがとう Stéphane!

*1:英語版を書くに当たって鬼軍曹 @xxputaxx のご指導を賜りました。ありがとうございました!

Docker と Redis と Spring

TL; DR

  • 職場のウインドーズでも spring-boot したい
    • spring-security したい
    • spring-session したい
  • ウインドーズに redis を入れる気はないので docker で入れる
    • docker-machine 便利

Docker Toolbox

下記 URL を参考に Docker Toolbox をインストールする。インストーラをダウンロードしてデフォルトのまま Enter を連打すればいい。

Docker

公式ドキュメントのメンテナンスが追い付いていないようで、書いてある通りにやってもうまくいかない、と言うかそもそも書いてある通りに実行できないので、勘と経験と度胸で進める。

まずは docker ホストを作る。ウインドーズはネイティブで docker ホストを実行できないので、Docker Toolbox に含まれる VirtualBox 上に docker ホストを立てる。この作業を簡単にしてくれるのが docker-machine コマンドで、例えば dev という名前の docker ホストは次のようにして作る。

c:\TEMP>docker-machine create --driver virtualbox dev
Running pre-create checks...
(dev) Default Boot2Docker ISO is out-of-date, downloading the latest release...
(dev) Latest release for github.com/boot2docker/boot2docker is v1.9.1
(dev) Downloading C:\Users\yu-umezawa\.docker\machine\cache\boot2docker.iso from https://github.com/boot2docker/boot2doc
ker/releases/download/v1.9.1/boot2docker.iso...
(dev) 0%....10%....20%....30%....40%....50%....60%....70%....80%....90%....100%
Creating machine...
(dev) Copying C:\Users\yu-umezawa\.docker\machine\cache\boot2docker.iso to C:\Users\yu-umezawa\.docker\machine\machines\
dev\boot2docker.iso...
(dev) Creating VirtualBox VM...
(dev) Creating SSH key...
(dev) Starting VM...
Waiting for machine to be running, this may take a few minutes...
Machine is running, waiting for SSH to be available...
Detecting operating system of created instance...
Detecting the provisioner...
Provisioning with boot2docker...
Copying certs to the local machine directory...
Copying certs to the remote machine...
Setting Docker configuration on the remote daemon...
Checking connection to Docker...
Docker is up and running!
To see how to connect Docker to this machine, run: docker-machine env dev

docker-machine ls で docker ホストを一覧できる。

c:\TEMP>docker-machine ls
NAME   ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER   ERRORS
dev    -        virtualbox   Running   tcp://192.168.99.100:2376           v1.9.1

作成した docker ホストに対して docker コマンドを実行するために、docker コマンドが参照する環境変数に docker ホストの情報を設定する。docker-machine env コマンドで設定すべき環境変数を確認できるので、出力された通りに設定する。

c:\TEMP>docker-machine env --shell cmd dev
SET DOCKER_TLS_VERIFY=1
SET DOCKER_HOST=tcp://192.168.99.100:2376
SET DOCKER_CERT_PATH=C:\Users\yu-umezawa\.docker\machine\machines\dev
SET DOCKER_MACHINE_NAME=dev
REM Run this command to configure your shell:
REM     FOR /f "tokens=*" %i IN ('docker-machine env --shell cmd dev') DO %i

c:\TEMP>SET DOCKER_TLS_VERIFY=1
c:\TEMP>SET DOCKER_HOST=tcp://192.168.99.100:2376
c:\TEMP>SET DOCKER_CERT_PATH=C:\Users\yu-umezawa\.docker\machine\machines\dev
c:\TEMP>SET DOCKER_MACHINE_NAME=dev

お約束の Hello World が正常に実行できることを確認する。

c:\TEMP>docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world

b901d36b6f2f: Pull complete
0a6ba66e537a: Pull complete
Digest: sha256:8be990ef2aeb16dbcb9271ddfe2610fa6658d13f6dfb8bc72074cc1ca36966a7
Status: Downloaded newer image for hello-world:latest

Hello from Docker.
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker Hub account:
 https://hub.docker.com

For more examples and ideas, visit:
 https://docs.docker.com/userguide/

Redis

docker の準備が整ったので、docker ホスト上で redis を実行するコンテナを起動する。指定しているオプションの詳細は下記の通り。

  • --name でコンテナに任意の名前を付ける
  • -d でデーモンとして起動する
  • -p で docker ホストの 6379 番ポートに対するアクセスを redis コンテナの同じく 6379 番ポートにフォワードする
c:\TEMP>docker run --name redis -d -p 6379:6379 redis
Unable to find image 'redis:latest' locally
latest: Pulling from library/redis

9ee13ca3b908: Pull complete
23cb15b0fcec: Pull complete
52a374d9c478: Pull complete
(snip)
Digest: sha256:7dfb86a89af97d915fbde39a91c87a0bf55d76f9fc979b89bfa05a5671f851c2
Status: Downloaded newer image for redis:latest
4101734092f09f3057433e02fdef26306c6ab501d602be2b7efc793f0ec48815

docker ps で docker コンテナを一覧できる。

c:\TEMP>docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
4101734092f0        redis               "/entrypoint.sh redis"   6 seconds ago       Up 5 seconds        0.0.0.0:6379->6379/tcp   redis

Spring

web, security, session, redis を追加した Spring Boot アプリケーションを作る。急にコマンドプロンプトではなく cygwin で作業しているが気にしない。

$ mkdir test && cd $_

$ curl https://start.spring.io/starter.tgz -d style=web -d style=security -d style=session -d style=redis -d name=test | tar -zxvf -
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 49996  100 49936  100    60  38051     45  0:00:01  0:00:01 --:--:-- 42318
mvnw
.mvn/
.mvn/wrapper/
src/
src/main/
src/main/java/
src/main/java/com/
src/main/java/com/example/
src/main/resources/
src/main/resources/static/
src/main/resources/templates/
src/test/
src/test/java/
src/test/java/com/
src/test/java/com/example/
.mvn/wrapper/maven-wrapper.jar
.mvn/wrapper/maven-wrapper.properties
mvnw.cmd
pom.xml
src/main/java/com/example/TestApplication.java
src/main/resources/application.properties
src/test/java/com/example/TestApplicationTests.java

TestApplication にいろいろ追加してセッション情報を Redis で管理する Web アプリケーションにする。

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
@EnableRedisHttpSession
public class TestApplication extends WebSecurityConfigurerAdapter {

    @RequestMapping("/")
    public String index() {
        return "Hello, Spring Boot!";
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic().and().authorizeRequests().anyRequest().authenticated();
    }

    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }
}

application.properties にデフォルトパスワードと docker ホストのアドレスを設定する。

security.user.password=password
spring.redis.host=192.168.99.100

アプリケーションが正常に起動することを確認する。Redis に繋がらないと起動時点で例外が発生してコケる。何度もコケた。

$ mvn spring-boot:run
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building test 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
(snip)
[INFO] --- spring-boot-maven-plugin:1.3.1.RELEASE:run (default-cli) @ demo ---

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.3.1.RELEASE)
(snip)
2016-01-05 15:58:15.195  INFO 7440 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-01-05 15:58:15.201  INFO 7440 --- [           main] com.example.TestApplication              : Started TestApplication in 2.838 seconds (JVM running for 5.199)

localhost:8080 にアクセスすると spring-security の機能で Basic 認証ダイアログが表示されるので、ユーザ名に user を、パスワードに application.properties に指定した password を入力する。

これでセッション情報が Redis に書き込まれるので、docker exec コマンドで redis コンテナに入って確認する。また急にコマンドプロンプトで作業しているが気にしない。

c:\TEMP>docker exec -it redis bash
root@4101734092f0:/data# redis-cli

127.0.0.1:6379> keys *
1) "spring:session:expirations:1451979000000"
2) "spring:session:sessions:25c0619f-1dbf-422e-bd8e-85c9650f1073"
3) "spring:session:sessions:e27ff8dc-a3cf-4bc6-9093-b506600cc3bb"

セッション情報が二つあるのは、未認証状態で favicon を取得した時のものと、認証後にアクセスした時のものっぽい。ひとまずここまででやりたいことは大体できた。

2016/01/20 修正。セッション情報が二つあるのは、セッション偽装対策のために認証前後で JSESSIONID が変わるためだった。

おまけ

flushdb で redis 上のデータを削除することができる。

127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty list or set)