CUBE SUGAR CONTAINER

技術系のこと書きます。

Apache Hive の Vectorization 機能を試す

今回は Apache Hive の Vectorization 機能を使ってパフォーマンスが向上するか試してみる。 Apache Hive では、通常 HDFS に保存されたデータを一行ずつ処理する。 それに対し Vectorization 機能を使うと、状況は限られるものの複数行をまとめて処理できるようになる。 結論から先に書くと、機能を有効にすることで多少のパフォーマンス向上はあることが分かった。

使った環境は次の通り。

$ cat /etc/redhat-release 
CentOS Linux release 7.4.1708 (Core) 
$ uname -r
3.10.0-693.17.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

ダミーデータを用意する

パフォーマンスを測るからには、それなりのデータ量が必要となるはずなのでダミーデータを生成する。 今回は Golang とフェイクデータを生成するパッケージの github.com/icrowley/fake を使った。

まずは Golang をインストールしてパッケージをダウンロードしておく。

$ sudo yum -y install golang git
$ go get github.com/icrowley/fake

以下のようなダミーデータ生成用のプログラムを用意した。

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

import (
  "fmt"
  "strconv"
  "os"
  "math/rand"
  "time"
  "github.com/icrowley/fake"
)

func main() {
  rand.Seed(time.Now().UnixNano())

  if len(os.Args) < 2 {
    fmt.Println("Too few arguments")
    fmt.Printf("%s N\n", os.Args[0])
    os.Exit(1)
  }

  N, err := strconv.Atoi(os.Args[1])
  if err != nil {
    fmt.Printf("Please enter the integer: %s\n", N)
    os.Exit(1)
  }

  for i := 0; i < N; i++ {
    name := fake.FirstName()
    age := rand.Intn(100)
    fmt.Printf("%d,%s,%d\n", i, name, age)
  }
}
EOF

上記のプログラムをビルドする。

$ go build dummy_users.go 

実行すると ID と名前と年齢のダミーデータを CSV で出力する。

$ ./dummy_users 10
0,Gregory,74
1,Sharon,35
2,Beverly,22
3,Mark,25
4,Ralph,12
5,Douglas,63
6,Catherine,44
7,Joyce,84
8,Bruce,87
9,Kevin,3

とりあえず 1000 万件 (行) のデータを生成しておく。

$ ./dummy_users 10000000 > dummy_users.csv

次のように 168MB の CSV ができた。

$ wc -l dummy_users.csv 
10000000 dummy_users.csv
$ du -m dummy_users.csv 
168    dummy_users.csv

ダミーデータを Hive のテーブルに読み込む

続いてはダミーデータを Apache Hive のテーブルに読み込む。

まずは Hive の CLI を起動する。

$ hive

先ほど生成したダミーデータと一致するようなテーブルを作っておく。

hive> CREATE TABLE users (
    >   id BIGINT,
    >   name STRING,
    >   age INT
    > )
    > ROW FORMAT DELIMITED
    > FIELDS TERMINATED BY ','
    > STORED AS TEXTFILE;
OK
Time taken: 1.243 seconds

あとは先ほど作った CSV をテーブルに読み込むだけ。

hive> LOAD DATA LOCAL INPATH 'dummy_users.csv' OVERWRITE INTO TABLE users;
Loading data to table default.users
OK
Time taken: 12.993 seconds

ORC フォーマットのテーブルにコピーする

Apache Hive の Vectorization 機能は、当初は ORC ファイルフォーマットだけに対応していた。 ただし Apache Hive のバージョン 2.1 以降では、それ以外のフォーマットもサポートされているらしい。 とはいえ、軽く触ったところ ORC の方がパフォーマンス向上が望めそうなので、そちらを使うことにする。

[HIVE-12878] Support Vectorization for TEXTFILE and other formats - ASF JIRA

まずは、先ほどとスキーマは同じでファイルフォーマットだけ ORC に変更したテーブルを用意する。

hive> CREATE TABLE users_orc (
    >   id BIGINT,
    >   name STRING,
    >   age INT
    > )
    > ROW FORMAT DELIMITED
    > FIELDS TERMINATED BY ','
    > STORED AS ORC;
OK
Time taken: 0.089 seconds

あとは、元のテーブルからデータをコピーしてくるだけ。

hive> INSERT INTO TABLE users_orc SELECT * FROM users;
...
OK
Time taken: 47.313 seconds

Vectorization 機能を有効にしてみる

Vectorization 機能は hive.vectorized.execution.enabledhive.vectorized.execution.reduce.enabled で有効・無効を切り替える。 次のように、デフォルトでは無効化されている。

hive> set hive.vectorized.execution.enabled;
hive.vectorized.execution.enabled=true
hive> set hive.vectorized.execution.reduce.enabled;
hive.vectorized.execution.reduce.enabled=true

この項目を true にすれば機能が有効になる。

hive> set hive.vectorized.execution.enabled=true;
hive> set hive.vectorized.execution.reduce.enabled=true;

Vectorization 機能が使えない状況では単に設定が無視されるだけなので、とりあえず有効にしておいても良さそう。

機能の有無でパフォーマンスを比較する

Vectorization 機能は、比較的単純なクエリを実行するときに使えるらしい。 そこで、次のような年齢ごとのユーザ数を集計するクエリで比較してみよう。

hive> SELECT
    >   age,
    >   COUNT(1)
    > FROM users_orc
    > GROUP BY age;
...
0  100244
1  100033
2  99509
3  100067
4  99326
5  100026
...
95 99622
96 99675
97 100287
98 99751
99 100240

機能の切り替えを簡単にしたいので、設定を ~/.hiverc という設定ファイルに書き込むことにする。 ここに書いておくと Hive の CLI を起動するとき、自動でそこに記述されている内容を読み込んでくれる。

$ cat << 'EOF' > ~/.hiverc
set hive.vectorized.execution.enabled=false;
set hive.vectorized.execution.reduce.enabled=false;
EOF

