2021.04.08

○千万規模のコスト減!?OSSでHadoopクラスタを運用・管理したい ~ High Availabilityの章 ~

目次
  1. 1.はじめに
  2. 2.HadoopクラスタHA化の概要
    1. 2-1.Zookeeper・ZKFC
    2. 2-2.NameNodeのHA
    3. 2-3.ResourManagerのHA
  3. 3.Ansible AWXによるHadoop HAクラスタ構成管理
    1. 3-1.全体の流れ
    2. 3-2.システム設定
    3. 3-3.JDK・Hadoopのインストール
    4. 3-2.Zookeeperのインストール
    5. 3-2.HDFSのセットアップ
    6. 3-2.YARNのセットアップ
  4. 4.HAの確認
  5. 5.まとめ

1.はじめに

こんにちは。大規模データ分析基盤運用チームのY.S.です。今回はOSSでHadoop管理を頑張るシリーズの第2弾です。

前回はAnsible AWXを触ってみて、HDFSとYARNのプロビジョニングplaybookを実行してみました。HadoopをOSSで管理したいそもそものモチベーション等は前回の記事をご参照ください。

前回作成したHadoopではマスターノードが1台だけでしたが、これだとマスターノードに障害が起きた場合に全体が止まったり、データが消失したりしてしまうので、現場でこの構成で運用することはありません。

そこで今回は、マスターノードとスレーブノードをそれぞれ複数台用意してHigh Availability(HA)構成にしてみようと思います。ワケあって前回作ったクラスタが消失したので、「HAでないクラスタをHAにする」のではなく、最初からHAでクラスタを作ります。

基本的には、playbookにHAに必要なミドルウェアやconfigのデプロイタスクを書いていくだけです。だた、一部実際にノードに入って手でコマンドを打つ必要があるので、完全にplaybookを叩くだけでHAができるというわけにはいかなかったです。

今回のゴールは、NameNodeとResourceManagerの自動Failoverが機能していることを確認できればよしとします。

2.HadoopクラスタのHA化

Hadoopクラスタのマスターノードは単一障害点となるため、ノードを複数台用意してactive/standbyのHAを構成します。
普段はマスターノードのうち一台がactiveノードとして機能し、残りはstandbyとして待機していて、activeノードが停止した際にstandbyノードをactiveノードへ昇格(failover)させることで全体が停止することを防ぎます。NameNodeやResourceManager自体には自動でfailoverする仕組みはなく、コーディネーションエンジンのZookeeperやZookeeperFailoverController(ZKFC)と組み合わせて自動failoverを実現します。

2-1.Zookeeper・ZKFC

Zookeeperは分散システムを構築するためのコーディネーションエンジンで、共有設定、分散Lock、メンバー取得等の機能を実現します。ZKFCはマスターノード上で動くプロセスで、そのノードのヘルスモニタリングや、Lockの獲得等を行います。

各ノードのZKFCとZookeeperの間にはセッションが張られていて、activeノードのZKFCはZookeeper上にLockを保持しています。このLockはセッションが消えると自動で解消されます。ZKFCが自身のノードがクラッシュしたと判断した場合はそのセッションは破棄されますが、それがactiveノードのセッションである場合、Zookeeperは他の正常なZKFCに対してfailoverの開始を通知します。

通知を受けた各ZKFCはLockを獲得しようとし、早い者勝ちで一つのノードだけがLockを獲得し、新たなactiveノードに昇格します。

2-1.NameNodeのHA

HDFSは、ファイルをblock単位に分割してそれぞれのblockを複数のDataNodeに格納し、各blockがどのDataNodeに格納されているかのメタデータをNameNodeが管理します。
NameNodeを冗長化するには、メタデータをactive/standby間で共有しておき、failoverの前後でメタデータに齟齬が生まれないようにする必要があります。

これを実現するためにJournalNodeを導入します。JournalNodeはクラスタ上の複数ノード上で起動するプロセスで、standbyノードのメタデータをactiveノードのものと同期させるためのものです。activeノードは、自身が保持するメタデータに変更を加える際、各JournalNodeに変更を伝えます。standbyノードはJournalNodeから変更を読み取り、自身が保持するメタデータを更新します。failoverの際は、activeに昇格する予定のノードがJournalNodeから全ての情報を読み取ったことを確認した後にactiveに昇格します。

