まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

2017年07月

このブログでも何度か Node.js のネタを扱ってますが、非同期処理に悩まされることが多いので、自分の理解の意味でもまとめておくことにしました。

まず理解の大前提として、Node.js はシングルスレッドで動作するため、いわゆる並列処理はできない仕様になっています。そして非同期に処理を実行することができます。これによって並列処理ができなくても、何かの時間のかかる処理があった場合にその終了を待たずに次の処理に進むことができることを意味しています。 ただ、この辺りがややこしく難解になっていることも事実です。

例えば REST API などの HTTP リクエストを行って、その結果が得られたら、その得られた結果の JSON オブジェクトの値を使って処理をする、などというよくあるケースでもこの問題に直面します。

まずは何が難解なのかを紹介します。REST API だとローカル環境で気軽に試せないので、わざと実行に時間のかかる関数を用意して説明します。

例えばこのような処理を考えてみます:
// test.js

// わざと1秒かけてから、パラメータの2倍の数値を出力する関数
function func1( x ){
  setTimeout( function(){
    console.log( 2 * x );
  }, 1000 );
}

// 初期値を設定して出力
var n0 = 10;
console.log( 'n0 = ' + n0 );

// 初期値を上記関数のパラメータに入れて実行
func1( n0 );

この中では func1 という関数を定義しています。setTimeout を使ってわざと1秒(1000ミリ秒)待ってから、パラメータの値の2倍を画面に出力する、という関数です。 この関数を n0 (=10 に設定)という変数をパラメータにして実行する、というものです。なので "20" という結果が出力されることを期待しています。

この内容を test.js というファイルに保存して、実行してみます(青字部分が出力結果):
$ node test
n0 = 10
20

期待通りに "20" と出力されました。とりあえずここまでは成功です。


さて問題はここからです。上記例では関数 func1 の中で console.log が実行されて出力までを行いました。これを func1 からは与えた数値を2倍した結果を受け取るようにして、func1 の外側で出力するように変更してみます。深く考えずにやるとこんな感じでしょか?
// test.js(注 正しく動きません)

// わざと1秒かけてから、パラメータの2倍の数値を出力する関数
function func1( x ){
  setTimeout( function(){
    console.log( 2 * x );
  }, 1000 );
}

// わざと1秒かけてから、パラメータの2倍の数値を戻す関数
function func2( x ){
  setTimeout( function(){
    return ( 2 * x );
  }, 1000 );
}

// 初期値を設定して出力
var n0 = 10;
console.log( 'n0 = ' + n0 );

// 初期値を上記関数のパラメータに入れて実行し、戻り値を出力
var n1 = func2( n0 );
console.log( 'n1 = ' + n1 );

func1 をほぼコピペして func2 という関数を作りました。console.log の代わりに return にして、値を返すようにしています。呼び出し元からはこの func2 を実行して、得られた結果を出力するようにしています。

これを先程と同様に実行するとこうなります:
$ node test
n0 = 10
n1 = undefined

n0(10)の値の2倍の "20" という結果を期待していたのですが、"undefined" と表示されてしまいます(正確に書くと、上記の2行はすぐに表示されますが、更に1秒くらい経過してから終了します)。この理由は最初に書いたように func2 は非同期に実行されているので、return の行が実行されるまでには1秒かかります。しかしその前に(return の行が実行される前に)関数そのものの処理は終了してしまいます。つまり値が戻る前に(戻っていない値を受け取ることになっている)n1 という変数を出力しているので "undefined" になっているのでした。

このように、期待通りに動かなかった理由は明白なのですが、ではどうすればこの関数が期待通りに動く(1秒後に与えられた結果を戻り値として戻し、受け取った側がその値を出力する)ようにできるでしょうか?これが今日紹介する大きなテーマです。


結論を先に紹介すると、ここで Promise オブジェクトを使って関数を修正し、受け取った側もその変更に合わせて一部書き直す必要があります。具体的には以下のように修正します:
// test.js

// わざと1秒かけてから、パラメータの2倍の数値を出力する関数
function func1( x ){
  setTimeout( function(){
    console.log( 2 * x );
  }, 1000 );
}

// わざと1秒かけてから、パラメータの2倍の数値を戻す関数
function func2( x ){
  return new Promise( function( resolve ){
    setTimeout( function(){
      resolve( 2 * x );
    }, 1000 );
  });
}

