CUBE SUGAR CONTAINER

技術系のこと書きます。

Apache Hive を HiveServer2 経由で操作する

Apache Hive を操作する方法としては、以前から hive というコマンドラインツールが提供されている。 ただ、この方法だと hive コマンドがインストールされたホストでしか Apache Hive を操作できない。 また、Hadoop エコシステムを形成する別のソフトウェアと連携させるときにも共通プロトコルがないと都合が悪い。 そのため、今では HiveServer2 というサービスを起動して JDBC 経由で操作する方法が用意されている。 今回は、そのやり方について書く。

使った環境は次の通り。

$ cat /etc/redhat-release 
CentOS Linux release 7.4.1708 (Core) 
$ uname -r
3.10.0-693.11.1.el7.x86_64
$ hive --version
Hive 2.3.2
Git git://stakiar-MBP.local/Users/stakiar/Desktop/scratch-space/apache-hive -r 857a9fd8ad725a53bd95c1b2d6612f9b1155f44d
Compiled by stakiar on Thu Nov 9 09:11:39 PST 2017
From source with checksum dc38920061a4eb32c4d15ebd5429ac8a
$ hadoop version
Hadoop 2.8.3
Subversion https://git-wip-us.apache.org/repos/asf/hadoop.git -r b3fe56402d908019d99af1f1f4fc65cb1d1436a2
Compiled by jdu on 2017-12-05T03:43Z
Compiled with protoc 2.5.0
From source with checksum 9ff4856d824e983fa510d3f843e3f19d
This command was run using /home/vagrant/hadoop-2.8.3/share/hadoop/common/hadoop-common-2.8.3.jar

メタストアに使う MariaDB をセットアップする

Apache Hive はテーブルのスキーマ定義などメタデータを RDBMS に保存する。 今回は、その保存先として MariaDB を使うことにした。

まずは MariaDB をインストールして起動する。

$ sudo yum -y install mariadb-server
$ sudo systemctl enable mariadb
$ sudo systemctl start mariadb

続いてログイン用のアカウントを設定する。 ここでは root ユーザを使っているけど、本来はちゃんと Hive 専用のアクセス制御されたユーザを作るのが望ましい。

$ mysqladmin password rootpassword -u root

一旦、上記のアカウントで MariaDB にログインできることを確かめておこう。

$ mysql -uroot -prootpassword
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 8
Server version: 5.5.56-MariaDB MariaDB Server

Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]>

後は MariaDB を操作するための Java のドライバをインストールしておこう。 今回は yum でインストールできるものを使った。

$ sudo yum -y install mysql-connector-java
$ ln -s /usr/share/java/mysql-connector-java.jar $HIVE_HOME/lib

Hive を設定する

続いて Apache Hive の設定ファイル (hive-site.xml) を編集する。 先ほどセットアップした MariaDB をメタストアとして使う設定と HiveServer2 の認証情報を設定している。

$ cat << 'EOF' > $HIVE_HOME/conf/hive-site.xml
<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
  <property>
    <name>javax.jdo.option.ConnectionURL</name>
    <value>jdbc:mysql://localhost/metastore?createDatabaseIfNotExist=true</value>
  </property>
  <property>
    <name>javax.jdo.option.ConnectionDriverName</name>
    <value>com.mysql.jdbc.Driver</value>
  </property>
  <property>
    <name>hive.metastore.warehouse.dir</name>
    <value>/user/hive/warehouse</value>
  </property>
  <property>
    <name>javax.jdo.option.ConnectionUserName</name>
    <value>root</value>
  </property>
  <property>
    <name>javax.jdo.option.ConnectionPassword</name>
    <value>rootpassword</value>
  </property>
  <property>
    <name>hive.server2.authentication</name>
    <value>NONE</value>
  </property>
  <property>
    <name>hive.server2.enable.doAs</name>
    <value>false</value>
  </property>
</configuration>
EOF

HiveServer2 は外部のサービスと連携するため、認証の仕組みも用意されている。 LDAP との連携などもできるようになっているが、ここではそういった認証の仕組みを用いない (NONE) ことを指定している。

メタストアを初期化する

設定を投入したら、まずはメタストアを初期化する。 これで Apache Hive がメタデータを管理するテーブルなどが MariaDB に用意される。

$ schematool -dbType mysql -initSchema 2>/dev/null
Metastore connection URL:    jdbc:mysql://localhost/metastore?createDatabaseIfNotExist=true
Metastore Connection Driver :  com.mysql.jdbc.Driver
Metastore connection User:   root
Starting metastore schema initialization to 2.3.0
Initialization script hive-schema-2.3.0.mysql.sql
Initialization script completed
schemaTool completed
$ schematool -dbType mysql -info 2>/dev/null
Metastore connection URL:    jdbc:mysql://localhost/metastore?createDatabaseIfNotExist=true
Metastore Connection Driver :  com.mysql.jdbc.Driver
Metastore connection User:   root
Hive distribution version:   2.3.0
Metastore schema version:    2.3.0
schemaTool completed

メタストアサービスを起動する

続いてはメタストアのサービスを起動する。

$ hive --service metastore

メタストアは独立したホストで管理することも想定されているため、このようにサービスとして分離されている。 可用性の面からは、むしろ独立したホストとして用意することがおすすめされているそうだ。

デフォルトではメタストアのサービスが TCP の 9083 ポートでサービスが立ち上がる。

$ ss -tlnp | grep :9083
LISTEN     0      50           *:9083                     *:*                   users:(("java",pid=25826,fd=468))

