This article was contributed by The MemCachier Add-on
MemCachier manages and scales clusters of memcache servers so you can focus on your app. Tell us how much memory you need and get started for free instantly. Add capacity later as you need it.
follow @MemCachier on Twitter
Memcache を使用した Express.js アプリケーションのスケーリング
最終更新日 2020年06月12日(金)
Table of Contents
Memcache は、Web アプリとモバイルアプリバックエンドのパフォーマンスとスケーラビリティを改善する技術です。ページの読み込みが遅すぎる場合や、アプリにスケーラビリティの問題がある場合は、Memcache の使用を検討してください。小規模なサイトであっても、Memcache の導入によってページの読み込みを高速化し、将来の変化にアプリを対応させることができます。
このガイドでは、単純な Express 4 アプリケーションを 作成して Heroku にデプロイし、Memcache を追加してパフォーマンスのボトルネックを軽減する方法を示します。
前提条件
このガイドの手順を完了する前に、以下のすべての条件を満たしていることを確認してください。
- Node.js の知識がある (Express.js の知識もあれば理想的です)
- Heroku ユーザーアカウント (無料ですぐにサインアップ)
- 「Heroku スターターガイド (Node.js)」の手順を理解している
- Node.js、
npm
、Heroku CLI がコンピュータにインストールされている
Heroku への Express.js アプリケーションのデプロイ
Express.js は、最小限を指向し、アプリケーションスケルトンを必要としないフレームワークです。次のように、Node.js アプリを作成し、依存関係として express
を追加するだけです。
$ mkdir express_memcache
$ cd express_memcache
$ npm init
# choose a package name and make sure the entry point is app.js
$ npm install express
開発を簡素化するために、テンプレートエンジンを使用します。このチュートリアルでは ejs
を使用しますが、mustache
、pug
、nunjucks
など、任意のエンジンを使用できます。
$ npm install ejs
必要なパッケージをすべてインストールしたので、アプリのコードを追加できます。訪問者が送信した数よりも小さい最大の素数を計算するページを作成します。
app.js
を作成し、次のコードを貼り付けます。
var express = require("express");
var app = express();
// Set template engine
app.set('view engine', 'ejs')
// Bind the app to a specified port
var port = process.env.PORT || 3000;
app.listen(port);
console.log("Listening on port " + port);
// Super simple algorithm to find largest prime <= n
var calculatePrime = function(n){
var prime = 1;
for (var i = n; i > 1; i--) {
var is_prime = true;
for (var j = 2; j < i; j++) {
if (i % j == 0) {
is_prime = false;
break;
}
}
if (is_prime) {
prime = i;
break;
}
}
return prime;
}
// Set up the GET route
app.get('/', function (req, res) {
if(req.query.n) {
// Calculate prime and render view
var prime = calculatePrime(req.query.n);
res.render('index', { n: req.query.n, prime: prime});
}
else {
// Render view without prime
res.render('index', {});
}
});
次に、対応するビューを追加しましょう。views/index.ejs
ファイルを作成し、ejs
で拡張した次の HTML を貼り付けます。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<title>Express.js caching example</title>
</head>
<body>
<div class="container">
<h1>
Express.js caching example
</h1>
<p class="lead">For any number N (max 10000), we'll find the largest prime number
less than or equal to N.
</p>
<!-- Form to submit a number -->
<form class="form-inline" action="/">
<input type="text" class="form-control" name="n" />
<input type="submit" class="btn btn-primary" value="Find Prime" />
</form>
<hr>
<!-- Show the result -->
<% if (locals.prime) { %>
<div class="alert alert-primary">
<p class="lead">Largest prime less or equal than <%= n %> is <%= prime %></p>
</div>
<% } %>
<!-- TODO: Error handling -->
</div>
</body>
</html>
動作するアプリが完成したので、node app.js
を実行して起動できます。
Heroku 上でアプリを動作させるには、アプリの実行方法を指示する Procfile
を作成する必要があります。
$ echo web: node app.js > Procfile
アプリを Heroku にデプロイするには、Git リポジトリでアプリが管理されている必要があります。まず、.gitignore
ファイルを作成します。
$ echo node_modules/ > .gitignore
次に、リポジトリを作成してアプリの初期状態をコミットします。
$ git init
$ git add .
$ git commit -m 'Initial express app'
最後に、Heroku アプリを作成し、そのアプリにコードをプッシュして、実行中のアプリを調べます。
$ heroku create
$ heroku config:add NODE_ENV=production
$ git push heroku master
$ heroku open
Express.js ミドルウェアの記述方法を学ぶ
素数計算アプリは動作はしますが、大きな欠点が 1 つあります。ユーザーが文字列などの無効な入力を送信できてしまうことです。入力を検証するために、Express でミドルウェアを作成します。
Express で利用可能な検証ミドルウェアパッケージは何種類かあり、 ほとんどの場合、そのいずれかを使用することをお勧めします。このチュートリアルでは、例証目的のために独自の検証を作成します。
Express ミドルウェアは通常、リクエストとそれに対応する応答の詳細を検査し、必要に応じて変更する関数のチェーンで構成されます。各関数は 3 つのパラメータを取ります。
request
オブジェクトresponse
オブジェクト- チェーン内の次のミドルウェア関数を表す
next
関数
各ミドルウェア関数では、必要に応じて request
および response
オブジェクトを
変更できます。その後、next
ミドルウェア関数を呼び出すか、または return
を呼び出してチェーンを途中で終了させることができます。
このアプリのために、送信されたクエリを解析し、それが 10000 より小さい数値かどうかをチェックする検証ミドルウェア関数を作成します。
- そうである場合、関数は
next
を呼び出します。 - そうでない場合、関数はエラー応答を
return
します。
この関数を app.js
に追加し、GET
ルートの処理時に呼び出します。
// ...
var validate = function(req, res, next) {
if(req.query.n) {
number = parseInt(req.query.n, 10);
if(isNaN(number) || number < 1 || number > 10000){
res.render('index', {error: 'Please submit a valid number between 1 and 10000.'});
return;
}
req.query.n = number;
}
next();
}
app.get('/', validate, function (req, res) {
// ...
検証ミドルウェアからエラーメッセージが返されることがあり、その場合、index.ejs
ビューに表示する必要があります。
<!-- Show the result -->
<!-- ... -->
<!-- Error handling -->
<% if (locals.error) { %>
<div class="alert alert-danger">
<p class="lead"><%= error %></p>
</div>
<% } %>
変更をコミットしてデプロイします。
$ git commit -am 'Add input validation'
$ git push heroku master
アプリを開き、何か無効なクエリを送信して、実際にエラーメッセージが返されるかどうか確認します。
キャッシングを Express に追加する
Memcache はインメモリの分散キャッシュです。そのプライマリ API は、SET(key, value)
と GET(key)
の 2 つの操作で構成されます。
Memcache は、複数のサーバーに分散していますが、操作は一定の時間に実行されるハッシュマップ (または辞書) のようなものです。
Memcache の最も一般的な用途は、コストの高いデータベースクエリや HTML レンダリングをキャッシュし、これらの高コスト操作を繰り返す必要をなくすことです。
Memcache のセットアップ
Express で Memcache を使用するには、まず実際の Memcache キャッシュをプロビジョニングする必要があります。これは、MemCachier アドオンから無料で簡単に入手できます。
$ heroku addons:create memcachier:dev
これにより、MEMCACHIER_SERVERS
、MEMCACHIER_USERNAME
、MEMCACHIER_PASSWORD
の 3 つの環境設定が Heroku アプリケーションに 追加され、キャッシュに接続できるようなります。
Express でキャッシュを使用するには、npm
を使用して memjs
をインストールする必要があります。
$ npm install memjs
また、app.js
でその設定を行う必要もあります。
// ...
var memjs = require('memjs')
var mc = memjs.Client.create(process.env.MEMCACHIER_SERVERS, {
failover: true, // default: false
timeout: 1, // default: 0.5 (seconds)
keepAlive: true // default: false
})
// ...
コストの高い計算のキャッシュ
コストの高い計算の結果をキャッシュすることが良い考えであるのには、2 つの理由があります。
- キャッシュから結果を取得した方がずっと高速であり、ユーザーエクスペリエンスも向上します。
- コストの高い計算には多くの CPU リソースが使われ、アプリのその他の部分の動作も低速になる可能性があります。
今回の素数計算では、入力値を 10000 までに制限しているため、実際にはコストの高い計算は発生しません。ただし、チュートリアルの目的のために、素数の計算はコストが高い計算なのでキャッシュしたいと仮定します。
これを実現するために、次のように app.js
で GET
ルートを変更してみます。
// ...
app.get('/', validate, function (req, res) {
if(req.query.n) {
var prime;
var prime_key = 'prime.' + req.query.n;
// Look in cache
mc.get(prime_key, function(err, val) {
if(err == null && val != null) {
// Found it!
prime = parseInt(val)
}
else {
// Prime not in cache (calculate and store)
prime = calculatePrime(req.query.n)
mc.set(prime_key, '' + prime, {expires:0}, function(err, val){/* handle error */})
}
// Render view with prime
res.render('index', { n: req.query.n, prime: prime });
})
}
else {
// Render view without prime
res.render('index', {});
}
});
// ...
これらの変更を Heroku にデプロイし、いくつかの数値を送信して素数を見つけます。
$ git commit -am 'Add caching'
$ git push heroku master
ページは以前と同じように動作するはずです。ただし、内部では、すでに計算された素数がキャッシュされるようになりました。キャッシュ内で何が起きているかを見るために、MemCachier のダッシュボードを開きます。
$ heroku addons:open memcachier
ダッシュボードでは、素数をリクエストするたびに統計を更新できます。最初に数値を入力すると、get misses
が増加します。それ以降は、同じ数値をリクエストするたびに get hit
が増加するはずです。
レンダリングされたビューのキャッシュ
HTML ビューのレンダリングは概してコストの高い計算なので、可能であれば常に、レンダリングされたビューをキャッシュするべきです。Express では、ミドルウェアを使用してこれを容易に実現できます。(クエリパラメータを含む) 与えられた URL のビューがキャッシュに存在するかどうかをチェックする cacheView
ミドルウェア関数を app.js
に追加してみましょう。
- 存在する場合、ビューは即時にキャッシュから送信されます。
- 存在しない場合、レンダリングされたビューをキャッシュするために
send
関数を応答オブジェクトにラップし、next
関数を呼び出します。
// ...
var cacheView = function(req, res, next) {
var view_key = '_view_cache_' + req.originalUrl || req.url;
mc.get(view_key, function(err, val) {
if(err == null && val != null) {
// Found the rendered view -> send it immediately
res.send(val.toString('utf8'));
return;
}
// Cache the rendered view for future requests
res.sendRes = res.send
res.send = function(body){
mc.set(view_key, body, {expires:0}, function(err, val){/* handle error */})
res.sendRes(body);
}
next();
});
}
app.get('/', validate, cacheView, function (req, res) {
// ...
これはとても簡単で、正しく動作します。しかし、ビューに変更がある場合は注意が必要です。ページに変化がある場合の例として、個々の数値と、それに対して計算される最大素数に 「Like」 (いいね) ボタンを追加してみましょう。index.ejs
ファイルで、計算された素数のすぐ下にこのボタンを配置します。
<!-- ... -->
<!-- Show the result -->
<% if (locals.prime) { %>
<div class="alert alert-primary">
<p class="lead">Largest prime less or equal than <%= n %> is <%= prime %></p>
<p>Likes: <%= likes %></p>
<form method='POST'>
<input type="hidden" name="n" value="<%= n %>" />
<input type="submit" class="btn btn-primary" value="Like!" />
</form>
</div>
<% } %>
<!-- ... -->
この “いいね” は POST
リクエストを介して送信され、その入力を解析するには body-parser
パッケージが必要です。
$ npm install body-parser
ここで、POST
ルートのコントローラーを app.js
に作成し、送信された “いいね” を変数に格納することができます。
“いいね” を変数に格納するのは適切ではありません。アプリが再起動するたびに、すべての “いいね” が 消去されてしまうからです。ここでは、便宜上そのようにしているだけです。本番アプリケーションでは、 このような情報はデータベースに保存してください。
// ...
var bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Like storage (in a serious app you should use a permanent storage like a database)
var likes = {}
app.post('/', function (req, res) {
likes[req.query.n] = (likes[req.query.n] || 0) + 1
res.redirect('/?n=' + req.query.n)
});
// ...
さらに、"いいね" が render
関数に渡されることを確認する必要もあります。
これは GET
コントローラーで行われます。
// ...
// Render view with prime
res.render('index', { n: req.query.n, prime: prime, likes: likes[req.query.n] || 0 });
// ...
ページが変化する場合の問題を実証するために、現在の実装をコミットしてテストしてみましょう。
$ git commit -am 'Add view caching'
$ git push heroku master
数値を送信すると、最大素数に加えてその下に [Like] ボタンが表示されるようになります。しかし、Like! をクリックしても “いいね” のカウントは増加しません。これは、ビューがキャッシュされているからです。
これを解決するには、キャッシュされたビューが更新されるたびにそのビューを無効化する必要があります。
// ...
app.post('/', function (req, res) {
mc.delete('_view_cache_/?n=' + req.body.n, function(err, val){/* handle error */});
likes[req.query.n] = (likes[req.query.n] || 0) + 1
res.redirect('/?n=' + req.query.n)
});
// ...
もう一度 Heroku にデプロイします。
$ git commit -am 'Fix view caching'
$ git push heroku master
“いいね” の数が増えることを確認できるようになりました。
セッションのキャッシング
Heroku では、再起動時に内容が失われる一時的なファイルシステムが dyno に備わっているため、セッション情報をディスクに保存することは推奨されていません。
Memcache は、タイムアウトがある短命セッションの情報を保存するのには適しています。しかし、Memcache はあくまでキャッシュであって永続的ではないため、寿命の長いセッションについては、データベースなどの永続的なストレージオプションの方が適しています。
Express でセッションを使用するには、express-session
が必要です。Memcache でセッションを保存するには、connect-memjs
が必要です。
$ npm install express-session connect-memjs
app.js
での設定は簡単です。
//...
var session = require('express-session');
var MemcachedStore = require('connect-memjs')(session);
// Session config
app.use(session({
secret: 'ClydeIsASquirrel',
resave: 'false',
saveUninitialized: 'false',
store: new MemcachedStore({
servers: [process.env.MEMCACHIER_SERVERS],
prefix: '_session_'
})
}));
//...
セッションを自由に使用できるようになりました。Express でのセッションの使用法についての詳細は、express-session のドキュメントを参照してください。