// 初期値を設定して出力
var n0 = 10;
console.log( 'n0 = ' + n0 );

// 初期値を上記関数のパラメータに入れて実行し、戻り値を出力
func2( n0 ).then( function( n1 ){
  console.log( 'n1 = ' + n1 );
});

まず関数 func2 側は、Promise オブジェクトを新規に作成します。Promise オブジェクトは処理が成功した場合の関数をパラメータに指定します。上図だと
function( resolve ){
  setTimeout( function(){
    resolve( 2 * x );
  }, 1000 );
}

という関数がパラメータに指定されているので、成功するとこの関数が実行されます(今回は使っていませんが、第二パラメータを指定した場合は失敗時に実行する処理を指定したことになります)。この処理の中で1秒待って、指定したパラメータを2倍して resolve とする、ということになります。

そしてこの関数 func2 を呼び出す側も少し変更が必要になります。 func2() 関数の実行結果をそのまま変数として受け取るのではなく、成功した場合(今回の例だと1秒待って2倍になった値が返された場合)の処理を .then() 内に渡して処理することになります。この then 内の処理で計算結果(resolve で処理された内容)を n1 という変数で受け取って console.log で表示する、という内容にしています。

こうして修正した test.js を実行すると、以下のような結果になります(実際には n0 = 10 がすぐに表示され、1秒くらい待ってから n1 = 20 の行が表示されて終了します):
$ node test
n0 = 10
n1 = 20

Node.js の関数内で非同期処理を実行して、その非同期処理の終了を待って値を受け取るような関数を作る場合は、Promise オブジェクトを使って上記のように記述します、という紹介でした。非同期実行に慣れていないと、この辺りで戸惑うことが多いと感じたので、まとめておきました。


コンテナ環境のスタンダードともいえる docker が、いつの間にかラズパイでも動くようになってました:
DOCKER COMES TO RASPBERRY PI

2017070701


動くようになった当初は導入方法も(コマンドが用意されたり、専用イメージが用意されたり、・・)色々あったようですが、現状では IA 環境と同様の方法でインストールできるようになっていました。具体的にはこの(curl と sh を使った)コマンドだけで導入できます:
$ curl -sSL https://get.docker.com | sh

ただ、この方法で導入しただけだと root ユーザーでないと動かせません。通常の(非 root )ユーザーのままで docker を使うには、そのユーザーを docker グループに所属させる必要があります。具体的には以下のコマンドを実行した上で再ログインすると有効になります(pi ユーザーの場合の例です):
$ sudo usermod -aG docker pi

実際にいくつかのコマンドを実行してみました。まずは docker 環境の確認:
$ docker info
Containers: 1
 Running: 0
 Paused: 0
 Stopped: 1
Images: 2
Server Version: 17.05.0-ce
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host macvlan null overlay
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 9048e5e50717ea4497b757314bad98ea3763c145
runc version: 9c2d8d184e5da67c95d601382adf14862e4f2228
init version: 949e6fa
Kernel Version: 4.9.28-v7+
Operating System: Raspbian GNU/Linux 8 (jessie)
OSType: linux
Architecture: armv7l
CPUs: 4
Total Memory: 923.4MiB
Name: raspberrypi
ID: TI35:OH7M:MCCS:PUBJ:RFNY:O2LP:MA6F:CECD:3BUW:7NFL:LSV2:A3FO
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
Experimental: false
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false

WARNING: No swap limit support
WARNING: No cpu cfs quota support
WARNING: No cpu cfs period support
WARNING: No cpuset support

↑docker 17.05 が動いていることが分かりました。あと分かっていたことですがメモリは 1GB ・・

