Docker で debian 9 をベースに Hadoop(version3.1.3) コンテナ作成

### 目的
- ローカルで手軽に Hadoop のテストができる環境を構築したい
- centos 7 をベースに作成した際に[課題](https://blueskyarea.hatenablog.com/entry/2020/05/30/201331)があったので、debian 9 ベースで作成してみたい  

### ディレクトリ構造
```bash
├── base
│   ├── config
│   │   ├── core-site.xml
│   │   ├── hadoop-env.sh
│   │   ├── hdfs-site.xml
│   │   ├── mapred-site.xml
│   │   ├── workers
│   │   └── yarn-site.xml
│   ├── Dockerfile
│   └── script
│       └── start.sh
├── docker-compose.yml
```

### 設定ファイルの準備
#### core-site.xml
とりあえず、fs.default.name だけ定義していればサービスの起動は可能なはずです。

```bash
<configuration>
  <property>
    <name>fs.default.name</name>
    <value>hdfs://localhost:9000</value>
  </property>
</configuration>
```

#### hdfs-site.xml
データの保存先として、/hadoop-data を指定しています。

```bash
<configuration>
  <property>
    <name>dfs.replication</name>
    <value>1</value>
  </property>
  <property>
    <name>dfs.name.dir</name>
    <value>file:///hadoop-data/dfs/name</value>
  </property>
  <property>
    <name>dfs.data.dir</name>
    <value>file:///hadoop-data/dfs/data</value>
  </property>
</configuration>
```

#### yarn-site.xml
- 低スペックのローカルマシンで動作させるため、デフォルトのメモリ割り当て量(8192MB)とコア割当数(8)から減らしています。
- yarn.nodemanager.aux-services の設定は、エラー "org.apache.hadoop.yarn.exceptions.InvalidAuxServiceException: The auxService:mapreduce_shuffle does not exist" を回避するために必要。

```bash
<configuration>
  <property>
    <name>yarn.resourcemanager.hostname</name>
    <value>doc-hmaster101.local</value>
  </property>
  <property>
    <name>yarn.scheduler.maximum-allocation-mb</name>
    <value>4098</value>
  </property>
  <property>
    <name>yarn.nodemanager.resource.memory-mb</name>
    <value>4098</value>
  </property>
  <property>
    <name>yarn.nodemanager.resource.cpu-vcores</name>
    <value>4</value>
  </property>
  <property>
    <name>yarn.nodemanager.aux-services</name>
    <value>mapreduce_shuffle</value>
  </property>
</configuration>
```

#### mapred-site.xml
Java8 から Virtual Memory の使用量が増えたことにより、container が kill されてしまう可能性があるので、設定によりを抑制する必要があります。

```bash
<configuration>
    <property>
        <name>mapreduce.framework.name</name>
        <value>yarn</value>
    </property>
    <property>
        <name>yarn.app.mapreduce.am.env</name>
        <value>HADOOP_MAPRED_HOME=/opt/hadoop/current</value>
    </property>
    <property>
        <name>mapreduce.map.env</name>
        <value>HADOOP_MAPRED_HOME=/opt/hadoop/current</value>
    </property>
    <property>
        <name>mapreduce.reduce.env</name>
        <value>HADOOP_MAPRED_HOME=/opt/hadoop/current</value>
    </property>
    <!-- To avoid 2.4 GB of 2.1 GB virtual memory used. Killing container--> 
    <property>
        <name>mapreduce.map.java.opts</name>
        <value>-XX:ReservedCodeCacheSize=100M -XX:MaxMetaspaceSize=256m -XX:CompressedClassSpaceSize=256m -Xmx1536m -Xms512m</value>
    </property> 
    <property>
        <name>mapreduce.reduce.java.opts</name>
        <value>-XX:ReservedCodeCacheSize=100M -XX:MaxMetaspaceSize=256m -XX:CompressedClassSpaceSize=256m -Xmx1536m -Xms512m</value>
    </property> 
</configuration>
```

#### hadoop-env.sh
HDFS_XXXX_USER や YARN_XXXX_USER の定義は必須。Hadoop 2.x では必要なかった気がします。

```bash
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64/
export HADOOP_HOME=/opt/hadoop/current
export HADOOP_CONF_DIR=/opt/hadoop/current/etc/hadoop/
export HADOOP_HDFS_HOME=$HADOOP_HOME
export HADOOP_MAPRED_HOME=$HADOOP_HOME
export HADOOP_YARN_HOME=$HADOOP_HOME
export HADOOP_OPTS=-Djava.net.preferIPv4Stack=true

export HDFS_NAMENODE_USER=root
export HDFS_DATANODE_USER=root
export HDFS_SECONDARYNAMENODE_USER=root
export YARN_RESOURCEMANAGER_USER=root
export YARN_NODEMANAGER_USER=root
```

#### workers
Hadoop-slave 用に起動するコンテナのホスト名を記述しておきます。

```bash
doc-hslave101.local
```

### Dockerfile をつくる
実際にはいきなり書き始めるのではなくて、docker run -it debian:9 /bin/bash でコンテナを起動し、そのコンテナ上で各種インストールや動作確認を進めています。
その上で動作確認できたコマンドを Dockerfile に落としています。

```bash
FROM debian:9
LABEL maintainer "blueskyarea"

# Set login shell as bash
SHELL ["/bin/bash", "-c"]

# install dev tools
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
  vim \
  systemd \
  net-tools \
  curl \
  netcat \
  gnupg \
  telnet \
  rsync \
  openssh-server \
  openssh-client
      
# passwordless ssh
RUN ssh-keygen -q -N "" -t dsa -f /etc/ssh/ssh_host_dsa_key <<< y
RUN ssh-keygen -q -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key <<< y
RUN ssh-keygen -q -N "" -t rsa -f /root/.ssh/id_rsa <<< y
RUN cp /root/.ssh/id_rsa.pub /root/.ssh/authorized_keys

# Java
RUN apt-get install -y openjdk-8-jdk
ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64/
ENV PATH $PATH:$JAVA_HOME/bin

# Hadoop
ENV HADOOP_VERSION 3.1.3
ENV HADOOP_URL https://www.apache.org/dist/hadoop/common/hadoop-$HADOOP_VERSION/hadoop-$HADOOP_VERSION.tar.gz
RUN mkdir /opt/hadoop
RUN set -x \
    && curl -fSL "$HADOOP_URL" -o /tmp/hadoop.tar.gz --insecure \
    && tar -xvf /tmp/hadoop.tar.gz -C /opt/hadoop/ \
    && rm -f /tmp/hadoop.tar.gz*

RUN ln -nfs /opt/hadoop/hadoop-$HADOOP_VERSION /opt/hadoop/current
RUN ln -s /opt/hadoop/current/etc/hadoop /etc/hadoop
RUN mkdir /opt/hadoop/current/logs
RUN mkdir /hadoop-data

ENV HADOOP_HOME /opt/hadoop/current
ENV PATH $PATH:$HADOOP_HOME/bin
ENV HDFS_NAMENODE_USER root
ENV HDFS_DATANODE_USER root
ENV HDFS_SECONDARYNAMENODE_USER root
ENV YARN_RESOURCEMANAGER_USER root
ENV YARN_NODEMANAGER_USER root

COPY base/config/core-site.xml $HADOOP_HOME/etc/hadoop/core-site.xml
COPY base/config/hdfs-site.xml $HADOOP_HOME/etc/hadoop/hdfs-site.xml
COPY base/config/mapred-site.xml $HADOOP_HOME/etc/hadoop/mapred-site.xml
COPY base/config/yarn-site.xml $HADOOP_HOME/etc/hadoop/yarn-site.xml
COPY base/config/hadoop-env.sh $HADOOP_HOME/etc/hadoop/hadoop-env.sh
COPY base/config/workers $HADOOP_HOME/etc/hadoop/workers
COPY base/script/start.sh $HADOOP_HOME/start.sh
RUN chmod +x $HADOOP_HOME/start.sh
RUN rm -rf /var/lib/apt/lists/*

EXPOSE 8020 8040 8088 9000 9870 19888 50070

CMD ["/opt/hadoop/current/start.sh"]
```

### docker-compose.xml をつくる
Hadoop-master と Hadoop-slave のコンテナを作成します。
元になるイメージは同じ(hadoop-base)ですが、動作させるプロセスが異なってきます。

```bash
version: '2'

services:
  # Hadoop-master
  hmaster101:
    build: 
      context: .
      dockerfile: ./base/Dockerfile
    image: hadoop-base
    hostname: doc-hmaster101.local
    container_name: hmaster101
    ports:
      - "8040:8040"
      - "8088:8088"
      - "9000:9000"
      - "9870:9870"
      - "19888:19888"
      - "50070:50070"
    mem_limit: 1g
    networks:
      hadoop_nw:
        ipv4_address: 172.30.0.2
    extra_hosts:
      - "doc-hslave101.local:172.30.0.3"
    environment:
      TAG: HMASTER
  
  # Hadoop-slave
  hslave101:
    build: 
      context: .
      dockerfile: ./base/Dockerfile
    image: hadoop-base
    hostname: doc-hslave101.local
    container_name: hslave101
    mem_limit: 2g
    networks:
      hadoop_nw:
        ipv4_address: 172.30.0.3
    extra_hosts:
      - "doc-hmaster101.local:172.30.0.2"
    environment:
      TAG: HSLAVE

networks:
  hadoop_nw:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.30.0.0/16
          gateway: 172.30.0.254
```

### 起動用スクリプトをつくる
- namenode などを起動するためには、ssh サービスが必要となるため、最初に ssh サービスをスタートさせています。
- Hadoop-slave(TAGがHSLAVE) のコンテナ上では特にスクリプトは実行させません。
- Hadoop-master(TAGがHMASTER) のコンテナ上で "start-hdfs" と "start-yarn" を実行します。
- コンテナを起動し続けるために、とりあえず while ループを回しています(docker-compose 上で tty: true を指定しても動作しなかったため)

```bash
#!/bin/bash
set -e

/etc/init.d/ssh start
if [ "${TAG}" = 'HSLAVE' ]; then
  while true; do sleep 1000; done 
fi

if [ "${TAG}" = 'HMASTER' ]; then
  echo "start-hdfs" &&
  rm -rf /hadoop-data/dfs/* &&
  ${HADOOP_HOME}/bin/hadoop namenode -format &&
  ${HADOOP_HOME}/sbin/start-dfs.sh
  echo "start-yarn" &&
  ${HADOOP_HOME}/sbin/start-yarn.sh
  while true; do sleep 1000; done 
fi
```

### 動作確認
#### Hadoop Master 側プロセス

```bash
$ docker exec hmaster101 jps
1088 Jps
756 ResourceManager
278 NameNode
487 SecondaryNameNode
```

#### Hadoop Slave 側プロセス
サービス起動用のスクリプト(start-dfs.sh と start-yarn.sh)を実行したのは、Hadoop Master 側のみですが、Hadoop Slave 側でのプロセスもきちんと起動していることが確認できます。

```bash
$ docker exec hslave101 jps
81 DataNode
264 Jps
175 NodeManager
```

dfsadmin により、1台の Datanode が active になっていることが確認できます。

```bash
$ docker exec hmaster101 hdfs dfsadmin -report
Configured Capacity: 66355564544 (61.80 GB)
Present Capacity: 12480409600 (11.62 GB)
DFS Remaining: 12480385024 (11.62 GB)
DFS Used: 24576 (24 KB)
DFS Used%: 0.00%
Replicated Blocks:
	Under replicated blocks: 0
	Blocks with corrupt replicas: 0
	Missing blocks: 0
	Missing blocks (with replication factor 1): 0
	Low redundancy blocks with highest priority to recover: 0
	Pending deletion blocks: 0
Erasure Coded Block Groups: 
	Low redundancy block groups: 0
	Block groups with corrupt internal blocks: 0
	Missing block groups: 0
	Low redundancy blocks with highest priority to recover: 0
	Pending deletion blocks: 0

-------------------------------------------------
Live datanodes (1):

Name: 172.30.0.3:9866 (doc-hslave101.local)
Hostname: doc-hslave101.local
Decommission Status : Normal
Configured Capacity: 66355564544 (61.80 GB)
DFS Used: 24576 (24 KB)
Non DFS Used: 50473996288 (47.01 GB)
DFS Remaining: 12480385024 (11.62 GB)
DFS Used%: 0.00%
DFS Remaining%: 18.81%
Configured Cache Capacity: 0 (0 B)
Cache Used: 0 (0 B)
Cache Remaining: 0 (0 B)
Cache Used%: 100.00%
Cache Remaining%: 0.00%
Xceivers: 1
Last contact: Sun May 31 10:27:33 UTC 2020
Last Block Report: Sun May 31 10:25:40 UTC 2020
Num of Blocks: 0
```

#### MapReduce で PI の計算処理
よくテスト実行で使われる処理です。
もし JVM オプションにてネイティブ領域の使用最大量を制限していなかった場合、MapReduce の処理時に "2.4 GB of 2.1 GB virtual memory used. Killing container" のようなエラーが発生します。

```bash
$ docker exec hmaster101 /opt/hadoop/current/bin/hadoop jar /opt/hadoop/current/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar pi 2 100
Number of Maps  = 2
Samples per Map = 100
（省略）
2020-MM-dd 14:02:32,055 INFO mapreduce.Job: Running job: job_1590928186996_0002
2020-MM-dd 14:02:54,990 INFO mapreduce.Job: Job job_1590928186996_0002 running in uber mode : false
2020-MM-dd 14:02:54,993 INFO mapreduce.Job:  map 0% reduce 0%
2020-MM-dd 14:03:08,413 INFO mapreduce.Job:  map 50% reduce 0%
2020-MM-dd 14:03:18,688 INFO mapreduce.Job:  map 100% reduce 0%
2020-MM-dd 14:03:31,866 INFO mapreduce.Job:  map 100% reduce 100%
2020-MM-dd 14:03:32,895 INFO mapreduce.Job: Job job_1590928186996_0002 completed successfully
（省略）
Job Finished in 65.878 seconds
2020-MM-dd 14:03:33,387 INFO sasl.SaslDataTransferClient: SASL encryption trust check: localHostTrusted = false, remoteHostTrusted = false
Estimated value of Pi is 3.12000000000000000000
```


### 結果
- 処理速度は遅いですが、軽量な MapReduce のジョブであれば動作することが確認できました。
- ローカルテスト用のため、root で全部処理してしまっています。hadoop ユーザなど作成して権限を分けたい。
- centos7 ベースで作成するよりも制約が少なくスムーズに実現ができました。
※より軽量のベースを使うため、alpine をベースに挑戦もしましたが ash でスクリプトを実行しようとするため、シンタックスの問題で Hadoop の各サービスを開始することができませんでした
- Hadoop Web UI (http://localhost:8088/cluster) にもアクセスできます

### 参考
[Hadoop 2.7.1を擬似分散モードで動かす](https://qiita.com/bwtakacy/items/6491b3d11754492f9702)
[How to Setup Apache Hadoop 3.1.1](https://medium.com/@danielgomes_58969/how-to-setup-apache-hadoop-3-1-1-multi-node-cluster-on-ubuntu-18-04-899c1bcf23be)
[org.apache.hadoop.yarn.exceptions.InvalidAuxServiceException: The auxService:mapreduce_shuffle does not exist](https://stackoverflow.com/questions/33345262/org-apache-hadoop-yarn-exceptions-invalidauxserviceexception-the-auxservicemap)
[Hadoop2をJava8で動かしたときにハマった](https://qiita.com/bwtakacy/items/cb0a2e35ef7469f54625)
[[Java 8] Over usage of virtual memory](https://issues.apache.org/jira/browse/YARN-4714)