Browse Tag: hadoop

EMR 上の Hadoop / Spark の Web UI にアクセスする方法

プライベートサブネット上に EMR 上の Hadoop / Spark の Web UI をインターネット経由でアクセスしたくなりました。

ちゃんとした方法ですと、EMR上でSparkのWeb UIにアクセスする方法 | Developers.IOにあるとおり、次の2つ選択肢があるようです。

  1. SSH トンネリング
  2. SSH トンネリング + SOCK プロキシ

どちらも SSH を利用するため、外部からは接続はできません。

あとは、AWS VPC VPN 接続を使ってプライベートサブネットに接続する選択肢が考えられますが、この場合拠点に VPN が必要です。

そのような環境はなかったので、同一プライベートサブネット上の Nginx からプロキシする方法で設定してみました。

まず試してみると、ほとんどプライベート DNS になっているため、Nginx プロキシでレスポンスボディを変更する必要がありました。

レスポンスボディを書き換えることができるモジュールを探してみたところ、openresty/replace-filter-nginx-moduleyaoweibin/ngx_http_substituions_filter_module がありました。

どちらのモジュールも試したところ、前者の replace-filter-nginx-module は複数箇所の書き換えがなぜかうまくできず、結果的には後者の  ngx_http_substitutions_filter_module を使うことになりました。

