LoginSignup
20
18

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-02-12

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 を使いました。

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

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 はこのようにしました。

/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 からユーザー情報を取得します。


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

/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
}

s3aws-sdk の S3 です。

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

Post のとき

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

/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 のオブジェクトがすでに存在していないか )を確認したあとでユーザーを追加しています。


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

/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 に保存するコードはこうしました。

/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 に書いておきました。

ローカルと 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 は興味深い選択肢を提供しています。まだ日本から使う場合のレイテンシは大きいですが、それをパスできるケースでは有用となるのではないでしょうか。

20
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
18