HiveServer2 のサービスを起動する

続いて HiveServer2 のサービスを起動する。

$ hive --service hiveserver2

デフォルトでは TCP の 10000 ポートでサービスが立ち上がる。

$ ss -tlnp | grep :10000
LISTEN     0      50           *:10000                    *:*                   users:(("java",pid=26338,fd=475))

Beeline で Apache Hive を操作する

これで Apache Hive を HiveServer2 から操作する準備が整った。

HiveServer2 経由で Apavhe Hive を操作するには Beeline という新しいクライアントが用意されている。 次のようにして JDBC 経由で接続しよう。 URI の後ろに続くのは接続に使うアカウントの情報で、認証に NONE を指定したときはセットアップしたホストのものを使うらしい。

$ beeline -u jdbc:hive2://localhost:10000 vagrant vagrant
...
0: jdbc:hive2://localhost:10000>

上記は beeline コマンドを実行するタイミングで接続先を指定しているけど、次のように起動した後に接続することもできる。

$ beeline
...
beeline> !connect jdbc:hive2://localhost:10000

Beeline を使ってみる

試しに適当なデータを入れてみよう。

まずはテーブルを定義する。

0: jdbc:hive2://localhost:10000> CREATE TABLE users (
. . . . . . . . . . . . . . . .>   name STRING,
. . . . . . . . . . . . . . . .>   age INT
. . . . . . . . . . . . . . . .> );
No rows affected (1.01 seconds)

テーブルができた。

0: jdbc:hive2://localhost:10000> SHOW TABLES;
+-----------+
| tab_name  |
+-----------+
| users     |
+-----------+
1 row selected (0.663 seconds)

とはいえ Apache Hive はデータの読み取り時にスキーマを適用するシステムになっている。 そのため、実際にレコードを投入してみないと上手くいっているかは分からない。

0: jdbc:hive2://localhost:10000> INSERT INTO TABLE users VALUES ("Alice", 20);
...
No rows affected (31.243 seconds)

投入したレコードを確認してみる。

0: jdbc:hive2://localhost:10000> SELECT * FROM users;
+-------------+------------+
| users.name  | users.age  |
+-------------+------------+
| Alice       | 20         |
+-------------+------------+
1 row selected (0.514 seconds)

ちゃんとデータの投入も上手くいったようだ。

めでたしめでたし。

プログラミング Hive

プログラミング Hive

  • 作者: Edward Capriolo,Dean Wampler,Jason Rutherglen,佐藤直生,嶋内翔,Sky株式会社玉川竜司
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2013/06/15
  • メディア: 大型本
  • この商品を含むブログ (3件) を見る

ビッグデータ分析・活用のためのSQLレシピ

ビッグデータ分析・活用のためのSQLレシピ

Apache Hive のメタストアに MariaDB を使う

Apache Hive はテーブルのスキーマ定義などメタデータを RDBMS に保存する。 デフォルトでは Java で書かれた組み込み RDBMS の Apache Derby が使われる。 今回は、その Apache Derby の代わりに MariaDB (MySQL) を使う方法について。

使った環境は次の通り。 Apache Hadoop と Apache Hive はメタストアの初期化直前までセットアップされていることを想定している。

$ cat /etc/redhat-release 
CentOS Linux release 7.4.1708 (Core) 
$ uname -r
3.10.0-693.11.1.el7.x86_64
$ hive --version
Hive 2.3.2
Git git://stakiar-MBP.local/Users/stakiar/Desktop/scratch-space/apache-hive -r 857a9fd8ad725a53bd95c1b2d6612f9b1155f44d
Compiled by stakiar on Thu Nov 9 09:11:39 PST 2017
From source with checksum dc38920061a4eb32c4d15ebd5429ac8a
$ hadoop version
Hadoop 2.8.3
Subversion https://git-wip-us.apache.org/repos/asf/hadoop.git -r b3fe56402d908019d99af1f1f4fc65cb1d1436a2
Compiled by jdu on 2017-12-05T03:43Z
Compiled with protoc 2.5.0
From source with checksum 9ff4856d824e983fa510d3f843e3f19d
This command was run using /home/vagrant/hadoop-2.8.3/share/hadoop/common/hadoop-common-2.8.3.jar

コミュニティ版の Apache Hive をセットアップする方法については以下の記事に記載した。

blog.amedama.jp

下準備

まずは Apache Hive のメタストアとして使うための MariaDB をインストールしておく。

$ sudo yum -y install mariadb-server

インストールが終わったら MariaDB のサービスを起動する。

$ sudo systemctl enable mariadb
$ sudo systemctl start mariadb

続いてログイン用の情報を設定する。 今回は手早く root ユーザでやっちゃうけど、本来ならアクセス制御をしたユーザでやるのが好ましい。

$ mysqladmin password rootpassword -u root

上記で設定したアカウントで MariaDB にログインできることを確認しておこう。

$ mysql -uroot -prootpassword
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 8
Server version: 5.5.56-MariaDB MariaDB Server

Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]>

これで準備が整った。

Hive のメタデータ保存先に MariaDB を指定する

Hive の設定は hive-site.xml という設定ファイルで行う。 ここに、先ほどセットアップした MariaDB への接続情報を記載する。

