MSX-BASIC の1画面プログラミング技術

この記事は MSX Advent Calendar 2015 の6日目の記事です。

1980年代に MSX・FAN という雑誌にファンダムという MSX-BASIC プログラム投稿コーナーがあり、私も投稿して腕を磨いていたものです。で、ファンダムでは1画面プログラム部門、N画面プログラム部門、10画面プログラム部門、というようにプログラムの長さに応じてカテゴリ分けがありました。

ここで1画面というのは、MSX のスクリーンモード0、つまり幅40文字、高さ24文字のテキスト表示で1画面に収まる分量のコードのことです。当然ながら短いコードで多くの機能や面白さを実現できれば評価が高まるわけで、自然と1画面プログラム部門などは極限までのコード削減テクニックが競われておりました。それは、スパゲティプログラムの奨励事例 - スパゲティプログラム - Wikipedia に載ってしまうくらいに。

当時私は中学生でしたが、人をあっと言わせたい一心でわけもわからぬままコード圧縮技術の探求に邁進したものです。本記事では、当時から伝わる伝統芸能の保存のため(笑)、その数々のテクニックを記録しておこうと思います。

初級

変数名を1文字にする

まあ一番初めに思いつきますね。これはその後 iアプリ(携帯向け Java)が登場した時、10KBという制限の中にいかにプログラムを詰め込むかという課題に対し、1文字クラス名、1文字メソッド名を使用するというテクニックに引き継がれるわけです(笑)。

行番号を1文字にする

BASIC のプログラムは伝統的に 10 から始まって 20, 30, ... と増やしていくものですが、行番号に2文字使うのは無駄なので1文字に。

空白は全て削除

例えば FOR 文は「FOR I = 0 TO 100」と書くのが可読性が高いコードでしょうが、ここは黙って「FORI=0TO100」。FOR と 変数 I がくっついて心配になりますが、FORI という命令はないので全然平気。MSX-BASIC では空白はすべて削除することが可能です。ただし「X OR Y」というようなコードを「XORY」としてしまうと予約語の「XOR」と解釈されてエラーとなってしまう罠もあるので注意です。

演算子の優先順位を把握して括弧を削減

加算乗除だけでなく、関係演算子、論理演算子、剰余など、演算子の優先順位を正確に把握すれば不要な括弧を取り除くことができます。

リプレイはRUN

ゲームなどで始めからやり直す場合の処理は面倒なので、単純に RUN を実行。

中級

1行の文字数を40文字の倍数に近づける

行の途中で改行してしまうとその後の空白の分が無駄になってしまうので、画面の右端まで使い切るように命令をコロン(:)でつなげてコードを埋めます。さらに、改行してしまうと行番号+空白の2文字分も無駄になるので、ジャンプ命令などが不要であれば、1行の上限255文字に一番近い40の倍数である240文字に近づけておきたいところです。

比較演算子の結果を制御または計算に利用

MSX-BASIC では比較演算子の結果は真なら -1、偽なら 0 が返るので例えば

IF A > 5 THEN B = 0 ELSE B = 12

なら次のように書き換えることが可能です。

B=-(A<=5)*12

また、条件付きループのコードは次のように書けます。

FORI=0TO1:(何かの処理):I=-(A=1):NEXT

スプライトパターンの定義

スプライトの定義は普通

10 FOR I = 0 TO 7
20 A$ = A$ + CHR$(VAL("&H" + MID$("21D4D4D427B2AAB3", I*2 + 1, 2)))
30 NEXT
40 SPRITE$(0) = A$

なんて書くと、ビットパターンを16進数で直感的に表せるので便利なわけです。あ、現代プログラマはあまり慣れていないかもしれませんが、8ビットマシンプログラマはビットパターンを高速に16進数に置き換えることができるのです(笑)。ところが1画面プログラムでは文字数がもったいないのでこうです。たまたまビットパターンの文字が制御文字になってしまうと使えないのが難点ですが。

SPRITE$(0)="!ヤヤヤ'イェウ"

NEXTの変数の省略

NEXTの対象のループ変数は省略。ただし2重ループを閉じる時はNEXTJ,Iのようにまとめると良い場合も。

キーの入力待ち

10 IF STRIG(0) = 0 THEN GOTO 10

によってキーの入力待ちが実現できますが、1行を消費してしまうのがもったいないので次のように行の途中に書けるようにします。

FORI=0TO1:I=-STRIG(0):NEXT

LINE ではなく DRAW

グラフィックモード(SCREEN 2 など)で垂直、水平、斜め45度の直線などを描画する場合は、LINE を使いよりも DRAW を使うことでコードが短くなる場合があります。

LINE(80,100)-(180,100),15

例えば上記のコードは下記のように書き直せます。

DRAW"S4BM80,100C15R100"

命令の省略形を使う

例えば PUT SPRITE 命令の定義は次の通りです。

PUT SRPITE <スプライト面番号>[,{(X,Y)|STEP(X,Y)}[,<色>[,<スプライトパターン番号>]]]

あるスプライトの色を変えるだけであれば座標は省略可能なので、次のように短縮できます。

PUTSPRITE0,,8

上級

ストライプ操作にVPOKEを使う

スプライトを消すために、例えば画面外の位置にスプライト表示を行うコードは次のような感じです(スクリーンモード1〜3の場合)。

PUT SPRITE 0, (0, 192)

が、VPOKE を使って VRAM の内容を直接書き換えることでコードを短くすることができます。スプライト#0 の Y座標を格納するVRAMアドレスは 6912 番地なので、こんな感じ。

VPOKE6912,192

この他にも、画面表示関係は VPOKE の使用により効率的なコードになるケースが多く、応用が効きます。

エスケープシーケンスの利用

スクリーンモード1などで任意の文字座標に文字列を表示する場合のコードはこんな感じです。

10 LOCATE 8,10
20 PRINT"PRESS ANY KEY"

これはエスケープシーケンス CHR$(27) + "Y" + CHR$(32 + y) + CHR$(32 + x) を用いてこう書けます。多用する場合は E$ = CHR$(27) のように変数に格納しておけばさらに短くなるでしょう

PRINTCHR$(27)+"Y*(PRESS ANY KEY"

表示する文字が1文字だけであれば、上記の VPOKE で VRAM を直接書き換える方法も効果的です。

変態級

マシン語直書き

