ローカルマシンで Docker を使って Hadoop の分散処理環境の構築を試みた

#### 1. 背景

ローカルマシンで Docker を使って Hadoop の分散処理環境の構築を試みたときのログです。
Docker を使い始めた頃に挑戦した内容のため、最適化されていない部分が多々あります。
とりあえず、動くものを作りたいというのが動機でした。

Windows10 上の VirtualBox で Ubuntu18.04を動作させ、その上で Docker を用いて Hadoop 環境をインストールしようとしています。
その VirtualBox には、8192MBのメモリと、2CPUを割り当てています。

|ホスト|ゲスト(VirtualBox)|割当メモリ|割当CPU|
|---|---|---|---|
|Windows10|Ubuntu18.04|8192MB|2CPU|

今では、Hadoop クラスタの Docker イメージは企業からも公開されています。HDPからも sandbox でありますが、私のローカル環境ではスペック不足でフリーズしてしまいました。
[sandbox](https://hub.docker.com/r/hortonworks/sandbox-hdp-standalone)

##### Docker バージョンの確認
この記事を書くために、動かした Docker のバージョンです。
実際に挑戦したのは、2年ほど前だったと思いますので、当時動かしたときはもっと古いバージョンだったはずです。

```bash:version
$ docker version
Client:
 Version:           18.09.7
 API version:       1.39
 Go version:        go1.10.1
 Git commit:        2d0083d
 Built:             Wed Jul  3 12:13:59 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server:
 Engine:
  Version:          18.09.7
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.10.1
  Git commit:       2d0083d
  Built:            Mon Jul  1 19:31:12 2019
  OS/Arch:          linux/amd64
  Experimental:     false
```

#### 2. Dockerfile
ファイル名は "Dockerfile.hadoop" です。
とにかく RUN コマンドで順番にインストールするようにしていました。
RUN コマンドを実行する度にイメージレイヤーが生成され、イメージのサイズが大きくなるなど知らなかったため、ほとんどのコマンド一つにつき、RUN コマンドを書いています。
ベースを centos6 にした理由は単に馴染みがあったためです。

```bash:Dockerfile.hadoop
FROM centos:centos6
LABEL maintainer "blueskyarea"

USER root

# 外部コマンドのインストール
RUN yum update -y libselinux; \
    yum install -y curl which tar sudo openssh-server openssh-clients rsync; \
    yum clean all

# パスワードなし ssh
# 複数のコンテナ間で ssh 接続することを想定
RUN ssh-keygen -q -N "" -t dsa -f /etc/ssh/ssh_host_dsa_key
RUN ssh-keygen -q -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key
RUN ssh-keygen -q -N "" -t rsa -f /root/.ssh/id_rsa
RUN cp /root/.ssh/id_rsa.pub /root/.ssh/authorized_keys

# Javaのインストール
RUN yum install -y java-1.8.0-openjdk
ENV JAVA_HOME /usr/lib/jvm/jre
ENV PATH $PATH:$JAVA_HOME/bin
RUN rm /usr/bin/java && ln -s $JAVA_HOME/bin/java /usr/bin/java

# HDP レポジトリを登録
ENV HADOOP_GROUP hadoop
ADD http://public-repo-1.hortonworks.com/HDP/centos6/2.x/updates/2.6.2.0/hdp.repo /etc/yum.repos.d/hdp.repo

# zookeeperのインストール
# ディレクトリ構成に一貫性がなくて分かりにくい
ENV ZOOKEEPER_USER zookeeper
ENV ZOOKEEPER_LOG_DIR /var/log/zookeeper
ENV ZOOKEEPER_PID_DIR /var/run/zookeeper
ENV ZOOKEEPER_DATA_DIR /grid/hadoop/zookeeper/data
ENV ZOOKEEPER_CONF_DIR /etc/zookeeper/conf

RUN yum install -y zookeeper-server hadoop

RUN mkdir -p $ZOOKEEPER_LOG_DIR
RUN chown -R $ZOOKEEPER_USER:$HADOOP_GROUP $ZOOKEEPER_LOG_DIR
RUN chmod -R 755 $ZOOKEEPER_LOG_DIR

RUN mkdir -p $ZOOKEEPER_PID_DIR
RUN chown -R $ZOOKEEPER_USER:$HADOOP_GROUP $ZOOKEEPER_PID_DIR
RUN chmod -R 755 $ZOOKEEPER_PID_DIR

RUN mkdir -p $ZOOKEEPER_DATA_DIR
RUN chmod -R 755 $ZOOKEEPER_DATA_DIR
RUN chown -R $ZOOKEEPER_USER:$HADOOP_GROUP $ZOOKEEPER_DATA_DIR

# なぜ、一度削除しているのか
RUN rm -r $ZOOKEEPER_CONF_DIR
RUN mkdir -p $ZOOKEEPER_CONF_DIR
RUN chmod a+x $ZOOKEEPER_CONF_DIR
RUN chown -R $ZOOKEEPER_USER:$HADOOP_GROUP $ZOOKEEPER_CONF_DIR/../
RUN chmod 755 $ZOOKEEPER_CONF_DIR/../

# Hadoopのインストール
RUN umask 0022
RUN yum install -y hadoop-hdfs hadoop-libhdfs hadoop-yarn hadoop-mapreduce hadoop-client openssl

# Snappyのインストール
# インストールする必要はあったのか？
RUN yum install -y snappy snappy-devel

# LZOのインストール
# インストールする必要はあったのか？
RUN yum install -y lzo lzo-devel hadooplzo hadooplzo-native

# NameNodeの環境設定
ENV DFS_NAME_DIR /grid/hadoop/hdfs/nn
ENV HDFS_USER hdfs
RUN mkdir -p $DFS_NAME_DIR
RUN chown -R $HDFS_USER:$HADOOP_GROUP $DFS_NAME_DIR
RUN chmod -R 755 $DFS_NAME_DIR

# SecondaryNameNodeの環境設定
ENV FS_CHECKPOINT_DIR /grid/hadoop/hdfs/snn
RUN mkdir -p $FS_CHECKPOINT_DIR
RUN chown -R $HDFS_USER:$HADOOP_GROUP $FS_CHECKPOINT_DIR
RUN chmod -R 755 $FS_CHECKPOINT_DIR

# DataNodeの環境設定
ENV DFS_DATA_DIR /grid/hadoop/hdfs/dn
RUN mkdir -p $DFS_DATA_DIR
RUN chown -R $HDFS_USER:$HADOOP_GROUP $DFS_DATA_DIR
RUN chmod -R 750 $DFS_DATA_DIR

# NodeManagerの環境設定
ENV YARN_LOCAL_DIR /grid/hadoop/yarn/local
ENV YARN_USER yarn
RUN mkdir -p $YARN_LOCAL_DIR
RUN chown -R $YARN_USER:$HADOOP_GROUP $YARN_LOCAL_DIR
RUN chmod -R 755 $YARN_LOCAL_DIR

ENV YARN_LOCAL_LOG_DIR /grid/hadoop/yarn/logs
RUN mkdir -p $YARN_LOCAL_LOG_DIR
RUN chown -R $YARN_USER:$HADOOP_GROUP $YARN_LOCAL_LOG_DIR
RUN chmod -R 755 $YARN_LOCAL_LOG_DIR

# HDFS Logsの環境設定
ENV HDFS_LOG_DIR /var/log/hadoop/hdfs
RUN mkdir -p $HDFS_LOG_DIR
RUN chown -R $HDFS_USER:$HADOOP_GROUP $HDFS_LOG_DIR
RUN chmod -R 755 $HDFS_LOG_DIR

# Yarn Logsの環境設定
ENV YARN_LOG_DIR /var/log/hadoop/yarn
RUN mkdir -p $YARN_LOG_DIR
RUN chown -R $YARN_USER:$HADOOP_GROUP $YARN_LOG_DIR
RUN chmod -R 755 $YARN_LOG_DIR

# HDFS Processの環境設定
ENV HDFS_PID_DIR /var/run/hadoop/hdfs
RUN mkdir -p $HDFS_PID_DIR
RUN chown -R $HDFS_USER:$HADOOP_GROUP $HDFS_PID_DIR
RUN chmod -R 755 $HDFS_PID_DIR

# Yarn Process IDの環境設定
ENV YARN_PID_DIR /var/run/hadoop/yarn
RUN mkdir -p $YARN_PID_DIR
RUN chown -R $YARN_USER:$HADOOP_GROUP $YARN_PID_DIR
RUN chmod -R 755 $YARN_PID_DIR

# JobHistory Server Logsの環境設定
ENV MAPRED_LOG_DIR /var/log/hadoop/mapred
ENV MAPRED_USER mapred
RUN mkdir -p $MAPRED_LOG_DIR
RUN chown -R $MAPRED_USER:$HADOOP_GROUP $MAPRED_LOG_DIR
RUN chmod -R 755 $MAPRED_LOG_DIR

# JobHistory Server Process IDの環境設定
ENV MAPRED_PID_DIR /var/run/hadoop/mapred
RUN mkdir -p $MAPRED_PID_DIR
RUN chown -R $MAPRED_USER:$HADOOP_GROUP $MAPRED_PID_DIR
RUN chmod -R 755 $MAPRED_PID_DIR

# Hadoop の環境設定
ENV HADOOP_CONF_DIR /etc/hadoop/conf
RUN chown -R $HDFS_USER:$HADOOP_GROUP $HADOOP_CONF_DIR/../
RUN chmod -R 755 $HADOOP_CONF_DIR/../

# 設定ファイルおよび、スクリプトをコンテナにコピー
COPY files/zookeeper/* $ZOOKEEPER_CONF_DIR/
COPY files/hadoop/* /etc/hadoop/conf/
COPY scripts/hdfs-setup.sh /
COPY scripts/start-zookeeper.sh /
COPY scripts/cmd.sh /

# ポート開放
EXPOSE 8020 8040 8088 9000 19888 50070

# コンテナ起動時に呼ばれるスクリプト
CMD ["/cmd.sh"]
```

#### 3. docker-compose
上記の Dockerfile.hadoop を元に、hadoop273 という名前で Docker イメージを作成します。
その Docker イメージを元に、2つのコンテナを生成します。
- Resource Manager 用のコンテナ(container-hdp-rm1001)
- Node Manager 用のコンテナ(container-hdp-nm2001)

また、専用の docker network(hadoop_nw)を構築します。
これはコンテナ間でのネットワーク通信用です。

```bash:docker-compose.yml
version: '2'

services:
  # Resource Manager
  rm1001:
    build:
      context: .
      dockerfile: ./Dockerfiles/Dockerfile.hadoop
    image: hadoop273
    hostname: hdp-rm1001.docker
    container_name: container-hdp-rm1001
    ports:
      - "8040:8040"
      - "8088:8088"
      - "9000:9000"
      - "19888:19888"
      - "50070:50070"
    mem_limit: 1g
    networks:
      hadoop_nw:
        ipv4_address: 172.20.0.2
    extra_hosts:
      - "hdp-nm2001.docker:172.20.0.3"
    environment:
      TAG: RM

  # Node Manager
  nm2001:
    build:
      context: .
      dockerfile: ./Dockerfiles/Dockerfile.hadoop
    image: hadoop273
    hostname: hdp-nm2001.docker
    container_name: container-hdp-nm2001
    ports:
      - "45454:45454"
      - "50010:50010"
    depends_on:
      - "rm1001"
    mem_limit: 1g
    networks:
      hadoop_nw:
        ipv4_address: 172.20.0.3
    extra_hosts:
      - "hdp-rm1001.docker:172.20.0.2"
    environment:
      TAG: NM

networks:
  hadoop_nw:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.20.0.0/16
          gateway: 172.20.0.254
```

#### 4. スクリプトファイルたち
##### cmd.sh (コンテナ起動時に呼ばれるスクリプト)
上記の docker-compose.yml で定義したTAG情報を元に、そのコンテナを ResourceManager(とNameNode) として起動するか、NodeManager(とDataNode）として起動するか判断させています。
また、このスクリプトが終了してしまうと、コンテナも終了してしまうため、スクリプトの最後で無限ループをつくり、スクリプト（コンテナ）が終了してしまわないようにしています。

```bash:cmd.sh
#!/bin/bash
if [ "$TAG" = 'RM' ]; then
  # ResourceManager の起動
  /usr/hdp/2.6.2.0-205/hadoop-yarn/sbin/yarn-daemon.sh start resourcemanager &&
  rm -rf /tmp/hadoop-root/dfs/data/current &&
  # NameNode の起動
  echo "Y" | /usr/hdp/2.6.2.0-205/hadoop/bin/hdfs --config /etc/hadoop/conf namenode -format &&
  /usr/hdp/2.6.2.0-205/hadoop/sbin/hadoop-daemon.sh start namenode &&
  sleep 60 &&
  /bin/bash /hdfs-setup.sh &&
  # JobHistoryServer の起動
  /usr/hdp/2.6.2.0-205/hadoop-mapreduce/sbin/mr-jobhistory-daemon.sh start historyserver &&
  # 無限ループをつくり、このスクリプトが終了しないようにしている
  while true; do sleep 1000; done
elif [ "$TAG" = 'NM' ]; then
  rm -rf /grid/hadoop/hdfs/dn/* &&
  sleep 30 &&
  # NodeManager の起動
  /usr/hdp/2.6.2.0-205/hadoop-yarn/sbin/yarn-daemon.sh start nodemanager &&
  # DataNode の起動
  /usr/hdp/2.6.2.0-205/hadoop/sbin/hadoop-daemon.sh start datanode &&
  /bin/bash /start-zookeeper.sh &&
  # 無限ループをつくり、このスクリプトが終了しないようにしている
  while true; do sleep 1000; done
fi
```

##### hdfs-setup.sh (cmd.sh 内で呼ばれるスクリプト)
HDFSに /user/my_data ディレクトリを作成しようとしています。必須ではなく、単にディレクトリを作成しようとしているだけです。

```bash:hdfs-setup.sh
#!/bin/bash
set -e
hdfs dfs -mkdir -p /user/my_data
exit 0
```

##### start-zookeeper.sh (cmd.sh 内で呼ばれるスクリプト)
zookeeper を起動しようとしています。そのコンテナが NodeManager の場合、このスクリプトが呼び出されます。

```bash:start-zookeeper.sh
#!/bin/bash
set -e
su - zookeeper -c "export ZOOCFGDIR=/usr/hdp/2.6.2.0-205/zookeeper/conf ; export ZOOCFG=zoo.cfg; source /usr/hdp/2.6.2.0-205/zookeeper/conf/zookeeper-env.sh ; export ZOO_LOG_DIR=/var/log/zookeeper ;  /usr/hdp/current/zookeeper-server/bin/zkServer.sh start"
exit 0
```

#### 5. 実行
docker-compose.yml ファイルが存在するディレクトリ上にて、docker-compose up -d を実行すると、ビルド・プルを自動で行ってから、依存関係に沿った順番でコンテナを起動してくれるはずです。
コマンドをまとめずに、RUNコマンドを一つずつのコマンドに対して書いたため、95ステップもあります。

```bash:docker-compose
$ docker-compose up -d
Creating network "273_hadoop_nw" with driver "bridge"
Building rm1001
Step 1/95 : FROM centos:centos6
centos6: Pulling from library/centos
...
 ---> 89b88369b754
Successfully built 89b88369b754
Successfully tagged hadoop273:latest
```
約20分ほどで起動できました。
docker image のサイズが 約1.7GB と大きいです。
元のイメージ centos6 から 約1.5GB 増えています。
RUNコマンドを呼ぶ回数を減らして、イメージレイヤーを最適化できれば、数百MB程度は削れるかもしれません。

```bash:images
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hadoop273           latest              89b88369b754        18 minutes ago      1.71GB
centos              centos6             d0957ffdf8a2        9 months ago        194MB
```

コンテナも ResourceManager(NameNode)用 と NodeManager(DataNode)用が起動されています。

```bash:containers
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                                                                                                                                  NAMES
9028c6b408bd        hadoop273           "/cmd.sh"           11 minutes ago      Up 11 minutes       8020/tcp, 8040/tcp, 8088/tcp, 9000/tcp, 0.0.0.0:45454->45454/tcp, 19888/tcp, 50070/tcp, 0.0.0.0:50010->50010/tcp                       container-hdp-nm2001
f6d1d724aca4        hadoop273           "/cmd.sh"           11 minutes ago      Up 11 minutes       0.0.0.0:8040->8040/tcp, 0.0.0.0:8088->8088/tcp, 0.0.0.0:9000->9000/tcp, 0.0.0.0:19888->19888/tcp, 8020/tcp, 0.0.0.0:50070->50070/tcp   container-hdp-rm1001
```

試しにNodeManager側のコンテナで bash を起動し、hdfs コマンドを実行してみます。
hdfs-setup.sh 内で作成する /user/my_data が生成されていることが確認できましたので HDFS が使える状態になっていることが分かります。

```
$ docker exec -it container-hdp-nm2001 bash
# hdfs dfs -ls /user
Found 1 items
drwxr-xr-x   - root supergroup          0 2019-12-13 13:50 /user/my_data
```

#### 6. さいごに
当初の目標であった"ローカルマシンで Docker を使って Hadoop の分散処理環境の構築"は確認することができました。
ここに HBase や Spark などを使って、実際のデータで分散処理や格納などを行うとすると、やはりスペック的に動作ができないなど、厳しい面が出てくると思われますが、小さいデータでも扱えるテスト環境が作れると楽しいだろうと思います。
イメージサイズが大きいことや、コンテナを起動し続けるために、無理やり while ループを使っているなど、ほかにも改善ポイントはたくさんありますが、初めての docker を見よう見まねでも動いたことには嬉しかったです。