ラズパイ(というか ARM )で使えるコンテナを探してみました:
$ docker search armhf-
NAME                                               DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
container4armhf/armhf-alpine                       Automatically built base images of Alpine ...   67                   [OK]
container4armhf/armhf-busybox                      Automated build of Busybox for armhf devic...   8                    [OK]
orax/alpine-armhf                                  Daily built Alpine-Linux Docker image for ...   7                    [OK]
forumi0721alpinearmhf/alpine-armhf-transmission    alpine-armhf-transmission                       3                    [OK]
forumi0721alpinearmhf/alpine-armhf-minidlna        alpine-armhf-minidlna                           3                    [OK]
forumi0721alpinearmhf/alpine-armhf-nginx           alpine-armhf-nginx                              2                    [OK]
forumi0721alpinearmhf/alpine-armhf-tvheadend       alpine-armhf-tvheadend                          2                    [OK]
forumi0721alpinearmhf/alpine-armhf-jenkins         alpine-armhf-jenkins                            2                    [OK]
forumi0721alpinearmhfbuild/alpine-armhf-gpio-bpi   alpine-armhf-gpio-bpi                           1                    [OK]
forumi0721alpinearmhf/alpine-armhf-gogs            alpine-armhf-gogs                               1                    [OK]
forumi0721alpinearmhf/alpine-armhf-samba           alpine-armhf-samba                              1                    [OK]
forumi0721alpinearmhf/alpine-armhf-owncloud        alpine-armhf-owncloud                           1                    [OK]
forumi0721alpinearmhf/alpine-armhf-vsftpd          alpine-armhf-vsftpd                             1                    [OK]
forumi0721alpinearmhf/alpine-armhf-postgresql      alpine-armhf-postgresql                         1                    [OK]
forumi0721alpinearmhf/alpine-armhf-wordpress       alpine-armhf-wordpress                          1                    [OK]
forumi0721alpinearmhfbuild/alpine-armhf-netatalk   alpine-armhf-netatalk                           1                    [OK]
forumi0721alpinearmhf/alpine-armhf-mariadb         alpine-armhf-mariadb                            1                    [OK]
forumi0721alpinearmhf/alpine-armhf-certbot         alpine-armhf-certbot                            1                    [OK]
forumi0721alpinearmhf/alpine-armhf-noip-curl       alpine-armhf-noip-curl                          0                    [OK]
forumi0721alpinearmhf/alpine-armhf-nodejs          alpine-armhf-nodejs                             0                    [OK]
forumi0721alpinearmhf/alpine-armhf-scepg           alpine-armhf-scepg                              0                    [OK]
forumi0721archarmhf/arch-armhf-dev                 arch-armhf-dev                                  0                    [OK]
forumi0721ubuntuarmhf/ubuntu-armhf-dev             ubuntu-armhf-dev                                0                    [OK]
forumi0721alpinearmhf/alpine-armhf-dev             alpine-armhf-dev                                0                    [OK]
forumi0721debianarmhf/debian-armhf-dev             debian-armhf-dev                                0                    [OK]


そんなに数は多くないけど、nginx やら jenkins やら wordpress やら mariadb やら・・・基本的なのは揃ってそうです。


ラズパイはメモリが 1GB しかないので、そもそも docker を動かすにはいろいろ厳しい環境だとは思いますが、一方でケースや SD カード含めて 6~7000 円程度で docker 環境を揃えることができてしまう、ということでもあります。今度はこれのクラスタリングにも挑戦してみようかな・・・

Raspberry Pi3 Model B ボード&ケースセット 3ple Decker対応 (Element14版, Clear)




ラズパイ(ラズベリーパイ)上で検索エンジンである ElasticSearch が動くことがわかったので、自分も試してみました:
2017070601


まず、ElasticSearch そのものにラズパイネイティブ版が存在しているわけではありません。 ElasticSearch は Java アプリケーションなので実行には Java が必要であり、Java が有効な環境であれば理論上は動きます。というわけで最初にラズパイに JDK 8 を導入します(導入済みであれば、ここは読み飛ばしても可)。
$ sudo apt-get install oracle-java8-jdk

$ java -version
Java(TM) SE Runtime Environment (build 1.8.0_65-b17)
Java HotSpot(TM) Client VM (build 25.65-b01, mixed mode)

Java が使えるようになったので改めて ElasticSearch を導入します。ダウンロードページで確認すると、この記事を書いている 2017/Jul/06 時点での最新バージョンは 5.4.3 でした:
2017070602



これをラズパイ上にダウンロードして展開します:
$ cd ~
$ wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.4.3.zip
$ unzip elasticsearch-5.4.3.zip
$ rm elasticsearch-5.4.3.zip
$ mv elasticsearch-5.4.3 elasticsearch
$ cd elasticsearch

