Node.jsにおける大量のデータ処理の際の非同期処理コールバックがスタックしすぎる問題への対処

ストリーミング処理の落とし穴

Mongoose(MongoDBのODM)を使って、コレクションからほんの10万ほどのドキュメントを処理するスクリプトを実行した時でした。処理が固まってしまうのです。なぜでしょう?

各ドキュメント処理では、コールバックを伴う非同期的な処理を含みます。
問題のCoffeeScriptのコードは以下のようなイメージです:

Foo.find()
    .sort value:-1
    .stream()
    .on 'data', (d)->
        someTask d

someTask = (d)->
    doAwesomeJob (result)->
        console.log "Great!"

モデルFooのドキュメントを全て読み込み、各ドキュメントをストリームに流し込んでいます。
メモリを節約するためにストリームの手法を採用しています。
doAwesomeJobは、例えばAPIを叩くとか、そういう非同期な処理です。
しかし、doAwesomeJobのコールバック関数がなかなか呼ばれない。遅い。ついには全く呼ばれなくなる。

イベントループのキューが溢れている

Node.jsアプリケーションはシングルスレッドです。
下図のように、イベントループ機構が備わっていて、Event Queueに溜まった処理キューを順次消化していく仕組みになっています。

event-loop

(via http://misclassblog.com/interactive-web-development/node-js/)

つまり、Node.jsはひとつずつしかキューを処理できないのです。
上記スクリプトで、ストリームのイベントのInvocationが、大量にこのイベントキューに押し寄せている事を想像してください。
doAwesomeJobのコールバックは、その非同期処理の完了時点で最後列にエンキューされます。
しかし、非同期処理の間にドキュメント読み出しイベントの処理キューが大量に溜まります。
その結果、doAwesomeJobのコールバックが呼ばれるまでに時間がかかってしまっているのでした。

似た問題に直面している人がいました。

RabbitMQのメッセージキューがいちどに大量に来た時に、イベントループのキューが溢れるようです。
問題は僕のケースより少し深刻なようです。見なかったことにしましょう

ストリームを分割しよう

溢れるのなら、分割すればいいですね。
以下のように変更を加えました:

processNext = (offset, num, on_completion)->
    i = 0, j = 0, closed = false
    Foo.find()
        .sort value:-1
        .skip offset
        .limit num
        .stream()
        .on 'data', (d)->
            ++j
            someTask d, ->
                ++i
                if i==j && closed
                    processNext offset+i, num, on_completion
        .on 'close', ->
            closed = true
            on_completion() if j==0

someTask = (d, callback)->
    doAwesomeJob (result)->
        console.log "Great!"
        callback()

processNext 0, 100, ->
    console.log "done!"

ドキュメントを100個ずつ読み出して、処理して、終わったら次の100個・・というフローに変更しました。
今のところ、これで上手く動いています。

でもこのコードは少し汚いので、よりエレガントに書くためにasyncを使うことをお薦めします。
drainで完了をフックするといいと思います。

このストリームの分割では、ストリームを処理している間にドキュメントが追加・削除される事を想定していません。
コレクションの変更まで考慮する場合は、想定される変更内容によって変わると思います。

ご参考まで!

自前ホストのHubotとSlackを連携させる方法

images hubot-featured

最近、チャットツールをHipChatからSlackに乗り換えました!

Slackとは?

いわゆるチャットツールです。技術者向け。
SlackはHipChatと違ってUIがリッチで分かりやすいです。オシャレ!
HipChatと同じくAPIを備えており、GitHubやAsanaなど様々なサービスと連携させる事ができます。
そして、タイトルにもあるHubotとも連携させる事ができます。

Hubotとは?

チャット上で使えるボットです。Node.jsで書かれています。GitHub社が開発しました。
ボットというとTwitterのボットを想像するかもしれませんが、基本は同じです。
ボットに向けて、決まった書式でメッセージを送ると、そのメッセージに応じた処理を行います。
例えば、Jenkinsのビルドを実行したり、アプリケーションをデプロイしたり、ネコ画像を拾ってきたりします。

Hubotを導入するメリット

従来は個別のターミナル内で行っていたタスク指示が、チャットウインドウに移ります。
この事のメリットは主に二つあります。

  1. 作業ログが残る
  2. チームとのコミュニケーションと作業が統合される

ビルドやデプロイなどは、いつ誰がやったのか残っている事が望ましいです。
また、チームワークにおいてはそのタスクが属人的にならないようにする事が重要です。
タスク実行をチャットウインドウから行う事で、直接的な作業が会話にシームレスに混ざります。
それによって、デプロイなどの手順や気をつけるべき点が自然と共有される形になります。

このようにチャットを使ったサーバ運用をChatOpsと呼びます。

Herokuではなく自前サーバのHubotと連携させたい

Slack用Hubotプラグインがあるので、書かれている手順に従えば簡単に設定できます。
日本語の情報もnanapiなどに掲載されています。
しかし、どれもHubotをHerokuにデプロイする場合の手順です。
いやいや、俺は自前サーバにHubotを稼働させる場合の手順が知りたいんだ
という訳で、このページでは自前ホストのHubotとSlackを連携させる手順について説明します。

Hubotのインストール

既に自前サーバで動作している方は読み飛ばして下さい。

  • Hubotをインストール
    $ npm install -g hubot coffee-script
    
  • botの作成
    $ hubot --create [path_name]
    $ cd [path_name]
    
  • Slack用アダプタのインストール
    $ npm install hubot-slack --save
    
  • 試しにローカルで起動
    $ ./bin./hubot
    

Slackとの連携設定

Slackの以下のページから、環境変数に設定する値を取得します。

Screen Shot 2014-07-08 at 11.15.43 PM

それぞれ、環境変数に設定します。

$ export HUBOT_SLACK_TOKEN=<token>
$ export HUBOT_SLACK_TEAM=<team>
$ export HUBOT_SLACK_BOTNAME=<bot name>

Hubotは、HTTPインターフェースも備えています。これはデフォルトで8080ですが、PORTの環境変数を指定することで変更できます。

$ export PORT=9999

ファイヤーウォールの中にいる場合は、このポートで外からhubotにアクセスできるようにしておきましょう。
EC2の場合はセキュリティグループを確認しておきましょう。
そして、あなたのHubot HTTPインターフェースへのURLをSlackのHubot URLの欄に記入します。

Screen Shot 2014-07-08 at 11.23.14 PM

これで、SlackはHubotへのメッセージをこのHTTPインターフェースに送信するようになります。
さあ、Hubotを起動してみましょう。

$ ./bin/hubot --adapter slack

Screen Shot 2014-07-08 at 11.26.54 PM

成功です!
Enjoy ChatOps!

おサイフケータイをキャリア契約無しのAndroidスマホで利用する方法

どうも、食あたりで悶絶中のnoradaikoです。
気を紛らすためにブログを書きます。

おサイフケータイを使ってみたい

僕はiPhone 3GSの時代からずっとiPhoneなんですよね。
おサイフケータイやスマート決済というものが日本では普及していてとても便利なのに、全くその恩恵に与れないのは勿体ないと感じたのがきっかけです。
しかし、それだけのためにキャリアを2台分も契約するのはちょっとコストパフォーマンスが悪すぎる。

そこで、キャリア契約無しのAndroidスマホおサイフケータイを使おうと検討したところ、いろいろ注意点が分かったのでここにまとめたいと思います!

おサイフケータイ対応のスマホを選ぼう

HTC J One

Androidスマホは実に沢山の種類が出ていますが、中にはおサイフケータイに非対応のものがあるので注意が必要です。
例えば、GoogleのNexusは非対応です。

僕は、HTC J Oneを購入しました!2013年の夏モデルです。
ちなみに海外向けのHTC Oneは非対応です。あぶねえ!
キャリアはauで、SIM無しです。

モバイルSuicaはSIM無しでは使えない

ここで衝撃的な事実です。
モバイルSuicaは、専用アプリの起動時にSIMカードが入っているかどうかを確認する処理があります。
もし購入したスマホがSIM無しだった場合、アプリが起動後に即終了します。
つまり、利用にはSIMが必要なんですね。
ちなみに、nanacoや楽天Edy、WAONは正しく設定できました。

解約済SIMカードを購入しよう

au SIM Card

上述の通り、モバイルSuicaを使うために正規のSIMカードが必要です。
実際に差してみるまで確証がなかったのですが、SIMは解約済みのもので大丈夫みたいです。

SIMカードには2つの軸で種類があるので注意です。それは、サイズバージョンです。
購入したAndroidスマホの仕様欄をよく見て、どれが対応しているのか必ず確認しましょう。
HTC J Oneの場合は、下記のタイプでした。

au Micro ICカード(LTE)

詳しくは au ICカード(Wiki)を参照してください。
ちなみに僕はYahoo!オークションで購入しました。

電波が無くても大丈夫

無事モバイルSuicaを設定できたものの、一点気になる事があります。
それは、キャリア契約をしていないので当然通常はネットワーク切断の状態である事です。
ドキドキしながら改札にタッチしてみたところ、通れました!!

これは、NFCチップ間の通信で読み書きが完結しているためだと思います。
Android OSはもはや関係ないという事ですね。

チャージにはネット接続が必要

アプリはチャージする際にインターネット通信をしている様子。
なので、もし外でチャージが必要になったらその場でインターネット接続が必要になります。

iPhoneのテザリングでネット接続しよう

AndroidとiPhoneとのテザリング方法にはいくつかありますが、Bluetoothテザリングは出来ません。ペアリングが何度やっても失敗します。
ここではWi-Fiを使ったテザリングをしましょう。以下のページが参考になります。

iPhoneでインターネット共有設定が完了したら、Android側でWi-Fi接続の設定画面を開き、iPhone端末を選択しましょう。
あっさり繋がったと思います。

iPhoneはいつおサイフケータイに対応するのか

AppleはiBeacon推しのようですから、NFC搭載は微妙なところですね。
仮にNFCが搭載されたとしても、日本独自のおサイフケータイ規格には対応しない気がします。

Fluentdのtd-agentをDebian Wheezyにインストールする方法

fluentd

現在、td-agentが正式にしているのは Ubuntu 12.04 LTS /PreciseUbuntu 10.04 LTS / Lucid で、Debianは悲しいかな非対応。
現時点での最新版のバージョンは 1.1.18-1 だ。
precise向けのdebパッケージは、Wheezyでは libc6 (>= 2.14) の依存関係が満たせないので入れられない。

Debian Wheezy(7.1) に td-agent をインストールしようとして躓いたのでメモ を参照すると、どうやら少し古い v1.1.17-1 だと入れられるようだ。
念のため、本ページでもリンクを貼っておく:

調査メモ

v1.1.17-1のcontrolファイルは以下のようになっている。

Package: td-agent
Version: 1.1.17-1
Architecture: amd64
Maintainer: Kazuki Ohta <k @treasure-data.com>
Installed-Size: 76728
Depends: libc6 (>= 2.11), libssl0.9.8 (>= 0.9.8k-1), libxml2 (>= 2.7.4), libxslt1.1 (>= 1.1.18), libyaml-0-2, zlib1g (>= 1:1.2.2)
Recommends: ntp
Section: Network
Priority: optional
Homepage: http://treasure-data.com/
Description: A data collector agent for Treasure Data

最新版(v1.1.18-1)のcontrolファイル:

Package: td-agent
Version: 1.1.18-1
Architecture: amd64
Maintainer: Kazuki Ohta </k><k @treasure-data.com>
Installed-Size: 92187
Depends: libc6 (>= 2.14), libssl1.0.0 (>= 1.0.0), libxml2 (>= 2.7.4), libxslt1.1 (>= 1.1.25), libyaml-0-2, zlib1g (>= 1:1.2.2), libssl0.9.8
Recommends: ntp
Section: Network
Priority: optional
Homepage: http://treasure-data.com/
Description: A data collector agent for Treasure Data

libssl1.0.0libssl0.9.8の二つに依存しているのは置いといて、libc6のバージョンが2.14に上がっている。
Wheezyはlibc6(v2.13-38)なので、問題はlibc6の依存関係。

debファイルをいじってv2.13に下げてインストールすると、以下のエラーを得た:

% sudo dpkg -i td-agent_1.1.18-1_amd64.deb
[sudo] password for nora:
Selecting previously unselected package td-agent.
(Reading database ... 66348 files and directories currently installed.)
Unpacking td-agent (from td-agent_1.1.18-1_amd64.deb) ...
Setting up td-agent (1.1.18-1) ...
Adding system user `td-agent' (UID 106) ...
Adding new group `td-agent' (GID 108) ...
Adding new user `td-agent' (UID 106) with group `td-agent' ...
Not creating home directory `/home/td-agent'.
Installing default conffile /etc/td-agent/td-agent.conf ...
insserv: script walknote-v2-share: service mongodb already provided!
insserv: script sspe-connection_queue_invocator: service mongodb already provided!
insserv: script pptp-etoile: service mongodb already provided!
insserv: script walknote-v2-push_notifier: service mongodb already provided!
insserv: script walknote-v2-usertrack_maintainer: service mongodb already provided!
/bin/echo: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.14' not found (required by /usr/lib/fluent/jemalloc/lib/libjemalloc.so)
start-stop-daemon: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.14' not found (required by /usr/lib/fluent/jemalloc/lib/libjemalloc.so)
.

libjemalloc.soが2.14を切望している。
この時点で、Wheezy向けにパッケージ作成するのは諦めた。

Treasure Dataさん、Wheezy対応ぜひお願いします。

ターミナルが文字化けした時の対処方法

Screen Shot 2014-02-08 at 7.46.36 PM

朝起きたら、ログを表示していたターミナルがエラい事に・・。
上のスクショのようにアルファベットさえ文字化けしてしまう事ってたまにありますよね。
例えば、バイナリファイルを開いてしまった時とか。
この状態になると、いつもターミナルを一旦閉じて開き直していました。

が。

ターミナルを開き直さなくても元に戻せる方法があったのです!!
方法は簡単。

#!bash
echo ^[c

エスケープシーケンスのcを入力するので、下記のようなキーボード入力を行います。

echo <ctrl -V><esc>c<enter>

ほら!!
なおりましたね 🙂
tmux内のttyでも有効なので、お試しあれ^^

[vim] 挿入モードからノーマルモードに戻った時にペーストモードを自動で解除する方法

vim

vimで長い文字列をコピペする時は、オートインデントやオートコンプリートをオフにするために、:set pasteでペーストモードに入りますよね。で、挿入モードになって、ペーストする。
最後に、ノーマルモードに戻って:set nopasteで解除。

めんどい!!

そんなずっとペーストモードでいる事は無いので、挿入モードからノーマルモードに戻る時に自動で解除して欲しいですよね。
その方法をご紹介します!
チョー簡単。下記コマンドを打つだけ!

" Turn off paste mode when leaving insert
autocmd InsertLeave * set nopaste

Auto commandを使って、InsertLeaveイベント時にset nopasteコマンドを実行するという意味です。
これを.vimrcに書いておけばOKです!