Hadoop UI を /cluster、Spark UI を /spark、としていますが、ちょっと長くなりますが、次のようにかなり無理矢理ですが、設定することで無事うまくいきました。
なお、proxy まわりの設定は必要最低限の設定にしてあります。

    # Ganglia
    location /ganglia {
        proxy_pass http://EMR Master Public DNS:80/ganglia;
        proxy_redirect off;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # Hadoop UI
    location /cluster {
        resolver [Private DNS ※Ansible でいうと ansible_dns.nameservers[0] とかになりますね] valid=60s;
        set $backend "";
        set $my_host "";
        set $my_port "";

        if ($request_uri ~ /cluster/_/(.+)$) {
            set $backend $1;
        }

        if ($request_uri ~* ^/cluster/_/(.+?):(\d+)\/.+$) {
            set $my_host $1;
            set $my_port $2;
        }

        if ($backend != "") {
            proxy_pass http://$backend;
            break;
        }

        if ($request_uri ~* /cluster/static/(.+)$) {
            proxy_pass http://EMR Master Public DNS:8088/static/$1;
            break;
        }

        # この設定が、レスポンスボディを変更する設定です
        # Then の書き換えは必要ないですが、見栄え的に修正しました
        subs_filter "Then, " ". Then, " r;
        subs_filter "href=\"/static" "href=\"/cluster/static" r;
        subs_filter "src=\"/static" "src=\"/cluster/static" r;
        subs_filter "<a href=\"http://(.+):8088\"" "<a href=\"/cluster\"" r;
        subs_filter "<a href='http://(.+):8088/(.+)'" "<a href='/$2'" r;
        subs_filter "<a href=\"http://(.+):8088/cluster/(.+)\"" "<a href=\"/cluster/_/$1:8088/cluster/$2\"" r;

        subs_filter "<a href=\"//(.+):8042\"" "<a href=\"/cluster/_/$1:8042/node\"" r;
        subs_filter ",\"<a href='http://(.+):8042'" ",\"<a href='/cluster/_/$1:8042/node'" r;
        subs_filter ",\"<a href='http://(.+):8042/(.+)'" ",\"<a href='/cluster/_/$1:8042/$2'" r;
        subs_filter "http-equiv=\"refresh\" content=\"1; url=http://(.*)\"" "http-equiv=\"refresh\" content=\"1; url=https://$host/cluster/_/$1\"" r;

        subs_filter "<a href='http://.+:20888/(.+)'" "<a href='/spark'" r;
        subs_filter "<a href=\"http://.+:20888/(.+)\"" "<a href=\"/spark\"" r;

        subs_filter "href=\"/node\/node" "href=\"$request_uri" r;
        subs_filter "href=\"/node\/allApplications" "href=\"$request_uri/allApplications" r;
        subs_filter "href=\"/node\/allContainers" "href=\"$request_uri/allContainers" r;

        subs_filter "href=\"/jobhistory/(.+)" "href=\"/cluster/_/$my_host:$my_port/jobhistory/$1" r;
        subs_filter "href=\"/(conf)" "href=\"/cluster/_/$my_host:$my_port/$1" r;
        subs_filter "href=\"/(logs)" "href=\"/cluster/_/$my_host:$my_port/$1" r;
        subs_filter "href=\"/(stacks)" "href=\"/cluster/_/$my_host:$my_port/$1" r;
        subs_filter "href=\"/(jmx)(.+)" "href=\"/cluster/_/$my_host:$my_port/$1$2" r;

        proxy_pass http://EMR Master Public DNS:8088/cluster;
        proxy_redirect off;
     }

    location /spark {
        resolver [Private DNS ※Ansible でいうと ansible_dns.nameservers[0] とかになりますね] valid=60s;
        set $backend "";
        set $my_host "";
        set $my_port "";

        if ($request_uri ~ /spark/_/(.+)$) {
            set $backend $1;
         }

        if ($request_uri ~* ^/spark/_/(.+?):(\d+)\/.+$) {
            set $my_host $1;
            set $my_port $2;
        }

        if ($backend != "") {
            proxy_pass http://$backend;
            break;
        }

        if ($request_uri ~* /spark/static/(.+)$) {
            proxy_pass http://EMR Master Public DNS:18080/static/$1;
            break;
        }

        if ($request_uri ~* /spark/history/(.+)$) {
            proxy_pass http://EMR Master Public DNS:18080/history/$1;
            break;
        }

        # この設定が、レスポンスボディを変更する設定です
        subs_filter "href=\"/\"" "href=\"/spark\"" r;
        subs_filter "href=\"/static" "href=\"/spark/static" r;
        subs_filter "src=\"/static" "src=\"/spark/static" r;
        subs_filter "href=\"/history/application_(\w+)/(\d+)\"" "href=\"/spark/history/application_$1/$2/jobs/\"" r;
        subs_filter "?id=" "/?id=";
        subs_filter "href=\"/\?page(.+?)\"" "href=\"/spark/?page$1\"" r;

        subs_filter "<a href=\"http://(.+):8042/(.+)\"" "<a href=\"/cluster/_/$1:8042/$2\"" r;

        proxy_set_header Accept-Encoding "";

       proxy_pass http://EMR Master Public DNS:18080/spark;
       proxy_redirect off;
}

このようにして頑張って書き換えてプロキシさせてあげると、Hadoop/Spark UI をウェブ経由で手軽に見ることができます。

もちろん、上記にあげたポートは Security Group で許可してする必要があります。node も含めると、具体的には 80, 8041, 8042, 8088, 18080, 19888, 20888, ポートの接続許可が必要です。
は、EMR のクラスターごとの固有設定になりますので、僕はプライベート DNS を Route 53 に登録して、その内部ドメインを参照しています。

なお、この方法ですと、EMR がバージョンアップしたときにはその都度 UI に変更があれば subs_filter の変更が必要になります。

上の設定は EMR Release 4.6.0 Hadoop 2.72. + Spark 1.6.1 の環境で動作確認しています。

# 6/16 追記

一部 /spark の書き換えルールを修正しました。

この設定で問題がないと思ったんですが、Spark History Server どうも同じ URL なのにも関わらず初回は 302 を返すため、http 経由でもアクセス可能として、次のような設定を入れる必要がありました。
具体的には、 “https:// server_name/spark/history/application_1464686385340_0492/1/jobs/” にアクセスすると、”http://server_name/history/application_1464686385340_0492/1/jobs/” に 302 リダイレクトされます。。。

location /history {
    rewrite ^/(.*) https://server_name/spark/$1 redirect;
}

CDH2 から CDH3u2 にアップデートしてみた

CDH2 から CDH3 Update 2 からのアップデートしてみましたので、簡単にその方法を紹介します。本番環境でアップデートするのに要した時間は、約 1 時間くらいです。

公式ドキュメントはここにあります。

まず、CDH3 ではユーザまわりの変更が入っています。

CDH3 でのユーザアカウントの変更

  •  hdfs: NameNode, DataNodes, Secondary NameNode(従来の hadoop ユーザにあたります、CDH3 にアップデートすると自動的に hadoop ユーザが hdfs ユーザに変更されるので注意してください)
  • mapred: JobTracker and TaskTrackers(JobTracker と TaskTracker 用に追加された新しいユーザになります)

ローカルファイルシステムのディレクトリオーナー

  • dfs.name.dir: hdfs:hadoop 700
  • dfs.data.dir: hdfs:hadoop 700
  • mapred.local.dir: mapred:hadoop 755
  • HADOOP_LOG_DIR: anyuser:hadoop 775

HDFS 上のディレクトリオーナーの変更

  • mapred.system.dir: mapred:hadoop 700
  • / (root directory): hdfs:hadoop 755

Step1: アップグレードする前の準備
1. すべての Hadoop サービスをシャットダウンします

$ sudo -u hadoop /usr/lib/hadoop-0.20/bin/stop-all.sh

2. hadoop ユーザのプロセスがないことを動いていないことを確認します

$ sudo su hadoop -s /bin/bash -c "jps"

3. NameNode マシン上の HDFS メタデータをバックアップします
SecondaryNode がある場合は、NameNode のおメタデータをバックアップしているためスキップしても大丈夫だと思います
メタデータをバックアップするために、dfs.name.dir の場所を確認します

$ grep -C1 dfs.name.dir /etc/hadoop/conf/hdfs-site.xml

dfs.name.dir
/var/lib/hadoop-0.20/cache/hadoop/dfs/name
$ cd /var/lib/hadoop-0.20/cache/hadoop/dfs/name
$ sudo tar -cvf /root/nn_backup_data.tar .

Step2: Hadoop のコアパッケージ hadoop-0.20 のアップグレード

1. local yum repostiory 設定で、CDH2 から CDH3 に切り替えます

2. すべての Hadoop クラスターのホストにおいて、パッケージのアップグレードする

$ sudo yum upgrade hadoop-0.20*
Hadoop version 0.20.1+169.127-1 -> 0.20.2+923.142-1 になる(2011/11/15 時点)

その他必要なパッケージを、それぞれのホストにインストールします

  • NameNode: hadoop-0.20-namenode, hadoop-0.20-jobtracker
  • Secondary NameNode: hadoop-0.20-secondarynamenode
  • Slave: hadoop-0.20-datanode, hadoop-0.20-tasktracker

3. hadoop-env.sh の HADOOP_MASTER 環境変数を確認します(僕の環境の場合、設定がなかったのでスキップしました)

4. Hadoop システムアカウントを確認します

$ id hdfs
$ id mapred

hadoop ユーザは削除されるので注意します

5. HDFS データのオーナーを確認します
hdfs-site.xml の dfs.name.dir, dfs.data.dir の設定を確認します
dfs.data.dir の設定がない場合は、hdfs-site.xml に追加します

hdfs-site.xml の設定が変わるので注意しましょう。hadoop-0.20-conf-pseudo をインストールして、その設定ファイルをベースに設定するといいと思います。また、hadoop-0.20-dfs.data.dir の設定が hdfs-site.xml に存在しないと CDH3 はシャットダウンしてしまうらしいので注意しましょう


$ ls -ld /var/lib/hadoop-0.20/cache/hadoop/dfs/name
drwxr-xr-x 4 hdfs hadoop 4096 Nov 6 16:49 /var/lib/hadoop-0.20/cache/hadoop/dfs/name/

もし、オーナーがおかしければ変更します

$ sudo chown -R hdfs:hadoop /var/lib/hadoop-0.20/cache/hadoop/dfs/name

6. MapReduce のローカルディレクトリのオーナーとパーミッションを変更します

$ ls -ld /var/lib/hadoop-0.20/cache/hadoop/mapred/local
drwxr-xr-x 4 hdfs hadoop 4096 Nov 6 16:38 /var/lib/hadoop-0.20/cache/hadoop/mapred/local/
$ sudo chown -R mapred /var/lib/hadoop-0.20/cache/hadoop/mapred/local

7. すべてのホストのログディレクトリのオーナーとパーミッションを変更します

$ sudo chgrp hadoop /var/log/hadoop-0.20
$ sudo chmod g+w /var/log/hadoop-0.20
$ sudo chown mapred /var/log/hadoop-0.20/userlogs

8. HDFS メタデータをアップグレードします
別のターミナル画面で、NameNode のログを確認しておきます

namenode$ sudo tail -f /var/log/hadoop-0.20/hadoop-hadoop-namenode*

メタデータをアップグレードします

namenode$ sudo /etc/init.d/hadoop-0.20-namenode upgrade

ログで、次のメッセージが表示されると成功したということになります
2011-11-06 17:15:27,390 INFO org.apache.hadoop.hdfs.server.common.Storage: Upgrade of /var/lib/hadoop-0.20/cache/hadoop/dfs/name is complete.
NameNode は、DataNode に接続するまでの間、セーフモードになっているため、注意しましょう(このあと DataNode を開始すると、自動的にセーフモードが解除されます)

9. HDFS を開始します
それぞれのスレーブホストにて、HDFS を開始します

slaveX$ sudo /etc/init.d/hadoop-0.20-datanode start

すこし待って、DataNode のアップグレードが完了すると、次のメッセージが NameNode のログに出力されます

Safe mode will be turned off automatically in X seconds.

DataNode のアップグレードプロスが完了すると、NameNode は自動的にセーフモードを終了して、HDFS は通常のフルオペレーションが可能になります。そのときは、NameNode のログに、次のような内容が出力されます

...
The reported blocks 8 has reached the threshold 0.9990 of total blocks 8. Safe mode will be turned off automatically in X seconds.
...
2011-11-07 18:33:28,009 INFO org.apache.hadoop.hdfs.StateChange: STATE* Leaving safe mode after 137 secs.
...
2011-11-07 18:33:28,009 INFO org.apache.hadoop.hdfs.StateChange: STATE* Safe mode is OFF.
...

Safe Mode 中かどうかは、次のコマンドで確認することができます

namenode$ sudo hadoop dfsadmin -safemode get
Safe mode is OFF

10. HDFS 上の mapred.system.dir ディレクトリのオーナーを変更します
NamaNode に mapred.system.dir の設定がない場合は、hdfs://namenode/var/lib/hadoop-0.20/cache/hadoop/mapred/system になるため、なければ作成します。

namenode$ sudo -u hdfs hadoop fs -chown -R mapred hdfs://namenode/var/lib/hadoop-0.20/cache/hadoop/mapred/system

11. それぞれのホストで、SecondaryNameNode と MapReduce を開始します

secodarynamenode$ sudo /etc/init.d/hadoop-0.20-secondarynamenode start
slaveX$ sudo /etc/init.d/hadoop-0.20-jobtracker start
slaveX$ sudo /etc/init.d/hadoop-0.20-tasktracker start

念のため、プロセスを確認します

$ ps eaf | grep -i job
$ ps eaf | grep -i task

12. 基本的なクラスター操作を確認します
適当なディレクトリを作成します

namenode$ sudo -u hdfs hadoop fs -mkdir /test

HDFS のユーザディレクトリのオーナーを変更してみます

$ sudo -u hdfs hadoop fs -chown joe /user/joe

通常ユーザで計算ジョブを走らせてみます

$ hadoop-0.20 jar /usr/lib/hadoop-0.20/hadoop-0.20.2*examples.jar pi 10 10000

Job Finished in 61.474 seconds
Estimated value of Pi is 3.14120000000000000000

13. HDFS アップグレードを終了させます

namenode$ sudo -u hdfs hadoop dfsadmin -finalizeUpgrade

http://:50070/ の Upgrades 欄を確認して「There are no upgrades in progress.」と表示されていることを確認します。このメッセージが表示されていれば、無事成功したことになります。

その他、実際の運用面では、Hadoop のジョブは、mapred ユーザで実行したほうが楽です。(他のユーザで実行するには、HADOOP_LOG_DIR やその他 Hadoop のディレクトリのパーミッションを変更する必要があります)

すこしだけ、ジョブの実行スピードが速くなったような気がします。

Hadoop で syslog 経由でログを出力する方法

Hadoop を使いはじめたのですが、Hadoop で出力されるログを syslog 経由で出力するように設定してみました。最初は、log4j.properties だけ書き換えればよいかと思ったのですが、これだけでは syslog 経由でログを出力できませんでした。

Hadoop は、CDH のバージョン 0.18.3 を使っています。

まず、/usr/lib/hadoop/bin/hadoop-daemon.sh で、log4j の logger 環境変数を使えるように次のように変更します。次のパッチでは、念のためローカルのログファイル名も変更できるようにしてあります。

/usr/lib/hadoop/bin/hadoop-daemon.sh
28a29,30> #   HADOOP_LOGFILE  The log file name. Default is hadoop-$HADOOP_IDENT_STRING-$command-$HOSTNAME.log
> #   HADOOP_ROOT_LOGGER   The root appender. Default is INFO,console
84a87,94
> # log configuration
> if [ “$HADOOP_LOGFILE” = “” ]; then
>   export HADOOP_LOGFILE=hadoop-$HADOOP_IDENT_STRING-$command-$HOSTNAME.log
> fi
> if [ “$HADOOP_ROOT_LOGGER” = “” ]; then
>   export HADOOP_ROOT_LOGGER=”INFO,DRFA”
> fi
>
86,87d95< export HADOOP_LOGFILE=hadoop-$HADOOP_IDENT_STRING-$command-$HOSTNAME.log
< export HADOOP_ROOT_LOGGER=”INFO,DRFA”

次に、/etc/hadoop/conf/hadooop-env.sh に、次のように追加します。

export HADOOP_ROOT_LOGGER=”INFO,SYSLOG,DRFA”

最後に、/etc/hadoop/conf/log4j.properties に syslog で出力するための log4j の appender を追加します。

#
# syslog
#
log4j.appender.SYSLOG=org.apache.log4j.net.SyslogAppender
log4j.appender.SYSLOG.facility=local0
log4j.appender.SYSLOG.layout=org.apache.log4j.PatternLayout
log4j.appender.SYSLOG.layout.ConversionPattern=%p %c{2}: %m%n
log4j.appender.SYSLOG.SyslogHost=127.0.0.1
log4j.appender.SYSLOG.threshold=INFO
log4j.appender.SYSLOG.Header=true
log4j.appender.SYSLOG.FacilityPrinting=true

あとは、Hadoop を再起動すれば、127.0.0.1 のサーバに 514 番ポート、UDP、local0 ファシリティ、にて Hadoop のログが転送されます。どうやら、TCP にすることができないみたいです。

hadoop-daemon.sh の方は、本家でバージョン 0.21 で対応予定のようです。

参考資料