さて、他のプラットフォームだとこのまま実行してもいいのですが、ラズパイの場合はシステムスペックの問題でデフォルトの設定のままだとメモリ不足になりやすいという問題があります。そこで実行前にメモリの設定を変更しておきます。22 行目と 23 行目(か、そのあたり)で、"-Xms2g" と "-Xmx2g" という指定がされており、これによって ElasticSearch に 2GB のメモリを使うよう指定されています。ここを以下の赤字のように書き換えて、メモリ使用量を 256MB にするよう変更します:
$ vi config/jvm.options

  :
  :
-Xms256m   ← -Xms2g から変更
-Xmx256m   ← -Xmx2g から変更
  :
  :

これでメモリの問題は解決した(はず)なので、改めて ElasticSearch を起動します:
$ ./bin/elasticsearch
[2017-07-06T10:41:17,203][WARN ][o.e.b.Natives ] unable to load JNA native support library, native methods will be disabled. : : [2017-07-06T10:41:39,511][INFO ][o.e.h.n.Netty4HttpServerTransport] [lonXjYa] publish_address {127.0.0.1:9200}, bound_addresses {[::1]:9200}, {127.0.0.1:9200} [2017-07-06T10:41:39,537][INFO ][o.e.n.Node ] [lonXjYa] started [2017-07-06T10:41:39,579][INFO ][o.e.g.GatewayService ] [lonXjYa] recovered [0] indices into cluster_state

いくつかの警告メッセージが表示されますが、ラズパイ上で ElasticSearch 5.4.3 が起動しています。確認のため、同じマシンから curl でアクセスしてみましょう:
$ curl http://localhost:9200/
{
  "name" : "lonXjYa",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "4hqvLhXQT0uqTaKOI02idg",
  "version" : {
    "number" : "5.4.3",
    "build_hash" : "eed30a8",
    "build_date" : "2017-06-22T00:34:03.743Z",
    "build_snapshot" : false,
    "lucene_version" : "6.5.1"
  },
  "tagline" : "You Know, for Search"
}

↑こんな感じのメッセージが表示されれば成功です。なお、ElasticSearch を終了するには実行中の端末で Ctrl+C を入力します:
    :
    :
[2017-07-06T10:41:39,537][INFO ][o.e.n.Node               ] [lonXjYa] started
[2017-07-06T10:41:39,579][INFO ][o.e.g.GatewayService     ] [lonXjYa] recovered [0] indices into cluster_state

^C[2017-07-06T10:52:09,484][INFO ][o.e.n.Node               ] [lonXjYa] stopping ...
[2017-07-06T10:52:09,579][INFO ][o.e.n.Node               ] [lonXjYa] stopped
[2017-07-06T10:52:09,581][INFO ][o.e.n.Node               ] [lonXjYa] closing ...
[2017-07-06T10:52:09,668][INFO ][o.e.n.Node               ] [lonXjYa] closed

$ 
  (↑Ctrl+C(赤字部分)を入力して終了する様子)


実はいま、個人的にはラズパイを開発環境として使う機会がそれなりにあります(ラズパイにリモートログインして vi でガシガシ、という感じ)。そのローカルシステム内に ElasticSearch 環境まで構築できる時代になったとは・・・今以上に開発がはかどりますね。


最近、従来種ではなかったはずの猛毒アリである「火蟻(ヒアリ)」が日本で見つかった、というニュースを耳にします:
ヒアリ、東京で発見 ついに関東上陸 大井埠頭のコンテナから


外来種かつ毒虫という特徴から、素手で捕まえたり、触ったりするのが非常に危険なアリです。とはいえ、この季節は普通のアリを見かけることも多いので、ヒアリかどうかを判断するのが難しい問題もあります。

さて、最近話題の画像認識を使って蟻の画像を識別させて、「それがヒアリかどうか?」を判断することはできるでしょうか? IBM WatsonVisual Recognition API を使って試してみました。

当初は「まずはヒアリを学習させて・・・」と思っていたのですが、調べてみたら IBM Watson の Visual Recognition V3 では標準機能でヒアリを識別する機能を持っているようでした(これに気付いた時はちと驚きました)。というわけで普通に公開されているデモ用ページを使い、カスタマイズなしの標準機能だけで試してみました。
Visual Recognition Demo

2017070701


標準機能で画像認識を試す場合は、ブラウザで上記ページにアクセスして、赤枠部分をクリックし識別させたい画像を PC 内から指定するだけです。非常に簡単です。


今回、まずはこのヒアリの画像を指定してみました:
fire1


しばらく考えて・・・
2017070702