まずは機能を無効にした状態で、次のように 10 回ほどクエリを実行したときの時間を測ってみた。

$ SQL='SELECT age, COUNT(1) FROM users_orc GROUP BY age'
$ for i in {1..10}; do hive -e "$SQL" 2>&1 | tail -n 1; done;
Time taken: 48.642 seconds, Fetched: 100 row(s)
Time taken: 46.033 seconds, Fetched: 100 row(s)
Time taken: 48.642 seconds, Fetched: 100 row(s)
Time taken: 47.711 seconds, Fetched: 100 row(s)
Time taken: 47.276 seconds, Fetched: 100 row(s)
Time taken: 48.677 seconds, Fetched: 100 row(s)
Time taken: 47.498 seconds, Fetched: 100 row(s)
Time taken: 46.391 seconds, Fetched: 100 row(s)
Time taken: 46.013 seconds, Fetched: 100 row(s)
Time taken: 48.593 seconds, Fetched: 100 row(s)

次は設定ファイルで機能を有効化する。

$ cat << 'EOF' > ~/.hiverc
set hive.vectorized.execution.enabled=true;
set hive.vectorized.execution.reduce.enabled=true;
EOF

先ほどと同じように実行時間を測ってみた結果が次の通り。 何だか、ほんのり速くなっているような?

$ for i in {1..10}; do hive -e "$SQL" 2>&1 | tail -n 1; done;
Time taken: 45.088 seconds, Fetched: 100 row(s)
Time taken: 43.822 seconds, Fetched: 100 row(s)
Time taken: 45.188 seconds, Fetched: 100 row(s)
Time taken: 45.157 seconds, Fetched: 100 row(s)
Time taken: 43.627 seconds, Fetched: 100 row(s)
Time taken: 45.167 seconds, Fetched: 100 row(s)
Time taken: 45.897 seconds, Fetched: 100 row(s)
Time taken: 45.647 seconds, Fetched: 100 row(s)
Time taken: 43.263 seconds, Fetched: 100 row(s)
Time taken: 44.603 seconds, Fetched: 100 row(s)

実行にかかった時間を比較する

なんだか、ほんのり速くなっているような感じはするけど確証がないので詳しく見ていくことにしよう。 これ以降の部分は、別に Apache Hive とは関係がないので、どういった環境を使っても構わない。

調査するために、まずは Python のサードパーティ製パッケージの scipynumpy をインストールしておく。

$ pip install scipy numpy

Python の REPL を起動する。

$ python

機能を有効にする前と後の実行時間を numpy の配列として用意する。

>>> import numpy as np
>>> before = np.array([
...     48.642,
...     46.033,
...     48.642,
...     47.711,
...     47.276,
...     48.677,
...     47.498,
...     46.391,
...     46.013,
...     48.593,
... ])
>>> after = np.array([
...     45.088,
...     43.822,
...     45.188,
...     45.157,
...     43.627,
...     45.167,
...     45.897,
...     45.647,
...     43.263,
...     44.603,
... ])

平均実行時間を比べると、機能を有効にしたときの方が 3 秒前後短いようだ。

>>> before.mean()
47.5476
>>> after.mean()
44.7459

ということで Vectorization 機能はパフォーマンスの向上に有効でした。 ...というのは、あまりにも短絡的なので、一応検定しておくことにする。

ウェルチの t 検定で、機能を有効にした方の平均が「有意に小さい」を対立仮説に「有意に小さくない」を帰無仮説として検定する。

>>> _, p = stats.ttest_ind(before, after, equal_var=False)
>>> p / 2
3.957745312666858e-06

p-value は非常に小さな値なので、仮に有意水準 1% でも余裕で帰無仮説は棄却され対立仮説を採用することになる。

最初、F 検定からの単純 t 検定を使っていたんだけど、どうやら最近は等分散とか関係なくウェルチの t 検定でええやんってことみたい。 ちなみに、最初の方法でも等分散かつ有意だった。

laboratoryofbiology.blogspot.jp

めでたしめでたし。

プログラミング Hive

プログラミング Hive

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

統計学入門 (基礎統計学?)

統計学入門 (基礎統計学?)

Apache Hive で圧縮形式のデータを扱う

Apache Hive のテーブルを構成するデータは、デフォルトでは無圧縮になっている。 しかし、設定を変更することで圧縮形式のデータも扱うことができる。 そこで、今回は Apache Hive で圧縮形式のデータを扱ってみることにする。

データを圧縮することには、主に二つのメリットがある。 まず一つ目は HDFS 上のサイズが小さくなるのでディスク容量の節約になること。 そして二つ目こそ本命だけどサイズが小さくなるので読み出しにかかるディスク I/O の負荷も下げることができる。 Hadoop においてディスク I/O は最もボトルネックになりやすいところなので、これは重要となる。

使った環境は次の通り。

$ 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

準備

まずは、ひとまず Hive の CLI を起動しておく。

$ hive

起動したら、次のように set 命令を使って設定を確認しておこう。 hive.exec.compress.output は出力結果を圧縮形式にするかを設定する項目になっている。 そして mapred.output.compression.codec は圧縮に使うコーデックを指定する項目になっている。

hive> set hive.exec.compress.output;
hive.exec.compress.output=false
hive> set mapred.output.compression.codec;
mapred.output.compression.codec=org.apache.hadoop.io.compress.DefaultCodec

上記のように、デフォルトでは出力結果を圧縮しないようになっている。

結果が圧縮されないことを確認したところで、まずは無圧縮のテーブルを作ってみよう。 以下の設定ではデータが CSV で保存されることになる。

hive> CREATE TABLE users (
    >   name STRING,
    >   age INT
    > )
    > ROW FORMAT DELIMITED
    > FIELDS TERMINATED BY ','
    > STORED AS TEXTFILE;
OK
Time taken: 12.171 seconds

レコードを追加する。

