Now という PaaS に注目しています。
Now については こちらの記事 で詳しく触れているのでここでは省略しますが、Now は RDS や DynamoDB のようなデータベースに相当するサービスを提供していません。
今回は Now で使用するデータベースとして S3 を使ってみたので、その手法とパフォーマンスをまとめました。
これによって、サーバもデータベースもデプロイのことすらも 考えない 構成になり、開発者が開発に集中することができます。
TL;DR
ユーザー情報を jsonwebtoken で暗号化して S3 に書き込み/読み込みするというアプリケーションを開発しました。このプロジェクトは FRAME00 というサービスの試験的な 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 を使いました。
今回のソースコードです。
'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 はこのようにしました。
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 からユーザーを取得するコードはこんな感じです。
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 はこのようになります。
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 のオブジェクトがすでに存在していないか )を確認したあとでユーザーを追加しています。
重複の確認をするコードは、こんな感じです。
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 に保存するコードはこうしました。
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 に書いておきました。
ローカルと 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 がデフォルトなので、ローカルもバージョンを合わせておきます。
sudo n 7.0.0
Gist にあるコードを走らせます。11 回ずつ計測して、初回のリクエストを集計から除外しました。
node --harmony-async-await performance.js http://0.0.0.0:3000/users 11
node --harmony-async-await performance.js https://xxxxx.now.sh/users 11
結果
除外した初回のリクエストも含めたすべての数値は Gist にまとめました。
以下は幾何平均と算術平均です。
ローカルで東京リージョンの 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 は興味深い選択肢を提供しています。まだ日本から使う場合のレイテンシは大きいですが、それをパスできるケースでは有用となるのではないでしょうか。