Zookeeperのアドレスをcore-site.xmlに、JournalNodeに関する設定をhdfs-site.xmlにそれぞれ記載した後、NameNodeのフォーマット等の手動処理を若干行うとHA化が完了します。詳しい手順については後述します。

2-1.ResourceManagerのHA

ResouceManagerのHA化はNameNodeのそれと比べてシンプルで、yarn-site.xmlにZookeeperや各ResourceManegerのアドレスを設定するだけです。configを配布した後にResourceManagerを起動すればHA化が完了しています。

3.Ansible AWXによるHadoop HAクラスタ構成管理

今回はマスターノード2台、スレーブノード3台の計5台構成とします。各ノードに乗っているコンポーネントは以下の通りです。
  • マスターノード1: NameNode, JournalNode, Zookeeper Server, Zookeeper Client, ZKFC, ResouceManager
  • マスターノード2: NameNode, JournalNode, Zookeeper Server, Zookeeper Client, ZKFC, ResouceManager
  • スレーブノード1: DataNode, JournalNode, Zookeeper Client, NodeManager
  • スレーブノード2: DataNode, Zookeeper Client, NodeManager
  • スレーブノード3: DataNode, Zookeeper Client, NodeManager
  • Hadoopでは各コンポーネントで多くのポートを使用しますが、それらのポートをいちいち開けたり閉じたりするのは大変なので、全通しのプライベートネットワークを構築してノード同士はそこで通信させます。
    また、failoverの際にマスターノード同士でssh通信を行うので、お互いの公開鍵を登録しておきます。

    3-1.全体の流れ

    下記の流れでクラスタの構築を進めます。ZookeeperのインストールとHDFSのセットアップはplaybookの実行だけでなく、ある程度手作業も必要でした。
  • システム設定
  • JDK・Hadoopのインストール
  • Zookeeperのインストール
  • HDFSのセットアップ
  • YARNのセットアップ
  • 3-2.システム設定

    Hadoopのデプロイをする前に、システム設定に関するroleを実行します。Hadoopを動作させるのに必要な部分を抜粋して下記で説明します。

    3-2-1.yumライブラリインストール

    クラスタを構築するのに必要なyumライブラリをインストールするタスクです。今回はfailoverの際に使用するfuserコマンドをmasterノードにインストールします。
    - name: install psmisc (for fuser command)
      yum:
        name: psmisc
        state: latest
    

    3-2-2.シンボリックリンク

    YARNのタスクを実行する際に、何故か頑なに/bin/javaを見にいこうとしてエラーになったので、JDKのインストール先である/etc/javaからのシンボリックリンクを予め張っておきます。(前回のブログの時はこのようなことはなかったのですが、HA化したことが原因かは不明です。。)
    - name: create symlink
      file:
        src: /etc/java/bin/java
        dest: /bin/java
        state: link
        force: yes
    

    3-3.JDK・Hadoopのインストール

    ansible playbookで各ノードにJDKとHadoopをインストールします。JDKのインストール手順は前回の投稿と同じです。
    Hadoopのインストールも基本的に前回と同じですが、新たに下記のような、hadoop-env.shをデプロイするタスクを追加し、JavaやHadoopのPATHを追記しています。

    deploy-config.yml

    - name: hadoop env    
      blockinfile: 
        path: /etc/hadoop/etc/hadoop/hadoop-env.sh   
        create: yes        
        insertafter: EOF   
        marker: "# {mark} ANSIBLE Fish-shell basic setup." 
        block: "{{item}}"
      with_file:
        - files/hadoop-env.sh
    

    files/hadoop-env.sh

    export JAVA_HOME=/etc/java
    export HADOOP_HOME=/etc/hadoop
    export PATH=${PATH}:${JAVA_HOME}/bin:${HADOOP_HOME}/bin:${HADOOP_HOME}/sbin
    

    3-4.Zookeeperのインストール

    下記のplaybookの手順でzookeeperのインストール・セットアップを行います。ただ、ZookeeperServerを起動するには、各ノードのzookeeperのデータディレクトリ(ここでは/etc/zookeeper/data/)にmyidというファイルを作り、下記のzoo.cfgに書かれているサーバー番号の数字をmyidに記載する必要があります。なので実際の手順としては、ansibleでconfig配る -> myidを作成・編集する -> 手動でZookeeperServer起動という流れになりました。

    zookeeper_install.yml

    - name: defrost-zip-confirm
      stat: path=/etc/apache-zookeeper-3.6.2-bin/
      register: result
    
    - name: Defrost hadoop tar file
      unarchive: 
        src: /var/lib/awx/volumes/file/gz/apache-zookeeper-3.6.2-bin.tar.gz
        dest: /etc
      when: not result.stat.exists
    
    - name: Create symbolic link to zookeeper
      file: 
        src: /etc/apache-zookeeper-3.6.2-bin
        dest: /etc/zookeeper
        state: link
    
    - name: Create zookeeper data dir
      file:
        path: /opt/zookeeper/data
        state: directory
    
    - name: deploy zoo.cnf 
      template:
        src: "conf/zoo.cfg.j2"
        dest: "/etc/zookeeper/conf/zoo.cfg"
    
      notify: 
        - start_zookeeper_server_daemon
    

    zoo.cnf.j2

    tickTime=2000
    dataDir=/opt/zookeeper/data
    clientPort=2181
    initLimit=5
    syncLimit=2
    server.1=192.168.10.2:2888:3888
    server.2=192.168.10.3:2888:3888
    server.3=192.168.10.5:2888:3888
    
    server番号とipを対応付ける必要があるので、zoo.cfgにはZookeeperServerのipをベタ書きしています。ただ、運用していてZookeeperServerの構成が変わることは殆どないので、寧ろベタ書きの方が管理しやすいかもとも思いました。

    3-5.HDFSのセットアップ

    configを配布し、NameNode, DataNode, JournalNodeを起動するタスクを実行しますが、途中で色々と手作業でやる部分があります。
    配布するconfigファイルは前回と同等、core-site.xml、hdfs-site.xml、slavesです。前回からの差分として、HA化に伴ってJournalNodeとZookeeper関係の設定が増えています。

    3-1.core-site.xml.j2

    <configuration>
    <property>
    <name>fs.default.name</name>
    <value>hdfs://mycluster</value>
    </property>
    <property>
    <name>hadoop.tmp.dir</name>
    <value>/var/tmp/hadoop-tmp</value>
    </property>
    
    ##ZK configs
    <property>
        <name>ha.zookeeper.quorum</name>
        {% for host in groups.ZookeeperServer %}
        <value>{{ host }}:2181</value>
        {% endfor %}
    </property>
    </configuration>
    

    3-2.hdfs-site.xml.j2

    HA構成になるに伴い、各NameNodeのホスト、JournalNodeのホスト・ポート、failoverの設定などを追加しました。
    dfs.ha.fencing.ssh.private-key-filesには、NameNodeから他のNameNodeにsshするためのno-pass秘密鍵を指定します。公開鍵は、予めauthorized_keysに登録しておく必要があります。
    <configuration>
      <property>
          <name>dfs.replication</name>
          <value>1</value>
      </property>
      <property>
          <name>dfs.data.dir</name>
          <value>/opt/hdfs/data</value>
      </property>
      <property>
          <name>dfs.name.dir</name>
          <value>/opt/hdfs/name</value>
      </property>
      <property>
          <name>dfs.namenode.datanode.registration.ip-hostname-check</name>
          <value>false</value>
      </property>
    
      <property>
        <name>dfs.nameservices</name>
        <value>mycluster</value>
      </property>
      <property>
        <name>dfs.ha.namenodes.mycluster</name>
        <value>nn1,nn2</value>
      </property>
    
      <property>
        <name>dfs.namenode.rpc-address.mycluster.nn1</name>
        <value>192.168.10.2:8020</value>
      </property>
      <property>
        <name>dfs.namenode.rpc-address.mycluster.nn2</name>
        <value>192.168.10.3:8020</value>
      </property>
    
      <property>
        <name>dfs.namenode.http-address.mycluster.nn1</name>
        <value>192.168.10.2:9870</value>
      </property>
      <property>
        <name>dfs.namenode.http-address.mycluster.nn2</name>
        <value>192.168.10.3:9870</value>
      </property>
    
      <property>
        <name>dfs.namenode.shared.edits.dir</name>
        <value>qjournal://192.168.10.2:8485;192.168.10.3:8485;192.168.10.5:8485;/mycluster</value>
      </property>
    
      <property>
        <name>dfs.client.failover.proxy.provider.mycluster</name>
        <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>
      </property>
    
      <property>
        <name>dfs.ha.fencing.methods</name>
        <value>sshfence</value>
      </property>
      <property>
        <name>dfs.ha.fencing.ssh.private-key-files</name>
        <value>/root/.ssh/id_rsa</value>  ##SSH KEY from ZKFC server to each NNs
      </property>
      <property>
        <name>dfs.ha.fencing.ssh.connect-timeout</name>
        <value>30000</value>
      </property>
    
      <property>
        <name>fs.defaultFS</name>
        <value>hdfs://mycluster</value>
      </property>
    
      <property>
        <name>dfs.journalnode.edits.dir</name>
        <value>/opt/hdfs/journal</value>
      </property>
    
      <property>
        <name>dfs.ha.nn.not-become-active-in-safemode</name>
        <value>true</value>
      </property>
    
      ##ZK configs
      <property>
        <name>dfs.ha.automatic-failover.enabled</name>
        <value>true</value>
    </property>
    </configuration>
    
    configを配った後は、JournalNodeとNameNodeのフォーマット作業とZKFCの起動を行います。
    1. 1.NameNodeのうち1つのノードで$Hadoop_Home/bin/hdfs namenode -formatを実行
    2. 2.フォーマットしたNameNodeのデータ(ここでは/opt/hadoop/namenode/current)をもう一つのNameNodeにコピー
    3. 3.各JournalNodeで、JournalNodeのデータ(ここでは/opt/hadoop/journalnode/current)を削除
    4. 4.1でフォーマットしたものとは別のNameNodeで、$Hadoop_Home/bin/hdfs namenode -initializeShareEditsを実行
    5. 5.NameNodeのうち1つのノードで、$Hadoop_Home/bin/hdfs zkfc -formatZKを実行
    6. 6.各NameNodeで、$Hadoop_Home/bin/hdfs –daemon start zkfcを実行
    これで自動failoverが有効になったNameNodeが起動します。

    3-6.YARNのセットアップ

    YARNのセットアップは簡単で、HAの設定を追加したconfigを配ってResourceManagerやNodeManagerを起動するだけです。
    Managerの起動は前回同等、configを配った際にhandlerで行うので、roleを実行するだけでHAのYARNの出来上がりです。

    yarn-site.xml.j2

    HA構成に伴って追加された項目として、ResourceManagerの設定とZookeeperサーバーの指定があります。
    <configuration>
      <property>
        <name>yarn.resourcemanager.ha.enabled</name>
        <value>true</value>
      </property>
      <property>
        <name>yarn.resourcemanager.cluster-id</name>
        <value>yarn-cluster</value>
      </property>
      <property>
        <name>yarn.resourcemanager.ha.rm-ids</name>
        <value>rm1,rm2</value>
      </property>
      <property>
        <name>yarn.resourcemanager.hostname.rm1</name>
        <value>192.168.10.2</value>
      </property>
      <property>
        <name>yarn.resourcemanager.hostname.rm2</name>
        <value>192.168.10.3</value>
      </property>
      <property>
        <name>hadoop.zk.address</name>
        {% for host in groups.ZookeeperServer %}
        <value>{{ host }}:2181</value>
        {% endfor %}
      </property>
    </configuration>
    

    4.HAの確認

    上記の手順でデプロイしたクラスタで、マスターコンポーネントの自動failoverを確認します。

    4-1.NameNode

    haadminで確認すると、下記のようにmaster1がactive、master2がstandbyとして起動していることがわかります。
    [root@master1 ~]# /etc/hadoop/bin/hdfs haadmin -getAllServiceState
    master1:8020                                       active    
    master2:8020                                       standby
    
    次に、master1のNameNodeプロセスを終了し、master2でhaadminを確認すると、master1のNameNodeとのconnectionが切れていることと、master2がactiveに昇格していることが確認できます。
    [root@master2 ~]# /etc/hadoop/bin/hdfs haadmin -getAllServiceState
    2021-04-01 11:04:13,505 INFO ipc.Client: Retrying connect to server: master1/192.168.10.2:8020. Already tried 0 time(s); retry policy is RetryUpToMaximumCountWithFixedSleep(maxRetries=1, sleepTime=1000 MILLISECONDS)
    master1:8020                                       Failed to connect: Call From master2/192.168.10.3 to master1:8020 failed on connection exception: java.net.ConnectException: 接続を拒否されました; For more details see:  http://wiki.apache.org/hadoop/ConnectionRefused
    master2:8020
    
    最後に、master1のNameNodeを再起動してからhaadminで、master1がstandbyとなっていることがわかります。
    [root@master2 ~]# /etc/hadoop/bin/hdfs haadmin -getAllServiceState
    master1:8020                                       standby   
    master2:8020                                       active 
    
    以上で、NameNodeの自動failoverが実現されていることを確認できました。

    4-2.ResourceManager

    ResourceManagerも同等に自動failoverを確認します。
    [root@master1 ~]# /etc/hadoop/bin/yarn rmadmin -getServiceState rm1
    active
    [root@master1 ~]# /etc/hadoop/bin/yarn rmadmin -getServiceState rm2
    standby
    
    master1のResourceManagerを停止。
    [root@master2 ~]# /etc/hadoop/bin/yarn rmadmin -getServiceState rm1
    2021-04-08 01:11:15,239 INFO ipc.Client: Retrying connect to server: master1/192.168.10.2:8033. Already tried 0 time(s); retry policy is RetryUpToMaximumCountWithFixedSleep(maxRetries=1, sleepTime=1000 MILLISECONDS)
    Operation failed: Call From master2/192.168.10.3 to master1:8033 failed on connection exception: java.net.ConnectException: 接続を拒否されました; For more details see:  http://wiki.apache.org/hadoop/ConnectionRefused
    [root@master2 ~]# /etc/hadoop/bin/yarn rmadmin -getServiceState rm2
    active
    
    master1のResourceManagerを再起動。
    [root@master2 ~]# /etc/hadoop/bin/yarn rmadmin -getServiceState rm1
    standby
    [root@master2 ~]# /etc/hadoop/bin/yarn rmadmin -getServiceState rm2
    active
    
    ResourceManagerでも自動failoverが実現できています。

    5.まとめ

    今回はOSSでHadoopクラスタ管理を頑張る第2弾ということで、NameNodeとResourceManagerをHA構成にしてクラスタを構築するansibleプロジェクトを作成して、実際にデプロイしてみました。
    ZookeeperとHDFSのセットアップの際に手動での作業が必要になるため、完全な自動化とはいきませんでした。ただ、マスターコンポーネントのHAは一度構築してしまえばあまり弄ることはないと思うので、そこまでデメリットは感じていません。
    また、ホストを記載するconfigの一部で、ansibleのマジック変数を上手く使えずにホストをベタ書きしたところがありました。ただ、ここもそうそう変更する箇所ではないと思うので、無理にマジック変数を使わず、むしろベタ書きの方が管理のしやすさという視点ではいいかもと思いました。

    最後に

    次世代システム研究室では,データサイエンティスト/機械学習エンジニアを募集しています。ビッグデータの解析業務など次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら,ぜひ募集職種一覧からご応募をお願いします。皆さんのご応募をお待ちしています。

    Pocket

    関連記事