hive> INSERT INTO TABLE users VALUES
    >   ("Alice", 20),
    >   ("Bob", 25),
    >   ("Carol", 30);
...
OK
Time taken: 40.852 seconds

この状態で、テーブルを構成するデータがどのように保存されているかをまずは確認しておこう。 テーブルの保存先は SHOW CREATE TABLE で確認できる。

hive> SHOW CREATE TABLE users;
OK
CREATE TABLE `users`(
  `name` string, 
  `age` int)
ROW FORMAT SERDE 
  'org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe' 
WITH SERDEPROPERTIES ( 
  'field.delim'=',', 
  'serialization.format'=',') 
STORED AS INPUTFORMAT 
  'org.apache.hadoop.mapred.TextInputFormat' 
OUTPUTFORMAT 
  'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
  'hdfs://master:9000/user/hive/warehouse/users'
TBLPROPERTIES (
  'transient_lastDdlTime'='1518700567')
Time taken: 0.203 seconds, Fetched: 16 row(s)

上記の LOCATION が保存先を表している。

別のターミナルで hdfs dfs -ls コマンドを使って上記のパスを確認してみよう。

$ hdfs dfs -ls /user/hive/warehouse/users
Found 1 items
-rwxrwxr-x   2 vagrant supergroup         25 2018-02-15 13:16 /user/hive/warehouse/users/000000_0

ディレクトリの中には 000000_0 というファイルだけがあることが分かる。

hdfs dfs -cat コマンドを使って内容を確認してみよう。

$ hdfs dfs -cat /user/hive/warehouse/users/000000_0
Alice,20
Bob,25
Carol,30

すると、これが CSV ファイルで先ほど投入したレコードが入っていることが分かる。 デフォルトでは、このようにファイルが無圧縮で HDFS 上にそのまま保存されることになる。

テーブルを構成するデータを圧縮形式にする

続いてはテーブルを構成するデータを圧縮したものにしてみよう。 それには、前述した設定項目を編集する。

hive> set hive.exec.compress.output=true;
hive> set mapred.output.compression.codec=org.apache.hadoop.io.compress.GzipCodec;

ここではデータを GZIP で圧縮するように設定した。

先ほど作った無圧縮のテーブルから、新しいテーブルを作ってみる。 テーブル自体の設定自体は先ほどと変えておらず中身は CSV になるよう指定している。

hive> CREATE TABLE users_gzip
    >   ROW FORMAT DELIMITED
    >   FIELDS TERMINATED BY ','
    >   STORED AS TEXTFILE
    > AS SELECT * FROM users;
...
OK
Time taken: 28.895 seconds

先ほどと同じように保存先の HDFS を確認してみよう。

$ hdfs dfs -ls /user/hive/warehouse/users_gzip
Found 1 items
-rwxrwxr-x   2 vagrant supergroup         45 2018-02-15 13:22 /user/hive/warehouse/users_gzip/000000_0.gz

すると、今度はディレクトリの中にあるファイルに .gz という名前がついていることが分かる。

ファイルをローカルにダウンロードしてこよう。

$ hdfs dfs -get /user/hive/warehouse/users_gzip/000000_0.gz

file コマンドを使って形式を確認すると、ちゃんと GZIP ファイルとなっている。

$ file 000000_0.gz
000000_0.gz: gzip compressed data, from Unix

gunzip コマンドを使って圧縮ファイルを解凍してみよう。

$ gunzip 000000_0.gz

解凍したファイルを確認すると CSV ファイルになっていることが分かる。

$ cat 000000_0 
Alice,20
Bob,25
Carol,30

このように設定を切り替えることでテーブルを構成するデータを圧縮形式にできる。

圧縮形式によるサイズの違いを比べてみる

続いては圧縮形式を変えることでテーブルのサイズがどのように変化するか見てみる。 違いを確かめるには、ある程度のサイズが必要なのでダミーデータを作ることにした。

ダミーデータの生成には Python の faker を使うことにする。 pip コマンドを使ってインストールしよう。

$ sudo yum -y install epel-release
$ sudo yum -y install python-pip
$ sudo pip install faker

インストールができたら Python の REPL を起動する。

$ python

次のようにして 10 万件のダミーデータが入った CSV ファイルを作る。

>>> from faker import Faker
>>> fake = Faker()
>>> import random
>>> N = 100000
>>> with open('users.csv', 'w') as f:
...     for _ in range(N):
...         age = random.randint(0, 100)
...         name = fake.last_name()
...         f.write(name + ',' + str(age) + '\n')
...

こんな感じでダミーデータの入った CSV ファイルができる。

$ wc -l users.csv
100000 users.csv
$ head users.csv 
Moore,82
Jensen,40
Robinson,42
White,11
Atkinson,56
Small,17
Wilson,76
Johnson,64
Moody,85
Barnes,61

それを Hive のテーブルに取り込む。

hive> LOAD DATA LOCAL INPATH 'users.csv' OVERWRITE INTO TABLE users;
Loading data to table default.users
OK
Time taken: 1.026 seconds

上手くいけば次のように 10 万件のデータが見えるようになる。

hive> SELECT COUNT(1) FROM users;
...
OK
100000
Time taken: 39.95 seconds, Fetched: 1 row(s)

GZIP

まずは GZIP 形式から。 先ほど作ったテーブルは一旦消しておこう。

DROP TABLE users_gzip;

圧縮に使うコーデックとして GZIP を指定する。

hive> set hive.exec.compress.output=true;
hive> set mapred.output.compression.codec=org.apache.hadoop.io.compress.GzipCodec;

先ほど作った 10 万件のテーブルをコピーして新しいテーブルを作る。

hive> CREATE TABLE users_gzip
    >   ROW FORMAT DELIMITED
    >   FIELDS TERMINATED BY ','
    >   STORED AS TEXTFILE
    > AS SELECT * FROM users;
...
OK
Time taken: 27.286 seconds