$ cat << 'EOF' > $HIVE_HOME/conf/hive-site.xml
<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
  <property>
    <name>javax.jdo.option.ConnectionURL</name>
    <value>jdbc:mysql://localhost/metastore?createDatabaseIfNotExist=true</value>
  </property>
  <property>
    <name>javax.jdo.option.ConnectionDriverName</name>
    <value>com.mysql.jdbc.Driver</value>
  </property>
  <property>
    <name>hive.metastore.warehouse.dir</name>
    <value>/user/hive/warehouse</value>
  </property>
  <property>
    <name>javax.jdo.option.ConnectionUserName</name>
    <value>root</value>
  </property>
  <property>
    <name>javax.jdo.option.ConnectionPassword</name>
    <value>rootpassword</value>
  </property>
</configuration>
EOF

MariaDB (MySQL) を使う場合、接続用のドライバが必要になる。 今回は yum でインストールできるものを使うことにした。 もちろん Web サイトからダウンロードしてきても構わない。

$ sudo yum -y install mysql-connector-java
$ ln -s /usr/share/java/mysql-connector-java.jar $HIVE_HOME/lib

あとは schematool コマンドを使ってメタストアを初期化する。 これで必要なデータベースやテーブル類が一式作られる。 メタストアに MariaDB (MySQL) を使うときは -dbType オプションに mysql を指定するのがポイントとなる。

$ schematool -dbType mysql -initSchema 2>/dev/null
Metastore connection URL:    jdbc:mysql://localhost/metastore?createDatabaseIfNotExist=true
Metastore Connection Driver :  com.mysql.jdbc.Driver
Metastore connection User:   root
Starting metastore schema initialization to 2.3.0
Initialization script hive-schema-2.3.0.mysql.sql
Initialization script completed
schemaTool completed

メタストアの情報がちゃんと初期化されたことを -info オプションで確認しておこう。

$ schematool -dbType mysql -info 2>/dev/null
Metastore connection URL:    jdbc:mysql://localhost/metastore?createDatabaseIfNotExist=true
Metastore Connection Driver :  com.mysql.jdbc.Driver
Metastore connection User:   root
Hive distribution version:   2.3.0
Metastore schema version:    2.3.0
schemaTool completed

Apache Hive でテーブルを作ってみる

次は Apache Hive を使ったことで、ちゃんとメタストアに情報が保存されることを確認してみよう。

Hive CLI を起動する。

$ hive

適当にテーブルを定義してみる。

hive> CREATE TABLE users (
    >   name STRING,
    >   age INT
    > );
OK
Time taken: 1.498 seconds

あとは MariaDB のメタストア用データベースに、上記で定義したテーブル情報が保存されていることを確認する。

$ mysql -uroot -prootpassword metastore -e "SELECT * FROM TBLS\G";
*************************** 1. row ***************************
            TBL_ID: 1
       CREATE_TIME: 1517579457
             DB_ID: 1
  LAST_ACCESS_TIME: 0
             OWNER: vagrant
         RETENTION: 0
             SD_ID: 1
          TBL_NAME: users
          TBL_TYPE: MANAGED_TABLE
VIEW_EXPANDED_TEXT: NULL
VIEW_ORIGINAL_TEXT: NULL
IS_REWRITE_ENABLED:  

どうやら、ちゃんと保存されているようだ。

めでたしめでたし。

プログラミング Hive

プログラミング Hive

  • 作者: Edward Capriolo,Dean Wampler,Jason Rutherglen,佐藤直生,嶋内翔,Sky株式会社玉川竜司
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2013/06/15
  • メディア: 大型本
  • この商品を含むブログ (3件) を見る

ビッグデータ分析・活用のためのSQLレシピ

ビッグデータ分析・活用のためのSQLレシピ

まっさらな状態から Docker イメージを作る

Docker イメージというと、一般的には既存の Docker イメージをベースにして作る機会が多い。 そうしたとき Dockerfile にはベースとなるイメージを FROM 命令で指定する。 とはいえ、既存のイメージをベースにしない、まっさらな状態からイメージを作ることもできる。 それが FROM 命令に scratch を指定した場合になる。 今回は FROM scratch でまっさらな状態から Docker イメージを作ってみることにする。

試した環境は次の通り。

$ docker version
Client:
 Version:   18.01.0-ce
 API version:   1.35
 Go version:    go1.9.2
 Git commit:    03596f5
 Built: unknown-buildtime
 OS/Arch:   darwin/amd64
 Experimental:  false
 Orchestrator:  swarm

Server:
 Engine:
  Version:  18.01.0-ce
  API version:  1.35 (minimum version 1.12)
  Go version:   go1.9.2
  Git commit:   03596f5
  Built:    Wed Jan 10 20:13:12 2018
  OS/Arch:  linux/amd64
  Experimental: false

Ubuntu 16.04 LTS の Docker イメージを作ってみる

Docker コンテナというのは、結局のところシステムから隔離された Linux の一プロセスに過ぎない。 そのプロセスが動作するのに必要なファイルをまとめたものが Docker イメージということになる。 つまり、例えば特定の OS のルートファイルシステム一式さえあれば、その OS の Docker イメージが作れることになる。 ここでは例として Ubuntu 16.04 LTS の Docker イメージを作ってみることにする。

まずは公式で公開されている Ubuntu 16.04 LTS のルートファイルシステム一式をダウンロードしてくる。

$ wget http://cdimage.ubuntu.com/ubuntu-base/releases/16.04/release/ubuntu-base-16.04.3-base-amd64.tar.gz

あとは、それをファイルシステムのルートに展開した Dockerfile を用意すれば良い。 ADD 命令を使うと tar.gz ファイルは第二引数のパスに展開される。