はい、結果がでました!"pharaoh ant"(ファラオ蟻、何それ?)とかに混じって "fire ant"(ヒアリ)という識別結果が表示されています!検索スコアも 0.80 と中々高い結果になっています(赤枠部分参照):
2017070703


では次はこの「黒蟻」の画像を指定してみます:

kuro1


結果はこうでした。"carpenter ant" は日本では「黒蟻」と呼ばれているものです(ちなみに "sanguinary ant" は「銀蟻」です)。そして "fire ant" とは識別されませんでした。これも正解です:
2017070704


いくつかの画像で試してみたので、その結果を表にしておきます:
画像正解識別結果スコア成否
 fire1
ヒアリファラオ蟻
ヒアリ

銀蟻
0.81
0.80
0.60
 fire2
ヒアリヒアリ0.60
 fire3
ヒアリヒアリ
軍隊アリ
0.83
0.50
 kuro1
黒蟻黒蟻
銀蟻
0.81
0.50
 kuro2
黒蟻黒蟻0.93
 gin1
銀蟻銀蟻0.51
 gin2
銀蟻銀蟻
黒蟻
0.64
0.60


おおーっ! 適当に集めた画像で試してみただけですが、それなりの精度で検索できているように思えます。 カスタマイズなしの標準機能だけでもいい感じでした。ぶっちゃけ想定以上です(笑)。


皆さんもアリ画像を使って上記サイトで色々試してみてください。なかなかの精度で調べてくれそうですよ。

#最初は学習させるつもりで蟻の画像を大量に集めて見ていたので、気持ち悪くなってきた・・・

IBM Bluemix からも提供されている IBM の DBaaS サービスである dashDB に Node.js からアクセスする方法を紹介します。実際には dashDB だけでなく、DB2 のサービスやオンプレミスデータベースへも同様に応用できますが、今回は Bluemix 上の DB2/dashDB 関連サービスを例に紹介します:
2017063002


dashDB は行指向/列指向型のテーブルをどちらも作成することができるリレーショナル・データベースのサービスですが、そのデータベースシステムとしての実体は IBM DB2 です。というわけで、このライブラリを使ってアクセスします:
https://www.npmjs.com/package/ibm_db

2017063001


まず以下のコマンドを実行して ibm_db をインストールします(このコマンドだけで DB2 ODBC Driver ごとインストールされます):
$ npm install ibm_db


そして以下のようなコードを用意して dashDB にアクセスします:

(settings.js)
exports.db_host = 'dashdb-entry-yp-XXXXXXXX.services.dal.bluemix.net';
exports.db_port = 50000;
exports.db_name = 'BLUDB';
exports.db_username = 'dashNNNN';
exports.db_password = 'PASSWORD';

(sample.js)
var ibm_db = require( 'ibm_db' );
var settings = require( './settings' );

var db_str = "DATABASE=" + settings.db_name
  + ";HOSTNAME=" + settings.db_host
  + ";UID=" + settings.db_username
  + ";PWD=" + settings.db_password
  + ";PORT=" + settings.db_port
  + ";PROTOCOL=TCPIP";
var sql = "select OBJECTID, NAME from SAMPLES.GEO_CUSTOMER limit 10";

ibm_db.open( db_str, function( err, conn ){
  if( err ) return console.log( err );

  conn.query( sql, function( err, data ){
    if( err ) console.log( err );
    else console.log( data );

    conn.close( function(){
      console.log( 'done.' );
    });
  });
});

settings.js の中身はユーザー名やパスワードといった dashDB に接続するためのサービス資格情報です。IBM Bluemix の画面から取得できる値を使って、実際の値で書き換えて使ってください:

2017063003


アプリケーションの実体は sample.js です。今回の例ではシンプルに接続して、サンプルデータとして GEO_CUSTOMER テーブルから OBJECTID と NAME の値を 10 件だけ取得する、という SQL (青字部分)を実行しました。また settings.js で定義した情報を取り出して接続文字列(赤字部分)を生成しています。

node コマンドで sample.js を実行して、以下のような結果が表示されれば成功です:
$ node sample.js
[ { OBJECTID: 1322, NAME: 'Kami Labarbera' },
  { OBJECTID: 1323, NAME: 'Johnathon Tunney' },
     :
  { OBJECTID: 1587, NAME: 'Althea Alcazar' } ]
done.







 

このページのトップヘ