この通りデータがコピーされた。

hive> SELECT COUNT(1) FROM users_gzip;
OK
100000
Time taken: 0.207 seconds, Fetched: 1 row(s)

テーブルのサイズは hdfs dfs -du コマンドを使って確認できる。

$ hdfs dfs -du -h /user/hive/warehouse | grep users
974.5 K  /user/hive/warehouse/users
335.9 K  /user/hive/warehouse/users_gzip

GZIP で圧縮するとテーブルのサイズが約 34% まで小さくなった。

BZIP2

同じことを BZIP2 でもやる。

hive> set hive.exec.compress.output=true;
hive> set mapred.output.compression.codec=org.apache.hadoop.io.compress.BZip2Codec;
hive> CREATE TABLE users_bzip2
    >   ROW FORMAT DELIMITED
    >   FIELDS TERMINATED BY ','
    >   STORED AS TEXTFILE
    > AS SELECT * FROM users;
...
OK
Time taken: 27.634 seconds

Snappy

続いては Snappy で。

hive> set hive.exec.compress.output=true;
hive> set mapred.output.compression.codec=org.apache.hadoop.io.compress.SnappyCodec;
hive> CREATE TABLE users_snappy
    >   ROW FORMAT DELIMITED
    >   FIELDS TERMINATED BY ','
    >   STORED AS TEXTFILE
    > AS SELECT * FROM users;
...
OK
Time taken: 26.979 seconds

LZ4

最後に LZ4 を。

hive> set hive.exec.compress.output=true;
hive> set mapred.output.compression.codec=org.apache.hadoop.io.compress.Lz4Codec;
hive> CREATE TABLE users_lz4
    >   ROW FORMAT DELIMITED
    >   FIELDS TERMINATED BY ','
    >   STORED AS TEXTFILE
    > AS SELECT * FROM users;
...
OK
Time taken: 26.433 seconds

圧縮率を比較する

これで一通り出揃ったのでサイズを確認してみよう。 ちなみに圧縮に使えるコーデックは Hadoop のバージョンによって異なる。

$ hdfs dfs -du -h /user/hive/warehouse | grep users
974.5 K  /user/hive/warehouse/users
228.6 K  /user/hive/warehouse/users_bzip2
335.9 K  /user/hive/warehouse/users_gzip
583.6 K  /user/hive/warehouse/users_lz4
603.8 K  /user/hive/warehouse/users_snappy

上記から圧縮したときのサイズは BZIP2 < GZIP < LZ4 < Snappy ということが分かった。 ただし、圧縮率の高いコーデックは解凍に時間がかかる。 つまり、ディスク I/O の負担は減るが、代わりに CPU の負担が増えることになる。

また、圧縮形式を決める上では、その形式が「スプリット可能」かについても注意する必要があるらしい。 スプリット可能というのは、大きなファイルを分割して複数のマッパー、リデューサーで処理できることを表している。 前述した圧縮形式の中では BZIP2 についてはスプリット可能、LZ4 と GZIP と Snappy はスプリット不能となっている。 スプリット不能な圧縮形式では、大きなファイルも一つのマッパー、リデューサーで処理しなければならない。 そのため、分散処理のメリットが薄れてしまう。

ただし、上記はあくまで「大きなファイルがある」ことが前提になっている。 つまり、テーブルを構成するファイルが全て適度な大きさになっていればスプリット不能でも問題にはならない。 また GZIP や Snappy といった圧縮形式がスプリット不能なのは、あくまでテキストファイルで保存する場合に限られるようだ。 後述する Sequence ファイルや ORC ファイルといったフォーマットで保存するなら、スプリット可能になるらしい。

一つのテーブルをまぜこぜの圧縮形式で構成してみる

ここまでやってきて、ちょっとした疑問が浮かぶ。 圧縮形式を Hive の設定項目で切り替えるということは、テーブル自体にはそのメタ情報が保存されていないことを意味する。 だとすれば、一つのテーブルを複数の圧縮形式が入り乱れた状態で作ることもできるのだろうか。 先に結論から書くと、これはできるようになっている。

検証用のテーブルを用意する。

hive> CREATE TABLE users_mixed (
    >   name STRING,
    >   age INT
    > )
    > ROW FORMAT DELIMITED
    > FIELDS TERMINATED BY ','
    > STORED AS TEXTFILE;
OK
Time taken: 0.091 seconds

圧縮に使うコーデックの設定を切り替えながらレコードを追加する。

hive> set hive.exec.compress.intermediate=true;
hive> set mapred.output.compression.codec=org.apache.hadoop.io.compress.SnappyCodec;
hive> INSERT INTO TABLE users_mixed VALUES ("Alice", 20);
...
hive> set mapred.output.compression.codec=org.apache.hadoop.io.compress.GzipCodec;
hive> INSERT INTO TABLE users_mixed VALUES ("Bob", 25);
...
hive> set mapred.output.compression.codec=org.apache.hadoop.io.compress.BZip2Codec;
hive> INSERT INTO TABLE users_mixed VALUES ("Carol", 30);

レコードを追加した後、テーブルを構成するファイルを確認すると、次のような結果が得られる。 それぞれのファイルの末尾には異なる拡張子がついていて、複数の圧縮形式が混在している状況だ。

$ hdfs dfs -ls /user/hive/warehouse/users_mixed
Found 3 items
-rwxrwxr-x   2 vagrant supergroup         54 2018-02-15 13:49 /user/hive/warehouse/users_mixed/000000_0.bz2
-rwxrwxr-x   2 vagrant supergroup         27 2018-02-15 13:48 /user/hive/warehouse/users_mixed/000000_0.gz
-rwxrwxr-x   2 vagrant supergroup         19 2018-02-15 13:48 /user/hive/warehouse/users_mixed/000000_0.snappy

この状態であっても、テーブルからはちゃんとレコードを読み出せる。