$ cat << 'EOF' > Dockerfile 
FROM scratch
ADD ubuntu-base-16.04.3-base-amd64.tar.gz /
EOF

上記の Dockerfile をビルドする。

$ docker build -t example/scratch-ubuntu1604 .
Sending build context to Docker daemon  46.12MB
Step 1/2 : FROM scratch
 ---> 
Step 2/2 : ADD ubuntu-base-16.04.3-base-amd64.tar.gz /
 ---> 2e6f55bc5efd
Successfully built 2e6f55bc5efd
Successfully tagged example/scratch-ubuntu1604:latest

ちゃんと Docker イメージが登録された。

$ docker images
REPOSITORY                   TAG                 IMAGE ID            CREATED             SIZE
example/scratch-ubuntu1604   latest              2e6f55bc5efd        15 seconds ago      120MB

上記でビルドした Docker イメージからコンテナを起動してみよう。 起動するプログラムは bash にした。

$ docker run -it example/scratch-ubuntu1604 /bin/bash
root@f3359e8630cc:/#

どうやら、ちゃんと動作している。

root@f3359e8630cc:/# ls /
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@f3359e8630cc:/# cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.3 LTS"

もちろん実用という面では他にも色々とケアすべきところはあるだろうけど、これだけ単純な作業で作ることができた。

シングルバイナリだけから成る Docker イメージを作ってみる

前述した通り Docker イメージというのはコンテナとして起動する Linux プロセスの動作に必要なファイル一式を指す。 逆説的には、起動する Linux プロセスの動作に必要ないファイルというのはイメージの中で無駄ということになる。 つまり、究極的にはプロセスの動作に必要なファイル一式がシングルバイナリにまとめられていれば、そのファイルだけあれば良いことになる。 とはいえそんなことができるのか?というと Golang を使えば意外とできてしまう。 Golang は基本的に外部のライブラリに依存せず、動作に必要な全てを自前で用意している。 そのため、ビルド後のバイナリは動作に libc すら必要としない。 さすが世界で最もコンテナを活発に利用している企業がデザインした言語という感じがする。

前置きが多少長くなったけど、ここからは実際にシングルバイナリだけから成るイメージを作っていく。 まずは Golang のサンプルコードを用意する。 内容は、単なるハローワールドにした。

$ cat << 'EOF' > helloworld.go 
package main

import "fmt"

func main() {
  fmt.Printf("Hello, World!\n")
}
EOF

上記をビルドする。

$ go build helloworld.go

ここで注意すべきなのは動作させる Docker ホストと同じアーキテクチャの Linux 上でビルドすること。 当たり前だけど OS やアーキテクチャが異なる環境でビルドしたバイナリだと動かないので。

$ file helloworld
helloworld: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

あとはビルドしたバイナリを組み込んだ Docker イメージを作るだけ。 次のようにシンプル極まりない Docker ファイルを用意した。

$ cat << 'EOF' > Dockerfile 
FROM scratch
ADD helloworld /

CMD ["/helloworld"]
EOF

上記をビルドして Docker イメージを作る。

$ docker build -t example/scratch-go .
Sending build context to Docker daemon  2.292MB
Step 1/3 : FROM scratch
 ---> 
Step 2/3 : ADD helloworld /
 ---> a2e0629ddc0c
Step 3/3 : CMD ["/helloworld"]
 ---> Running in e61813649513
Removing intermediate container e61813649513
 ---> 898df177330d
Successfully built 898df177330d
Successfully tagged example/scratch-go:latest

登録されたイメージを確認すると 2.29MB という小ささに収まっている。

$ docker images
REPOSITORY                   TAG                 IMAGE ID            CREATED              SIZE
example/scratch-go           latest              898df177330d        20 seconds ago       2.29MB
example/scratch-ubuntu1604   latest              2e6f55bc5efd        About a minute ago   120MB

登録されたイメージを元にコンテナを起動してみよう。

$ docker run -t example/scratch-go
Hello, World!

ちゃんとメッセージが表示されて上手くいったようだ。

めでたしめでたし。

詳解UNIXプログラミング 第3版

詳解UNIXプログラミング 第3版

Python: memory_profiler でプログラムのメモリ使用量を調べる

今回は memory_profiler というモジュールを使ってプログラムのメモリ使用量を調べる方法について紹介する。

pypi.python.org

このブログでは、以前に Python のプロファイラとして profile/cProfile や line_profiler について書いたことがある。 これまでに紹介したこれらのプロファイラは、主に時間計算量の調査が目的となる。 それに対して memory_profiler では、調べる対象は空間計算量となる。

blog.amedama.jp

blog.amedama.jp

使った環境は以下の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.12.6
BuildVersion:   16G1212
$ python --version
Python 3.6.4

下準備

まずは memory_profiler をインストールする。

$ pip install memory_profiler

スクリプトから memory_profiler を使う

まずは最も基本的な、スクリプトから memory_profiler を使う方法について。

memory_profiler では特定の関数のメモリ使用量をプロファイリングするのに @profile デコレータが使える。 例えば次のサンプルコードでは my_func() 関数を @profile デコレータでプロファイル対象としてマークしている。 関数の内容は変数の入った大きなリストを作って、それを del 文で削除するというものになる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from memory_profiler import profile


@profile
def my_func():
    # 整数の入った大きなリストを用意する
    a = [0] * (2 * 10 ** 7)
    # 変数を削除する
    del a
    # 先ほどより少し小さなリストを用意する
    b = [0] * (10 ** 6)
    # 変数を削除する
    del b