BASIC プログラムにもかかわらず、コメント部分に直接バイナリのマシン語コードを記述するという荒技です。BASIC のコードはマシン語コードをロードする必要最小限のみ。まったく理解することが不可能です。しかしながら、比較的大規模なコードを非常に高速に実行できるため、1画面プログラムとは思えないほどの挙動を示します。下記のコードは、BASIC プログラムがメモリの &H8000 番地から格納され、最初のコメント(' 以降)の位置のバイナリが &H801C 番地に来ることを見越して、USR 関数のアドレスを書き換えて直接呼び出しています。

1 CLEAR9,&HD000:DEFUSR=&H801C:A=USR(0)'ン!、♠!0ハッGゆ>7ヨ0W7まク9#ェ...

コーディングゼロでエンディングメッセージを実現

私が昔「8192階建ての塔」という縦スクロールジャンプアクションゲームを作ったときの話。塔を登っていくとどこまで進んだかをカウントしている変数の値が増え、プレーヤーはそれをスコアとして競います。サイズに余裕がないのでゴールに到達したときの画面表示もできません。が、カウント変数が32767を超えたとき(階数は4で割った値なので8192階に達したとき)に Illegal function call エラーが起きることに気づきました。ならばそれをエンディングとして使用すればよい、ということでタイトルが決定しました。なんつって。

おまけ

参考までに1画面プログラム「8192階建ての塔」のコードを載せておきます。40x24文字びっしり埋まってます。

f:id:nagixx:20151206042512g:plain

1 SCREEN1,1,0:COLOR15,0,0:WIDTH24:KEYOFF
:DEFINTA-Z:DEFFNA=VPEEK(6208+X¥8+(Y¥8)*3
2)ORVPEEK(6209+(X+7)¥8+(Y¥8)*32):FORI=1T
O32:VPOKE14335+I,ASC(MID$("jjョ-kェ}まjj.jェ
jFfjj/-はl アjj.jljFJ",I,1))-46AND255:NEXT
2 A=RND(-9):VPOKE8208,50:FORI=0TO20:VPOK
E6211+i*32,133:VPOKE6236+I*32,133:NEXT:F
ORI=0TO9:LOCATERND(1)*19,2+I*2:PRINT"●●●
●●●":NEXT:LOCATE0,22:PRINTSTRING$(24,133
);:X=32:Y=160:A=1:F=1:SOUND1,0:SOUND7,62
3 X=X+A*4-2:P=PXOR1:GOSUB6:IFFNA=32THENY
=Y+8:SOUND8,0:GOTO3ELSEB=S:S=STRIG(0):IF
STHENJ=JMOD12+1:SOUND0,99-J:SOUND8,15:GO
TO3ELSESOUND8,0:IFB=0ANDS=0THENJ=0:GOTO3
4 X=X+A*4-2:Y=Y-J:J=J-1:GOSUB6:IFJ<0ANDF
NA<>32THENJ=0:Y=(Y¥8)*8:GOTO3ELSEIFY>64T
HEN4ELSEF=F+1:FORI=0TO3:C=C+1:LOCATE0,2:
PRINTCHR$(27)+"L":Y=Y+8:IFCMOD2=0THENLOC
ATERND(1)*9+(C/2MOD2)*10,2:PRINT"●●●●●●"
5 GOSUB6:NEXT:LOCATE10,0:PRINTF"F":GOTO4
6 PUTSPRITE0,(X,Y),7,A*2+P:A=AXOR-(X=32O
RX=208):IFY<176THENRETURNELSEPLAY"SM999L
64DD-C":FORI=0TO20:LOCATE,2:PRINTCHR$(27
)+"M":NEXT:FORI=0TO1:I=STICK(0):NEXT:RUN

注:2行目および4行目の「●」(文字コード133)は、GRAPHキー+「]」で入力

ネストデータのカラム指定方法について

この記事は Apache Drill Advent Calendar 2015 の5日目の記事です。

通常の SQL はリレーショナルデータを対象とするため、テーブルは行と列からなるフラットな構造です。SELECT 文で特定のカラムの値を取り出すには、単にカラム名を指定するだけです。

しかし、Apache Drill では JSON や Parquet のようなネストデータ(入れ子構造を持つデータ)もクエリの対象にすることができます。その場合、特定の階層の特定のフィールドの値を取り出すにはどのように指定すればよいでしょうか。

サンプルとして次のような JSON 形式のデータを用意します。

