HDFS on Kubernetesの検証

# 概要
　HadoopやSparkで用いられるデータを格納するストレージとしてHDFSがあります。このHDFSをKubernetes上に構築することで、[Rook](https://rook.io)に代表されるCloud Native Storageの構築検証を行います。
　以前、[Apache Spark on Kubernetesの検証](https://qiita.com/ysakashita/items/1f87646ae804ba509d07)にて、Sparkの計算処理についてKubernetes上での実行を検証しました。今回構築するHDFSにより、[Apache Spark on Kubernetesの検証](https://qiita.com/ysakashita/items/1f87646ae804ba509d07)でのspark-submit コマンドの`--files`オプションで指定するHDFSもKubernetesで管理できるようになります。

(spark-submit コマンド例)

```
$ bin/spark-submit \
    --master k8s://https://<k8s-apiserver-host>:<k8s-apiserver-port> \
    --deploy-mode cluster \
    --name spark-pi \
    --class org.apache.spark.examples.SparkPi \
    --jars https://path/to/dependency1.jar,https://path/to/dependency2.jar
    --files hdfs://host:port/path/to/file1,hdfs://host:port/path/to/file2
    --conf spark.executor.instances=5 \
    --conf spark.kubernetes.container.image=<spark-image> \
    https://path/to/examples.jar
```

# 検証環境
- Kubernetes環境
  - Kubernetes v1.11.1 (master x1, worker x2)
  - Rook v0.8

- HDFS環境
  - Zookeeper (x 3)
    HDFSの各nodeを管理
  - Namenode (x 2)
    Datanode に格納するデータのメタ情報を管理
  - Journalnode (x 3)
     複数Namenode が存在した場合に、コンフリクション起こしたデータの整合性を取るためのノード。多数決により  
    決めるため奇数個のnodeが必要
  - Datanode (x 3)
    データを格納。DeamonSetとして定義され、Kubernetesのnode毎にデプロイ。

# 事前設定
## RookのBlockStorageの設定
　今回、HDFSのデータを格納するためのストレージとして、RookのBlockStorageを使います。
Rookについては、[Cloud NativeなストレージRookの検証](https://qiita.com/ysakashita/items/f27c0948b4e339c5d1e0)にまとめているので、ご参照ください。
RookのBlockStorageの設定として、StorageClassのリソースをデプロイします。
今回作成するStorageClassを`storageclass.yaml`として保存します。

```storageclass.yaml
apiVersion: ceph.rook.io/v1beta1
kind: Pool
metadata:
  name: replicapool
  namespace: rook-ceph
spec:
  replicated:
    size: 3
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
   name: rook-ceph-block
provisioner: ceph.rook.io/block
parameters:
  pool: replicapool
  clusterNamespace: rook-ceph
```

kubectlコマンドを使って、`storageclass.yaml`をデプロイします。

```
$ kubectl create -f storageclass.yaml 
pool.ceph.rook.io "replicapool" created
storageclass.storage.k8s.io "rook-ceph-block" created

$ kubectl get sc
NAME              PROVISIONER          AGE
rook-ceph-block   ceph.rook.io/block   23s
```

## HelmのTillerのセットアップ

HDFS on Kubernetesではhelmを使ってデプロイするため、事前にhelmの環境を準備します。
Helmの詳細情報は、[公式サイト](https://helm.sh)をご参照ください。

```
$ helm init
$HELM_HOME has been configured at /Users/ysakashi/.helm.

Tiller (the Helm server-side component) has been installed into your Kubernetes Cluster.

Please note: by default, Tiller is deployed with an insecure 'allow unauthenticated users' policy.
For more information on securing your installation see: https://docs.helm.sh/using_helm/#securing-your-helm-installation
Happy Helming!
```

これにより、kube-systemネームスペースにtiller-deployのPodがデプロイされます。

# 検証
## kubernetes-HDSのダウンロード
　今回の検証では、[apache-spark-on-k8s/kubernetes-HDFS](https://github.com/apache-spark-on-k8s/kubernetes-HDFS)にて公開されているHelm Chartを使って構築します。まず、始めにGitHubからダウンロードします。

```
$ git clone https://github.com/apache-spark-on-k8s/kubernetes-HDFS.git
$ cd kubernetes-HDFS/charts
```
以降、`kubernetes-HDFS/charts `ディレクトリで検証を行います。

## Zookeeper のデプロイ
　ZookeeperをデプロイするためのYAMLファイルをダウンロードします。

```
$ wget https://raw.githubusercontent.com/kubernetes/contrib/master/statefulsets/zookeeper/zookeeper.yaml
```

ダウンロードした`zookeeper.yaml`を編集し、RookのBlockStorageをZookeeperのデータ格納用Volumeに割り当てるようにします。変更箇所の前後を以下に示します。
`spec.volumeClaimTemplates.storageClassName`にStorageClassで設定した`rook-ceph-block`を指定します。

```zookeeper.yaml(変更箇所)
...
 volumeClaimTemplates:
  - metadata:
      name: datadir
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: rook-ceph-block
      resources:
        requests:
          storage: 10Gi
```

編集した`zookeeper.yaml`を使ってZookeeperをデプロイします。

```
$ kubectl create -f zookeeper.yaml
service "zk-svc" created
configmap "zk-cm" created
poddisruptionbudget "zk-pdb" created
statefulset "zk" created
```

## JournalnodeとNamenodeのデプロイ
　NamenodeをデプロイするKubernetesのnodeにラベルを設定します。

```
$ kubectl label nodes k8s hdfs-namenode-selector=hdfs-namenode-0
node "k8s" labeled
```

次に、Namenode、Datanodeの設定ファイル(core-site.xml, hdfs-site.yaml)を定義したConfigMapをデプロイします。

```
$ helm install -n my-hdfs-config --set fullnameOverride=hdfs-config hdfs-config-k8s
NAME:   my-hdfs-config
LAST DEPLOYED: Wed Jul 25 21:04:04 2018
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/ConfigMap
NAME         DATA  AGE
hdfs-config  2     0s
```

続いて、Journalnodeをデプロイします。
まず、デプロイの前に、Journalnode のHelmテンプレート`hdfs-journalnode-k8s/templates/journalnode-statefulset.yaml`を編集し、JournalnodeのデータをRookのBlockStorageを割り当てるようにします。変更箇所の前後を以下に示します。
`spec.volumeClaimTemplates.storageClassName`にStorageClassで設定した`rook-ceph-block`を指定します。

```hdfs-journalnode-k8s/templates/journalnode-statefulset.yaml(変更箇所)
... 
volumeClaimTemplates:
  - metadata:
      name: editdir
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: rook-ceph-block
      resources:
        requests:
          storage: {{ .Values.editdataVolumeSize }}
```

編集したHelmテンプレートを使い、Journalnodeをデプロイします。

```
$ helm install -n my-hdfs-journalnode hdfs-journalnode-k8s
NAME:   my-hdfs-journalnode
LAST DEPLOYED: Wed Jul 25 21:08:53 2018
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1beta1/StatefulSet
NAME              DESIRED  CURRENT  AGE
hdfs-journalnode  3        1        1s

==> v1beta1/PodDisruptionBudget
NAME              MIN AVAILABLE  MAX UNAVAILABLE  ALLOWED DISRUPTIONS  AGE
hdfs-journalnode  2              N/A              0                    1s

==> v1/Service
NAME              TYPE       CLUSTER-IP  EXTERNAL-IP  PORT(S)            AGE
hdfs-journalnode  ClusterIP  None        <none>       8485/TCP,8480/TCP  1s
```

次に、Namenodeをデプロイします。
デプロイの前に、Zookeeper, Journalnodeと同様にHelmテンプレート`hdfs-namenode-k8s/templates/namenode-statefulset.yaml`を編集し、NamenodeのデータをRookのBlockStorageを割り当てるようにします。変更箇所の前後を以下に示します。
`spec.volumeClaimTemplates.storageClassName`にStorageClassで設定した`rook-ceph-block`を指定します。

```hdfs-namenode-k8s/templates/namenode-statefulset.yaml(変更箇所)
...
  volumeClaimTemplates:
  - metadata:
      name: metadatadir
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: rook-ceph-block
      resources:
        requests:
          storage: {{ .Values.metadataVolumeSize }}
```

編集したHelmテンプレートを使い、Namenodeをデプロイします。

```
$ helm install -n my-hdfs-namenode hdfs-namenode-k8s
NAME:   my-hdfs-namenode
LAST DEPLOYED: Wed Jul 25 21:23:36 2018
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1beta1/PodDisruptionBudget
NAME           MIN AVAILABLE  MAX UNAVAILABLE  ALLOWED DISRUPTIONS  AGE
hdfs-namenode  1              N/A              0                    1s

==> v1/ConfigMap
NAME                         DATA  AGE
hdfs-namenode-start-scripts  3     1s

==> v1/Service
NAME           TYPE       CLUSTER-IP  EXTERNAL-IP  PORT(S)             AGE
hdfs-namenode  ClusterIP  None        <none>       8020/TCP,50070/TCP  1s

==> v1beta1/StatefulSet
NAME           DESIRED  CURRENT  AGE
hdfs-namenode  2        1        1s
```

デプロイしたJournalnode, Namenodeを確認します。
Namenodeは、`hostNetwork: true`, `dnsPolicy: ClusterFirstWithHostNet`として指定しているためIPはPodが割り当てられたNodeのIPが割り当てられています。これは、Namenodeが障害などによりダウンし、Podが再作成した場合でもIPが変更にならないようにするためとのことです。
また、Journalnode,Namenodeのデータ保存用のPersistentVolumeには、指定した通りにRookのBlockStorageが割り当てられています。

```
$ kubectl get pod -o wide
NAME                 READY     STATUS    RESTARTS   AGE       IP             NODE
hdfs-journalnode-0   1/1       Running   0          18m       10.244.1.165   k8s-node1
hdfs-journalnode-1   1/1       Running   0          18m       10.244.2.136   k8s-node2
hdfs-journalnode-2   1/1       Running   0          17m       10.244.0.179   k8s
hdfs-namenode-0      1/1       Running   0          4m        192.168.0.24   k8s-node1
hdfs-namenode-1      1/1       Running   0          3m        192.168.0.25   k8s-node2
zk-0                 1/1       Running   0          27m       10.244.1.164   k8s-node1
zk-1                 1/1       Running   0          27m       10.244.2.135   k8s-node2
zk-2                 1/1       Running   0          26m       10.244.0.178   k8s

$ kubectl get pvc -o wide
NAME                          STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
datadir-zk-0                  Bound     pvc-3adffab1-9002-11e8-afe5-080027571559   10Gi       RWO            rook-ceph-block   28m
datadir-zk-1                  Bound     pvc-53492817-9002-11e8-afe5-080027571559   10Gi       RWO            rook-ceph-block   27m
datadir-zk-2                  Bound     pvc-6dfb96c9-9002-11e8-afe5-080027571559   10Gi       RWO            rook-ceph-block   26m
editdir-hdfs-journalnode-0    Bound     pvc-7fec7a7d-9003-11e8-afe5-080027571559   20Gi       RWO            rook-ceph-block   19m
editdir-hdfs-journalnode-1    Bound     pvc-910f3baf-9003-11e8-afe5-080027571559   20Gi       RWO            rook-ceph-block   18m
editdir-hdfs-journalnode-2    Bound     pvc-a5422ba8-9003-11e8-afe5-080027571559   20Gi       RWO            rook-ceph-block   18m
metadatadir-hdfs-namenode-0   Bound     pvc-8e0e3b53-9005-11e8-afe5-080027571559   100Gi      RWO            rook-ceph-block   4m
metadatadir-hdfs-namenode-1   Bound     pvc-ac3fad9f-9005-11e8-afe5-080027571559   100Gi      RWO            rook-ceph-block   3m

$ kubectl get pv -o wide
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM                                 STORAGECLASS      REASON    AGE
pvc-3adffab1-9002-11e8-afe5-080027571559   10Gi       RWO            Delete           Bound     default/datadir-zk-0                  rook-ceph-block             28m
pvc-53492817-9002-11e8-afe5-080027571559   10Gi       RWO            Delete           Bound     default/datadir-zk-1                  rook-ceph-block             28m
pvc-6dfb96c9-9002-11e8-afe5-080027571559   10Gi       RWO            Delete           Bound     default/datadir-zk-2                  rook-ceph-block             27m
pvc-7fec7a7d-9003-11e8-afe5-080027571559   20Gi       RWO            Delete           Bound     default/editdir-hdfs-journalnode-0    rook-ceph-block             19m
pvc-8e0e3b53-9005-11e8-afe5-080027571559   100Gi      RWO            Delete           Bound     default/metadatadir-hdfs-namenode-0   rook-ceph-block             4m
pvc-910f3baf-9003-11e8-afe5-080027571559   20Gi       RWO            Delete           Bound     default/editdir-hdfs-journalnode-1    rook-ceph-block             19m
pvc-a5422ba8-9003-11e8-afe5-080027571559   20Gi       RWO            Delete           Bound     default/editdir-hdfs-journalnode-2    rook-ceph-block             18m
pvc-ac3fad9f-9005-11e8-afe5-080027571559   100Gi      RWO            Delete           Bound     default/metadatadir-hdfs-namenode-1   rook-ceph-block             4m
```

以上で、Journalnode, Namenodeのデプロイは完了です。

## Datanodeのデプロイ
　続いて、データを保存するDatanodeをデプロイします。
Datanodeを、KubernetesのMaster Nodeへデプロイしたくない場合には、以下を設定します。今回は、この設定はスキップします。

```
kubectl label node YOUR-MASTER-NAME hdfs-datanode-exclude=yes
```

Datanodeをデプロイします。
Datanodeは、DeamonSetとして定義されているため、各Nodeに一個づつデプロイされます。
データの格納場所としては、各Nodeの`/hdfs-data`ディレクトリをhostPathとしてPodへ割り当てます。

```
$ helm install -n my-hdfs-datanode hdfs-datanode-k8s
NAME:   my-hdfs-datanode
LAST DEPLOYED: Wed Jul 25 21:40:31 2018
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1beta1/DaemonSet
NAME           DESIRED  CURRENT  READY  UP-TO-DATE  AVAILABLE  NODE SELECTOR  AGE
hdfs-datanode  3        3        0      3           0          <none>         0s

==> v1/Pod(related)
NAME                 READY  STATUS             RESTARTS  AGE
hdfs-datanode-4z9v8  0/1    ContainerCreating  0         0s
hdfs-datanode-kbxgf  0/1    ContainerCreating  0         0s
hdfs-datanode-s2tb9  0/1    ContainerCreating  0         0s
```

kubectlを使いデプロイしたDatanodeを確認します。
Datanodeも、Namespace同様に`hostNetwork: true`, `dnsPolicy: ClusterFirstWithHostNet`として指定しているためIPはPodが割り当てられたNodeのIPが割り当てられています。

```
$ kubectl get pod -o wide
NAME                  READY     STATUS      RESTARTS   AGE       IP             NODE
hdfs-datanode-4z9v8   1/1       Running     0          6m        192.168.0.24   k8s-node1
hdfs-datanode-kbxgf   1/1       Running     0          6m        192.168.0.23   k8s
hdfs-datanode-s2tb9   1/1       Running     0          6m        192.168.0.25   k8s-node2
...
```
以上で、Datanodeのデプロイが完了です。
これで、HDFSがKubernetes上に構築されました。

## hdfs-clientのデプロイ
構築したHDFSを操作するためのClinetツールをデプロイします。

```
$ helm install -n hdfs-client hdfs-client
NAME:   hdfs-client
LAST DEPLOYED: Wed Jul 25 21:54:10 2018
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1beta1/Deployment
NAME         DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
hdfs-client  1        1        1           0          0s

==> v1/Pod(related)
NAME                          READY  STATUS             RESTARTS  AGE
hdfs-client-64588bbd96-lzdmc  0/1    ContainerCreating  0         0s
```

hdfs-clientのPodがデプロイされた後、kubectlコマンドでPodへログインし、ClientツールにてHDFSにアクセスすることが出来ます。Clientツールの使い方は、[公式ドキュメントのFileSystem Shell](http://hadoop.apache.org/docs/r2.8.4/hadoop-project-dist/hadoop-common/FileSystemShell.html)を参照してください。

```
$ kubectl exec -ti hdfs-client-64588bbd96-lzdmc /bin/bash
root@hdfs-client-64588bbd96-lzdmc:/# hdfs dfs -ls
```

以上で、HDFS on Kubernetesの構築検証は完了です。

# クリーンアップ
　Helmでデプロイしたリソースを削除します。

```
$ helm delete --purge hdfs-client my-hdfs-config my-hdfs-datanode my-hdfs-journalnode my-hdfs-namenode
release "hdfs-client" deleted
release "my-hdfs-config" deleted
release "my-hdfs-datanode" deleted
release "my-hdfs-journalnode" deleted
release "my-hdfs-namenode" deleted
```

次に、Zookeeperを削除します。

```
$ kubectl delete -f zookeeper.yaml 
service "zk-svc" deleted
configmap "zk-cm" deleted
poddisruptionbudget.policy "zk-pdb" deleted
statefulset.apps "zk" deleted
```

PersistentVolumeClaim, PersistentVolumeを一気に削除します。他に利用しているPersistenvVolumeClaim, PersistentVolumeがある場合は、ひとつづつ削除してください。

```
$ kubectl delete pvc --all
persistentvolumeclaim "datadir-zk-0" deleted
persistentvolumeclaim "datadir-zk-1" deleted
persistentvolumeclaim "datadir-zk-2" deleted
persistentvolumeclaim "editdir-hdfs-journalnode-0" deleted
persistentvolumeclaim "editdir-hdfs-journalnode-1" deleted
persistentvolumeclaim "editdir-hdfs-journalnode-2" deleted
persistentvolumeclaim "metadatadir-hdfs-namenode-0" deleted
persistentvolumeclaim "metadatadir-hdfs-namenode-1" deleted

$ kubectl delete pv --all
persistentvolume "pvc-3adffab1-9002-11e8-afe5-080027571559" deleted
persistentvolume "pvc-53492817-9002-11e8-afe5-080027571559" deleted
persistentvolume "pvc-6dfb96c9-9002-11e8-afe5-080027571559" deleted
persistentvolume "pvc-7fec7a7d-9003-11e8-afe5-080027571559" deleted
persistentvolume "pvc-8e0e3b53-9005-11e8-afe5-080027571559" deleted
persistentvolume "pvc-910f3baf-9003-11e8-afe5-080027571559" deleted
persistentvolume "pvc-a5422ba8-9003-11e8-afe5-080027571559" deleted
persistentvolume "pvc-ac3fad9f-9005-11e8-afe5-080027571559" deleted
```

次に、RookのBlockStorage用のStorage Classを削除します。

```
$ kubectl delete -f storageclass.yaml 
pool.ceph.rook.io "replicapool" deleted
storageclass.storage.k8s.io "rook-ceph-block" deleted
```

最後に、各Nodeにログインし、Namenode,DatanodeからhostPathで利用したディレクトリを削除します。
下記を全Nodeで実行してください。

```
$ ssh k8s-node1
k8s-node1$ sudo rm -rf hdfs-data
k8s-node1$ sudo rm -rf hdfs-name
k8s-node1$ exit
```

# 感想
　今回、HDFSをKubernetes上に構築しました。ひとまず動きます。ただし、今回検証で利用したkubernetes-HDFSでは、`ClusterFirstWithHostNet`やDatanodeのデータ格納場所に`hostPath`を使っているため、少々柔軟性(Flexibility)が欠けている印象です。これらは、`ClusterFirstWithHostNet`はServiceでLoadBalancerやExternalNameを使用、`hostPath`は、全て外部ストレージを使うようにすることで、柔軟性を高めることができます。ただし、データの格納場所を外部ストレージにしても、現在のままだとNamenode, Datanodeの再作成にデータフォーマットが走ってしまい、データが消えてしまいます。データフォーマットが実行されないように少々ConfigMapで設定しているスクリプトファイルを工夫してあげる必要があります。
　このように、HDFS on Kubernetesの柔軟性を高めつつデータを保持し続けるために、頑張ってみるというのもアリかとは思います。しかし、使い方(ユースケース)を工夫する方が良いかもしれません。つまり、常にデータを保存し続けるためのストレージとしてのHDFSではなく、SparkやHadoopで利用する時のみデータを格納する一時的なデータ格納場所として利用するのが良いのかもしれません。これを実現するため、HDFS on Kubernetesを使いこなすには、ETLツールとの連携に取り組むのもアリかと思います。
　今回は、ひとまずkubernetes-HDFSを動かすことが出来ました。ただし、kubernetes-HDFS単体での活用シーンは考え所です。

# 参考情報
- [apache-spark-on-k8s/kubernetes-HDFS](https://github.com/apache-spark-on-k8s/kubernetes-HDFS)
- [HDFS on Kubernetes—Lessons Learned](https://databricks.com/session/hdfs-on-kubernetes-lessons-learned)