def main():
    my_func()


if __name__ == '__main__':
    main()

上記に適当な名前をつけたら、あとは普通に Python のスクリプトとして実行するだけ。 すると、標準出力にプロファイル結果が出力される。 出力内容は、左から「プログラムの行数、その行が評価された時点でのメモリ使用量、その行が評価されたことによる使用量の増減、対応するコード」となっている。

$ python example.py 2>/dev/null
Filename: example.py

Line #    Mem usage    Increment   Line Contents
================================================
     7     36.1 MiB     36.1 MiB   @profile
     8                             def my_func():
     9                                 # 整数の入った大きなリストを用意する
    10    188.7 MiB    152.6 MiB       a = [0] * (2 * 10 ** 7)
    11                                 # 変数を削除する
    12     36.1 MiB   -152.6 MiB       del a
    13                                 # 先ほどより少し小さなリストを用意する
    14     43.8 MiB      7.6 MiB       b = [0] * (10 ** 6)
    15                                 # 変数を削除する
    16     43.8 MiB      0.0 MiB       del b

上記を見ると興味深いことが分かる。 最初の変数 a は del 文を発行することで GC が実行されたのか、メモリ使用量は減っている。 それに対し変数 b では del 文を発行してもメモリ使用量は変化していない。

IPython から memory_profiler を使う

続いては IPython からアドホックに memory_profiler を使ってみる。 おそらく、実際のプロファイリングではこの方法を使うことが多いだろう。

まずは IPython をインストールしておく。

$ pip install ipython

先ほどと、ほぼ同じ内容のサンプルコードを用意する。 違いは my_func() 関数に @profile デコレータが付与されていないところだ。

#!/usr/bin/env python
# -*- coding: utf-8 -*-


def my_func():
    a = [0] * (2 * 10 ** 7)
    del a
    b = [0] * (10 ** 6)
    del b


def main():
    my_func()


if __name__ == '__main__':
    main()

上記で用意した example.py を IPython に読み込ませながら起動する。

$ ipython -i example.py

ちなみに、上記で起動と同時にモジュールを読み込ませているのは手順を省くためだけ。 単独で IPython を起動した後に my_func 関数をインポートしても、もちろん構わない。

$ ipython
...
In [1]: from example import my_func

memory_profiler の IPython 拡張を読み込む。

In [1]: %load_ext memory_profiler

あとは %mprun マジックコマンド経由で my_func() 関数を実行する。 これで、先ほどスクリプトから実行したのと同じ内容が得られる。

In [2]: %mprun -f my_func my_func()
Filename: /Users/amedama/Documents/temporary/example.py

Line #    Mem usage    Increment   Line Contents
================================================
     5     49.2 MiB     49.2 MiB   def my_func():
     6    201.8 MiB    152.6 MiB       a = [0] * (2 * 10 ** 7)
     7     49.2 MiB   -152.6 MiB       del a
     8     49.2 MiB      0.0 MiB       b = [0] * (10 ** 6)
     9     49.2 MiB      0.0 MiB       del b

処理内容が一行で収まるときは %memit マジックコマンドも便利だ。

In [3]: %memit list(range(10 ** 6))
peak memory: 86.92 MiB, increment: 27.92 MiB

In [4]: %memit list(range(10 ** 7))
peak memory: 437.59 MiB, increment: 378.59 MiB

プロファイル結果を matplotlib で折れ線グラフにプロットする

memory_profiler は matplotlib と連携してプロファイル結果をプロットする機能もある。

そのために、まずは matplotlib をインストールしておこう。

$ pip install matplotlib

サンプルコードを mprof run コマンド経由で実行する。

$ mprof run example.py
mprof: Sampling memory every 0.1s
running as a Python program...

実行が完了したら mprof plot コマンドでプロットされた結果を確認する。

$ mprof plot
Using last profile data.

こんな感じで結果が確認できる。 f:id:momijiame:20180204001410p:plain

ちなみにプロファイル結果は履歴が残る。 履歴は mprof list コマンドで確認できる。

$ mprof list
0 mprofile_20180203230956.dat 23:09:56 03/02/2018

インデックスを指定すれば過去の実行結果のプロットが確認できる。

$ mprof plot 0

履歴を削除するにはインデックスを指定して mprof rm コマンドを実行する。

$ mprof rm 0

あるいは、全ての履歴を削除したいなら mprof clean コマンドを使っても構わない。

$ mprof clean

めでたしめでたし。

PySpark の UDF (User Defined Function) を試す

今回は PySpark の UDF (User Defined Function) 機能を使ってみる。 UDF というのはユーザが定義した関数を使って Spark クラスタで分散処理をするための機能になっている。 柔軟に処理を記述できるメリットがある一方で、パフォーマンスには劣るというデメリットもある。 この特性は、ユーザが定義した処理をワーカーに配布した上で Python インタプリタに解釈させる特性に由来している。 今回は、そんな UDF を DataFrame API と Spark SQL という二つの API を通して使ってみることにした。

使った環境は次の通り。 クラスタは YARN で管理している。

$ pyspark --version
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /___/ .__/\_,_/_/ /_/\_\   version 2.2.1
      /_/
                        
Using Scala version 2.11.8, OpenJDK 64-Bit Server VM, 1.8.0_161
Branch 
Compiled by user felixcheung on 2017-11-24T23:19:45Z
Revision 
Url 
Type --help for more information.

下準備

まずは UDF で処理する DataFrame オブジェクトを用意しよう。

最初に、ユーザの情報を模した RDD を定義する。

