まるまるこふこふ

数々の次元が崩壊し、全ての生命が塵と化すのを見てきた。私ほどの闇の心の持ち主でも、そこには何の喜びも無かった。

MySQL の order by と index

MySQLの order by と index の仕組みがわからなくなったので調査。

前提の自分の仮定

  • MySQLは降順インデックスをサポートしないので order by desc にインデックスを使用できない
  • (user_id, point)という複合インデックスがあれば、where user_id = ? order by point ascというクエリはインデックスを最大限に使用できる

準備

MySQLのバージョンは 5.1.61

CREATE TABLE `sample` (
  `id` int(10) unsigned NOT NULL,
  `user_id` int(10) unsigned NOT NULL,
  `point` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `i1` (`user_id`,`point`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
mysql> select count(*) from sample;
+----------+
| count(*) |
+----------+
|  1048576 |
+----------+
1 row in set (0.22 sec)

試す

mysql> explain select * from sample where user_id = 50;
+----+-------------+--------+------+---------------+------+---------+-------+------+-------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref   | rows | Extra       |
+----+-------------+--------+------+---------------+------+---------+-------+------+-------------+
|  1 | SIMPLE      | sample | ref  | i1            | i1   | 4       | const |   98 | Using index |
+----+-------------+--------+------+---------------+------+---------+-------+------+-------------+
1 row in set (0.00 sec)

user_id を where 句に指定すると当然i1インデックスを使用してselectできる。 key_lenint型なので4

mysql> explain select * from sample where user_id = 50 and point = 1000;
+----+-------------+--------+------+---------------+------+---------+-------------+------+-------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref         | rows | Extra       |
+----+-------------+--------+------+---------------+------+---------+-------------+------+-------------+
|  1 | SIMPLE      | sample | ref  | i1            | i1   | 8       | const,const |    1 | Using index |
+----+-------------+--------+------+---------------+------+---------+-------------+------+-------------+
1 row in set (0.00 sec)

user_id 及び point を指定しても、i1インデックスを最大限利用して select できる。key_len8なので、user_id及びpointまで インデックスを使用している。

mysql> explain select * from sample order by user_id asc limit 10;
+----+-------------+--------+-------+---------------+------+---------+------+------+-------------+
| id | select_type | table  | type  | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+--------+-------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | sample | index | NULL          | i1   | 8       | NULL |   10 | Using index |
+----+-------------+--------+-------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

user_idascで order by。limitに10を指定してrowsが10なので インデックスを使用して絞っていることがわかる。

mysql> explain select * from sample order by user_id desc limit 10;
+----+-------------+--------+-------+---------------+------+---------+------+------+-------------+
| id | select_type | table  | type  | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+--------+-------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | sample | index | NULL          | i1   | 8       | NULL |   10 | Using index |
+----+-------------+--------+-------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

ascでなくdescでも同様。

mysql> explain select * from sample order by user_id asc, point asc limit 10;
+----+-------------+--------+-------+---------------+------+---------+------+------+-------------+
| id | select_type | table  | type  | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+--------+-------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | sample | index | NULL          | i1   | 8       | NULL |   10 | Using index |
+----+-------------+--------+-------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

(user_id, point)の複合キーなので、order by に user_id と pointを 指定しても絞り込める。

mysql> explain select * from sample order by user_id desc, point desc limit 10;
+----+-------------+--------+-------+---------------+------+---------+------+------+-------------+
| id | select_type | table  | type  | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+--------+-------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | sample | index | NULL          | i1   | 8       | NULL |   10 | Using index |
+----+-------------+--------+-------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

こちらもdescでも同様。

mysql> explain select * from sample order by user_id asc, point desc limit 10;
+----+-------------+--------+-------+---------------+------+---------+------+---------+-----------------------------+
| id | select_type | table  | type  | possible_keys | key  | key_len | ref  | rows    | Extra                       |
+----+-------------+--------+-------+---------------+------+---------+------+---------+-----------------------------+
|  1 | SIMPLE      | sample | index | NULL          | i1   | 8       | NULL | 1049137 | Using index; Using filesort |
+----+-------------+--------+-------+---------------+------+---------+------+---------+-----------------------------+
1 row in set (0.00 sec)

ascdescが混合するとインデックスが有効に活用されない。

mysql> explain select * from sample order by user_id desc, point asc limit 10;
+----+-------------+--------+-------+---------------+------+---------+------+---------+-----------------------------+
| id | select_type | table  | type  | possible_keys | key  | key_len | ref  | rows    | Extra                       |
+----+-------------+--------+-------+---------------+------+---------+------+---------+-----------------------------+
|  1 | SIMPLE      | sample | index | NULL          | i1   | 8       | NULL | 1049137 | Using index; Using filesort |
+----+-------------+--------+-------+---------------+------+---------+------+---------+-----------------------------+
1 row in set (0.00 sec)

desc asc の順でも同様。

mysql> explain select * from sample order by user_id asc, id asc limit 10;
+----+-------------+--------+-------+---------------+------+---------+------+---------+-----------------------------+
| id | select_type | table  | type  | possible_keys | key  | key_len | ref  | rows    | Extra                       |
+----+-------------+--------+-------+---------------+------+---------+------+---------+-----------------------------+
|  1 | SIMPLE      | sample | index | NULL          | i1   | 8       | NULL | 1049137 | Using index; Using filesort |
+----+-------------+--------+-------+---------------+------+---------+------+---------+-----------------------------+
1 row in set (0.00 sec)

pointでなくidをorder by に含めるとインデックスが効かない

mysql> explain select * from sample order by id asc,user_id asc limit 10;
+----+-------------+--------+-------+---------------+------+---------+------+---------+-----------------------------+
| id | select_type | table  | type  | possible_keys | key  | key_len | ref  | rows    | Extra                       |
+----+-------------+--------+-------+---------------+------+---------+------+---------+-----------------------------+
|  1 | SIMPLE      | sample | index | NULL          | i1   | 8       | NULL | 1049137 | Using index; Using filesort |
+----+-------------+--------+-------+---------------+------+---------+------+---------+-----------------------------+
1 row in set (0.00 sec)

order by の順序を逆にしてもしかり。

mysql> explain select * from sample order by id asc limit 10;
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------+
| id | select_type | table  | type  | possible_keys | key     | key_len | ref  | rows | Extra |
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------+
|  1 | SIMPLE      | sample | index | NULL          | PRIMARY | 4       | NULL |   10 |       |
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------+
1 row in set (0.00 sec)

idをorder by に指定すると、idはPRIMARY KEY なのでそっちの インデックスが使用される。

mysql> explain select * from sample where user_id = 50 order by point limit 10;
+----+-------------+--------+------+---------------+------+---------+-------+------+--------------------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref   | rows | Extra                    |
+----+-------------+--------+------+---------------+------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | sample | ref  | i1            | i1   | 4       | const |   98 | Using where; Using index |
+----+-------------+--------+------+---------------+------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

where 句にuser_id, order by にpointを使用すると、user_idだけ インデックスを使用して探索される。

mysql> explain select * from sample where user_id = 50 order by point desc limit 10;
+----+-------------+--------+------+---------------+------+---------+-------+------+--------------------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref   | rows | Extra                    |
+----+-------------+--------+------+---------------+------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | sample | ref  | i1            | i1   | 4       | const |   98 | Using where; Using index |
+----+-------------+--------+------+---------------+------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

descを使用しても同様の結果。

結果

  • MySQLは降順インデックスをサポートしないが order by desc にインデックスを使用できる
  • (user_id, point)という複合インデックスがあっても、where user_id = ? order by point ascというクエリは user_id 部分までしかインデックスが使用されない

IDCFクラウド上に Elasticsearch 2.0 + kibana 4構築

IDCFクラウド上に Elasticsearch 2.0 + kibana 4を構築して
可視化して遊んでみます。

IDCFクラウドのS2 CentOS6.5です。

各種インストール

Elasticsearch は java で動くので java をインストール

# Install Java
sudo yum install -y java-1.8.0-openjdk
# Install Setting Elasticsearch 2.0
sudo rpm --import https://packages.elastic.co/GPG-KEY-elasticsearch
vim /etc/yum.repos.d/elasticsearch.repo

elasticsearch.repo に以下の記述をします。

[elasticsearch-2.0]
name=Elasticsearch repository for 2.0 packages
baseurl=http://packages.elastic.co/elasticsearch/2.x/centos
gpgcheck=1
gpgkey=http://packages.elastic.co/GPG-KEY-elasticsearch
enabled=1
# Install Elasticsearch 2.0
sudo yum install elasticsearch
chkconfig --add elasticsearch
sudo service elasticsearch start

日本語解析プラグインのkuromojiを入れます。

# Install Kuromoji
sudo yum install -y /usr/lib64/libssl3.so
/usr/share/elasticsearch/bin/plugin install analysis-kuromoji
# Install Kibana 4.3.0
wget https://download.elastic.co/kibana/kibana/kibana-4.3.0-linux-x64.tar.gz
tar xvzf kibana-4.3.0-linux-x64.tar.gz
mv kibana-4.3.0-linux-x64 kibana

sense は kibana のプラグインです。kibana の可視化とは関係ありませんが、
kibana 上から query を記述・発行できるエディタが使用できるので便利です。

# Install Sense
./bin/kibana plugin --install elastic/sense
# start kibana
cd kibana/
./bin/kibana >/dev/null 2>&1 &

ポートを開ける

kibana はデフォで5601ポートを使用するので、5601のポートを開放します。

kibana 4から静的ファイルベースでなく Node.jsのアプリケーションになったため、 Elasticsearch のポートを開ける必要はありません。

データを突っ込む

東方創想話というサイトがあります。
東方プロジェクトの二次創作SSが投稿されるサイトです。
実はこのサイト、JSON APIを公開しているので、それを取得して、Elasticsearchにぶっこむコードを書きます。

API · mfakane/Megalopolis Wiki · GitHub

JSONスクレイピングするスクリプトですが、Node.js を使って今回は書きました。

# Install Node
sudo yum install -y epel-release
yum install -y nodejs npm --enablerepo=epel

Node.js でスクレイピングするときのポイントですが、

  1. 非同期でhttpアクセスするので、逐次処理でアクセスしないと、一斉にアクセスされて先方に迷惑がかかる
  2. timestamp 型のデータを突っ込んでおくと、kibana で時系列順に可視化できて便利。

というのがあります。1については、Promise と Array.prototype.reduce を使うと解決できます。

コードは下記にて公開しておきます。使用される方は、先方に迷惑のかからないようにしてください。

github.com

kibana

kibana にアクセスします。デフォルトでは最新15分のデータを表示するので、2005年〜2010年辺りにデータを絞ってみます。

このように Elasticsearch にぶっこんだデータが表示されたことかと思います。

可視化

東方創想話にはタグ機能があるので、これでキャラを絞ってデータを可視化できたりします。

古明地こいしに絞って、Y軸にSSの投稿数、X軸に時系を取り、Area chartで表示しました。 東方地霊殿の発表が2008年頃なので、2008年ごろから投稿数が始まっているのも納得です。

こちらはLine chart 。八雲紫の投稿数をY軸に取って表示しています。

こちらは pie chart 。円グラフのことですね。タグで領域分けしているのですが、 うまいこといっておらず、1文字だけで領域分けされていますね。

これはインデックス時にタグを kuromoji でアナライズしちゃったせいですね。 terms で絞りたいカラムは not_analyzed にする必要があります。

最後に vertical bar chart。棒グラフのことですね。 創想話自体の投稿件数を時系列順に表示しています。

終わり

X軸・Y軸に取る値をさまざまに変えられるので、elasticsearch の aggregation 機能をよく知れば、 もっと面白いデータを見ることができるかもしれない。

【後編】Bootstrap テーマ Honoka を fork してみる

先日、Honoka を実際に fork して、Frandreという Bootstrapテーマを作ってリリースしました。

Frandre

Ubuntu 12.04を意識した配色となっております。 Bootwatch の United のHonoka バージョンだと 言えばわかりやすいでしょうか。

前回までの続き

前回では Honoka をちょっとカスタマイズする方法について 書きました。

後編では、Honoka をフォークしてそれっぽい Bootstrap テーマを作りたいと思います。

配色を変える

Honoka の配色は全て、scss/_variables.scss にまとまっています。

自動コンパイル

$ npm run-script server

上記を実行すると、sassファイルの変更を察知して、自動で sassをcssコンパイルしてくれます。 立ち上げておきましょう。

gh-pages の作成

Githubには gh-pages という機能があります。 詳しい解説は他のサイトに任せるとして、この機能を使うと Github上に静的サイトを構築できます。

Frandre の紹介サイトも http://sairoutine.github.io/Frandre/ という githubページ上に構築されています。

Honoka には gh-pages というブランチがあるので そのブランチに切り替えて index.html などを編集しましょう。

終わり

思ったより書くことがなかった。配色が scss/_variables.scss にまとまってるので、想像以上に手軽に Honoka の fork テーマは作れます。

みんなも積極的に Honoka を fork して自分だけの Bootstrap テーマを作ろう!

終わり

【前編】Bootstrap テーマ Honoka を fork してみる

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

Bootstrap テーマ「Honoka」

Bootstrap テーマに Honoka というのがあります。
「日本語も美しく表示できる」という謳い文句に加え、
アニメキャラにインスパイアされた配色設定が好みで、
社内ツールや個人のWebサイトなどでよく使わせて頂いてます。

そんな Honoka ですが、色違いのForkテーマとして
UmiNico などが存在します。

これは僕も Honoka を fork するしかないですね!

準備

Honoka をイジるにあたって、必要な技術知識は下記です。
ふわっとだけ調べて、概要を把握しておきます。

Node.js
Grunt
Bower
Ruby
Bundler
Sass

Build してみる

Honoka は Sass を使って書かれています。
build することで、 Sass -> cssコンパイルされるわけですね。

まずは Build を試してみます。

Build 方法は公式のWikiに既にまとまってます。

Wikiのとおりに各種 npm モジュールや bundle モジュールを入れましたら、
build を実行します。build手順はgruntによって定義されているので、私がしなくちゃいけないのは下記コマンド一発です。

$ grunt build

build された cssdist/css/ 以下に保存されます。git clone した段階で
既に build された css が存在します。build されたことを確認したいので、
一旦削除します。

$ rm ./dist/css/bootstrap.css
$ rm ./dist/css/bootstrap.min.css

dist/bootstrap-ja.html を表示してみましょう。bootstrap.css を削除しちゃったので、
見るに耐えないページが表示されたことだと思います。では build します。

$ grunt build
Running "clean:build" (clean) task
>> 7 paths cleaned.

Running "bower:install" (bower) task
>> Installed bower packages
>> Copied packages to /Users/hogehoge/Desktop/Honoka/dist

Running "setAutoPrefixerConfig" task

Running "scsslint:bootstrap" (scsslint) task
Running scss-lint on bootstrap
>> 5 files are lint free

Running "scsslint:assets" (scsslint) task
Running scss-lint on assets
>> 1 file is lint free

Running "sass:bootstrap" (sass) task

Running "sass:assets" (sass) task

Running "autoprefixer:bootstrap" (autoprefixer) task
>> 1 autoprefixed stylesheet created.

Running "autoprefixer:assets" (autoprefixer) task
>> 1 autoprefixed stylesheet created.

Running "csscomb:bootstrap" (csscomb) task
>> Using custom config file "bower_components/bootstrap/less/.csscomb.json"...
>> Sorting file "dist/css/bootstrap.css"...

Running "csscomb:assets" (csscomb) task
>> Using custom config file "bower_components/bootstrap/less/.csscomb.json"...
>> Sorting file "dist/assets/css/example.css"...

Running "cssmin:minify" (cssmin) task
>> 1 file created. 165.35 kB → 134.81 kB

Running "replace:banner" (replace) task

Done, without errors.

成功しました。./dist/cssbootstrap.cssbootstrap.min.css ができていることだと思います。
先ほど開いた dist/bootstrap-ja.html をリロードするとキレイなページに戻りましたね。

ちょっと加工してみる

honoka の背景色は灰色っぽい色ですが、これを白 #fff に変更したいと思います。

build される前の scss は ./scss にあります。

背景色が定義されているのは ./scss/honoka/_variables.scss です。
35行目付近に下記の記述があるので #f9f9f9 -> #fff に書き換えます。

//** Background color for `<body>`.
$body-bg:               #f9f9f9 !default;

再度 build

$ grunt build

dist/bootstrap-ja.html をリロードすると、背景が白くなりました!

次回

長くなったので後編に続きます。

【後編】Bootstrap テーマ Honoka を fork してみる - まるまるこふこふ

ログ出力ライブラリ node-bunyan

この記事はNode.js Advent Calendar その2 の5日目の記事です。

express のアプリケーションログ

express でアプリケーションログを出力したい。

express-generator などで express アプリを作ると、最初から morgan
使われていますが、これはアクセスログを保存するもので、
アプリケーションログを出力するものではありません。

有名ドコロを探ってみたところ、log4jsbunyan があるみたいなので、 bunyan 使います。

バニヤンと読むみたいです。ぶーにゃんって読んでました。ぶーにゃんかわいいよぶーにゃん。

インストール

npm install bunyan

インストールしましょう。インストールすると bunyan コマンドという
ログをいい感じに見やすくするツールもついてきます。これは後述します。

Introduction

$ cat hi.js
var bunyan = require('bunyan');
var log = bunyan.createLogger({name: 'myapp'});
log.info('hi');
log.warn({lang: 'fr'}, 'au revoir');

こんな感じのスクリプトを書いて実行します。 name にはアプリの名前をお好きに書いてあげてください。

$ node hi.js
{"name":"myapp","hostname":"banana.local","pid":40161,"level":30,"msg":"hi","time":"2013-01-04T18:46:23.851Z","v":0}
{"name":"myapp","hostname":"banana.local","pid":40161,"level":40,"lang":"fr","msg":"au revoir","time":"2013-01-04T18:46:23.853Z","v":0}

実行するとこんな感じのログが標準出力されます。bunyan は
JSONフォーマットでログ出力してくれるので
fluentd などでログを扱いやすいのが特徴です。

level というのはエラーレベルのことですね。bunyan には下記6つのAPIでログを出力することが
でき、それぞれ危険度に応じて level が設定されており、それが出力されております。

var bunyan = require('bunyan');
var log = bunyan.createLogger({name: 'myapp'});

log.trace('trace log'); // level 10
log.debug('debug log'); // level 20
log.info('info log'); // level 30
log.warn('warn log'); // level 40
log.error('error log'); // level 50
log.fatal('fatal log'); // level 60

bunyan コマンド

JSONフォーマットはコンピュータには扱いやすいですが、人間が読むことには適していません。 先ほどご紹介した bunyan コマンドを使うと、見やすい形に整形してくれます。

$ node hi.js | ./bin/bunyan
[2013-01-04T19:01:18.241Z]  INFO: myapp/40208 on banana.local: hi
[2013-01-04T19:01:18.242Z]  WARN: myapp/40208 on banana.local: au revoir (lang=fr)

標準出力からJSONフォーマットのログの内容を受けて、標準出力に整形後のログを
吐き出します。

※実際にはカラーリングされてて、より読みやすいです。

ログの出力設定

bunyan はデフォルトでは info 以上のログを標準出力に吐くようになってます。

// デフォルトの設定
var log = bunyan.createLogger({
    name: 'myapp',
    stream: process.stdout,
    level: 'info'
});

こんな感じで出力先をファイルにすることができます。

var log = bunyan.createLogger({
  name: 'myapp',
  streams: [
    {
      level: 'error',
      path: '/var/tmp/myapp-error.log'
    }
  ]
});

配列になってることからも分かる通り、出力先を複数設定することも可能です。

// info以上 -> 標準出力
// error以上 -> myapp-error.logに出力
var log = bunyan.createLogger({
  name: 'myapp',
  streams: [
    {
      level: 'info',
      stream: process.stdout
    },
    {
      level: 'error',
      path: '/var/tmp/myapp-error.log'
    }
  ]
});

ログファイルのローテーション

ログ・ファイルのローテーションはLinux の logratate を使っても良いのですが、
bunyan は自前でログローテション機能も持ってます。

var log = bunyan.createLogger({
    name: 'foo',
    streams: [{
        type: 'rotating-file',
        path: '/var/log/foo.log',
        period: '1d',
        count: 3
    }]
});

これで

/var/log/foo.log # 今日のログ
/var/log/foo.log.0 # 前日のログ
/var/log/foo.log.1 # 前々日のログ
/var/log/foo.log.2 # 前々々日のログ

と 1日ごとに 3つのログをローテーションしてくれます(それ以降の数のログは削除されます)

src

var log = bunyan.createLogger({src: true, ...});

createLogger 時に src: trueを設定すると、

{
  "name": "src-example",
  "hostname": "banana.local",
  "pid": 123,
  "component": "wuzzle",
  "level": 4,
  "msg": "This wuzzle is woosey.",
  "time": "2012-02-06T04:19:35.605Z",
  "src": {
    "file": "/Users/trentm/tm/node-bunyan/examples/src.js",
    "line": 20,
    "func": "Wuzzle.woos"
  },
  "v": 0
}

とより詳細な情報を吐いてくれます。開発環境で実行する時は true にしておくといいかも。

終わり

GithubREADME にもっとたくさんいいこと書いてあるよ!

IDCFクラウドを使った個人Webサービス構築

さいです。先日、東方Project の二次創作同人誌の感想を投稿・共有できる 香霖堂書店というのをリリースしました。(※ 2019/03/29 閉鎖しました)
現状、ユーザー数約50名、PV/1day 約200程度で
運営させていただいてます。

個人でWebサービスを作ることについては後日またお話させていただく
として、今回はサーバ構成についての話をさせていただきたいと思います。

サーバ構成

プロキシサーバー
Nginx です。sai-chan.com ドメイン香霖堂書店と一緒に
自分のポートフォリオサイトも運営しているため、
ユーザーのアクセスは、バーチャルホスト機能で、
www.sai-chan.com を ひまわり鎮痛剤 サーバーに、
korindo.sai-chan.com を 香霖堂書店に振り分けています。

個人ポートフォリオサイトAPサーバー
自分の自己紹介サイトです。⇛ ひまわり鎮痛剤
Apacheです。香霖堂書店とはまた別のサイトとして、
静的ファイルだけで構成されています。

香霖堂書店APサーバー
Node.js で動くアプリケーションサーバーです。
香霖堂書店に関わる動的ファイル、静的ファイル両方を配信しています。
またユーザーがアップロードした画像はAPサーバーに保存されています。

香霖堂書店DBサーバー
MySQL, Redis, ElasticSearch が同居して稼働しています。
データは基本的にMySQLに保存し、場合に応じて
キャッシュをRedisに、全文検索に必要なデータをElasticSearch に、
二重に保存しています。

開発用サーバー
開発は全てsshでこの開発用サーバーに入ってやってます。
本番環境と同じ環境を構築してるので、動作確認などが容易です。

クラウドサービス

全てのサーバーは IDCFクラウド のS1で構成されています。
個人Webサービスにおいて、レンタルサーバーや VPS、herokuのような IaaS を使わず、
クラウドサービスを利用する利点は、スケールアウトが容易なことだと思います。

クラウドサービスはサーバーの追加・削除が容易なので、Webサイトの負荷が増えたときに、
サーバのスペックを上げる方法だけでなく、サーバーを増やすことでも対応することができます。

IDCFクラウドはサーバーの追加・削除ができるAPIも公開してますので、
コマンド1つでAPサーバーの追加、あるいはアクセス数を監視して自動で
APサーバーの追加・削除を行う仕組みなんかも作れます。

IDCF クラウド

クラウドサービスだと有名ドコロに AWS や さくらクラウドなどあると思いますが、
IDCF クラウドを選択した理由は3点あって、

・日本語
・料金体系が明確

です。管理画面、マニュアルやドキュメントが全て日本語なので、
英語の苦手な私でも容易に触れます。

また最小スペックのS1だと料金が月々最大500円とおそらく類似のクラウドサービスの中で
一番安く また最大500円と謳っているのはクラウド初心者の私にとって

「サーバーをつけっぱなしにするとどれだけお金をとられるのかわからない」

みたいな不安がなく安心でした。

IDCFクラウドのS1で構築する際のポイント

先程も書きましたが、全てのサーバーはIDCFクラウドの中で一番安いS1(1台辺り月500円)で
構成されています。 S1のスペックは CPU1コア、メモリ1GB、ディスク15GBと最低辺です。

ネックになるのはCPUとメモリです。CPUやメモリ不足でサービスがダウンしないよう、
香霖堂書店ではキャッシュ処理やNodeによる非同期処理を避けて、レスポンスが遅くても
確実にページを返すようにしています。

一応、今のところそのような問題は起きてなく、一番処理の重そうなElasticSearchによる
全文検索処理でも 約0.025 秒 でレスポンスを返せてます。

またディスクについては15GBと割りと容量が余るので、ユーザーのアップロードした画像は、
各サイズのサムネイルを作ってディスクに保存し、リクエストのたびにサムネイルを作ることを
避けています。

その他IDCFクラウドの便利な点

新規サーバー作成がだいたい30秒もあれば終わります。サーバーのテンプレート機能もあるので、
ミドルウェアの性能検証なんかでサーバーを作ったり潰したりすることが多いので助かってます。

あとサーバー監視に Mackerel のFreeプランを使ってますが、IDCFクラウドのユーザーは
監視できるホスト数が無限だったり、グラフを1週間分表示できたりするので良いです。

終わり

というわけでみなさま、よいクラウドライフを。

Node.js の ActiveRecord ライブラリ Knex.jsを使う

今日の知見です〜。

Node.js からActiveRecordを用いてRDBMSにアクセスするに辺り、 knex.jsが便利でした。

var knex = require('knex')({
  client: 'mysql',
  connection: {
    host     : '127.0.0.1',
    user     : 'your_database_user',
    password : 'your_database_password',
    database : 'myapp_test'
  },
  pool: {
    min: 0,
    max: 7
  }
});

knex
.select('*')
.from('test')
.where('id', id)
.then(function(rows) {
  console.log(rows);
})
.catch(function(err) {
  console.log(err);
});

雑にサンプル書いたけど、コネクションプーリングをknex側でやってくれるのが良い。 インターフェイスはcallbackでもPromiseでも選べる(上のサンプルはPromiseで書いてる)

selectを2回発行するときとか

javascriptの非同期に慣れていないとよくやってしまう失敗だが、 1回目のselect の結果を受けて2回目のselect を発行するとき、

var user_id;
knex
.select('user_id')
.from('user')
.where('id', id)
.then(function(rows) {
  user_id = rows[0].user_id;
})
.catch(function(err) {
  console.log(err);
});

knex
.select('*')
.from('detail')
.where('user_id', user_id)
.then(function(rows) {
  console.log(rows);
})
.catch(function(err) {
  console.log(err);
});

user_id を取得してdetail テーブルからデータを引くコードだが、上記のコードはうまく動かない。 2つのselect 文が非同期に実行されるので、user_id を取得してから detail に select される保証はない。

次のように1つめのselect文のコールバックの中で2つめのselect文を呼べば select文の発行順番が保証される。

var user_id;
knex
.select('user_id')
.from('user')
.where('id', id)
.then(function(rows) {
  user_id = rows[0].user_id;
  knex
  .select('*')
  .from('detail')
  .where('user_id', user_id)
  .then(function(rows) {
    console.log(rows);
  })
  .catch(function(err) {
    console.log(err);
  });
})
.catch(function(err) {
  console.log(err);
});

コールバックがネストするので非常にコードが読みにくい。select文を発行するたびに ネストしないといけないのか?Promise はこうした callback地獄を解決するための手段だ。

var user_id;
knex
.select('user_id')
.from('user')
.where('id', id)
.then(function(rows) {
  user_id = rows[0].user_id;
  return knex
  .select('*')
  .from('detail')
  .where('user_id', user_id)
})
.then(function(rows) {
  console.log(rows);
})
.catch(function(err) {
  console.log(err);
});

1つめのthen で user テーブルへのselectが実行され、2つめのthen で detail テーブルへのselect が実行される。 両者のselect 文のどちらかでエラーが発生すれば最後のcatch でエラーが拾われる。 わかりやすい。

DBへの接続をシングルトンで保持する。

複数のライブラリからknexを呼ぶ場合、各ライブラリでknexを初期化すると その文だけDBへのコネクションが増えるので、knex インスタンスを1つに保持しつつ、 複数のライブラリからknex を使用したい。 そのような場合、シングルトンなクラスを作る必要がある。

Node.js の require の仕組みはシングルトンを非常に作りやすい。

// knex-singleton.js
var knex = require('knex')({
  client: 'mysql',
  connection: {
    host     : '127.0.0.1',
    user     : 'your_database_user',
    password : 'your_database_password',
    database : 'myapp_test'
  },
  pool: {
    min: 0,
    max: 7
  }
});
module.exports = knex;

このようなモジュールを作っておき、各ライブラリから

var knex = require('./knex-singleton');

knex.select('*').from('user').then(function(rows){ console.log(rows); });

こんな感じで使える。knex-singleton が何回呼び出されようが、knex インスタンスは常に1つになる。