この記事はFujitsu extended Advent Calender 2016の20日目の記事です。
やりたいこと
ここではDockerに関する操作を一切せずにHadoop環境をDockerコンテナで構築することを目指します。
前半は次のような方が手早くHadoop環境を作るまでの手順を書いています。
- Hadoopを使ってみたい
- Dockerを使えば環境が楽に用意できると聞いた
- でもDockerを勉強する時間はない
そのためにAnsibleを使うことにします。Ansibleを選んだのはDockerのインストールからコンテナの起動、停止までの全体の動作をプレイブックとして定義して処理を自動化できるからです。
後半では、擬似分散モードで動く既存のHadoopのDockerイメージに手を加えて分散モードで動くHadoopのDockerfileを作り、Ansibleを使ってDockerfileからイメージをビルドしてコンテナの起動と停止を自動化した手順を書いていきます。
動作環境
pipが使えるCentOS7系で動かすことを想定しています。
Hadoop環境の構築
Ansibleのプレイブックを実行してHadoop環境を用意したあと、MapReduceを実行してコンテナを停止するまでの手順を記します。
-
Ansibleのインストール(Ansible 2.2.0以上が必要)
$ yum install -y ansible.noarch
-
今回のプレイブックをGithubから取得する
$ git clone https://github.com/kuromt/ansible-hadoop-docker
-
構築する環境に合わせて設定ファイルを修正する
- vars/hadoop.yamlを開きansible-hadoop-docker/hadoop-dockerのディレクトリのパスを
hadoop_dockerfile_dir
に入れ、Hadoopのマスタに名前をつける。ワーカの項目には動かしたいコンテナの数だけ名前をつけていく。
vars/hadoop.ymlhadoop_dockerfile_dir: /root/ansible-hadoop/hadoop-docker hadoop_master: hadoop00 hadoop_worker: - hadoop01 - hadoop02
- vars/hadoop.yamlを開きansible-hadoop-docker/hadoop-dockerのディレクトリのパスを
-
(オプション)プロキシ環境下で利用する場合
プロキシ環境下で利用する場合、プロキシの設定をvars/proxy.ymlで定義してください。Dockerとプロキシの関係については一枚目のFujitsu Advent Calenderの9日目の記事に書かれています。
- vars/proxy.yamlにプロキシを設定する。
vars/proxy.ymlhttp_proxy: http://username:passwd@server:port https_proxy: https://username:passwd@server:port
- hadoop-docker/Dockerfileとroles/install-docker/tasks/main.ymlのコメントアウトされたプロキシのための設定を有効にする。
@@ -4,8 +4,8 @@ FROM sequenceiq/pam:centos-6.5 MAINTAINER nobuyuki-kuromatsu -ARG http_proxy -ARG https_proxy +#ARG http_proxy +#ARG https_proxy
'roles/install-docker/tasks/main.yml'@@ -5,14 +5,14 @@ file: path=/etc/systemd/system/docker.service.d state=directory
for environment behind proxy
-- name: set http proxy for docker.service.d
- blockinfile:
- dest: '/etc/systemd/system/docker.service.d/http-proxy.conf'
- create: yes
- block: |
-
[Service]
-
Environment="HTTP_PROXY={{ http_proxy }}" "HTTPS_PROXY={{ https_proxy }}"
- register: set_docker_proxy
+#- name: set http proxy for docker.service.d
+# blockinfile:
+# dest: '/etc/systemd/system/docker.service.d/http-proxy.conf'
+# create: yes
+# block: |
+# [Service]
+# Environment="HTTP_PROXY={{ http_proxy }}" "HTTPS_PROXY={{ https_proxy }}"
+# register: set_docker_proxy - name: start docker.
systemd: name=docker.service enabled=yes daemon_reload=yes state=restarted
@@ -26,8 +26,8 @@
path: "{{ hadoop_dockerfile_dir }}"
name: hadoop-docker
state: present - buildargs:
-
http_proxy: "{{ http_proxy }}"
-
https_proxy: "{{ https_proxy }}"
+# buildargs:
+# http_proxy: "{{ http_proxy }}"
+# https_proxy: "{{ https_proxy }}"
```
-
ansible-playbookの実行
実行するプレイブックの中身はこのようになっています。play-book.yml- hosts: localhost vars_files: - vars/hadoop.yml - vars/proxy.yml roles: - install-docker # DockerのインストールとHadoopイメージのビルド - start-master # Hadoopのマスタコンテナの起動 - start-worker # Hadoopのワーカコンテナの起動 # - stop-all # Hadoopのマスタとワーカコンテナの停止と削除
install-docker
はDockerのインストールとプロキシの設定をした後にHadoopイメージのビルドします。続くstart-master
とstart-worker
はHdoop用の仮想ネットワークを作りその上で分散モードのHadoopコンテナを起動します。コメントアウトしているstop-all
はHadoopコンテナを止めたいときに使います。このプレイブックを実行してみます。全て終わるとHadoopのマスタコンテナとワーカコンテナが動いているはずです。Dockerイメージのビルドには時間がかかるのでのんびり待ちます。
# プレイブックの実行 $ ansible-playbook play-book.yml [WARNING]: Host file not found: /usr/local/etc/ansible/hosts [WARNING]: provided hosts list is empty, only localhost is available PLAY [localhost] *************************************************************** TASK [setup] ******************************************************************* ok: [localhost] (略)
-
Hadoopのコンテナにログインして分散モードになっていることを確認する
今回作ったDockerイメージはHadoopのマスタコンテナでパスワードなしでrootとしてログインできるsshdが2122ポートで動いているので、そこにsshで接続します。Dockerコマンドを触らずにコンテナを操作するためにこのようなことをしていますが、絶対に本番環境等ではこのHadoopコンテナを利用しないでください。
Hadoopのマスタコンテナにログインしてワーカを認識していることを確認します。
$ ssh -p 2122 root@localhost
-bash-4.1# cd /usr/local/hadoop/bin/
NodeManagerの一覧を取得
-bash-4.1# ./yarn node -all -list
16/12/18 08:25:10 INFO client.RMProxy: Connecting to ResourceManager at hadoop00/172.18.0.2:8032
16/12/18 08:25:11 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
Total Nodes:2
Node-Id Node-State Node-Http-Address Number-of-Running-Containers
hadoop01:41279 RUNNING hadoop01:8042 0
hadoop02:46566 RUNNING hadoop02:8042 0
```
hadoo00コンテナのResourceManagerがhadoop01とhadoop02コンテナで動いているNodeManagerを認識していることがわかります。
-
HadoopでWordCountを動かしてみる
分散モードで動いていることを確認できたので、HDFS上にHadoopのREADME.txtを保存してWordCountを実行してみましょう。HDFSでは/root/inputディレクトリがあらかじめ作られているので、この中にファイルをいれます。# HDFSへのデータの保存 -bash-4.1# cd /usr/local/hadoop/bin
-bash-4.1# ./hdfs dfs -put ../README.txt input/
16/12/18 07:42:59 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
# WordCountの実行
-bash-4.1# ./hadoop jar ../share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.1.jar wordcount input output
16/12/18 07:44:16 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
16/12/18 07:44:16 INFO client.RMProxy: Connecting to ResourceManager at hadoop00/172.18.0.2:8032
16/12/18 07:44:17 INFO input.FileInputFormat: Total input paths to process : 33
16/12/18 07:44:18 INFO mapreduce.JobSubmitter: number of splits:33
16/12/18 07:44:18 INFO mapreduce.JobSubmitter: Submitting tokens for job: job_1482064378555_0001
16/12/18 07:44:18 INFO impl.YarnClientImpl: Submitted application application_1482064378555_0001
16/12/18 07:44:18 INFO mapreduce.Job: The url to track the job: http://hadoop00:8088/proxy/application_1482064378555_0001/
16/12/18 07:44:18 INFO mapreduce.Job: Running job: job_1482064378555_0001
16/12/18 07:44:29 INFO mapreduce.Job: Job job_1482064378555_0001 running in uber mode : false
16/12/18 07:44:29 INFO mapreduce.Job: map 0% reduce 0%
16/12/18 07:45:16 INFO mapreduce.Job: map 18% reduce 0%
16/12/18 07:45:25 INFO mapreduce.Job: map 21% reduce 0%
16/12/18 07:45:26 INFO mapreduce.Job: map 33% reduce 0%
16/12/18 07:45:27 INFO mapreduce.Job: map 42% reduce 0%
(略)
16/12/18 07:46:54 INFO mapreduce.Job: map 85% reduce 28%
16/12/18 07:47:01 INFO mapreduce.Job: map 100% reduce 28%
16/12/18 07:47:04 INFO mapreduce.Job: map 100% reduce 67%
16/12/18 07:47:05 INFO mapreduce.Job: map 100% reduce 100%
16/12/18 07:47:08 INFO mapreduce.Job: Job job_1482064378555_0001 completed successfully
16/12/18 07:47:09 INFO mapreduce.Job: Counters: 50
File System Counters
FILE: Number of bytes read=76647
FILE: Number of bytes written=4094462
FILE: Number of read operations=0
(略)
```
きちんと実行できました。他に扱いたいデータがあればホストからscp等でHadoopのマスタコンテナにコピーしてからHDFSに保存してください。
#### 注意
デフォルトのDockerの設定ではコンテナのローカル領域は10GBまでしかないため、大量のデータを置くことができません。あくまでお試し用としてお使い下さい。
ホストのディレクトリをコンテナがマウントすれば容量の制約を回避することもできます。その方法が書かれた記事は簡単に見つかるためここでは触れません。もしもホストのディレクトリをマウントした場合はHDFSのデータを保存するディレクトリの設定を変更して下さい。
-
コンテナを停止する
Hadoopを使ってみた後は、play-book.ymlのstop-all
のコメントアウトを外し、他のロールをコメントアウトしてから実行します。play-book.yml- hosts: localhost vars_files: - vars/hadoop.yml - vars/proxy.yml roles: # - install-docker # - start-master # - start-worker - stop-all
$ ansible-playbook play-book.yml [WARNING]: Host file not found: /usr/local/etc/ansible/hosts [WARNING]: provided hosts list is empty, only localhost is available PLAY [localhost] *************************************************************** TASK [setup] ******************************************************************* ok: [localhost] TASK [stop-all : stop and remove hadoop master container] ********************** changed: [localhost] TASK [stop-all : stop and remove hadoop worker containers] ********************* changed: [localhost] => (item=hadoop01) changed: [localhost] => (item=hadoop02) PLAY RECAP ********************************************************************* localhost : ok=3 changed=2 unreachable=0 failed=0
ということで、dockerコマンドを触ることなくHadoop環境をコンテナで用意して分散処理することができました。
後半
ここからは記事の後半です。
プレイブックやDockerfileをどのように作ったのかを書いていきます。
## 用意するもの
利用者にはDockerもHadoopも使ったことがない人を想定しているため、次のものを用意することにします。
- sequenceiq/hadoop-dockerに手を加えて分散モードで利用できるようにしたHadoopのDockerイメージ
- インストールやイメージのビルド、コンテナの操作といったdockerコマンドを必要とする処理を自動化するAnsibleのプレイブック
sequenceiq/hadoop-dockerについて
sequenceiq/hadoop-dockerは一つのコンテナの中で必要なHadoopのデーモンが全て動くいわゆる擬似分散モードになっています。コンテナを複数立ち上げてもそれらが連携することはなく一つのコンテナの中に閉じたHadoop環境が複数作られるだけです。
sequenceiq/hadoop-dockerは全てのデーモンがコンテナにとってのlocalhostで起動するためコンテナ間の通信を考慮する必要はありませんでしたが、分散モードを試したい今回はマスタとワーカにコンテナの役割を分けた上でワーカがNameNode(HDFSのマスタ)とResourceManager(YARNのマスタ)が起動しているマスタコンテナと相互に通信できるようにしなければなりません。
コンテナ起動時にマスタとワーカに期待する動作は次の通りです。
- マスタコンテナ
- Hadoopの設定ファイルで自分の名前でNameNodeとResourceManagerを設定した後に、コンテナ内でNameNodeとResourceManagerが起動する。
- ワーカコンテナ
- Hadoopの設定ファイル中を書き換えてマスタの場所を設定した後にDataNodeとNodeManagerを起動する。
分散モードのHadoopイメージのDockerfileを作る
まず、sequenceiq/hadoop-dockerのGithubのリポジトリからコードを取ってきて中身を確認してみます。
$ tree hadoop-docker
hadoop-docker
|-- Dockerfile # Hadoopが入った環境を整える。
|-- LICENSE
|-- README.md
|-- bootstrap.sh # コンテナ起動時にsshdとHadoopのデーモンを起動する
|-- core-site.xml.template # NameNodeの場所の設定。コンテナ配備後にsedで書き換えてcore-site.xmlとして$HADOOP_CONF_DIRに保存される
|-- hdfs-site.xml
|-- mapred-site.xml
|-- ssh_config
`-- yarn-site.xml # YARNの設定
元々の動作ではHadoopの設定の書き換えとデーモンの起動はbootstrap.shで行なっていたため、ほとんどがbootstrap.shの修正ですみます。
最初に、マスタとワーカのどちらで振舞うべきかを示す引数とマスタの場所を示す引数を追加し、役割に応じて立ち上げるデーモンを切り替えられるようにしました。
また、NameNodeとResouceManagerが動作する場所(コンテナ)を指し示すために設定ファイルのテンプレートを追加しました。NameNodeの場所を設定するfs.defaultFS
を記述するcore-site.xml.templateは既にあったため、追加するのはResouceManagerの場所を設定するyarn.resourcemanager.hostname
が書かれたyarn-site.xml.templateだけで済みました。
他の変更点として、Dockerfileイメージをビルドするときに欧州のサイトからHadoopを取得している最中にcurlがタイムアウトしたためHadoopの取得先を国内のミラーサイトに変えたり、Dockerfile中で行なっていたHDFSのフォーマットをbootstrap.shのマスタの処理の中に移したりといったものがあります。(細かい変更は他にもあります)
これで分散モードで動くHadoopのイメージの元になるDockerfileができました。
Dockerをインストールするプレイブックを作る
最初に述べたように、ここでは利用者にDockerに関する操作を一切させないことを目指しています。どうせならDockerコマンドだけではなくDockerのインストールも自動化したいと思ったので、そのためのinstall-docker
も用意することにしました。後になってからDockerのイメージのビルドといった下準備の処理もinstall-docker
に加わり、Hadoopコンテナを動かすまでの前準備を一括して行うロールになってしまいました。
AnsibleにはDockerイメージをいじるためのdocker_imageモジュールやDockerコンテナを操作するためのdocker_containerモジュールがあり、「Dockerを知らなくても」とタイトルで書いた部分のほとんどがこれらのモジュールを使ってタスクをあらかじめ定義することで実現しています。これらに必要なdocker-pyもこのロールの中でインストールしています。
イメージをビルドするタスクに該当する処理を見てみます。
(略)
- name: build hadoop-docker images.
docker_image:
path: "{{ hadoop_dockerfile_dir }}"
name: hadoop-docker
state: present
buildargs:
http_proxy: "{{ http_proxy }}"
https_proxy: "{{ https_proxy }}"
docker_imageモジュールはpathパラメータで指定されたディレクトリにあるDockerfileをビルドします。また、プロキシ環境下でイメージをビルドするために必要な設定も与えています。
これで分散モードのHadoopが動くDockerイメージのビルドができるようになりました。
個人的に気になったこと その1
このタスクを作っている時、docker_imageモジュールとDockerでイメージをビルドする時に叩くdocker build
コマンドのデフォルトの動作が違うことに気づきました。
docker build
コマンドでイメージをビルドする場合はイメージが既に存在していたとしても以前のビルド後にDockerfileに手を加えられているとそれを検知して修正後のDockerfileからイメージをビルドし直してくれるのですが、docker_imageモジュールはforce: yes
を指定しないとビルドし直してくれませんでした。
今回はDockerfileに手を加えることを想定していないため、force: yes
は外しています。
Hadoopのコンテナを起動するプレイブックを用意
Hadoopコンテナを起動するロールとしてstart-master
とstart-worker
を用意します。先ほど作ったイメージは起動時の引数を切り替えることでマスタとワーカの役割を分けることができるようにしていました。次に担保すべきことはコンテナ間で相互に通信ができることです。
Dockerはコンテナ間の通信を実現するためにいくつかの方法を持っています。よく紹介されている方法は--link container_name
で接続したいコンテナ名を指定することです。ただし、この方法は双方向の通信ではなく片方向の通信しかできない上に、既に動いているコンテナしか指定することができないため、順番に起動するHadoopのマスタとワーカのコンテナ間で双方向の通信を確保することができません。
そこで、後からコンテナを追加してもコンテナ名で通信が可能なユーザ定義の仮想ネットワークを作りそのネットワーク上でHadoopのコンテナを起動することにします。AnsibleはDockerの仮想ネットワークを操作するdocker_networkモジュールも持っているのでこれを使うことにします。
仮想ネットワークを作ってHadoopのマスタコンテナを起動するstart-master
のプレイブックを作ります。
- name: create hadoop-network
docker_network:
name: hadoop-network
- name: run hadoop master container
docker_container:
name: "{{ hadoop_master }}"
hostname: "{{ hadoop_master }}"
image: hadoop-docker
command: master {{ hadoop_master }}
state: started
purge_networks: yes
networks:
- name: hadoop-network
aliases:
- "{{ hadoop_master }}"
ports:
- "2122:2122"
- "9000"
- "19888:19888"
一つ目のタスクでHadoopコンテナを作るためのhadoop-networkを作っています。このタスクを実行したあとホスト側でDockerのネットワークを確認すると、デフォルトのbridge、host、noneのに加えて新しくhadoop-networkがbridgeドライバで作られていることが分かります。
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
ccdbd510466e bridge bridge local
71acb8dc92eb hadoop-network bridge local
2c76e290d424 host host local
834660ee4e16 none null local
二つ目のタスクはhadoop-networkネットワーク上でマスタコンテナを起動します。networks
パラメータの中のaliases
でこのコンテナをvars/hadoop.yml
で指定したマスタの名前で登録しているため、後から追加するワーカコンテナはマスタのaliasでマスタコンテナと通信することができるようになっています。
ワーカコンテナを起動するロールもマスタと同じように作りました。
個人的に気になったこと その2
よく見ると、コンテナを起動するタスクでpurge_networks: yes
というパラメータがあります。これは、networks
で指定しているネットワーク以外のネットワークから離脱するためのものです。
実はpurge_networks: yes
を指定せずにコンテナを起動すると、このコンテナはデフォルトのネットワークであるbridgeとhadoop-networkの両方に紐づけられました。すると、docker_containerモジュールのドキュメントのpublished_ports(portsのalias)
のパラメータの説明にあるように、networks
パラメータが指定されたコンテナはコンテナが所属するネットワークリストのうち最初に見つかったbridgeドライバのネットワークのipv4アドレスにバインドされるため、マスタの公開ポートは期待するhadoop-networkではなくbridgeに紐づけられてしまいます。すると、デフォルトのbridgeネットワークにはalias機能がないためコンテナ間の名前解決ができなくなってしまいワーカコンテナはマスタコンテナと通信することができませんでした。
試しにdocker_containerモジュールではなく以下のdockerコマンドでコンテナを起動するとhadoop-networkのみに紐づけられることが確認できました。
# コンテナの起動
$ docker run -d --network hadoop-network --name hadoop00 --hostname hadoop00 hadoop-docker master hadoop00
4d36cfe7b8f3e16c62a684a7696ea948cb457e46db5035701e9b447bf0fd9715
# bridgeネットワークに属するコンテナ
$ docker network inspect bridge |grep -A 4 Containers
"Containers": {},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
# hadoop-networkに属するコンテナ
$ docker network inspect hadoop-network |grep -A 4 Containers
"Containers": {
"4d36cfe7b8f3e16c62a684a7696ea948cb457e46db5035701e9b447bf0fd9715": {
"Name": "hadoop00",
"EndpointID": "7d03fb8a5839dbe9be41f44799acb1fd6ffa6b6ccddc3c14044bb85d050a49e3",
"MacAddress": "02:42:ac:12:00:02",
どなたか他に良い回避方法をご存知でしたら教えて下さい。
Hadoopのコンテナを停止するプレイブックを用意
コンテナを停止するためのロールであるstop-hadoop
を用意します。
コンテナを起動するタスクのうち不要なパラメータを消してstate: absent
にしただけです。
- name: stop and remove hadoop master container
docker_container:
name: "{{ hadoop_master }}"
state: absent
- name: stop and remove hadoop worker containers
docker_container:
name: "{{ item }}"
state: absent
with_items:
- "{{ hadoop_worker }}"
これでDockerのインストール、イメージのビルド、Hadoopの起動と停止を実現するプレイブックが揃いました。
まとめ
当初の目的通りDockerコマンドを使わずにHadoopのDockerコンテナで分散処理を試すことができました。
今回初めてAnsibleのDockerモジュールを触ってみたのですが、元々HadoopとDockerに慣れていたこともありそんなに苦労しないだろうと想定していました。ところが慣れによる先入観からAnsibleモジュールの思わぬ挙動になかなか気づかずコンテナ間で通信ができない原因の調査のために試行錯誤を繰り返すことになり大変でした。ただ、トラブルシューティングは好きなのでそういった苦労も含めて新しいモジュールを使うことを楽しみながら記事を書くことができました。
Dockerは便利なだけではなく楽しい技術です。まだDockerに触れたことがないという方は、このプレイブックを動かすとお手元のサーバにDocker環境が出来上がっているはずですので実際にDockerのコマンドを叩いてみることをオススメします。