>>> rdd = sc.parallelize([
...   ('Alice', 20),
...   ('Bob', 25),
...   ('Carol', 30),
... ])

続いて、上記 RDD のスキーマを定義する。

>>> from pyspark.sql.types import StructType
>>> from pyspark.sql.types import StructField
>>> from pyspark.sql.types import StringType
>>> from pyspark.sql.types import IntegerType
>>> schema = StructType([
...   StructField('name', StringType(), False),
...   StructField('age', IntegerType(), False),
... ])

RDD とスキーマから DataFrame を生成する。

>>> df = spark.createDataFrame(rdd, schema)

また、後ほど Spark SQL から操作する場合のことを考えて、この DataFrame をテーブルとして扱えるようにしておこう。

>>> df.registerTempTable('users')

UDF となる関数を定義する

続いて UDF にする関数を定義する。 これは、何の変哲もない Python の関数でしかない。 引数としてカラムの値を受け取って、何らかの加工したカラムの値を返すことになる。

>>> def double(column):
...     return column * 2
... 

今回のサンプルコードでは、受け取った引数を 2 倍にして返すという単純なものにした。

DataFrame API から UDF を使う

まずは DataFrame API で UDF を使う方法から。 これには pyspark.sql.functions.udf() という関数を使う。

>>> from pyspark.sql.functions import udf

上記の udf() 関数を使って、先ほど定義した関数をラップする。 このラップした udf_double() 関数が UDF として動作する。

>>> udf_double = udf(double)

後は pyspark.sql.functions にあるような関数と同じような使い勝手で UDF が使える。 例えばサンプルデータに UDF を使って age カラムの値を 2 倍してみよう。

>>> df.select(udf_double('age')).show()
+-----------+
|double(age)|
+-----------+
|         40|
|         50|
|         60|
+-----------+

上手くいったようだ。

上記だと、表示がちょっと分かりにくいかもしれないので他のカラムも同時に出力してみる。 カラムには alias() メソッドを使って名前が付けられる。

>>> df.select('*', udf_double('age').alias('doubled_age')).show()
+-----+---+-----------+
| name|age|doubled_age|
+-----+---+-----------+
|Alice| 20|         40|
|  Bob| 25|         50|
|Carol| 30|         60|
+-----+---+-----------+

Spark SQL から UDF を使う

続いては Spark SQL から UDF を使ってみる。

それには、まず spark.udf.register() 関数を使って定義した関数を UDF として登録する。

>>> spark.udf.register('udf_double', double)

あとは Spark SQL で処理する SQL 文の中で一般的な関数のように使うことができる。

>>> spark.sql('''
... SELECT
...   *,
...   udf_double(age) AS doubled_age
... FROM users
... ''').show()
+-----+---+-----------+
| name|age|doubled_age|
+-----+---+-----------+
|Alice| 20|         40|
|  Bob| 25|         50|
|Carol| 30|         60|
+-----+---+-----------+

ばっちり。

めでたしめでたし。

入門 PySpark ―PythonとJupyterで活用するSpark 2エコシステム

入門 PySpark ―PythonとJupyterで活用するSpark 2エコシステム

Sparkによる実践データ解析 ―大規模データのための機械学習事例集

Sparkによる実践データ解析 ―大規模データのための機械学習事例集

初めてのSpark

初めてのSpark

  • 作者: Holden Karau,Andy Konwinski,Patrick Wendell,Matei Zaharia,Sky株式会社玉川竜司
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2015/08/22
  • メディア: 大型本
  • この商品を含むブログ (4件) を見る

Docker コンテナの動作に必要な設定を起動時に渡す

今回は Docker コンテナを起動するタイミングで、コンテナの動作に必要な設定を受け渡す方法について書く。 やり方としては、大まかに分けて「環境変数を通して渡す」と「コマンドライン引数を通して渡す」という二つがある。 どちらの場合も docker run で実行するコマンドの中に設定を含めることになる。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.12.6
BuildVersion:   16G1212
$ docker version
Client:
 Version:   18.01.0-ce
 API version:   1.35
 Go version:    go1.9.2
 Git commit:    03596f5
 Built: unknown-buildtime
 OS/Arch:   darwin/amd64
 Experimental:  false
 Orchestrator:  swarm

Server:
 Engine:
  Version:  18.01.0-ce
  API version:  1.35 (minimum version 1.12)
  Go version:   go1.9.2
  Git commit:   03596f5
  Built:    Wed Jan 10 20:13:12 2018
  OS/Arch:  linux/amd64
  Experimental: false

環境変数を通して渡す

まずは環境変数を通して渡す一般的なやり方から。

最初は動作確認のために、起動時に環境変数の一覧を表示する Docker イメージを作ることにする。 その Dockerfile が次の通り。

$ cat << 'EOF' > Dockerfile
FROM alpine

CMD env
EOF

Alpine Linux をベースイメージにして、起動時に実行するコマンドを CMD 命令で指定している。 env コマンドは環境変数を一覧で表示する。 これ以上ないくらいシンプル。

上記の Docker ファイルをビルドして Docker イメージを作る。

$ docker build -t example/env .
...
Successfully tagged example/env:latest

上記でビルドした Docker イメージからコンテナを起動してみよう。 すると、コンテナ内で設定されている環境変数が出力される。

$ docker run -t example/env
HOSTNAME=4df5d62c61a1
SHLVL=1
HOME=/root
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/