hive> SELECT * FROM users_mixed;
OK
Carol   30
Bob 25
Alice   20
Time taken: 0.271 seconds, Fetched: 3 row(s)

どうしてこんなことができるかというと、拡張子からファイルの種類を自動的に推定して処理してくれるようになっているため。 ただし、この処理方法はデータを保存するフォーマットがテキストファイルの場合だから、という点に注意が必要となる。 後述する別のフォーマットでは、ファイルの種類に関する情報がファイル自体やテーブルのメタデータに書き込まれることになる。 そのため、まぜこぜにできない場合もある。

Sequence ファイルを圧縮してみる

ここまでの例では、全てファイルのフォーマットとしてテキストファイルを使っていた。 つまり、テーブル定義で STORED AS TEXTFILE としていた。 ここからは別のフォーマットを使った場合にどうなるか確認してみよう。 テキストファイルを使ったときとは、少し勝手が異なる場合があるようだ。

最初は Sequence ファイルというフォーマットを試してみよう。 このフォーマットはバイナリ形式になっている。 Sequence ファイルのフォーマットであれば、テキストファイルでスプリット不能だった圧縮形式でもスプリット可能になる。

まずは設定で GZIP を使った圧縮を有効にしておく。

hive> set hive.exec.compress.output=true;
hive> set mapred.output.compression.codec=org.apache.hadoop.io.compress.GzipCodec;

続いて Sequence ファイルを使ってデータを保存するようにしたテーブルを定義する。 具体的にはテーブルを定義する時点で STORED AS SEQUENCEFILE とする。

hive> CREATE TABLE users_sequence (
    >   name STRING,
    >   age INT
    > )
    > ROW FORMAT DELIMITED
    > FIELDS TERMINATED BY ','
    > STORED AS SEQUENCEFILE;
OK
Time taken: 0.07 seconds

作成したテーブルにレコードを追加してみよう。

hive> INSERT INTO TABLE users_sequence VALUES ("Alice", 20);
...
OK
Time taken: 27.658 seconds

レコードが追加できたら HDFS 上でどのようになっているか確認する。

$ hdfs dfs -ls /user/hive/warehouse/users_sequence
Found 1 items
-rwxrwxr-x   2 vagrant supergroup        168 2018-02-15 14:07 /user/hive/warehouse/users_sequence/000000_0

すると、先ほどのテキストファイルの場合とは異なりファイルに .gz の拡張子がついていない。

バイナリフォーマットのファイルなので、ちょっと乱暴だけど内容をテキストとして表示してみよう。

$ hdfs dfs -cat /user/hive/warehouse/users_sequence/000000_0
SEQ"org.apache.hadoop.io.BytesWritableorg.apache.hadoop.io.Text'org.apache.hadoop.io.compress.GzipCodec41e?:???Qt?j!!?p??LN?123ݟ?   

すると、ファイルの中に圧縮に使ったコーデックの情報が含まれていることが分かる。 このように Sequence ファイルでは、ファイル自体に圧縮形式の情報が含まれることになる。 また、このやり方ならファイルを丸ごと圧縮する方法ではないためスプリット可能になる理由もうなずける。 ちゃんとレコードの区切りを考えて各パートごとに圧縮した情報をメタデータとしてファイルに残しておけるからだ。

ちなみに Sequence ファイルについては hdfs dfs -text コマンドで中身が確認できるようになっている。

$ hdfs dfs -text /user/hive/warehouse/users_sequence/000000_0
18/02/15 14:16:13 INFO zlib.ZlibFactory: Successfully loaded & initialized native-zlib library
18/02/15 14:16:13 INFO compress.CodecPool: Got brand-new decompressor [.gz]
    Alice,20

一応 Snappy で圧縮したときの結果も確認しておこう。

hive> set mapred.output.compression.codec=org.apache.hadoop.io.compress.SnappyCodec;
hive> INSERT INTO TABLE users_sequence VALUES ("Bob", 25);
...
OK
Time taken: 29.17 seconds

さっきと同じようにファイルの中に圧縮に使ったコーデックの情報が記録されていることが分かる。

$ hdfs dfs -cat /user/hive/warehouse/users_sequence/000000_0_copy_1
SEQ"org.apache.hadoop.io.BytesWritableorg.apache.hadoop.io.Text)org.apache.hadoop.io.compress.SnappyCodech???7?
                                                                                                               ??h?   Bob,25

ORC

次は ORC (The Optimized Row Columnar) ファイルについて。 ORC ファイルは Sequence ファイルと同じようにバイナリ形式のフォーマットになっている。 Hive に最適化されたカラム志向型のデータフォーマットになっており高速に動作する。 いくつかの資料を見ると、この ORC ファイルを使うのが Hive のベストプラクティスのようだ。

jp.hortonworks.com

LanguageManual ORC - Apache Hive - Apache Software Foundation

ORC ファイルを圧縮するときは、これまでとはまたちょっと勝手が違っている。 何かというと、テーブル自体のメタデータに圧縮形式が記録されるため。 つまり、これまで使ってきた設定項目の hive.exec.compress.output などの内容は無視されるので注意が必要となる。

それでは ORC ファイルでデータを保存するテーブルを定義しよう。 実は、特に指定はないけどこれだけでファイルが GZIP で圧縮されるようにデフォルト値がなっている。

hive> CREATE TABLE users_orc (
    >   name STRING,
    >   age INT
    > )
    > ROW FORMAT DELIMITED
    > FIELDS TERMINATED BY ','
    > STORED AS ORC;
OK
Time taken: 0.143 seconds

レコードを追加してみよう。

hive> INSERT INTO TABLE users_orc VALUES ("Alice", 20);
...
OK
Time taken: 24.535 seconds

今回も、ファイルを確認すると拡張子には何もついていない。

$ hdfs dfs -ls /user/hive/warehouse/users_orc/
Found 1 items
-rwxrwxr-x   2 vagrant supergroup        295 2018-02-15 14:24 /user/hive/warehouse/users_orc/000000_0