$ cat /tmp/donuts.json
{
  "id": "0001",
  "type": "donut",
  "name": "Cake",
  "ppu": 0.55,
  "sales": 35,
  "topping":[
    { "id": "5001", "type": "None" },
    { "id": "5002", "type": "Glazed" },
    { "id": "5005", "type": "Sugar" },
    { "id": "5007", "type": "Powdered Sugar" },
    { "id": "5006", "type": "Chocolate with Sprinkles" },
    { "id": "5003", "type": "Chocolate" },
    { "id": "5004", "type": "Maple" }
  ]
}
{
"id": "0002",
...

トップ階層のデータを SELECT するのは、通常の SQL となんら変わりません。

0: jdbc:drill:zk=local> SELECT id, type, name, ppu
. . . . . . . . . . . > FROM dfs.`/tmp/donuts.json`;
+-------+--------+----------------+-------+
|  id   |  type  |      name      |  ppu  |
+-------+--------+----------------+-------+
| 0001  | donut  | Cake           | 0.55  |
| 0002  | donut  | Raised         | 0.69  |
| 0003  | donut  | Old Fashioned  | 0.55  |
| 0004  | donut  | Filled         | 0.69  |
| 0005  | donut  | Apple Fritter  | 1.0   |
+-------+--------+----------------+-------+
5 rows selected (0.18 seconds)

次に示すのは、「topping」をキーとする配列の中の特定の要素を取り出すクエリです。配列の要素を指定するために要素のインデックスを [ ] で囲って指定しています。ちょっと気をつけて欲しいのは、ここでの配列の要素は複数のキーと値のペアを含む Map データですが、データ全体を1カラムとして表示しており、その下の階層のキーでの分解までは行っていない点です。

0: jdbc:drill:zk=local> SELECT topping[3] AS top
. . . . . . . . . . . > FROM dfs.`/tmp/donuts.json`;
+------------+
|    top     |
+------------+
| {"id":"5007","type":"Powdered Sugar"} |
+------------+
1 row selected (0.137 seconds)

さて、下の階層のキーの値をカラムとして取り出すにはどうすればよいでしょうか。次のクエリは「tapping」配列の要素内部の「id」「type」の値を取り出すクエリです。ドット(.)でキー名をつなげて階層構造を指定しているのがわかります。

0: jdbc:drill:zk=local> SELECT tbl.topping[3].id as record,
. . . . . . . . . . . >        tbl.topping[3].type as first_topping
. . . . . . . . . . . > FROM dfs.`/tmp/donuts.json` as tbl;
+------------+---------------+
|   record   | first_topping |
+------------+---------------+
| 5007       | Powdered Sugar |
+------------+---------------+
1 row selected (0.133 seconds)

ここで一点重要なポイントは、ドットでキー名を連結してカラム名を指定する場合、先頭はテーブル名エイリアスである必要があることです(この例では tbl)。もし tbl が先頭になく、topping[3].id のような指定をしてしまうと、このクエリはエラーになります。標準 SQLカラム名にドットを使用する場合は、テーブル名が含まれている必要があり、Drill でもそれに従った表記になっているためです。

UNION 型: データ型が混在するカラムのサポート

この記事は Apache Drill Advent Calendar 2015 の3日目の記事です。

少し前の記事で、一つのカラムにデータ型が混在したデータを読むときの注意点を説明しました。

その後リリースされた Drill 1.3 で、[DRILL-3229] Create a new EmbeddedVector にて改良が進行中のコードが取り込まれたことにより、データ型が混在するカラムの取り扱いができるようになりました。具体的には、UNION 型というデータ型が新たに追加され、個々のフィールドごとに異なるデータ型を内部で保持できるようになっています。

以前の記事同様、次のようなデータを用意します(以前の記事の時のデータとはほんの少し異なりますが、その理由は後述)。

$ cat /tmp/sensor.json
[
  {"sensor_id":15, "timestamp":"2015-10-29 08:00:00.004", "temperature":14.8},
  {"sensor_id":15, "timestamp":"2015-10-29 08:05:00.011", "temperature":14.9},
  {"sensor_id":15, "timestamp":"2015-10-29 08:10:00.002", "temperature":15},
  {"sensor_id":15, "timestamp":"2015-10-29 08:15:00.012", "temperature":15.2},
  {"sensor_id":15, "timestamp":"2015-10-29 08:20:00.009", "temperature":15.3}
]

UNION 型を有効にするために、プロパティ exec.enable_union_type を true にします。

$ apache-drill-1.3.0/bin/drill-embedded
0: jdbc:drill:zk=local> ALTER SESSION SET `exec.enable_union_type` = true;

そして SELECT * してみると、エラーなくちゃんとクエリが実行されることがわかります。このデータの temperature カラムは小数点を含むデータと含まないデータがあるので DOUBLE 型と INT 型が混在しているのですが、UNION 型を有効にすることで両方の型を格納できるようになっています。

0: jdbc:drill:zk=local> SELECT * FROM dfs.`/tmp/sensor.json`;
+------------+--------------------------+--------------+
| sensor_id  |        timestamp         | temperature  |
+------------+--------------------------+--------------+
| 15         | 2015-10-29 08:00:00.004  | 14.8         |
| 15         | 2015-10-29 08:05:00.011  | 14.9         |
| 15         | 2015-10-29 08:10:00.002  | 15           |
| 15         | 2015-10-29 08:15:00.012  | 15.2         |
| 15         | 2015-10-29 08:20:00.009  | 15.3         |
+------------+--------------------------+--------------+
5 rows selected (0.17 seconds)

ただし temperature カラムに集計関数を使おうとするとエラーがでます。今のところ、集計関数は UNION 型をサポートしていないためです。

0: jdbc:drill:zk=local> SELECT
. . . . . . . . . . . >   sensor_id,
. . . . . . . . . . . >   AVG(temperature) temperature
. . . . . . . . . . . > FROM dfs.`/tmp/sensor.json`
. . . . . . . . . . . > GROUP BY sensor_id;
Error: UNSUPPORTED_OPERATION ERROR: Union type not supported in aggregate functions

Fragment 0:0

[Error Id: 4186f733-5b00-415b-ab0a-f85eb070d2bd on 192.168.111.4:31010] (state=,code=0)

このような場合は以前の記事同様 CAST を使ってやれば OK です。

0: jdbc:drill:zk=local> SELECT
. . . . . . . . . . . >   sensor_id,
. . . . . . . . . . . >   AVG(CAST(temperature AS DOUBLE)) temperature
. . . . . . . . . . . > FROM dfs.`/tmp/sensor.json`
. . . . . . . . . . . > GROUP BY sensor_id;
+------------+---------------------+
| sensor_id  |     temperature     |
+------------+---------------------+
| 15         | 15.040000000000001  |
+------------+---------------------+
1 row selected (0.339 seconds)

ところで、冒頭に述べたように、以前の記事と完全に同じデータを使わなかった理由は、現時点では UNION 型は FLATTEN 関数をサポートしていないためです。下のような "data" をキーとする配列があった場合、FLATTEN(data) でデータが展開できることを期待したいところですが、UNION 型が扱えないのでエラーになります。[DRILL-3229] Create a new EmbeddedVector はまだ「IN PROGRESS」なステータスなので、完了すればちゃんと動くようになるでしょう。

{
  "data":[
    {"sensor_id":15, "timestamp":"2015-10-29 08:00:00.004", "temperature":14.8},
    {"sensor_id":15, "timestamp":"2015-10-29 08:05:00.011", "temperature":14.9},
    ...
  ]
}

ちなみに、[DRILL-3229] Create a new EmbeddedVector はより大きな課題である [DRILL-3228] Implement Embedded Type の一部です。UNION 型に関連する機能が揃うバージョン 1.4 が出るころには、より使いやすくなっているはずです。

CSV ファイルのヘッダ行をカラム名に使う

この記事は Apache Drill Advent Calendar 2015 の2日目の記事です。

11月の Tokyo Apache Drill Meetup で出た質問の中に、「CSV ファイルのヘッダ行をカラム名に使いたいが可能か?」というものがありましたが、Drill 1.2 では先頭行をスキップするオプションはあったものの、行の内容をカラム名として使う機能は未実装でした。で、その後リリースされた Drill 1.3 の [DRILL-951] CSV header row should be parsed にて、その機能が追加されたので紹介します。

例えばこんなデータがあるとします。

$ head /tmp/personal_information.csv
連番,氏名,氏名(カタカナ),性別,電話番号,郵便番号,住所1,住所2,住所3,住所4,住所5,生年月日
1,佐川邦男,サガワクニオ,男,0959408329,852-8007,長崎県,長崎市,江の浦町,2-18,江の浦町スカイ401,1995/03/28
2,松本冨子,マツモトトミコ,女,0957833608,855-0882,長崎県,島原市,札の元町,3-13,,1978/10/06
3,内田史織,ウチダシオリ,女,0942977483,848-0133,佐賀県,伊万里市,黒川町真手野,3-5-1,,1989/05/25
4,曽根里沙,ソネリサ,女,0271234470,378-0077,群馬県,沼田市,石墨町,1-7-1,パーク石墨町204,1985/03/22
5,徳田雪乃,トクダユキノ,女,083932164,752-0980,山口県,下関市,長府黒門町,3-6-5,,1988/03/23
6,山下直人,ヤマシタナオト,男,099655084,891-1206,鹿児島県,鹿児島市,皆与志町,2-10-9,ザ皆与志町314,1956/05/11
7,真田敬三,サナダケイゾウ,男,0766626361,936-0827,富山県,滑川市,東福寺,2-8-14,タウン東福寺202,1972/12/10
8,山形順子,ヤマガタジュンコ,女,0194190969,020-0851,岩手県,盛岡市,向中野,4-1-20,向中野マンション407,1979/11/21
9,奥照雄,オクテルオ,男,0895179833,799-1361,愛媛県,西条市,広江,2-5-17,広江シティ215,1987/03/17

単純に Drill で SELECT * するとこんな風になります。

0: jdbc:drill:zk=local> SELECT * FROM dfs.`/tmp/personal_information.csv` LIMIT 10;
+--------------------------------------------------------------------------------------------------------------+
|                                                   columns                                                    |
+--------------------------------------------------------------------------------------------------------------+
| ["連番","氏名","氏名(カタカナ)","性別","電話番号","郵便番号","住所1","住所2","住所3","住所4","住所5","生年月日"]                             |
| ["1","佐川邦男","サガワクニオ","男","0959408329","852-8007","長崎県","長崎市","江の浦町","2-18","江の浦町スカイ401","1995/03/28"]      |
| ["2","松本冨子","マツモトトミコ","女","0957833608","855-0882","長崎県","島原市","札の元町","3-13","","1978/10/06"]               |
| ["3","内田史織","ウチダシオリ","女","0942977483","848-0133","佐賀県","伊万里市","黒川町真手野","3-5-1","","1989/05/25"]            |
| ["4","曽根里沙","ソネリサ","女","0271234470","378-0077","群馬県","沼田市","石墨町","1-7-1","パーク石墨町204","1985/03/22"]         |
| ["5","徳田雪乃","トクダユキノ","女","083932164","752-0980","山口県","下関市","長府黒門町","3-6-5","","1988/03/23"]               |
| ["6","山下直人","ヤマシタナオト","男","099655084","891-1206","鹿児島県","鹿児島市","皆与志町","2-10-9","ザ皆与志町314","1956/05/11"]    |
| ["7","真田敬三","サナダケイゾウ","男","0766626361","936-0827","富山県","滑川市","東福寺","2-8-14","タウン東福寺202","1972/12/10"]     |
| ["8","山形順子","ヤマガタジュンコ","女","0194190969","020-0851","岩手県","盛岡市","向中野","4-1-20","向中野マンション407","1979/11/21"]  |
| ["9","奥照雄","オクテルオ","男","0895179833","799-1361","愛媛県","西条市","広江","2-5-17","広江シティ215","1987/03/17"]          |
+--------------------------------------------------------------------------------------------------------------+
10 rows selected (0.34 seconds)

各行は配列型になっているので、各カラムにアクセスするには添字が必要でした。

0: jdbc:drill:zk=local> SELECT columns[1] AS 氏名, columns[4] AS 電話番号
. . . . . . . . . . . > FROM dfs.`/tmp/personal_information.csv`
. . . . . . . . . . . > WHERE columns[1] LIKE _UTF16'田中%';
+-------+-------------+
|  氏名   |    電話番号     |
+-------+-------------+
| 田中優| 011944797   |
| 田中伍朗  | 0864019970  |
| 田中啓司  | 097833684   |
+-------+-------------+
3 rows selected (0.223 seconds)

が、バージョン1.3以降では、ファイルの拡張子をcsvhにしておくと・・・

$ mv /tmp/personal_information.csv /tmp/personal_information.csvh

これだけで先頭行がカラム名としてセットされ、クエリの中で利用できるようになります。こいつは便利ですね!

0: jdbc:drill:zk=local> SELECT * FROM dfs.`/tmp/personal_information.csvh` LIMIT 10;
+-----+-------+-----------+-----+-------------+-----------+-------+-------+---------+---------+--------------+-------------+
| 連番  |  氏名   | 氏名(カタカナ)  | 性別  |    電話番号     |   郵便番号    |  住所1  |  住所2  |   住所3   |   住所4   |     住所5      |    生年月日     |
+-----+-------+-----------+-----+-------------+-----------+-------+-------+---------+---------+--------------+-------------+
| 1   | 佐川邦男  | サガワクニオ    || 0959408329  | 852-8007  | 長崎県   | 長崎市   | 江の浦町    | 2-18    | 江の浦町スカイ401   | 1995/03/28  |
| 2   | 松本冨子  | マツモトトミコ   || 0957833608  | 855-0882  | 長崎県   | 島原市   | 札の元町    | 3-13    |              | 1978/10/06  |
| 3   | 内田史織  | ウチダシオリ    || 0942977483  | 848-0133  | 佐賀県   | 伊万里市  | 黒川町真手野  | 3-5-1   |              | 1989/05/25  |
| 4   | 曽根里沙  | ソネリサ      || 0271234470  | 378-0077  | 群馬県   | 沼田市   | 石墨町     | 1-7-1   | パーク石墨町204    | 1985/03/22  |
| 5   | 徳田雪乃  | トクダユキノ    || 083932164   | 752-0980  | 山口県   | 下関市   | 長府黒門町   | 3-6-5   |              | 1988/03/23  |
| 6   | 山下直人  | ヤマシタナオト   || 099655084   | 891-1206  | 鹿児島県  | 鹿児島市  | 皆与志町    | 2-10-9  | ザ皆与志町314     | 1956/05/11  |
| 7   | 真田敬三  | サナダケイゾウ   || 0766626361  | 936-0827  | 富山県   | 滑川市   | 東福寺     | 2-8-14  | タウン東福寺202    | 1972/12/10  |
| 8   | 山形順子  | ヤマガタジュンコ  || 0194190969  | 020-0851  | 岩手県   | 盛岡市   | 向中野     | 4-1-20  | 向中野マンション407  | 1979/11/21  |
| 9   | 奥照雄   | オクテルオ     || 0895179833  | 799-1361  | 愛媛県   | 西条市   | 広江      | 2-5-17  | 広江シティ215     | 1987/03/17  |
| 10  | 三木安弘  | ミキヤスヒロ    || 0968649092  | 865-0008  | 熊本県   | 玉名市   | 石貫      | 2-20    | 石貫ハウス107     | 1966/06/17  |
+-----+-------+-----------+-----+-------------+-----------+-------+-------+---------+---------+--------------+-------------+
10 rows selected (0.666 seconds)

ちょっとだけ説明を付け加えると、この機能はストレージプラグインのフォーマット設定にある extractHeader というプロパティで有効になります。Embedded モードで起動している場合には、Drill シェル動作中にブラウザで http://localhost:8047 にアクセスし、「Storage」タブの「dfs」プラグインの設定を見てみると、csvh の拡張子がついている場合には機能が有効になることがわかります。

  "formats": {
    ...
    "csvh": {
      "type": "text",
      "extensions": [
        "csvh"
      ],
      "extractHeader": true,
      "delimiter": ","
    }
  }

Apache Drill 今日の一言 (MOTD)

この記事は Apache Drill Advent Calendar 2015 の1日目の記事です。

Drill のフロントエンド(シェル)である sqlline を起動すると、プロンプトの前に短いメッセージが表示されます。

$ sqlline -u jdbc:drill:zk=local
Dec 1, 2015 2:13:59 AM org.glassfish.jersey.server.ApplicationHandler initialize
INFO: Initiating Jersey application, version Jersey: 2.8 2014-04-29 01:25:26...
apache drill 1.3.0 
"this isn't your grandfather's sql"
0: jdbc:drill:zk=local>

これはいわゆる MOTD (Message of the Day) と呼ばれるもので、起動のたびにランダムに選ばれたメッセージが出てきます。

「これはお前のおじいちゃんの時代の SQL ではない!」

あれ、SQL ってそんな古い時代からあったっけ?(笑)

さて、何度も起動してみると色々なメッセージが見られます。

"drill baby drill"
(掘れ!掘れ!)

これ、元ネタはアメリカの共和党の副大統領候補だったサラ・ベイリンが2008年から2010年頃に油田をもっと掘れ!というスローガンで使ってたフレーズらしいです。

"a drill in the hand is better than two in the bush"
(どこぞの複数ツールをあれこれ求めるより手元のDrillのほうがいいよね!)

これは元のことわざとしては “a bird in the hand is better than two in the bush” で、意味は「二兎を追うものは一兎も得ず」に近いものだと思いますが、「これまで色々なソフトを使ってデータを集めて加工してたけど Drill ならこれ一つで十分だ」というような世界観を表していていいですね。

Drill を立ち上げるたびに出てくる、遊び心のあるメッセージは何とも和みますね。皆さんもどんなメッセージが出てくるか楽しみにしてみましょう!

Apache Drillで整数型と浮動小数点型が混じったJSONデータを読む時の注意

こんな感じのJSONデータがあるとします。気温を記録しているセンサーログデータ的なものですね。

$ cat /tmp/sensor.json
{
  "data":[
    {"sensor_id":15, "timestamp":"2015-10-29 08:00:00.004", "temperature":14.8},
    {"sensor_id":15, "timestamp":"2015-10-29 08:05:00.011", "temperature":14.9},
    {"sensor_id":15, "timestamp":"2015-10-29 08:10:00.002", "temperature":15},
    {"sensor_id":15, "timestamp":"2015-10-29 08:15:00.012", "temperature":15.2},
    {"sensor_id":15, "timestamp":"2015-10-29 08:20:00.009", "temperature":15.3}
  ]
}

これを単純にDrillでSELECTしてみようとすると、こんなエラーになってしまいます。

$ apache-drill-1.2.0/bin/drill-embedded
0: jdbc:drill:zk=local> SELECT * FROM dfs.`/tmp/sensor.json`;
Error: DATA_READ ERROR: You tried to write a BigInt type when you are using a ValueWriter of type NullableFloat8WriterImpl.

File  /tmp/sensor.json
Record  1
Line  5
Column  78
Field  temperature
Fragment 0:0

[Error Id: e958b13d-25c8-409c-a7d4-51b4359f40e6 on mbp:31010] (state=,code=0)

「temperature」というフィールドでエラーがあるというのがわかるのですが、これは8ビット浮動小数点型のWriterを値の内部書き込みに使っていたのに、8ビット整数型を書き込もうとしたことが原因です。次のApache Drillのドキュメントを見てみましょう。

Apache Drill - JSON Data Model
https://drill.apache.org/docs/json-data-model/

By default, Drill does not support JSON lists of different types. For example, JSON does not enforce types or distinguish between integers and floating point values. When reading numerical values from a JSON file, Drill distinguishes integers from floating point numbers by the presence or lack of a decimal point. If some numbers in a JSON map or array appear with and without a decimal point, such as 0 and 0.0, Drill throws a schema change error.

Drillでは小数点の有無によって整数型と浮動小数点型を区別しますが、JSONマップや配列内で異なる型の混在を許していないため、異なる型を読み込もうとした時にスキーマが変わった!とエラーを投げてしまうわけです。

これの対策の一つとして、Drillの「store.json.read_numbers_as_double」プロパティをtrueにすることで、すべての数値をDOUBLE型で読み込む方法があります。

0: jdbc:drill:zk=local> ALTER SESSION SET `store.json.read_numbers_as_double` = true;
+-------+---------------------------------------------+
|  ok   |                   summary                   |
+-------+---------------------------------------------+
| true  | store.json.read_numbers_as_double updated.  |
+-------+---------------------------------------------+
1 row selected (0.106 seconds)
0: jdbc:drill:zk=local> SELECT * FROM dfs.`/tmp/sensor.json`;
+------+
| data |
+------+
| [{"sensor_id":15.0,"timestamp":"2015-10-29 08:00:00.004","temperature":14.8},{"sensor_id":15.0,"timestamp":"2015-10-29 08:05:00.011","temperature":14.9},{"sensor_id":15.0,"timestamp":"2015-10-29 08:10:00.002","temperature":15.0},{"sensor_id":15.0,"timestamp":"2015-10-29 08:15:00.012","temperature":15.2},{"sensor_id":15.0,"timestamp":"2015-10-29 08:20:00.009","temperature":15.3}] |
+------+
1 row selected (0.115 seconds)

エラーは無くなりました。JSONドキュメント全体が1フィールドに詰め込まれてしまっているので、次のように展開しましょう。

0: jdbc:drill:zk=local> SELECT
. . . . . . . . . . . >   t.data.sensor_id sensor_id,
. . . . . . . . . . . >   t.data.`timestamp` `timestamp`,
. . . . . . . . . . . >   t.data.temperature temperature
. . . . . . . . . . . > FROM (
. . . . . . . . . . . >   SELECT FLATTEN(data) data FROM dfs.`/tmp/sensor.json`
. . . . . . . . . . . > ) t;
+------------+--------------------------+--------------+
| sensor_id  |        timestamp         | temperature  |
+------------+--------------------------+--------------+
| 15.0       | 2015-10-29 08:00:00.004  | 14.8         |
| 15.0       | 2015-10-29 08:05:00.011  | 14.9         |
| 15.0       | 2015-10-29 08:10:00.002  | 15.0         |
| 15.0       | 2015-10-29 08:15:00.012  | 15.2         |
| 15.0       | 2015-10-29 08:20:00.009  | 15.3         |
+------------+--------------------------+--------------+
5 rows selected (0.143 seconds)

ただ、これ本来整数であるべきsensor_idまで浮動小数点型になってしまってかっこ悪いですね・・。CAST関数を使えば型変換はできるのですが。

0: jdbc:drill:zk=local> SELECT
. . . . . . . . . . . >   CAST(t.data.sensor_id AS INT) sensor_id,
. . . . . . . . . . . >   CAST(t.data.`timestamp` AS TIMESTAMP) `timestamp`,
. . . . . . . . . . . >   t.data.temperature temperature
. . . . . . . . . . . > FROM (
. . . . . . . . . . . >   SELECT FLATTEN(data) data FROM dfs.`/tmp/sensor.json`
. . . . . . . . . . . > ) t;
+------------+--------------------------+--------------+
| sensor_id  |        timestamp         | temperature  |
+------------+--------------------------+--------------+
| 15         | 2015-10-29 08:00:00.004  | 14.8         |
| 15         | 2015-10-29 08:05:00.011  | 14.9         |
| 15         | 2015-10-29 08:10:00.002  | 15.0         |
| 15         | 2015-10-29 08:15:00.012  | 15.2         |
| 15         | 2015-10-29 08:20:00.009  | 15.3         |
+------------+--------------------------+--------------+
5 rows selected (0.337 seconds)

それから別の対策としては、「store.json.all_text_mode」プロパティをtrueにすることで、すべての値を文字列型で読み込む方法もあります。

0: jdbc:drill:zk=local> ALTER SESSION SET `store.json.all_text_mode` = true;
+-------+------------------------------------+
|  ok   |              summary               |
+-------+------------------------------------+
| true  | store.json.all_text_mode updated.  |
+-------+------------------------------------+
1 row selected (0.089 seconds)
0: jdbc:drill:zk=local> SELECT
. . . . . . . . . . . >   t.data.sensor_id sensor_id,
. . . . . . . . . . . >   t.data.`timestamp` `timestamp`,
. . . . . . . . . . . >   t.data.temperature temperature
. . . . . . . . . . . > FROM (
. . . . . . . . . . . >   SELECT FLATTEN(data) data FROM dfs.`/tmp/sensor.json`
. . . . . . . . . . . > ) t;
+------------+--------------------------+--------------+
| sensor_id  |        timestamp         | temperature  |
+------------+--------------------------+--------------+
| 15         | 2015-10-29 08:00:00.004  | 14.8         |
| 15         | 2015-10-29 08:05:00.011  | 14.9         |
| 15         | 2015-10-29 08:10:00.002  | 15           |
| 15         | 2015-10-29 08:15:00.012  | 15.2         |
| 15         | 2015-10-29 08:20:00.009  | 15.3         |
+------------+--------------------------+--------------+
5 rows selected (0.191 seconds)

集計などの操作をしないのであれば、このように値を解釈せずにそのまま読み込む方がシンプルで間違いがないでしょう。もし気温の平均値を求める場合、このまま集計関数を適用すると次のようにエラーになってしまいますので、

0: jdbc:drill:zk=local> SELECT
. . . . . . . . . . . >   t.data.sensor_id sensor_id,
. . . . . . . . . . . >   AVG(t.data.temperature) temperature
. . . . . . . . . . . > FROM (
. . . . . . . . . . . >   SELECT FLATTEN(data) data FROM dfs.`/tmp/sensor.json`
. . . . . . . . . . . > ) t
. . . . . . . . . . . > GROUP BY t.data.sensor_id;
Error: SYSTEM ERROR: SchemaChangeException: Failure while trying to materialize incoming schema.  Errors:
 
Error in expression at index -1.  Error: Missing function implementation: [castINT(BIT-OPTIONAL)].  Full expression: --UNKNOWN EXPRESSION--..

Fragment 0:0

[Error Id: 2626b393-96d8-4d6a-b169-5d787ee57c8d on mbp:31010] (state=,code=0)

集計対象のフィールドはCAST関数を使って浮動小数点型に変換すればうまくいきます。

0: jdbc:drill:zk=local> SELECT
. . . . . . . . . . . >   t.data.sensor_id sensor_id,
. . . . . . . . . . . >   AVG(CAST(t.data.temperature AS DOUBLE)) temperature
. . . . . . . . . . . > FROM (
. . . . . . . . . . . >   SELECT FLATTEN(data) data FROM dfs.`/tmp/sensor.json`
. . . . . . . . . . . > ) t
. . . . . . . . . . . > GROUP BY t.data.sensor_id;
+------------+---------------------+
| sensor_id  |     temperature     |
+------------+---------------------+
| 15         | 15.040000000000001  |
+------------+---------------------+
1 row selected (1.376 seconds)

Portable Hadoop Cluster

(*日本語の記事はこちら)

I have built a 3-nodes Hadoop Cluster which is packed in an aluminum case and easy to carry around. I use Intel's small form factor NUC PCs so that it is very compact. The size is 29 x 41.5 x 10 cm.

f:id:nagixx:20140609161347j:plainf:id:nagixx:20140609162049j:plain

The specifications are as follows.

  • 3 boxes of Intel NUC core i3 + 16GB memory + 128GB SSD
  • Gigabit Ethernet + IEEE 802.11na Wifi router
  • 13.3 inch HDMI-connected LCD monitor
  • 2.4GHz wireless keyboard + touchpad
  • A single power plug for the entire system
  • CentOS 6.6 + MapR M7 4.0.1 + CDH 5.3

Since each node has 16GB memory, most of applications work without any problem. I usually bring the cluster to meeting rooms and show demos to my customers. It is really useful because I can start the demos in one or two minutes if only one power outlet is available. A wifi connection is also available, so you I show the demos with a large screen using a LCD projector by accessing it from your laptop.

I have built this for Hadoop, but it can be used for any distributed software systems. For example, you can definitely use it as a demo system for MongoDB, Cassandra, Redis, Riak, Solr, Elasticsearch, Spark, Storm and so on. In fact, I run HP Vertica and Elasticsearch together with MapR in this portable cluster since MapR's NFS feature enables easy integration with any native applications.

The full parts list and how to build are shown below. Let's make your original one!

Parts List

The following parts list is as of June 10, 2014 including price information. This article is written based on the cluster built using those parts.

No.Part DescriptionUnit PriceAmount
01 Intel DC3217IYE (NUC barebone kit) 38,454 JPY 3
02 PLEXTOR PX-128M5M (128MB mSATA SSD) 14,319 JPY 3
03 CFD W3N1600Q-8G (8GB DDR3-1600 SO-DIMM x 2) 17,244 JPY 3
04 ELECOM LD-GPS/BK03 (0.3m CAT6 Gigabit LAN cable) 211 JPY 3
05 Miyoshi MBA-2P3 (Power cable 2-pin/3-pin conversion adapter) 402 JPY 3
06 FILCO FCC3M-04 (3-way splitter power cable) 988 JPY 1
07 Logitec LAN-W300N/IGRB (Gigabit/11na wifi router) 5,500 JPY 1
08 SANWA SUPPLY KB-DM2L (0.2m Power cable) 600 JPY 1
09 GeChic On-LAP 1302/J (13.3 inch LCD) 18,471 JPY 1
10 ELECOM T-FLC01-2220BK (2m Power cable) 639 JPY 1
11 EAPPLY EBO-013 (2.4GHz Wireless keyboard) 2,654 JPY 1
12 IRIS OYAMA AM-10 (Aluminum attache case) 2,234 JPY 1
13 Inoac A8-101BK (Polyethylene foam 10x1000x1000) 2,018 JPY 1
14 Kuraray 15RN Black (Velcro tape 25mm x 15cm) 278 JPY 2
15 Cemedine AX-038 Super X Clear (Versatile adhesive 20ml) 343 JPY 1

Half a year later, some parts are no longer sold in store, so I created a new list as of December 29, 2014. Currently, it will cost about 230,000 JPY in total. Note that some descriptions about component location or configuration may need to be updated. In particular, the NUC box was replaced with its successor model D34010WYK, which requires 1.35V low voltage memory and has a mini HDMI and a mini DisplayPort instead of a HDMI port. For this reason, the models of the memory and the LCD have also changed.

I'm using the wireless keyboard shown as No. 11 on the list, which I happened to have, but it is probably difficult to find one now. In that case, you can use Rii mini X1 or iClever IC-RF01 as an alternative. The reason I chose a device with 2.4GHz wireless technology rather than with Bluetooth is that it is recognized as a USB legacy device and can be used for the BIOS settings on boot.

No.Part DescriptionUnit PriceAmount
01 Intel D34010WYK (NUC barebone kit) 35,633 JPY 3
02 Transcend TS128GMSA370 (128MB mSATA SSD) 7,980 JPY 3
03 CFD W3N1600PS-L8G (8GB DDR3-1600 SO-DIMM x 2) 18,153 JPY 3
04 ELECOM LD-GPS/BK03 (0.3m CAT6 Gigabit LAN cable) 406 JPY 3
05 Diatec YL-3114 (Power cable 2-pin/3-pin conversion adapter) 494 JPY 3
06 FILCO FCC3M-04 (3-way splitter power cable) 1,008 JPY 1
07 Logitec LAN-W300N/IGRB (Gigabit/11na Wifi router) 6,000 JPY 1
08 SANWA SUPPLY KB-DM2L (0.2m Power cable) 429 JPY 1
09 GeChic On-LAP 1302 for Mac/J (13.3 inch LCD) 20,153 JPY 1
10 ELECOM T-FLC01-2220BK (2m Power cable) 994 JPY 1
11 Rii mini X1 (2.4GHz Wireless keyboard) 2,860 JPY 1
12 IRIS OYAMA AM-10 (Aluminum attache case) 2,271 JPY 1
13 Inoac A8-101BK (Polyethylene foam 10x1000x1000) 2,018 JPY 1
14 Kuraray 15RN Black (Velcro tape 25mm x 15cm) 270 JPY 2
15 Cemedine AX-038 Super X Clear (Versatile adhesive 20ml) 343 JPY 1

Assembling Bereborn Kits

It is quite easy to assemble NUC bareborn kits. Machines can be completed simply by installing memory and SSD.

First, when you loosen the screws at the four corners of the bottom panel and uncover it, you will see two SO-DIMM slots (on the left in the picture below) and two mini PCI Express slots (on the right in the picture).

f:id:nagixx:20140421091249j:plain

Insert two memory modules into SO-DIMM slots, and fix them until they get locked into the latches. Out of the two mini PCI Express slots, the lower slot is for the half-size wireless LAN module, and the upper slot is for the full-size mSATA compliant slot. A wireless LAN module is not used this time, so just insert an mSATA SSD into the upper slot and screw it down.

f:id:nagixx:20140421092516j:plain

After that, attach the bottom panel again and screw it down to complete the box.

Cutting Out the Cushioning Foam for the Aluminum Case

In order to pack three machines, AC adapters, a Wifi router, a LCD monitor and cables into this small case, they are required to be suitably arranged. In addition, they need to be securely fastened not to be moved when carrying the case around.

First, cut a 1cm-thick polyethylene foam sheet using a utility knife as pictured below. I chose a 15 times expansion polyethylene foam called PE-light A8, which is firm enough, because it is supposed to be used as a cushioning foam to hold relatively heavy parts.

f:id:nagixx:20140123123824j:plain

Next, attach the first layer on the bottom of the case and the second layer on top of it. This will act as a frame to hold three machines, AC adapters and cables.

I used Cemedine Super X Clear for the adhesion of the polyethylene foam sheets. Basically, polyethylene is a hard-to-bond material, but this sheet is textured and Super X is an elastic adhesive with flexibility, so there is no problem in adhesive property.

f:id:nagixx:20140123123933j:plainf:id:nagixx:20140123124057j:plain

On the upper side, velcro tapes are attached to secure a LCD monitor. These velcro tapes are self-adhesive but not strong enough to be attached to the surface of the cushioning foam on the upper side, so an adhesive bond is used.

f:id:nagixx:20140123123555j:plain

The corresponding velcro tapes are also attached to the back of the LCD monitor. It is smooth-faced in contrast, therefore the tapes can be put directly on it.

This LCD, GeChic On-LAP, is a product which is supposed to be attached to the back of a laptop PC and used as a secondary monitor, but I chose it because it is very thin and runs on USB bus power. The mounting hardware for attachment to a laptop is removed since it is unnecessary this time.

f:id:nagixx:20140123124354j:plain

In addition, three layers of the sheets are glued together to form the cushioning pad that covers LCD monitor to protect its surface as well as holds machines and cables when the case is closed.

The picture below is the LCD monitor (upper) side. The cutout in the bottom right corner is a space for monitor cables.

f:id:nagixx:20140123151513j:plain

Turning it over, you will see the machine (lower) side. The cutout of the double layer depth in the center is a storage space for a keyboard, and that of single layer depth at the bottom right is for a power plug.

f:id:nagixx:20140611165340j:plain

By the way, for those who don't want to struggle to measure and cut those complex shapes, the drawing is ready for download as a PDF.

f:id:nagixx:20140611121500j:plain

Link to the PDF of the drawing

Layout of Components and Wiring

Arrange all the components step by step. Place three machines, put the AC adapters side by side and bind cables compactly. Since NUC's AC adapter has a 3-pin plug, they are connected via the 3-pin-to-2-pin conversion adapters to the 3 way splitter cable, and finally connected to the 2-way power strip. Then, the HDMI cable and the USB cable for power supply from the LCD monitor are plugged to the leftmost machine. The USB dongle for the wireless keyboard is also plugged into it.

f:id:nagixx:20140123143455j:plain

Next, plug the L-type 2-pin power plug into the Wifi router and the other end into the remaining outlet of the power strip. And, connect the Ethernet port on each machine and the LAN-side port of the router using a LAN cable. The Wifi router is not fixed in place, but it fits into the space above the power split cable. There is no other Wifi router out in the market like this small-sized, self-powered Gigabit Ethernet router, so I was afraid of its end of sale... actually, it has already reached the end of sale. If you want to make this, find and get it as soon as possible!

f:id:nagixx:20140123151704j:plain

These are all the steps to assemble the system. Now put the cushioning pad on top of the LCD monitor and place the keyboard in it. The power strip cable needs to be laid out about two rounds along the edge of the case, and place the plug to the location as shown below. By arranging this way, the cushioning forms tightly hold the machines and cables and they will never be moved around once the case is closed.

f:id:nagixx:20140124004724j:plain

Wifi Router Configuration

Before installing the OS, configure the Wifi router to get the network environment ready. First, plug the power cable into a wall outlet to turn the Wifi router on. The default SSID and pre-shared key for Wifi connection are described on the wireless encryption key sticker included in the product package, so enter them to establish a connection. The default IP address for the Wifi router is '192.168.2.1'. Open a web browser, go to 'http://192.168.2.1' and login with user name 'admin', password 'admin'.

In the administration screen, edit the following settings for the Portable Hadoop Cluster, and leave everything else as default. The SSID and pre-shared key can be any values you like. Regarding IP address for the cluster, use the default subnet of 192.168.2.0/24, but change the IP address of the router to 192.168.2.254, reserve 192.168.2.1 to 192.168.2.9 for the cluster nodes and other purpose as fixed addresses, and assign the remaining 192.168.2.10 to 192.168.2.253 for DHCP.

Menu CategoryMenu ItemItemValue
Wireless Settings Basic Settings SSID mapr-demo
Multi SSID Uncheck 'Valid' to disable it
Security Settings Shared Key Any passphrase
Wired Settings LAN-Side Settings Device's IP Address 192.168.2.254
Default Gateway 192.168.2.254
DHCP Client Range 192.168.2.10 - 192.168.2.253

Click 'Apply' button after completion of editing, then the Wifi router will restart. Lastly, plug the network cable with Internet access into the WAN-side port of the router, and move on to the next step.

CentOS Installation

The next step is software installation and configuration. Since NUCs have no DVD drive, use a USB flash drive for booting and perform network installation. Use a Windows PC to create a bootable image for installation on a USB flash drive.

First, download UNetbootin for Windows and CentOS 6.6 minimal image to a Windows PC.

Plug a USB flush drive into the Windows PC and launch UNetbootin. Select 'Diskimage' and specify CentOS-6.6-x86_64-minimal.iso that was downloaded in the previous step. Select 'USB Drive' in the Type field, specify the USB flush drive in the Drive field, and then click 'OK'. It takes a while to finish the process, and eject the USB flush drive when the boot image is created.

Next, install the OS on each machine. Since a monitor and keyboard are required, set up one by one by plugging the HDMI cable and the USB dongle for the wireless keyboard in and out.

Plug the USB flash drive into a NUC, and press the power button on the top panel to boot the machine. I only remember vaguely, but the USB flash drive should be recognized as a boot device because the SSD is empty, and installation process will start. (If it doesn't start, hold F2 key during boot-up and select the USB flash drive in the [Boot] menu.)

I'm not going into detail about the CentOS installation process, but I will list a few important points as follows.

  • In the 'Installation Method' screen, select 'URL' as an installation media type. In the 'Configure TCP/IP' screen, proceed with the default settings of DHCP. In the 'URL Setup' screen, enter 'http://ftp.riken.jp/Linux/centos/6/os/x86_64/' in the topmost field.
  • In the host name setting screen, enter the host name 'node1', 'node2' and 'node3', respectively from left to right. In addition, click the 'Configure Network' button at the bottom left of the same screen, select the device 'System eth0' in the 'Wired' tab and go to 'Edit'. Select 'Manual' in the Method drop down menu in the 'IPv4 Settings' tab. Add the address '192.168.2.1', '192.168.2.2' and '192.168.2.3', respectively from left to right, with the netmask '24' and the gateway '192.168.2.254', and enter the DNS server '192.168.2.254'.
  • In the installation type selection screen, select 'Create Custom Layout' and create the layout as follows in the partition edit screen. Assign standard partitions and don't use LVM.
    DeviceSizeMount PointType
    /dev/sda1 200MB /boot/efi EFI
    /dev/sda2 200MB /boot ext4
    /dev/sda3 58804MB / ext4
    /dev/sda4 58804MB /mapr ext4
    /dev/sda5 4095MB   swap
  • In the installation software selection screen, select 'Desktop'.

When the installation completes, machines will restart. In the post installation setup screen after restart, 'Create User' is unnecessary for the moment, so proceed with blank. In the 'Date and Time', check 'Synchronize date and time over the network'. 'Kdump' is unnecessary. These are all the steps to setup the OS.

Miscellaneous OS Configuration

As a preparation for building the Hadoop Cluster, make miscellaneous OS configuration settings. Perform the following steps on each node.

Edit '/etc/sysconfig/i18n' as follows to set the system language to 'en_US.UTF-8'.

LANG="en_US.UTF-8"

Edit the following line in '/etc/sysconfig/selinux' to disable SELinux.

SELINUX=disabled

Now, restart the machine. This will disable SELinux, but some files are still labeled with a SELinux context, so delete such information with the following command.

# find / -print0 | xargs -r0 setfattr -x security.selinux 2>/dev/null

Add the following lines to '/etc/sysctl.conf'.

vm.overcommit_memory=0
net.ipv4.tcp_retries2=5

Run the following command to reflect the changes.

# sysctl -p

Edit '/etc/hosts' as follows.

127.0.0.1       localhost
192.168.2.1 node1
192.168.2.2 node2
192.168.2.3 node3

Run the following commands to stop the iptables services and disable them on startup.

# service iptables stop
# service ip6tables stop
# chkconfig iptables off
# chkconfig ip6tables off

Edit '/etc/ntp.conf' to configure NTP so that the clocks of three nodes are synchronized. The factory default settings are to synchronize with the NTP servers on the Internet. However, this Portable Hadoop Cluster doesn't always have the Internet connection, so configure every node to be synchronized with a local clock on node1.

On node1, edit the following lines in '/etc/ntp.conf'. Comment out the default server setting, and specify '127.127.1.0', which stands for host's local clock.

nrestrict 192.168.2.0 mask 255.255.255.0 nomodify notrap
server 127.127.1.0
#server 0.centos.pool.ntp.org
#server 1.centos.pool.ntp.org
#server 2.centos.pool.ntp.org

On node2 and node3, edit the following lines in '/etc/ntp.conf'. The reference source is set to node1.

server node1
#server 0.centos.pool.ntp.org
#server 1.centos.pool.ntp.org
#server 2.centos.pool.ntp.org

Restart the NTP service with the following command.

# service ntpd restart

Create the MapR system user and group with the following commands. Set the password of your choice.

# groupadd mapr -g 500
# useradd mapr -u 500 -g 500
# passwd mapr

It is convenient if the root user and the mapr user can login to other nodes via ssh without password. The following commands set up a password-less ssh login.

# ssh-keygen -t rsa -N '' -f ~/.ssh/id_rsa
# for host in node1 node2 node3; do ssh-copy-id $host; done
# su - mapr
$ ssh-keygen -t rsa -N '' -f ~/.ssh/id_rsa
$ for host in node1 node2 node3; do ssh-copy-id $host; done
$ exit

Install OpenJDK from the yum repository with the following command.

# yum install java-1.7.0-openjdk-devel

In the installation process, the partition '/dev/sda4' was created for MapR and mounted on '/mapr'. Since MapR accesses block devices directly, unmount this file system.

# umount /mapr

In addition, edit '/etc/fstab' to remove the line of /dev/sda4 so as not to be mounted on startup.

MapR Installation

The last step is the installation of the MapR distribution for Hadoop. Perform the following steps on each node.

First, create '/etc/yum.repos.d/maprtech.repo' to configure the MapR yum repository.

[maprtech]
name=MapR Technologies
baseurl=http://package.mapr.com/releases/v4.0.1/redhat/
enabled=1
gpgcheck=0
protect=1

[maprecosystem]
name=MapR Technologies
baseurl=http://package.mapr.com/releases/ecosystem-4.x/redhat
enabled=1
gpgcheck=0
protect=1

Since some EPEL packages are also required, run the following commands to configure the EPEL repository.

# wget http://download.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
# yum localinstall epel-release-6-8.noarch.rpm

Next, install MapR packages. For MapR 4.x, both MapReduce v1 and v2 (YARN) applications can run at the same time, but they can not share the same memory space. So, select either v1 or v2 for this cluster to use a limited 16GB memory efficiently.

Run the following commands for the MapReduce v1 configuration.

# yum install mapr-cldb mapr-core mapr-core-internal mapr-fileserver \
mapr-hadoop-core mapr-jobtracker mapr-mapreduce1 mapr-nfs mapr-tasktracker \
mapr-webserver mapr-zk-internal mapr-zookeeper

Run the following commands for the MapReduce v2 configuration. Since the History Server can be configured only on a single node, the mapr-historyserver package is installed only on node1.

# yum install mapr-cldb mapr-core mapr-core-internal mapr-fileserver \
mapr-hadoop-core mapr-historyserver mapr-mapreduce2 mapr-nfs mapr-nodemanager \
mapr-resourcemanager mapr-webserver mapr-zk-internal mapr-zookeeper

The following command will create the file that specifies the partition for the MapR data.

# echo "/dev/sda4" > disks.txt

The following command will perform initialization and configuration of MapR. After running this command, the appropriate services will start automatically. For MapReduce v1 configuration:

# /opt/mapr/server/configure.sh -N cluster1 -C node1,node2,node3 \
-Z node1,node2,node3 -F disks.txt

For MapReduce v2 configuration:

# /opt/mapr/server/configure.sh -N cluster1 -C node1,node2,node3 \
-Z node1,node2,node3 -RM node1,node2,node3 -HS node1 -F disks.txt

Once these configurations have been completed on all the three nodes, the MapR cluster will become active in a few minutes. However, there is the issue in MapR 4.0.1 that users of some browser may lose the ability to access MapR via HTTPS because recent version of them have removed support for older certificate cipher algorithms. Run the following commands on each node to install the patch for this issue. The last command will restart the Web servers, and the web interface will become active in a few minutes.

# wget http://package.mapr.com/scripts/mcs/fixssl
# chmod 755 fixssl
# ./fixssl

Open a web browser and go to 'http://192.168.2.1:8443', then the MapR Control System screen will be shown. Login to it with the user name 'mapr' and the password that you have set when creating the user.

f:id:nagixx:20141230001615p:plain

f:id:nagixx:20141230001642p:plain

Register the MapR cluster via Web and obtain a license key. By factory default, a Base License is applied, but if a free perpetual M3 license or a free 30-day trial M5 license is applied, the NFS access, high-availability and data management features such as snapshot will be enabled.

Click 'Manage Licenses' at the top right of the MapR Control System screen, and click 'Add Licenses via Web' button in the dialog appeared. You will be asked if going to MapR's web site, then click 'OK' to proceed. Once a form for user registration is filled out and submitted on the web site, select either a M3 or M5 license, and a license will be published. Go back to the MapR Control System screen, and click 'Add Licenses via Web' button in 'Manage Licenses' dialog to apply the obtained license.

Now, run a sample MapReduce job. This is a sample MapReduce job which calculates pi. For MapReduce v1:

$ hadoop -classic jar /opt/mapr/hadoop/hadoop-0.20.2/hadoop-0.20.2-dev-examples.jar pi 10 10000

For MapReduce v2:

$ hadoop -yarn jar /opt/mapr/hadoop/hadoop-2.4.1/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.4.1-mapr-1408.jar pi 10 10000

Lastly, add the settings that are specific to this Portable Hadoop Cluster. In the MapR's default configuration, relatively large memory is assigned to each MapR daemon process, but the machines in this cluster have a relatively small, 16GB memory, so edit the following lines in '/opt/mapr/conf/warden.conf' on each node to minimize the memory assigned so that much more memory will be assigned to MapReduce jobs.

service.command.jt.heapsize.max=256
service.command.tt.heapsize.max=64
service.command.cldb.heapsize.max=256
service.command.mfs.heapsize.max=4000
service.command.webserver.heapsize.max=512
service.command.nfs.heapsize.max=64
service.command.os.heapsize.max=256
service.command.warden.heapsize.max=64
service.command.zk.heapsize.max=256

For MapReduce v2 configuration, edit the following files in addition to the above.

/opt/mapr/conf/conf.d/warden.resourcemanager.conf

service.heapsize.max=256

/opt/mapr/conf/conf.d/warden.nodemanager.conf

service.heapsize.max=64

/opt/mapr/conf/conf.d/warden.historyserver.conf

service.heapsize.max=64

After completing the steps above, run the following command on each node to restart services so as to reflect the changes.

# service mapr-warden restart

Extra tips

Login Screen Resolution Settings

When the machine starts up, the screen of the leftmost machine node1 will appear on the LDC, but it is a bit blurred because the resolution of the login screen is not identical to that of LCD. In order to adjust it, run the following command to check the connected monitor name. In this case, it is 'HDMI-1'.

# xrandr -q
Screen 0: minimum 320 x 200, current 1366 x 768, maximum 8192 x 8192
VGA-0 disconnected (normal left inverted right x axis y axis)
HDMI-0 disconnected (normal left inverted right x axis y axis)
DP-0 disconnected (normal left inverted right x axis y axis)
HDMI-1 connected 1366x768+0+0 (normal left inverted right x axis y axis) 460mm x 270mm
1366x768 59.8*
1024x768 75.1 70.1 60.0
800x600 75.0 60.3
640x480 75.0 60.0
DP-1 disconnected (normal left inverted right x axis y axis)

Then, edit '/etc/X11/xorg.conf.d/40-monitor.conf' as follows.

Section "Monitor"
Identifier "HDMI-1"
Option "PreferredMode" "1366x768"
EndSection

Auto Login

When doing a demo using this cluster, it is convenient if one can automatically login and move on from the login screen to the desktop screen without interaction. If you like, edit '/etc/gdm/custom.conf' as follows. In this case, an automatic login will be performed as the mapr user after waiting for user input for 30 seconds.

[daemon]
TimedLoginEnable=true
TimedLogin=mapr
TimedLoginDelay=30

Shutdown Via Power Button

When shutting down the portable cluster, it is convenient as well if one can power off the machines without entering a password or click a button for confirmation but just by pressing the power button on the top panel of the NUCs. Edit '/etc/polkit-1/localauthority.conf.d/org.freedesktop.logind.policy' as follows to enable it.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
<policyconfig>
<action id="org.freedesktop.login1.power-off-multiple-sessions">
<description>Shutdown the system when multiple users are logged in</description>
<message>System policy prevents shutting down the system when other users are logged in</message>
<defaults>
<allow_inactive>yes</allow_inactive>
<allow_active>yes</allow_active>
</defaults>
</action>
</policyconfig>