続いてコンテナに環境変数を設定してみる。 これには docker run コマンドで -e (--env) オプションを指定する。 例えば、よくありがちなバインドするアドレスやポートっぽい値を設定してみよう。

$ docker run -e BIND_ADDRESS=127.0.0.1 -e BIND_PORT=8080 -t example/env
BIND_ADDRESS=127.0.0.1
HOSTNAME=2fb165c97c2a
SHLVL=1
HOME=/root
BIND_PORT=8080
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/

ちゃんと環境変数に BIND_ADDRESSBIND_PORT が設定されたことが分かる。

ちなみに、環境変数はファイル経由で渡すこともできる。 渡す変数の数が多いときは、こちらを使った方が良い。

まずは環境変数を羅列したファイルを用意する。

$ cat << 'EOF' > envfile.txt
BIND_ADDRESS=127.0.0.1
BIND_PORT=8080
EOF

あとはコンテナを起動するときに --env-file オプションでファイルを指定する。

$ docker run --env-file envfile.txt -t example/env
HOSTNAME=4ed1b4a9a45c
BIND_ADDRESS=127.0.0.1
SHLVL=1
BIND_PORT=8080
HOME=/root
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/

デフォルト値を設定する (Dockerfile)

上記でアプリケーションの設定として環境変数を使う下地が整った。 ただ、設定には一般的にデフォルト値がほしくなる。

そんなときは Dockerfile で ENV 命令を使うことができる。 先ほどの Dockerfile に ENV 命令を加えたものを用意しよう。

$ cat << 'EOF' > Dockerfile
FROM alpine

ENV BIND_ADDRESS 127.0.0.1
ENV BIND_PORT 8080

CMD env
EOF

上記のファイルをビルドする。

$ docker build -t example/env .
...
Successfully tagged example/env:latest

ビルドしたイメージからコンテナを起動してみよう。 特に -e (--env) オプションを使わなくても環境変数が出力されていることが分かる。

$ docker run -t example/env
BIND_ADDRESS=127.0.0.1
HOSTNAME=4c795ff84a32
SHLVL=1
HOME=/root
BIND_PORT=8080
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/

上記の値は -e (--env) オプションを指定することで上書きできる。

$ docker run -e BIND_ADDRESS=0.0.0.0 -e BIND_PORT=80 -t example/env
BIND_ADDRESS=0.0.0.0
HOSTNAME=de360066a866
SHLVL=1
HOME=/root
BIND_PORT=80
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/

デフォルト値を設定する (シェルスクリプト)

環境変数のデフォルト値を設定するには Dockerfile で ENV 命令を使う以外のやり方もある。 具体的には、コンテナが起動するタイミングでシェルスクリプトを実行して、その中で環境変数を操作する。

以下の Dockerfile では docker-entrypoint.sh というファイルをイメージに転送している。 そして ENTRYPOINT 命令を使うことで、コンテナの起動時にそのシェルスクリプトを実行するように設定されている。

$ cat << 'EOF' > Dockerfile 
FROM alpine

COPY docker-entrypoint.sh /usr/local/bin

ENTRYPOINT ["docker-entrypoint.sh"]
EOF

続いて、上記の Dockerfile で使っている docker-entrypoint.sh を用意する。 やっていることは単純で、環境変数がないときはデフォルト値を扱うように ${環境変数名:-デフォルト値} という記法を使うだけ。

$ cat << 'EOF' > docker-entrypoint.sh 
#!/bin/sh

echo ${BIND_ADDRESS:-127.0.0.1}
echo ${BIND_PORT:-8080}
EOF
$ chmod +x docker-entrypoint.sh 

上記で用意したファイル群から Docker イメージをビルドする。

$ docker build -t example/env .
...
Successfully tagged example/env:latest

イメージからコンテナを起動する。 特にオプションを指定のないときはシェルスクリプトで指定したデフォルト値が使われる。 また、先ほどと同じように -e (--env) オプションを指定することで値を上書きできる。

$ docker run -t example/env
127.0.0.1
8080
$ docker run -e BIND_ADDRESS=0.0.0.0 -e BIND_PORT=80 -t example/env
0.0.0.0
80

もちろんシェルスクリプトの中では受け渡された環境変数の内容をバリデーションしたりもできる。

コマンドライン引数を通して渡す

続いてはもう一つのやり方、コマンドライン引数を使う方法について。 このやり方は、基本的には前述したシェルスクリプトを使う方法の応用になっている。

まずは、先ほどと同じように起動時にシェルスクリプトを実行するような Docker ファイルを用意する。 ここでシェルスクリプトを実行するのに ENTRYPOINT 命令を使っているのがポイントになる。 これが CMD 命令だと上手くいかない。

$ cat << 'EOF' > Dockerfile 
FROM alpine

COPY docker-entrypoint.sh /usr/local/bin

ENTRYPOINT ["docker-entrypoint.sh"]
EOF

実行されるシェルスクリプトは、全てのコマンドライン引数を参照できる $@ 変数を echo コマンドで出力する。

$ cat << 'EOF' > docker-entrypoint.sh 
#!/bin/sh

echo $@
EOF
$ chmod +x docker-entrypoint.sh

上記をビルドしよう。

$ docker build -t example/opt .   
...
Successfully tagged example/opt:latest

上記のイメージからコンテナを起動するタイミングで、普段なら起動するコマンドを渡すところに適当な文字列を入れてみよう。

$ docker run -t example/opt foo bar baz                          
foo bar baz