中身を確認すると ORC という文字列が見えるため、どうやら確かに ORC ファイルのようだ。

$ hdfs dfs -cat /user/hive/warehouse/users_orc/000000_0
ORC
P8??be!1F%.Vǜ??T%??+


(((P
    AliceFPN(V?b?``???ь?`?IBH3?@? H???LlBL
                                                @??T???b?`
        ???`hg???`p?Q??`T??bbd?b?K?M?bNLOUb?`bfF+?+1F%.Vǜ??T%???`b??А`p?[??"!6
                                                                            (-0??ORC

続いては、どのように ORC ファイルのテーブルで圧縮形式を指定するか見ていこう。 その前に、一旦先ほど作ったテーブルは削除しておく。

hive> DROP TABLE users_orc;
OK
Time taken: 0.195 seconds

結論から先に書くと ORC ファイルでは TBLPROPERTIES という書式を用いて圧縮形式を指定する。 この中にキーバリュー形式で orc.compress に圧縮形式を指定する。 以下では SNAPPY を指定している。 次のクエリではダミーデータの入ったテーブルからレコードをコピーしている。

hive> CREATE TABLE users_orc_snappy
    >   ROW FORMAT DELIMITED
    >   FIELDS TERMINATED BY ','
    >   STORED AS ORC TBLPROPERTIES ("orc.compress" = "SNAPPY")
    > AS SELECT * FROM users;

同じように、指定なし、ZLIB、NONE を指定したものを作っていこう。

hive> CREATE TABLE users_orc
    >   ROW FORMAT DELIMITED
    >   FIELDS TERMINATED BY ','
    >   STORED AS ORC
    > AS SELECT * FROM users;
...
hive> CREATE TABLE users_orc_gzip
    >   ROW FORMAT DELIMITED
    >   FIELDS TERMINATED BY ','
    >   STORED AS ORC TBLPROPERTIES ("orc.compress" = "ZLIB")
    > AS SELECT * FROM users;
...
hive> CREATE TABLE users_orc_none
    >   ROW FORMAT DELIMITED
    >   FIELDS TERMINATED BY ','
    >   STORED AS ORC TBLPROPERTIES ("orc.compress" = "NONE")
    > AS SELECT * FROM users;

結果は次の通り。 指定しなかったものと GZIP が同じサイズになっており、デフォルトで圧縮が効いていることが確認できる。 今回のデータでは、圧縮をかけない NONE と Snappy のサイズの違いがほとんど出なかった。

$ hdfs dfs -du -h /user/hive/warehouse | grep users_orc
231.6 K  /user/hive/warehouse/users_orc
231.6 K  /user/hive/warehouse/users_orc_gzip
301.2 K  /user/hive/warehouse/users_orc_none
300.1 K  /user/hive/warehouse/users_orc_snappy

まとめ

今回は Apache Hive のテーブルを構成するファイルを圧縮する方法について扱った。 ファイルを圧縮すると、ディスク容量の節約やパフォーマンスの向上が見込める。 ただし、ファイルを圧縮するときは、その特性やファイルフォーマットとの相性について理解を深める必要もある。 例えば、テキストファイルでは圧縮形式によってスプリット可能・不能といった問題が出てくる。 また、フォーマットごとに圧縮形式の情報を何処に残すかが異なっていたり、使える形式についても違ったりすることも分かった。

プログラミング Hive

プログラミング Hive

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

Docker コンテナのログを syslog でリモートホストに飛ばす

今回は Docker コンテナのログを syslog で別のホストに飛ばしてみることにする。 言うまでもなく、ロギングはシステムを運用する上で欠かせない要素の一つ。

Docker には、あらかじめ複数のロギングドライバが組み込まれていて、それらを使い分けることができる。 今回使うのはその中の一つで syslog ドライバという名前がついている。 このドライバで飛ばした Docker コンテナのログを syslog-ng で受け取ってファイルに書き出してみよう。 ただし、リモートホストといっても TCP 越しにログを飛ばすだけで単一のホスト上で組んでしまうことにする。

ロギングドライバについての詳細はこのドキュメントに記載されている。

docs.docker.com

syslog ドライバに限った話についてはこちらに書いてある。

docs.docker.com

使った環境は次の通り。

$ cat /etc/redhat-release 
CentOS Linux release 7.4.1708 (Core) 
$ uname -r
3.10.0-693.17.1.el7.x86_64
$ sudo docker version
Client:
 Version:         1.12.6
 API version:     1.24
 Package version: docker-1.12.6-71.git3e8e77d.el7.centos.1.x86_64
 Go version:      go1.8.3
 Git commit:      3e8e77d/1.12.6
 Built:           Tue Jan 30 09:17:00 2018
 OS/Arch:         linux/amd64

Server:
 Version:         1.12.6
 API version:     1.24
 Package version: docker-1.12.6-71.git3e8e77d.el7.centos.1.x86_64
 Go version:      go1.8.3
 Git commit:      3e8e77d/1.12.6
 Built:           Tue Jan 30 09:17:00 2018
 OS/Arch:         linux/amd64

syslog-ng をインストールする

まずはログを受ける syslog-ng をインストールする。 CentOS 7 の場合は EPEL を入れると yum でインストールできるようになる。

$ sudo yum -y install epel-release
$ sudo yum -y install syslog-ng

続いて syslog-ng の設定ファイルを用意する。 ここでは TCP/514 番ポートで受けた syslog を /var/log/docker/docker.log に書き出すように設定している。

$ cat << 'EOF' | sudo tee /etc/syslog-ng/conf.d/docker.conf > /dev/null
source s_remote_tcp {
  tcp(ip(0.0.0.0), port(514));
};

destination d_docker {
  file("/var/log/docker/docker.log");
};

log {
  source(s_remote_tcp);
  destination(d_docker);
};
EOF

設定ファイルに不備がないかをチェックしておこう。 以下のコマンドで出力が何も返ってこなければ問題ない。 もし設定ファイルの書式を間違えていると、その箇所を教えてくれる。

$ sudo syslog-ng -s

続いて、ログを書き出す先のディレクトリを作っておこう。

$ sudo mkdir -p /var/log/docker

これでログを受ける準備が整ったので syslog-ng のサービスを起動する。

$ sudo systemctl enable syslog-ng
$ sudo systemctl start syslog-ng

ちゃんと設定通り TCP/514 番ポートで Listen していることが見て取れる。

$ sudo ss -tlnp | grep 514
LISTEN     0      128          *:514                      *:*                   users:(("syslog-ng",pid=3438,fd=8))

Docker をインストールする

続いて Docker をインストールする。 Docker は標準リポジトリからでもインストールできる。 ちょっとバージョンが古いけどね。

$ sudo yum -y install docker

インストールできたら Docker のサービスを起動する。

$ sudo systemctl enable docker
$ sudo systemctl start docker

Docker コンテナのログを syslog で飛ばす

あとはDocker コンテナの起動時にドライバとそのオプションを設定するだけ。 以下のように --log-driver オプションに syslog を指定すると syslog ドライバを使うようになる。 さらに --log-opt オプションで syslog-address を指定すると目当てのプロトコルとポートにログを送れる。

$ sudo docker run \
  --log-driver=syslog \
  --log-opt syslog-address=tcp://localhost:514 \
  -it alpine \
  ls /

syslog-ng の出力先のファイルを確認すると、ちゃんと実行結果がログとして残されていることが分かる。

$ sudo cat /var/log/docker/docker.log 
Feb 13 13:41:53 127.0.0.1 docker/f825d108f7fa[3590]: bin    etc    lib    mnt    root   sbin   sys    usr
Feb 13 13:41:53 127.0.0.1 docker/f825d108f7fa[3590]: dev    home   media  proc   run    srv    tmp    var

念のため、もうちょっとログの量が多い場合も試しておこう。 以下では MySQL のコンテナを起動している。

$ sudo docker run \
  --log-driver=syslog \
  --log-opt syslog-address=tcp://localhost:514 \
  -e MYSQL_ROOT_PASSWORD=rootpasswd \
  -it mysql

syslog-ng の出力先を確認すると、ちゃんと MySQL のログが出力されていることが分かる。

$ sudo tail /var/log/docker/docker.log 
Feb 13 13:45:18 127.0.0.1 docker/6b6389751769[3590]: 2018-02-13T13:45:18.041553Z 0 [Warning] 'user' entry 'mysql.session@localhost' ignored in --skip-name-resolve mode.
Feb 13 13:45:18 127.0.0.1 docker/6b6389751769[3590]: 2018-02-13T13:45:18.041564Z 0 [Warning] 'user' entry 'mysql.sys@localhost' ignored in --skip-name-resolve mode.
Feb 13 13:45:18 127.0.0.1 docker/6b6389751769[3590]: 2018-02-13T13:45:18.041597Z 0 [Warning] 'db' entry 'performance_schema mysql.session@localhost' ignored in --skip-name-resolve mode.
Feb 13 13:45:18 127.0.0.1 docker/6b6389751769[3590]: 2018-02-13T13:45:18.041606Z 0 [Warning] 'db' entry 'sys mysql.sys@localhost' ignored in --skip-name-resolve mode.
Feb 13 13:45:18 127.0.0.1 docker/6b6389751769[3590]: 2018-02-13T13:45:18.041622Z 0 [Warning] 'proxies_priv' entry '@ root@localhost' ignored in --skip-name-resolve mode.
Feb 13 13:45:18 127.0.0.1 docker/6b6389751769[3590]: 2018-02-13T13:45:18.043787Z 0 [Warning] 'tables_priv' entry 'user mysql.session@localhost' ignored in --skip-name-resolve mode.
Feb 13 13:45:18 127.0.0.1 docker/6b6389751769[3590]: 2018-02-13T13:45:18.043806Z 0 [Warning] 'tables_priv' entry 'sys_config mysql.sys@localhost' ignored in --skip-name-resolve mode.
Feb 13 13:45:18 127.0.0.1 docker/6b6389751769[3590]: 2018-02-13T13:45:18.054543Z 0 [Note] Event Scheduler: Loaded 0 events
Feb 13 13:45:18 127.0.0.1 docker/6b6389751769[3590]: 2018-02-13T13:45:18.054868Z 0 [Note] mysqld: ready for connections.
Feb 13 13:45:18 127.0.0.1 docker/6b6389751769[3590]: Version: '5.7.21'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)

ばっちり。

めでたしめでたし。

split コマンドでファイルを分割する

巨大なファイルを扱おうとすると、環境によってはクォータなどの影響を受けて取り回しが悪いことがある。 今回は、そんなときに split コマンドで一つのファイルを複数にばらして扱う方法について。 ここでは macOS を使ったけど GNU/Linux でも同じやり方ができる。

使った環境は次の通り。

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

バイナリファイルを分割する

まずは分割するために大きなバイナリファイルを用意する。 ゼロフィルされたものだと連結したとき壊れていないか確かめにくいのでランダムな値で構成した。 具体的には dd コマンドで入力を /dev/random デバイスにする。

$ dd if=/dev/random of=largefile bs=1m count=1000
1000+0 records in
1000+0 records out
1048576000 bytes transferred in 100.697699 secs (10413108 bytes/sec)

これでランダムな値の詰まった 1GB のバイナリファイルができた。 連結したときに内容が変わっていないか確かめるため MD5 のハッシュ値を確認しておこう。

$ ls -alF largefile 
-rw-r--r--  1 amedama  staff  1048576000  2 10 19:08 largefile
$ md5 largefile
MD5 (largefile) = 98fc2a9a7a18d9c8907662a88fb238c8

続いて分割したファイルを入れるディレクトリを用意する。

$ mkdir -p splits

そして split コマンドを使って先ほど作ったバイナリファイルを 100MB ずつに分割する。 バイナリファイルでは -b オプションを指定することで分割サイズが指定できる。 第一引数には分割するファイルを、第二引数には分割したファイルの保存先を指定する。 分割したファイルには後ろにアルファベットがつくので、ドットで終わるようにしておくと後から扱いやすくなる。

$ split -b 100m largefile splits/largefile.

分割したファイルはこんな感じ。

$ ls -alF splits
total 2048000
drwxr-xr-x  12 amedama  staff        408  2 10 19:09 ./
drwxr-xr-x   6 amedama  staff        204  2 10 19:09 ../
-rw-r--r--   1 amedama  staff  104857600  2 10 19:09 largefile.aa
-rw-r--r--   1 amedama  staff  104857600  2 10 19:09 largefile.ab
-rw-r--r--   1 amedama  staff  104857600  2 10 19:09 largefile.ac
-rw-r--r--   1 amedama  staff  104857600  2 10 19:09 largefile.ad
-rw-r--r--   1 amedama  staff  104857600  2 10 19:09 largefile.ae
-rw-r--r--   1 amedama  staff  104857600  2 10 19:09 largefile.af
-rw-r--r--   1 amedama  staff  104857600  2 10 19:09 largefile.ag
-rw-r--r--   1 amedama  staff  104857600  2 10 19:09 largefile.ah
-rw-r--r--   1 amedama  staff  104857600  2 10 19:09 largefile.ai
-rw-r--r--   1 amedama  staff  104857600  2 10 19:09 largefile.aj

分割したら、後はもちろん連結して元に戻すことになる。 split コマンドで分割したファイルは cat コマンドで連結できる。 ようするに順番に内容を出力して、それをファイルにリダイレクトするだけ。

$ cat splits/largefile.* > largefile.restored

分割したファイルを一つに連結したファイルができた。

$ ls -alF largefile.restored 
-rw-r--r--  1 amedama  staff  1048576000  2 10 19:09 largefile.restored

連結したファイルのハッシュ値を確認しておこう。 この値が分割する前のファイルと一致していれば、内容は変わっていないことが分かる。

$ md5 largefile.restored 
MD5 (largefile.restored) = 98fc2a9a7a18d9c8907662a88fb238c8

ハッシュ値は一致するので分割前と分割・連結後で内容は変わっていないことが分かった。

テキストファイルを分割する

先ほどはファイルをバイト単位で分割したけど、テキストファイルとして扱って行数で分割することもできる。 今度はそれを試してみよう。

先ほどと同様、ランダムな値の入ったテキストファイルを用意したい。 今回は Python を使って作ってみることにしよう。

$ python

以下のスニペットで、ランダムな 10 桁の数値が 10000 行連続するテキストファイルを作ってみた。

>>> import random
>>> with open('largefile.txt', 'w') as f:
...     for _ in range(10000):
...         line = '{:010d}\n'.format(random.randint(0, 9999999999))
...         while True:
...           wrote = f.write(line)
...           if wrote == len(line):
...               break
...           line = line[wrote:]
... 

こんな感じのファイルができる。

$ ls -alF largefile.txt 
-rw-r--r--  1 amedama  staff  110000  2 10 19:47 largefile.txt
$ head largefile.txt 
2959925144
8203396354
9888228717
0217007087
6089181232
0800076687
8748631182
4433119315
5389404148
7941794927

先ほどと同じように、連結後のファイルと比較するためのハッシュ値を確認しておこう。

$ md5 largefile.txt 
MD5 (largefile.txt) = 6b98e9d1ec56869609c5cfa29c996f5a

一旦、先ほど分割したファイルはお掃除しておく。

$ rm -rf splits/*

その上でファイルを分割する。 テキストファイルの場合は -l オプションで分割する行数を指定する。

$ split -l 1000 largefile.txt splits/largefile.txt.

このようにファイルが分割された。

$ ls -alF splits 
total 240
drwxr-xr-x  12 amedama  staff    408  2 10 19:49 ./
drwxr-xr-x   5 amedama  staff    170  2 10 19:49 ../
-rw-r--r--   1 amedama  staff  11000  2 10 19:49 largefile.txt.aa
-rw-r--r--   1 amedama  staff  11000  2 10 19:49 largefile.txt.ab
-rw-r--r--   1 amedama  staff  11000  2 10 19:49 largefile.txt.ac
-rw-r--r--   1 amedama  staff  11000  2 10 19:49 largefile.txt.ad
-rw-r--r--   1 amedama  staff  11000  2 10 19:49 largefile.txt.ae
-rw-r--r--   1 amedama  staff  11000  2 10 19:49 largefile.txt.af
-rw-r--r--   1 amedama  staff  11000  2 10 19:49 largefile.txt.ag
-rw-r--r--   1 amedama  staff  11000  2 10 19:49 largefile.txt.ah
-rw-r--r--   1 amedama  staff  11000  2 10 19:49 largefile.txt.ai
-rw-r--r--   1 amedama  staff  11000  2 10 19:49 largefile.txt.aj

wc コマンドで確認すると、ちゃんとそれぞれ 1000 行ずつに分割されているようだ。

$ wc -l splits/largefile.txt.aa
    1000 splits/largefile.txt.aa

続いては分割したファイルを連結する。 やり方はバイト単位で分割したときと同じ。

$ cat splits/largefile.txt.* > largefile.restored.txt

連結できた。

$ ls -alF largefile.restored.txt 
-rw-r--r--  1 amedama  staff  110000  2 10 19:50 largefile.restored.txt

ハッシュ値を確認しよう。 分割前のハッシュ値と一致しているため、分割・連結後も内容が変わっていないことが分かる。

$ md5 largefile.restored.txt
MD5 (largefile.restored.txt) = 6b98e9d1ec56869609c5cfa29c996f5a

めでたしめでたし。

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版