“考えない”アーキテクチャを実現する Now+S3 の手法とパフォーマンス

Now という PaaS に注目しています。

Now については [こちらの記事](http://qiita.com/aggre/items/f0cb9f8b8e8c54768e50) で詳しく触れているのでここでは省略しますが、Now は RDS や DynamoDB のようなデータベースに相当するサービスを提供していません。

今回は Now で使用するデータベースとして S3 を使ってみたので、その手法とパフォーマンスをまとめました。

これによって、サーバもデータベースもデプロイのことすらも **考えない** 構成になり、開発者が開発に集中することができます。

# TL;DR

ユーザー情報を [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) で暗号化して S3 に書き込み/読み込みするというアプリケーションを開発しました。このプロジェクトは [FRAME00]( http://frame00.com ) というサービスの試験的な API です。

Now + S3 を使うとき、Node が返すまでのパフォーマンスと、HTTP の総合的なパフォーマンスを計測しました。

Node と HTTP( Total ) のそれぞれで最良の結果を出したのは、S3 を北カリフォルニアリージョンで使用することでした。以下がその結果の幾何平均値です。

| | Node | Total |
| --- | --- | --- |
| 読み込み | 47.8014ms  | 869.7705ms |
| 書き込み | 88.5056ms | 891.4494ms |

Node と Total がかなり開く結果になったのは、Now のあるカリフォルニアと東京の地理的な問題でしょう。

このパフォーマンスをどう捉えるかについては、後ろのほうでまとめています。

では今回のプロジェクトについて詳しく見ていきます。

# ベネフィット

まず、S3 をデータベースとして扱うことで以下のようなベネフィットが得られると考えました。

- スケーリングが一切不要
- レプリケーションが一切不要
- 完全にスキーマレス

データベースではなくただのファイルシステムのように振る舞うため、データベース固有の問題とは無縁になります。また私自身がデータベースの技術に精通しているわけではなかったので、データベースを扱う際の *見えない不安* とも無関係になる、という個人的なベネフィットもありました。

Now によってサーバと、サーバへのデプロイは考える必要がなくなります。さらにデータベースも S3 にすることで **考えない** という構成ができそうです。

逆に心配だったのがパフォーマンスというわけです。

# アプリケーション

今回計測に使用したアプリケーションは、ユーザー機能をもつサービスのためのユーザー管理 API です。

メールアドレスなどユーザー固有のキーからデータを取得できれば良いという要件のため、リレーショナルである必要がなく、今回の狙いに最適でした。

Now で使用するサーバとして [micro](https://github.com/zeit/micro) を使いました。

今回のソースコードです。

```js:index.js
'use strict'

require( 'now-logs' )( '...Now Token String...' )

const url = require( 'url' )
const querystring = require( 'querystring' )
const { json, send } = require( 'micro' )
const api = require( './api' )
const cors = require( './lib/cors' )

module.exports = async function ( req, res ) {
	const parsedUrl = url.parse( req.url )
	const method = req.method.toLowerCase()
	console.time( method )

	res = cors( res )

	if ( method === 'options' ) {
		return ''
	}

	const token = req.headers.authorization ? req.headers.authorization.replace( /Bearer (.+)/, '$1' ) : null
	let payload
	let data

	switch ( method ) {
		case 'get':
			payload = querystring.parse( parsedUrl.query )
			break
		default:
			payload = await json( req )
	}
	switch ( parsedUrl.pathname ) {
		case '/users':
			data = await api.users[ method ]( payload, token )
			break
		case '/verifies':
			data = await api.verifies[ method ]( payload, token )
			break
		default:
			break
	}

	console.timeEnd( method )
	send( res, data.status, data.payload )
}
```

パフォーマンス計測のために `now-logs` を入れています。

HTTP メソッドが `get` ならユーザーの取得、`post` ならユーザーの追加、`put` ならユーザーの更新といった具合に動きます。

## Get のとき

`get` の際に呼び出される API はこのようにしました。

```js:/api/users/get.js
const token = require( '../../lib/token' )
const user = require( '../../lib/user' )

module.exports = async function ( payload, auth ) {
	const tokenDecode = await token.verify( auth )

	if ( !tokenDecode ) {
		return { status: 405, payload: { success: false, message: 'Authentication failed.' } }
	}

	let userData = await user.get( tokenDecode.mail )
	if ( !userData ) {
		return { status: 500, payload: { success: false, message: 'Could not get user information.' } }
	}

	delete userData.password

	return { status: 200, payload: { success: true, message: 'User information was acquired.', user: userData } }
}
```

トークンを `jsonwebtoken` でデコードして、デコード結果の中に含まれているはずのメールアドレスをキーとして S3 からユーザー情報を取得します。

---

`token` や `user` の詳細は省きますが、実際に S3 からユーザーを取得するコードはこんな感じです。

```js:/lib/user/index.js
user.get = async function ( mail ) {
	const params = {
		Bucket: bucket,
		Key: mail
	}
	const userData = await ( new Promise( async resolve => {
		s3.getObject( params, ( err, data ) => {
			resolve( err ? false : data.Body.toString( 'utf-8' ) )
		} )
	} ) )
	if ( !userData ) {
		return false
	}

	const verify = await token.verify( userData )
	if ( !verify ) {
		return false
	}

	return verify
}
```

`s3` は `aws-sdk` の S3 です。

S3 からオブジェクトの取得を試みたあと、`jsonwebtoken` でデコードして返します。オブジェクトが取得できないかデコードに失敗すれば `false` です。

## Post のとき

`post` の際に呼び出される API はこのようになります。

```js:/api/users/post.js
const token = require( '../../lib/token' )
const user = require( '../../lib/user' )

module.exports = async function ( payload ) {
	const mail = payload.mail
	const password = payload.password
	const userdata = {
		mail: mail,
		password: password
	}

	const userCheck = await user.check( mail )
	if ( !userCheck ) {
		return { status: 405, payload: { success: false, message: 'User already exists.' } }
	}

	const userToken = token.create( userdata )

	const userCreate = await user.create( mail, userToken )
	if ( !userCreate ) {
		return { status: 500, payload: { success: false, message: 'User could not register.' } }
	}

	delete userdata.password

	return { status: 200, payload: { success: true, message: 'User has been registered.', token: userToken, user: userdata } }
}
```

ユーザーのキーが重複していないか( S3 のオブジェクトがすでに存在していないか )を確認したあとでユーザーを追加しています。

---

重複の確認をするコードは、こんな感じです。

```js:/lib/user/index.js
user.check = async function ( mail ) {
	const params = {
		Bucket: bucket,
		Key: mail
	}
	const userCheck = await ( new Promise( async resolve => {
		s3.headObject( params, err => {
			resolve( Boolean( err ) )
		} )
	} ) )
	return userCheck
}
```

S3 オブジェクトのメタデータの取得を試みて、その成否で存在の有無を確認します。

実際に S3 に保存するコードはこうしました。

```js:/lib/user/index.js
user.create = async function ( mail, data ) {
	const params = {
		Bucket: bucket,
		Key: mail,
		ContentType: 'text/plain',
		Body: data
	}
	const userCreate = await ( new Promise( async resolve => {
		s3.putObject( params, err => {
			resolve( !err )
		} )
	} ) )
	return userCreate
}
```

# 計測

計測を走らせるコードは [Gist](https://gist.github.com/aggre/4541c4b510b3ba3c9bce7880d7460204) に書いておきました。

ローカルと Now でそれぞれ、Node と HTTP のパフォーマンスを計測します。

ローカルのマシンスペックはこうです。

| CPU | Memory | OS |
| --- | --- | --- |
| Core i7-6500U @ 2.50GHz × 4 | 7.7 GiB | Ubuntu 16.10 |

> 使っているのは私の大好きな ThinkPad X1 Carbon です。第 1 世代から使い続けていて、いまは第 4 世代のマシンです。

通信回線には NURO 光を Wi-Fi で使います。下り最大 2Gbps / 上り最大 1Gbps ですが、ルーターの性能上、伝送速度が最大 1.3Gbps に制限されます。

Now は Node 7.0.0 がデフォルトなので、ローカルもバージョンを合わせておきます。

```bash
sudo n 7.0.0
```

[Gist](https://gist.github.com/aggre/4541c4b510b3ba3c9bce7880d7460204) にあるコードを走らせます。11 回ずつ計測して、初回のリクエストを集計から除外しました。

```bash
node --harmony-async-await performance.js http://0.0.0.0:3000/users 11
```

```bash
node --harmony-async-await performance.js https://xxxxx.now.sh/users 11
```

## 結果

除外した初回のリクエストも含めたすべての数値は [Gist](https://gist.github.com/aggre/306d2b13b8bcaac2ac7036c7e53c1689) にまとめました。

以下は幾何平均と算術平均です。

### ローカルで東京リージョンの S3 を使うとき

幾何平均

| | Node | Total |
| --- | --- | --- |
| 読み込み | 175.3314ms  | 183.4879ms |
| 書き込み | 273.0733ms | 279.2066ms |

算術平均

| | Node | Total |
| --- | --- | --- |
| 読み込み | 280.6925ms  | 286.9486ms |
| 書き込み | 335.9702ms | 341.5781ms |

### ローカルで北カリフォルニアリージョンの S3 を使うとき

幾何平均

| | Node | Total |
| --- | --- | --- |
| 読み込み | 693.8310ms  | 699.9803ms |
| 書き込み | 1420.2984ms | 1425.9576ms |

算術平均

| | Node | Total |
| --- | --- | --- |
| 読み込み | 695.8251ms  | 701.9779ms |
| 書き込み | 1421.4378ms | 1427.1385ms |

### Now で東京リージョンの S3 を使うとき

幾何平均

| | Node | Total |
| --- | --- | --- |
| 読み込み | 516.2726ms  | 1269.1633ms |
| 書き込み | 1097.0449ms | 1937.3974ms |

算術平均

| | Node | Total |
| --- | --- | --- |
| 読み込み | 517.7446ms  | 1276.1739ms |
| 書き込み | 1102.3161ms | 1941.3304ms |

### Now で北カリフォルニアリージョンの S3 を使うとき

幾何平均

| | Node | Total |
| --- | --- | --- |
| 読み込み | 47.8014ms  | 869.7705ms |
| 書き込み | 88.5056ms | 891.4494ms |

算術平均

| | Node | Total |
| --- | --- | --- |
| 読み込み | 52.9632ms  | 899.4771ms |
| 書き込み | 90.0111ms | 902.2481ms |

---

Now で東京リージョンの S3 をつかったとき、あまりに遅かったので `nslookup` で Now の IP アドレスを調べたところ北カリフォルニアでした。

そこで S3 を北カリフォルニアリージョンに変えたところ、やはりパフォーマンスがかなり改善できました。

Node の中だけなら数十 ms で完結しているものの、北カリフォルニアから東京までの距離が影響してその十倍ほどのレイテンシが発生してしまいました。

# 適性

今回のようにユーザー情報の取得であれば、ユーザー毎には低頻度のアクセスでしょうからレイテンシは許容しやすいかもしれません。

逆に、よりシビアなシーンでの利用には向かないと言えそうです。

とはいえ、Now( 北カリフォルニア ) - 東京間の地理的な問題が大きいので、Now ではなく他のサービスで S3 をデータベースに使うことは検討できると思います。

Now にフィットするケースは、**フロントエンド** か、**結果整合性な API**、または **要求の低い API** ということになりそうです。

フロントエンドや結果整合性な API であればリソースが CDN から配信できるので、距離の問題は解決できます。代わりに CDN の構築だけ求められますが、大抵はプログラミング不要です。多少大きなレイテンシがあってもユーザーが離れないというシーンにおいては API のバックエンドとして Now を活用することは十分にできそうです。

# Now

アプリケーションのことしか考えたくない開発者にとって、Now は興味深い選択肢を提供しています。まだ日本から使う場合のレイテンシは大きいですが、それをパスできるケースでは有用となるのではないでしょうか。