すると、入力した文字列がそのまま出力された。 これはシェルスクリプトがコマンドライン引数を出力するようにした echo $@ による結果となる。 つまり、コマンドライン引数をシェルスクリプトに渡すことができたというわけ。

コマンドライン引数を解析する (getopts)

シェルスクリプトにコマンドライン引数さえ渡せてしまえば、あとはどうとでもなる。 一例として、ここでは getopts を使って引数を解析してみることにした。

起動するシェルスクリプトで getopts を使って受け取った引数を解析する。 そして、最終的には解析した変数を出力している。 ここでは -a オプションで渡した値が BIND_ADDRESS に、-p オプションで渡したあたいが BIND_PORT に格納される。

$ cat << 'EOF' > docker-entrypoint.sh 
#!/bin/sh

usage() {
  echo "Usage: $0 [-a bind-address] [-p bind-port]" 1>&2
  exit 1
}

while getopts a:p:h OPT
do
  case $OPT in
    a)  BIND_ADDRESS=$OPTARG
        ;;
    p)  BIND_PORT=$OPTARG
        ;;
    h)  usage
        ;;
  esac
done

echo ${BIND_ADDRESS:-127.0.0.1}
echo ${BIND_PORT:-8080}
EOF
$ chmod +x docker-entrypoint.sh 

上記を元にイメージをビルドしよう。

$ docker build -t example/opt .             
...
Successfully tagged example/opt:latest

そしてイメージからコンテナを起動する。 特に何も指定しないときはデフォルト値が表示され、オプションをコマンドライン引数で指定したときはその値が表示される。

$ docker run -t example/opt            
127.0.0.1
8080
$ docker run -t example/opt -a 0.0.0.0 -p 80
0.0.0.0
80

これだと、例えば -h を渡したときは usage を表示して終了みたいなことも簡単にできる。

$ docker run -t example/opt -h
Usage: /usr/local/bin/docker-entrypoint.sh [-a bind-address] [-p bind-port]

ちなみに上記のやり方を取るとデバッグしたいときにどうするんだって話になる。 起動時のパラメータの最後に bin/bash とか付けるだけではシェルスクリプトの起動が上書きできないので。 そんなときは --entrypoint オプションを使えば ENTRYPOINT 命令の内容を上書きできる。

$ docker run --entrypoint sh -it example/opt
/ # uname -a
Linux a6c3524f68a5 4.4.111-boot2docker #1 SMP Thu Jan 11 16:25:31 UTC 2018 x86_64 Linux

ばっちり。

Docker のホストとコンテナ間でファイルをやり取りする

Docker ホストとコンテナの間でファイルをやり取りするのって以前はかなり面倒だったと思う。 そんな記憶も今は昔、専用のコマンドが用意されてだいぶ簡単になっているようだ。

docker cp | Docker Documentation

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.12.6
BuildVersion:   16G1212
$ docker version
Client:
 Version:   18.01.0-ce
 API version:   1.35
 Go version:    go1.9.2
 Git commit:    03596f5
 Built: unknown-buildtime
 OS/Arch:   darwin/amd64
 Experimental:  false
 Orchestrator:  swarm

Server:
 Engine:
  Version:  18.01.0-ce
  API version:  1.35 (minimum version 1.12)
  Go version:   go1.9.2
  Git commit:   03596f5
  Built:    Wed Jan 10 20:13:12 2018
  OS/Arch:  linux/amd64
  Experimental: false

下準備

まずはファイルをやり取りするための Docker コンテナを起動しておく。

$ docker run -it alpine /bin/sh

Alpine Linux の Docker コンテナを起動してシェルに入った。

/ # uname -a
Linux 09a735dae425 4.4.111-boot2docker #1 SMP Thu Jan 11 16:25:31 UTC 2018 x86_64 Linux

起動したコンテナの ID を確認しておく。

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
09a735dae425        alpine              "/bin/sh"           12 seconds ago      Up 14 seconds                           stoic_mclean

ホストからコンテナにファイルをコピーする

まずは Docker ホストからコンテナにファイルをコピーする方から。

Docker ホスト側で、まずはコピーするファイルを用意する

$ echo "Hello, World" > greeting.txt

あとは docker cp コマンドを使ってファイルを転送するだけ。 コマンドのパラメータは scp コマンドのイメージに近い。 送信元を第一引数に、送信先を第二引数に指定する。 送信先にはコンテナ ID (もしくは名前) と、コロンで区切って転送先のパスを指定する。

$ docker cp greeting.txt 09a735dae425:/tmp

docker exec コマンドを使ってコンテナ上で cat コマンドを実行してファイルの状態を確認しよう。

$ docker exec 09a735dae425 cat /tmp/greeting.txt
Hello, World

ちゃんとファイルが送られたことが分かる。

コンテナからホストにファイルをコピーする

続いてはコンテナからホストにファイルをコピーするパターン。 この場合も、最終的には先ほどと同じように docker cp コマンドを使う。

まずはコンテナの中でコピーするファイルを用意しよう。 分かりやすいように、先ほど送り込まれたファイルを編集して使う。

/ # echo "Hello, Docker" > /tmp/greeting.txt

あとは Docker ホスト側で docker cp コマンドを使ってコンテナ上のファイルをホストに転送してくる。 今回は送信元となる第一引数の方にコンテナとファイルの絶対パスを指定する。

$ docker cp 09a735dae425:/tmp/greeting.txt .

コマンドを実行すると、ちゃんとコンテナにあったファイルが手元にコピーされたことが分かる。

$ cat greeting.txt 
Hello, Docker

いじょう。

めでたしめでたし。