ウェブサイト検索

Expressアプリケーションをデプロイし、App PlatformでMemCachierを使ってスケールする方法


著者は、Write for DOnationsプログラムの一環として寄付を受けるために、Free and Open Source Fundを選びました。

紹介

Expressは、Nodeを使って迅速なWebアプリやAPIを構築するための人気のフレームワークです。DigitalOceanのApp Platformは、コードリポジトリからアプリケーションを構成してデプロイするためのサービスとしてのプラットフォーム(PaaS)製品です。これにより、Expressアプリを迅速かつ効率的にデプロイすることができます。このチュートリアルでは、ExpressアプリケーションをDigitalOcean App Platformにデプロイし、次にDigitalOcean MarketplaceのMemCachierのアドオンを追加してキャッシュを使ってスケールさせます。MemCachierはmemcachedオブジェクトキャッシングシステムに準拠していますが、高可用性クラスタを使用した場合の障害シナリオの改善など、いくつかの利点があります。

最初に、素数を計算し、いいねボタンがあり、テンプレートエンジンを使用するExpressアプリを構築します。これらの機能により、後でいくつかのキャッシング戦略を実装できるようになります。その後、アプリのコードをGitHubにプッシュし、App Platformにデプロイします。最後に、アプリをより高速でスケーラブルにするために、3つのオブジェクトキャッシング技術を実装します。このチュートリアルの終わりまでには、リソース集約的な計算、レンダリングされたビュー、およびセッションのキャッシング技術を実装しながら、ExpressアプリケーションをApp Platformにデプロイできるようになります。

前提条件

  • あなたのコンピュータにNode.jsがインストールされている必要があります。これは、Ubuntu 22.04へのNode.jsのインストール方法に従って設定できます。他のオペレーティングシステムでは、Node.jsのインストールとローカル開発環境の作成方法に関する適切なガイドに従ってください。
  • Node.jsを使用した基本的なExpressサーバー。Node.jsとExpressの始め方に関するチュートリアルのステップ1-2に従ってください。
  • GitHubアカウントとローカルマシンにインストールされたGitが必要です。これらは、DigitalOcean App PlatformからデプロイするためにコードをGitHubにプッシュする必要があります。セットアップするには、Gitのインストール方法に関するチュートリアルを参照できます。
  • App PlatformにデプロイするためのDigitalOceanアカウント。このアプリをApp Platformで実行すると、料金が発生します。詳細については、App Platformの料金を参照してください。
  • FirefoxやChromeのようなウェブブラウザ。
  • Expressテンプレートエンジンの理解。
  • Expressミドルウェアの理解。このトピックについては、チュートリアル「Express.jsでカスタムミドルウェアを作成する方法」でさらに読むことができます。

ステップ 1 — Express テンプレートレンダリングビューの設定

このステップでは、Express用のテンプレートエンジンをインストールし、アプリのホームルート(GET /)用のテンプレートを作成し、そのテンプレートを使用するようにルートを更新します。テンプレートエンジンを使用すると、後でレンダリングされたビューをキャッシュできるため、リクエスト処理の速度が向上し、リソースの使用が減少します。

まず、エディターでまだ開いていない場合は、Expressサーバーのプロジェクトディレクトリに移動します。プロジェクトファイルをどこに保存したかを特定するには、「Node.jsとExpressの始め方」に関する前提条件のチュートリアルに戻ることができます。

あなたは、Expressで静的テンプレートファイルをアプリケーションで使用するためのテンプレートエンジンをインストールします。テンプレートエンジンは、テンプレートファイル内の変数を値に置き換え、テンプレートをHTMLファイルに変換します。このHTMLファイルは、リクエストに対する応答として送信されます。テンプレートを使用することで、HTMLの操作が容易になります。

Embedded JavaScriptテンプレート(ejs)ライブラリをインストールしてください。もし好みに応じて、Expressがサポートしている他のテンプレートエンジン(Mustache、Pug、またはNunjucksなど)を使用することもできます。

  1. npm install ejs

ejsがインストールされたので、Expressアプリをそれを使用するように設定します。

エディタでserver.jsファイルを開きます。その後、ハイライトされた行を追加してください。

const express = require('express');

const app = express();

app.set('view engine', 'ejs');

app.get('/', (req, res) => {
  res.send('Successful response.');
});

...

この行は、アプリケーション設定プロパティview engineをejsに設定します。

ファイルを保存してください。

注: このチュートリアルでは、view engine 設定を使用しますが、もう一つの便利な設定は views です。views 設定は、Express アプリがテンプレートファイルをどこに見つけるかを示します。デフォルト値は ./views です。

次に、viewsディレクトリを作成します。それから、views/index.ejsというファイルを作成し、エディタで開きます。

そのファイルに始まりのテンプレートマークアップを追加します:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Find the largest prime number</title>
  </head>
  <body>
    <h1>Find the largest prime number</h1>

    <p>
      For any number N, find the largest prime number less than or equal to N.
    </p>
  </body>
</html>

ファイルを保存してください。

作成したテンプレートを使用するようにルートを更新します。

ファイルserver.jsを開いて、ハイライトされたコードを更新してください:

...

app.get('/', (req, res) => {
  res.render('index');
});

...

レスポンスの render メソッドは、テンプレートの名前を最初のパラメーターとして受け取ります。この場合、index はファイル views/index.ejs と一致します。

アプリを再起動して変更を読み込んでください。ターミナルで CTRL+C を押して、実行中のサーバーを停止します。その後、サーバーを再起動します:

  1. node server.js

localhost:3000をウェブブラウザで訪問すると、あなたのテンプレートの内容が表示されます。

あなたのアプリには現在、テンプレートでレンダリングされたビューがありますが、まだ何も機能していません。次に、素数を見つける機能を追加します。

ステップ2 — Expressアプリに機能を追加する

このステップでは、素数を見つける機能と、いいねボタンを使って数字を好く機能を追加します。これらの機能は、ステップ4でApp Platformにデプロイした後にアプリと対話するために使用します。

素数を見つける

このセクションでは、アプリに N 以下の最大の素数を見つける関数を追加します。ここで N は任意の数を指します。

Nは、GETメソッドを使用してホームルート(/)にフォーム経由で送信され、Nはクエリパラメータとして追加されます:localhost:3000/?n=10(ここで10はサンプルクエリです)。ホームルートには、レンダリングされたビューを生成する複数のURLがあり、それぞれ個別にキャッシュできます。

views/index.ejs に N を入力するための入力要素を持つフォームを追加します。

...

<p>
  For any number N, find the largest prime number less than or equal to N.
</p>

<form action="/" method="get">
  <label>
    N
    <input type="number" name="n" placeholder="e.g. 10" required>
  </label>
  <button>Find Prime</button>
</form>

...

フォームのアクションは/に送信され、これはserver.js内のホームルートapp.get('/' ...)によって処理されます。フォームのメソッドがgetに設定されているため、データnはアクションURLにクエリパラメータとして追加されます。

ファイルを保存してください。

次に、nのクエリパラメータでリクエストが送信されると、そのデータをテンプレートに渡します。

server.jsに、ハイライトされたコードを追加してください:

...

app.get('/', (req, res) => {
  const n = req.query.n;
  
  if (!n) {
    res.render('index');
    return;
  }
  
  const locals = { n };
  res.render('index', locals);
});

...

これらの行は、リクエストに n という名前のクエリパラメータがあるかどうかを確認します。もしあれば、index ビューをレンダリングし、その値を渡します。そうでない場合は、データなしで index ビューを生成します。

注意: ユーザーの入力は常に信頼できるわけではないので、製品版アプリケーションのベストプラクティスは、Joiなどのライブラリを使用して入力を検証することです。

render メソッドには2つ目のオプションのパラメーター locals があります。このパラメーターは、ビューをレンダリングするためにテンプレートに渡されるローカル変数を定義します。ショートハンドのプロパティ名は locals オブジェクトの n プロパティを定義します。変数が割り当てられるオブジェクトのプロパティと同じ名前の場合、変数名は省略可能です。したがって、{ n: n } は { n } と書くことができます。

ファイルを保存してください。

テンプレートにデータが入ったので、それを表示できます。

views/index.ejs にハイライトされた行を追加して、N の値を表示します:

...

<form action="/" method="get">
  <label>
    N
    <input type="number" name="n" placeholder="e.g. 10" required>
  </label>
  <button>Find Prime</button>
</form>

<% if (locals.n) { %>
  <p>N: <%= n %></p>
<% } %>

...

このビューにローカル変数nが存在する場合、アプリにそれを表示するように指示します。

ファイルを保存し、アプリを更新するためにサーバーを再起動してください。フォームには素数を見つけるボタンが表示されます。アプリはユーザーの入力を受け入れ、それをフォームの下に表示できるようになります。

任意の数値をフォームに入力してください。フォームを送信すると、URLが変更され、nクエリパラメータが含まれるようになります。たとえば、40を入力した場合、URLはhttp://localhost:3000/?n=40のようになります。送信した値は、フォームの下にN: 40として表示されます。

今、Nの値を提出して表示できるようになったので、N以下の最大の素数を見つける関数を追加します。そして、その結果をビューに表示します。

utilsディレクトリを作成します。そして、utils/findPrime.jsというファイルを作成します。

findPrime.jsをエディタで開き、素数を見つける関数を追加してください:

/**
 * Find the largest prime number less than or equal to `n`
 * @param {number} n A positive integer greater than the smallest prime number, 2
 * @returns {number}
 */
module.exports = function (n) {
  let prime = 2; // initialize with the smallest prime number
  for (let i = n; i > 1; i--) {
    let isPrime = true;
    for (let j = 2; j < i; j++) {
      if (i % j == 0) {
        isPrime = false;
        break;
      }
    }
    if (isPrime) {
      prime = i;
      break;
    }
  }
  return prime;
};

JSDocコメントは関数を文書化しています。アルゴリズムは最初の素数(2)から始まり、次にから始まる数を繰り返し、各ループで数を1ずつ減少させます。この関数は、数が最小の素数である2になるまで、素数を探し続けてループします。

各ループは現在の数が素数であると仮定し、その仮定をテストします。現在の数が 1 と自分自身以外の約数を持っているかどうかを確認します。現在の数が 1 より大きく、自分自身より小さい任意の数で割り切れる場合、それは素数ではありません。その後、関数は次の数を試します。

ファイルを保存してください。

次に、find prime関数をserver.jsにインポートします:

const express = require('express');
const findPrime = require('./utils/findPrime');

...

自宅ルートコントローラーを更新して素数を見つけ、その値をテンプレートに渡します。まだserver.jsの中で、ハイライトされたコードを追加してください:

...

app.get('/', (req, res) => {
  const n = req.query.n;

  if (!n) {
    res.render('index');
    return;
  }
  
  const prime = findPrime(n);

  const locals = { n, prime };
  res.render('index', locals);
});

...

ファイルを保存してください。

さて、結果をテンプレートに表示するコードを追加します。views/index.ejsでNの値を表示してください:

...

<form action="/" method="get">
  <label>
    N
    <input type="number" name="n" placeholder="e.g. 10" required>
  </label>
  <button>Find Prime</button>
</form>

<% if (locals.n && locals.prime) { %>
  <p>
    The largest prime number less than or equal to <%= n %> is <strong><%= prime %></strong>.
  </p>
<% } %>
...

ファイルを保存してください。

サーバーを再起動してください。

機能をテストするために、任意の数を提出してください。例として、このチュートリアルでは10を使用します。10という数を提出すると、次のような応答が返されます:10以下の最大素数は7です。

あなたのアプリは現在、数字を受け取り、素数を見つけて表示することができます。次に、いいねボタンを追加します。

いいねボタンを追加する

現在、あなたのアプリは提出された各番号Nに基づいて異なるビューを生成できます。テキストの更新を除けば、これらのビューの内容は同じである可能性が高いです。このセクションに追加するいいねボタンは、ビューの内容を更新する方法を提供します。このボタンは、内容が変更されたときにキャッシュされたビューを無効にする必要性を示しており、チュートリアルの後でレンダリングされたビューをキャッシュする際に役立ちます。

いいねボタンがある場合、アプリはいいねデータを保存する場所が必要です。永続的なストレージが理想ですが、このチュートリアルの範囲ではデータベースを実装することは難しいため、メモリ内にいいねを保存します。そのため、データは一時的なものとなり、サーバーが停止するとすべてのデータが失われることを意味します。

server.jsを開いて、以下の変数を追加してください:

...

app.set('view engine', 'ejs');

/**
 * Key is `n`
 * Value is the number of 'likes' for `n`
 */
const likesMap = {};

...

likesMapオブジェクトは、リクエストされたすべての数のいいねを格納するための地図として使用されます。キーはnで、その値はnのいいねの数です。

数が送信されるときに、いいねの数を初期化する必要があります。まだ server.js の中で、いいねの数を N に対して初期化するためにハイライトされた行を追加してください:

...

  const prime = findPrime(n);

  // Initialize likes for this number when necessary
  if (!likesMap[n]) likesMap[n] = 0;

  const locals = { n, prime };
  res.render('index', locals);

...

このif文は、現在の番号に対するいいねが存在するかどうかを確認します。いいねが存在しない場合、likesMapsの数は0に初期化されます。

次に、ビューのためにローカル変数としてlikesを追加します:

...

  const prime = findPrime(n);

  // Initialize likes for this number when necessary
  if (!likesMap[n]) likesMap[n] = 0;

  const locals = { n, prime, likes: likesMap[n] };
  res.render('index', locals);

...

ファイルを保存してください。

今や、ビューには「いいね」のデータがあるので、その値を表示し、いいねボタンを追加できます。

views/index.ejsにLikeボタンのマークアップを追加してください。

...

<% if (locals.n && locals.prime) { %>
  <p>
    The largest prime number less than or equal to <%= n %> is <strong><%= prime %></strong>.
  </p>

  <form action="/like" method="get">
    <input type="hidden" name="n" value="<%= n %>">
    <input type="submit" value="Like"> <%= likes %>
  </form>
<% } %>
...

あなたの完成したファイルは今、以下の内容と一致しているはずです:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Find the largest prime number</title>
  </head>
  <body>
    <h1>Find the largest prime number</h1>

    <p>
      For any number N, find the largest prime number less than or equal to N.
    </p>

    <form action="/" method="get">
      <label>
        N
        <input type="number" name="n" placeholder="e.g. 10" required>
      </label>
      <button>Find Prime</button>
    </form>

    <% if (locals.n && locals.prime) { %>
      <p>
        The largest prime number less than or equal to <%= n %> is <strong><%= prime %></strong>.
      </p>
      <form action="/like" method="get">
        <input type="hidden" name="n" value="<%= n %>">
        <input type="submit" value="Like"> <%= likes %>
      </form>
    <% } %>
  </body>
</html>

ファイルを保存してください。

サーバーを再起動し、次に数字を入力してください。素数の結果の後にいいねボタンが表示され、いいね数は0になります。

いいねボタンをクリックすると、現在のNの値を隠し入力のクエリパラメータとして/likeにGETリクエストが送信されます。今のところ、あなたのアプリには対応するルートがまだないため、Cannot GET /likeという404エラーが表示されます。

これからリクエストを処理するルートを追加します。

server.jsに戻り、ルートを追加します:

...

app.get('/', (req, res) => {
  ...
});

app.get('/like', (req, res) => {
  const n = req.query.n;

  if (!n) {
    res.redirect('/');
    return;
  }

  likesMap[n]++;

  res.redirect(`/?n=${n}`);
});

...

この新しいルートは n が存在するかどうかを確認します。存在しない場合はホームにリダイレクトします。それ以外の場合は、この番号のいいねを増加させます。最後に、いいね ボタンがクリックされたビューにリダイレクトします。

あなたの完成したファイルは今、以下の内容と一致しているはずです:

const express = require('express');
const findPrime = require('./utils/findPrime');

const app = express();

app.set('view engine', 'ejs');

/**
 * Key is `n`
 * Value is the number of 'likes' for `n`
 */
const likesMap = {};

app.get('/', (req, res) => {
  const n = req.query.n;
  
  if (!n) {
    res.render('index');
    return;
  }
  
  const prime = findPrime(n);

  // Initialize likes for this number when necessary
  if (!likesMap[n]) likesMap[n] = 0;

  const locals = { n, prime, likes: likesMap[n] };
  res.render('index', locals);
});

app.get('/like', (req, res) => {
  const n = req.query.n;

  if (!n) {
    res.redirect('/');
    return;
  }

  likesMap[n]++;

  res.redirect(`/?n=${n}`);
});

const port = process.env.PORT || 3000;
app.listen(port, () =>
  console.log(`Example app is listening on port ${port}.`)
);

ファイルを保存してください。

アプリを再起動し、いいねボタンを再度テストしてください。クリックする度にいいねのカウントが増加します。

注意: このルートでは、GETの代わりにPOSTメソッドを使用することもできます。リソースに対して更新が行われるため、よりRESTfulです。このチュートリアルでは、フォームのPOSTリクエストボディの処理を紹介する代わりに、今馴染みのあるリクエストのクエリパラメータで作業できるようにするためにGETを使用しています。

あなたのアプリは完全に機能する機能を備えたので、App Platformに展開する準備が整いました。次のステップでは、gitを使ってアプリのコードをコミットし、そのコードをGitHubにプッシュします。

ステップ3 — コードリポジトリの作成

このステップでは、展開に必要なすべてのファイルを保持するコードリポジトリを作成します。まず、コードをgitにコミットし、その後GitHubリポジトリにプッシュします。このリポジトリを使用してApp Platformで展開します。

コードをGitにコミットする

このセクションでは、コードをgitにコミットしますので、GitHubにプッシュする準備が整います。

注意: ユーザー名で設定を構成していない場合は、Gitを設定し、SSHを使用してGitHubアカウントを認証してください。

まず、git リポジトリを初期化します:

  1. git init

次に、Gitにアプリの依存関係を除外するように指示します。.gitignoreという新しいファイルを作成し、以下を追加してください:

node_modules

macOS file
.DS_Store

注: .DS_Storeの行はmacOS特有のものであり、他のオペレーティングシステムには存在する必要はありません。

ファイルを保存して閉じてください。

今、すべてのファイルをgitに追加します:

  1. git add .

最後に、次のコマンドでその変更をコミットします:

  1. git commit -m "Initial commit"

-mオプションはコミットメッセージを指定するために使用され、任意のメッセージで更新することができます。

コードをコミットした後、このような出力が得られます:

[main (root-commit) deab84e] Initial commit
 6 files changed, 1259 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 package-lock.json
 create mode 100644 package.json
 create mode 100644 server.js
 create mode 100644 utils/findPrime.js
 create mode 100644 views/index.ejs

あなたはコードをgitにコミットしました。次に、それをGitHubにプッシュします。

GitHubリポジトリにコードをプッシュする

アプリのコードがgitにコミットされたので、GitHubにプッシュする準備ができました。次に、そのコードをDigitalOcean App Platformに接続し、デプロイすることができます。

まず、ブラウザで GitHub にログインし、express-memcache という新しいリポジトリを作成します。README、.gitignore、またはライセンスファイルなしの空のリポジトリを作成します。リポジトリはプライベートまたはパブリックに設定できます。また、新しいリポジトリの作成に関する GitHub のドキュメントを参照することもできます。

ターミナルに戻り、新しく作成したリポジトリをリモートのオリジンとして追加し、ユーザー名を更新します:

  1. git remote add origin https://github.com/your_username/express-memcache.git

このコマンドは、Gitにあなたのコードをどこにプッシュするかを指示します。

次に、デフォルトブランチmainの名前を変更します:

  1. git branch -M main

最後に、コードをリポジトリにプッシュします:

  1. git push -u origin main

要求された場合は、認証情報を入力してください。

次のような出力が得られます:

Enumerating objects: 10, done.
Counting objects: 100% (10/10), done.
Delta compression using up to 8 threads
Compressing objects: 100% (7/7), done.
Writing objects: 100% (10/10), 9.50 KiB | 9.50 MiB/s, done.
Total 10 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/your_username/express-memcache.git
 * [new branch]      main -> main
Branch 'main' set up to track remote branch 'main' from 'origin'.

あなたのアプリのコードは現在GitHubにあり、App Platformによってデプロイする準備が整いました。

ステップ4 — アプリプラットフォームへのデプロイ

このステップでは、ExpressアプリをDigitalOcean App Platformにデプロイします。App Platformアプリを作成し、GitHubリポジトリへのアクセスを許可してから、デプロイします。

最初に、PORT 環境ラベルから設定を読み取ることができるように、環境設定を更新します。

アプリの環境設定を更新する

このセクションでは、アプリのポート構成を環境変数から読み込めるように、Expressサーバーを拡張します。デプロイのたびに構成が変更される可能性があるため、この更新により、アプリがそのApp Platform環境からポートを読み取れるようになります。

エディタで server.js ファイルを開きます。次に、ファイルの一番下で、ハイライトされたコードを更新して既存の app.listen 行を置き換え、新しい const port 行を追加します:

...

const port = process.env.PORT || 3000;
app.listen(port, () =>
  console.log(`Example app is listening on port ${port}.`)
);

このコードは、PORT 環境変数が存在する場合はそれを使用し、そうでなければポート 3000 にデフォルトすることを示しています。

あなたのアプリは、これからデプロイするApp Platform環境からポートを読み取ることができるようになります。

アプリプラットフォームでのアプリの作成と展開

アプリプラットフォームを使用してアプリを設定できるようになりました。

このアプリを App Platform で実行すると料金が発生します。ウェブサービスは1秒単位で請求され(最低1分からスタート)、料金はレビュー画面に表示されます。詳細は App Platform の料金をご覧ください。

まず、DigitalOceanアカウントにログインします。アプリダッシュボードから、作成をクリックし、次にアプリをクリックします。また、App Platformでアプリを作成する方法についての製品ドキュメントもご覧いただけます。

ソースコードからリソースを作成の画面で、サービスプロバイダーとしてGitHubを選択します。その後、DigitalOceanにリポジトリへのアクセスを許可します。最良の方法は、デプロイしたいリポジトリだけを選択することです。まだ行っていない場合は、DigitalOcean GitHubアプリをインストールするように促されます。リストからリポジトリを選択し、次へをクリックします。

リソース画面で、プランの編集をクリックして、プランのサイズを選択します。このチュートリアルでは、express-memcacheリソースのために、最小サイズのウェブサービス(512 MB RAM | 1 vCPU)を持つベーシックプランを使用します。ベーシックプランと最小のウェブサービスは、このサンプルExpressアプリに十分なリソースを提供します。プランを設定したら、戻るをクリックしてください。

次に、左側のナビゲーションバーで情報タブをクリックし、アプリがある地域を確認してください。この地域は、次のステップでMemCachierのDigitalOcean Marketplaceアドオンを追加する際に必要です。

最後に、レビュータブをクリックし、次にリソースを作成ボタンをクリックしてアプリを構築しデプロイします。ビルドが実行されるには少し時間がかかります。終了すると、デプロイされたアプリへのリンク付きの成功メッセージが届きます。

これまでに、素数を見つけるExpressアプリを作成し、いいねボタンを追加しました。アプリのコードをGitにコミットし、GitHubにプッシュし、その後App Platformにアプリをデプロイしました。

あなたのExpressアプリをより速く、よりスケーラブルにするために、3つのオブジェクトキャッシング戦略を実装します。次のステップで作成するキャッシュが必要です。

ステップ5 — MemCachierを使用したオブジェクトキャッシュの設定

このステップでは、オブジェクトキャッシュを作成して設定します。このチュートリアルには、memcached互換のキャッシュが機能します。DigitalOceanマーケットプレイスからMemCachierアドオンを使用してそれを準備します。MemCachierキャッシュは、インメモリのキー・バリューストアです。

まず、DigitalOcean MarketplaceからMemCachierアドオンを追加します。MemCachierアドオンのページにアクセスして、MemCachierを追加をクリックしてください。次の画面で、前にメモしたあなたのApp Platformアプリがあるリージョンを選択します。アプリとキャッシュは同じリージョンにあるべきで、レイテンシーをできるだけ低く保つことができます。リージョンを再度確認する必要がある場合は、App Platformアプリの設定を確認できます。オプションでプランを選択することもできます。その後、MemCachierを追加をクリックしてキャッシュをプロビジョニングします。

リージョン名からスラッグへのマッピングを理解するためには、DigitalOceanの利用可能なデータセンターを参照してください。例えば、リージョンサンフランシスコはsfo3にマッピングされます。

次に、Expressアプリをキャッシュを使用するように構成します。Add-Onsダッシュボードにアクセスし、その後、MemCachier Add-Onの名前をクリックしてダッシュボードを開きます。MemCachier Add-Onダッシュボードで、Configuration VariablesのShowボタンをクリックして、MEMCACHIER_USERNAME、MEMCACHIER_PASSWORD、およびMEMCACHIER_SERVERSの値を表示します。これらの値をメモしておいてください。次に必要になります。

今、あなたはアプリのためにMemCachierの設定変数を環境変数として保存します。App Platformアプリのダッシュボードに戻り、設定をクリックします。次に、コンポーネントの下で、express-memc…をクリックします。環境変数セクションまでスクロールし、編集をクリックして、MemCachierダッシュボードから取得した三つのキー(MEMCACHIER_USERNAME、MEMCACHIER_PASSWORD、MEMCACHIER_SERVERS)とそれに対応する値を追加します。MEMCACHIER_PASSWORDについては、値がパスワードであるため、暗号化を確認してください。保存をクリックして、アプリを更新します。

さあ、アプリがキャッシュと通信できるように、保存した環境変数を使用して、Expressアプリでメモリキャッシュクライアントを設定します。

ターミナルで memjs ライブラリをインストールします:

  1. npm install memjs

次に、servicesディレクトリを作成してください。それから、services/memcache.jsというファイルを作成し、エディタで開きます。ファイルの先頭で、memjsをインポートし、キャッシュクライアントを設定します:

const { Client } = require('memjs');

module.exports = Client.create(process.env.MEMCACHIER_SERVERS, {
  failover: true,
  timeout: 1,
  keepAlive: true,
});

ファイルを保存してください。

このコードはMemCachierキャッシュクライアントを作成します。オプションに関しては、failoverがtrueに設定されており、MemCachierの高可用性クラスターを利用します。サーバーが故障した場合、そのサーバーに保存されているすべてのキーに対するコマンドは、次の利用可能なサーバーに自動的に送信されます。デプロイされたアプリにとって、timeoutを1秒に設定するのが、デフォルトの0.5秒よりも良いです。keepAlive: trueは、アイドル状態でもキャッシュへの接続を開いたままにし、接続を作成するのが遅いので、キャッシュが効果的であるためには望ましいです。

このステップでは、DigitalOcean MarketplaceのMemCachier Add-Onを使用してキャッシュをプロビジョニングしました。次に、キャッシュの設定をApp Platform環境変数として追加し、memjsを使用してクライアントを構成できるようにし、Expressアプリがキャッシュと通信できるようにしました。

すべてが準備完了です。次に、Expressでキャッシングを実装し始めます。

ステップ6 — MemCachierを使ったExpressでのキャッシングの実装

Expressアプリがデプロイされ、MemCachierアドオンがプロビジョニングされたので、オブジェクトキャッシュを使用できるようになりました。このステップでは、3つのオブジェクトキャッシング技術を実装します。まず、リソース集約的な計算をキャッシュして、使用速度と効率を向上させます。次に、ユーザー入力後にレンダリングされたビューをキャッシュしてリクエスト処理を改善し、チュートリアルを超えてアプリをスケールアップすることを想定して短命のセッションをキャッシュする技術を実装します。

高リソース計算のキャッシング

このセクションでは、リソースを多く消費する計算をキャッシュしてアプリの速度を上げ、より効率的なCPUの使用を実現します。findPrime関数は、大きな数が入力されるとリソースを多く消費する計算です。その結果をキャッシュし、計算を繰り返す代わりに利用可能なときにそのキャッシュされた値を提供します。

まず、server.jsを開いてmemcacheクライアントを追加します:

const express = require('express');
const findPrime = require('./utils/findPrime');
const memcache = require('./services/memcache');

...

それから、計算された素数をキャッシュに保存します:

...

  const prime = findPrime(n);

  const key = 'prime_' + n;

  memcache.set(key, prime.toString(), { expires: 0 }, (err) => {
    if (err) console.log(err);
  });

...

ファイルを保存してください。

このsetメソッドは、最初のパラメータとしてキーを取り、2番目に文字列の値を取りますので、prime数を文字列に変換します。3番目のoptions引数は、保存されたアイテムが決して期限切れにならないことを保証します。4番目の最終パラメータはオプションのコールバックであり、存在する場合はエラーが発生します。

注: ベストプラクティスとして、キャッシュエラーは優雅に処理されるべきです。キャッシュは改善であり、失敗した場合にアプリがクラッシュしてはなりません。アプリはキャッシュなしでも、多少遅くなりますが、完全に正常に動作することができます。

注意: 現在、あなたのアプリはローカルで動作し続けますが、キャッシュなしで動作します。memcache.setが呼ばれるとエラーが出力されます。キャッシュサーバーが見つからないためです:

MemJS: Server <localhost:11211> failed after (2) retries with error - connect ECONNREFUSED 127.0.0.1:11211
Error: No servers available
...

このチュートリアルの残りの部分では、ローカルキャッシングは必要ありません。もしそれを機能させたい場合、localhost:11211 で memcached を実行すれば良いでしょう。これは memjs のデフォルトです。

次に、変更をステージしてコミットします。

  1. git add . && git commit -m "Add memjs client and cache prime number"

次に、これらの変更をGitHubにプッシュします。これにより、自動的にApp Platformにデプロイされるはずです。

  1. git push

あなたのアプリプラットフォームのダッシュボードは、デプロイ済みメッセージから、アプリがビルド中であることを示すメッセージに変わります。ビルドが完了したら、ブラウザでアプリを開き、数を入力してその最大の素数を見つけてください。

注:ダッシュボードにサービスを待機中のメッセージが表示されることがあります。このメッセージは通常、自動で解決されます。もし長く残る場合は、アプリを更新してビルドがデプロイされているか確認してください。

次に、アドオンのダッシュボードに戻り、名前付きサービスの MemCachierを表示 オプションをクリックして、キャッシュの分析ダッシュボードを表示します。

このダッシュボードでは、All Time StatsボードのSet Cmdsオプションと、StorageボードのItems統計がそれぞれ1増加しました。数値を提出するたびに、Set CmdsとItemsは両方とも増加します。新しい統計を読み込むには、Refreshボタンを押す必要があります。

注: アプリプラットフォームでアプリのログを確認することは、デバッグに役立ちます。アプリのダッシュボードから実行ログをクリックして表示します。

キャッシュに保存されたアイテムを使用することができます。アイテムがキャッシュされているかどうかを確認し、キャッシュにあればそこから提供します。そうでなければ、以前と同様に素数を見つけます。

server.jsに戻り、ハイライトされた行でファイルを更新してください。既存の行を修正し、キャッシュ用の新しい行を追加します:

...

app.get('/', (req, res) => {
  const n = req.query.n;

  if (!n) {
    res.render('index');
    return;
  }

  let prime;

  const key = 'prime_' + n;

  memcache.get(key, (err, val) => {
    if (err) console.log(err);

    if (val !== null) {
      // Use the value from the cache
      // Convert Buffer string before converting to number
      prime = parseInt(val.toString());
    } else {
      // No cached value available, find it
      prime = findPrime(n);

      memcache.set(key, prime.toString(), { expires: 0 }, (err) => {
        if (err) console.log(err);
      });
    }

    // Initialize likes for this number when necessary
    if (!likesMap[n]) likesMap[n] = 0;

    const locals = { n, prime, likes: likesMap[n] };
    res.render('index', locals);
  });
});

...

ファイルを保存してください。

このコードは、letキーワードを使用して値なしでprimeを初期化します。値が再割り当てされるためです。その後、memcache.getがキャッシュされた素数の取得を試みます。コントローラーのコードの大部分は、リクエストの処理方法を決定するためにその結果が必要であるため、memcache.getのコールバックに存在します。キャッシュされた値が利用可能な場合は、それを使用します。そうでなければ、素数を見つけるために計算を行い、以前と同様に結果をキャッシュに保存します。

memcache.get コールバックで返される値は Buffer なので、prime を数値に戻す前に文字列に変換します。

変更をコミットしてGitHubにプッシュしてデプロイします:

  1. git add . && git commit -m "Check cache for prime number" && git push

まだキャッシュされていない番号をアプリに送信すると、Set Cmds、Items、およびget missesの統計があなたのMemCachierダッシュボードで1ずつ増加します。このミスは、アイテムを設定する前にキャッシュから取得しようとするために発生します。アイテムはキャッシュに存在せず、ミスが発生し、その後アイテムが保存されます。キャッシュされた番号を送信すると、get hitsが増加します。

あなたは現在、リソース集約的な計算をキャッシュしています。次に、アプリのレンダリングされたビューをキャッシュします。

キャッシュされたレンダリングビュー

このセクションでは、ミドルウェアを使用して、Expressアプリによってレンダリングされたビューをキャッシュします。以前に、ejs をテンプレートエンジンとして設定し、提出された各数値 N のためのビューをレンダリングするテンプレートを作成しました。レンダリングされたビューは作成するのにリソースを多く消費する可能性があるため、それをキャッシュすることでリクエスト処理を速め、リソースを減らすことができます。

まず、middlewareディレクトリを作成します。それから、middleware/cacheView.jsというファイルを作成し、エディタで開きます。cacheView.jsにミドルウェア関数のために次の行を追加します。

const memcache = require('../services/memcache');

/**
 * Express middleware to cache views and serve cached views
 */
module.exports = function (req, res, next) {
  const key = `view_${req.url}`;

  memcache.get(key, (err, val) => {
    if (err) console.log(err);

    if (val !== null) {
      // Convert Buffer string to send as the response body
      res.send(val.toString());
      return;
    }
  });
};

まず、memcache クライアントをインポートします。次に、view_/?n=100 のようなキーを宣言します。次に、memcache.get を使って、そのキーのビューがキャッシュにあるかどうかを確認します。もしエラーがなく、そのキーに対して値が存在すれば、特に何もする必要はないので、リクエストはビューをクライアントに返して終了します。

次に、ビューがキャッシュされていない場合は、それをキャッシュしたいと思います。そのためには、強調表示された行を追加してres.sendメソッドをオーバーライドします:

...

module.exports = function (req, res, next) {
  const key = `view_${req.url}`;

  memcache.get(key, (err, val) => {
    if (err) console.log(err);

    if (val !== null) {
      // Convert Buffer to UTF-8 string to send as the response body
      res.send(val.toString());
      return;
    }

    const originalSend = res.send;
    res.send = function (body) {
      memcache.set(key, body, { expires: 0 }, (err) => {
        if (err) console.log(err);
      });

      originalSend.call(this, body);
    };
  });
};

あなたは、キャッシュにビューを保存する関数でres.sendメソッドをオーバーライドし、通常通り元のsend関数を呼び出します。thisコンテキストがオーバーライドされなかった場合のものに設定されるように、callを使って元のsend関数を呼び出します。正しいthis値が指定されるように、無名関数式(アロー関数ではない)を使用することを確認してください。

次に、強調表示された行を追加して、制御を次のミドルウェアに渡します。

...

/**
 * Express middleware to cache views and serve cached views
 */
module.exports = function (req, res, next) {
  const key = `view_${req.url}`;

  memcache.get(key, (err, val) => {
    if (err) console.log(err);

    if (val !== null) {
      // Convert Buffer to UTF-8 string to send as the response body
      res.send(val.toString());
      return;
    }

    const originalSend = res.send;
    res.send = function (body) {
      memcache.set(key, body, { expires: 0 }, (err) => {
        if (err) console.log(err);
      });

      originalSend.call(this, body);
    };

    next();
  });
};

...

nextを呼び出すと、アプリの次のミドルウェア関数が呼び出されます。しかし、あなたのケースでは他にミドルウェアがないため、コントローラが呼び出されます。Expressのres.renderメソッドは、ビューをレンダリングし、その後内部的にレンダリングされたビューを使用してres.sendを呼び出します。したがって、ホームルートのコントローラでは、res.renderが呼び出されると、オーバーライド関数が呼び出され、ビューがキャッシュに保存されてから、最終的に元のsend関数が呼び出されてレスポンスが完了します。

ファイルを保存してください。

注: コントローラーの render メソッドにコールバックを渡すこともできますが、キャッシュされる各ルートについてコントローラー内でビューキャッシュコードを重複させる必要があります。

今、server.jsにビューキャッシングミドルウェアをインポートします:

const express = require('express');
const findPrime = require('./utils/findPrime');
const memcache = require('./services/memcache');
const cacheView = require('./middleware/cacheView');

...

強調表示されたコードを追加して、GET / ホームルートで使用します:

...

app.get('/', cacheView, (req, res) => {
  ...
});

...

ファイルを保存してください。

それから、変更をコミットし、GitHubにプッシュしてデプロイします:

  1. git add . && git commit -m "Add view caching" && git push

アプリに数を入力する際は、すべてが通常通りに機能するはずです。新しい数を入力すると、Set Cmds、Items、およびget missesのためのMemCachierダッシュボードの統計が2増加します。1回は素数計算のため、もう1回は表示のためです。同じ数でアプリを更新すると、MemCachierダッシュボードに1つのget hitが追加されます。キャッシュからの表示が正常に取得されるため、素数結果を取得する必要はありません。

注: Expressアプリケーション設定のview cacheは、デフォルトで本番環境で有効になっています。このビューキャッシュはテンプレートの出力の内容をキャッシュするのではなく、基盤となるテンプレート自体のみをキャッシュします。リクエストごとにビューが再描画されるため、キャッシュが有効になっていても変更されません。したがって、実装したレンダリングビューキャッシングとは異なりますが、補完的なものです。

ビューをキャッシュするようになったので、いいねボタンが機能しなくなったことに気付くかもしれません。likesの値をログに記録すると、実際にその値は変化します。しかし、いいねの数が変わると、キャッシュされたビューも更新する必要があります。ビューが変更されたときには、キャッシュされたビューを無効にする必要があります。

次に、likesが変更されると、キャッシュからそれを削除してキャッシュされたビューを無効にします。server.jsに戻り、redirect関数を更新し、強調表示された行を追加します:

...

app.get('/like', (req, res) => {
  const n = req.query.n;

  if (!n) {
    res.redirect('/');
    return;
  }

  likesMap[n]++;

  // The URL of the page being 'liked'
  const url = `/?n=${n}`;

  res.redirect(url);
});

...

このビューのいいね数が変更されたため、キャッシュされたバージョンは無効になります。likesが変更されたときにキャッシュからいいね数を削除するために、ハイライトされた行を追加してください:

...
  const url = `/?n=${n}`;

  // The view for this URL has changed, so the cached version is no longer valid, delete it from the cache.
  const key = `view_${url}`;
  memcache.delete(key, (err) => {
    if (err) console.log(err);
  });

  res.redirect(url);
...

あなたの server.js ファイルは今、次の内容と一致しているはずです:

const express = require('express');
const findPrime = require('./utils/findPrime');
const memcache = require('./services/memcache');
const cacheView = require('./middleware/cacheView');

const app = express();

app.set('view engine', 'ejs');

/**
 * Key is `n`
 * Value is the number of 'likes' for `n`
 */
const likesMap = {};

app.get('/', cacheView, (req, res) => {
  const n = req.query.n;

  if (!n) {
    res.render('index');
    return;
  }

  let prime;

  const key = 'prime_' + n;

  memcache.get(key, (err, val) => {
    if (err) console.log(err);

    if (val !== null) {
      // Use the value from the cache
      // Convert Buffer string before converting to number
      prime = parseInt(val.toString());
    } else {
      // No cached value available, find it
      prime = findPrime(n);

      memcache.set(key, prime.toString(), { expires: 0 }, (err) => {
        if (err) console.log(err);
      });
    }

    // Initialize likes for this number when necessary
    if (!likesMap[n]) likesMap[n] = 0;

    const locals = { n, prime, likes: likesMap[n] };
    res.render('index', locals);
  });
});

app.get('/like', (req, res) => {
  const n = req.query.n;

  if (!n) {
    res.redirect('/');
    return;
  }

  likesMap[n]++;

  // The URL of the page being 'liked'
  const url = `/?n=${n}`;

  // The view for this URL has changed, so the cached version is no longer valid, delete it from the cache.
  const key = `view_${url}`;
  memcache.delete(key, (err) => {
    if (err) console.log(err);
  });

  res.redirect(url);
});

const port = process.env.PORT || 3000;
app.listen(port, () =>
  console.log(`Example app is listening on port ${port}.`)
);

ファイルを保存してください。

変更をコミットしてプッシュしてデプロイします。

  1. git add . && git commit -m "Delete invalid cached view" && git push

アプリのいいねボタンが今動作するようになります。ビューにいいねが付けられた時、あなたのMemCachierダッシュボードで以下の統計が変わります:

  • 削除ヒット はビューが削除されるにつれて増加します.
  • 取得ミスが増加するのは、ビューが削除され、キャッシュに存在しないためです。
  • ヒットを取得する 増分は、素数がキャッシュ内で見つかったためです。
  • Set Cmds は、更新されたビューがキャッシュに追加されるため、増加します。
  • アイテムは、ビューが削除され再追加されても変わりません。

あなたはレンダリングされたビューのキャッシングを実装し、ビューが変更されるとキャッシュされたビューを無効にしました。最終的に実装する戦略はセッションキャッシングです。

セッションのキャッシュ

このセクションでは、Expressアプリにセッションを追加しキャッシュします。キャッシュをセッションストレージとして使用します。セッションの一般的な使用例はユーザーログインですので、今後のユーザーログインシステムを実装するための前提として、セッションのキャッシングについてのこのセクションを考慮することができます(ただし、ユーザーログインシステムはこのチュートリアルの範囲を超えています)。短命のセッションをキャッシュに保存することは、多くのデータベースに保存するよりも高速でスケーラブルな場合があります。

注: キャッシュはタイムアウトする短命のセッションを保存するのに理想的です。しかし、キャッシュは永続的ではありません。長命のセッションには、データベースなどの永続的なストレージソリューションがより適しています。

express-sessionツールをインストールして、あなたのExpressアプリにセッションを追加し、connect-memjsをインストールしてMemCachierキャッシュをセッションストアとして使用できるようにします:

  1. npm install express-session connect-memjs

server.jsで、express-sessionとconnect-memjsをインポートします:

const express = require('express');
const findPrime = require('./utils/findPrime');
const memcache = require('./services/memcache');
const cacheView = require('./middleware/cacheView');
const session = require('express-session');
const MemcacheStore = require('connect-memjs')(session);

...

ファイルを保存してください。

セッションミドルウェアは、connect memcachedモジュールに渡され、express.session.Storeから継承できるようになります。

server.js まだ、セッションミドルウェアを設定して、キャッシュをストアとして使用します。強調表示された行を追加してください:

...

app.set('view engine', 'ejs');

app.use(
  session({
    secret: 'your-session-secret',
    resave: false,
    saveUninitialized: true,
    store: new MemcacheStore({
      servers: [process.env.MEMCACHIER_SERVERS],
      prefix: 'session_',
    }),
  })
);

...

secretはセッションクッキーに署名するために使用されます。必ず、your-session-secretをユニークな文字列で更新してください。

注意: 本番環境では、秘密情報を設定するために環境変数を使用するべきです。そのためには、secret: process.env.SESSION_SECRET || 'your-session-secret'を使用して秘密情報を設定できますが、App Platformのダッシュボードでも環境変数を設定する必要があります。

resaveは、リクエスト中に変更されていない場合にセッションを再保存することを強制します。アイテムをキャッシュに再保存する必要がないため、falseに設定します。

saveUninitialized: false は、ユーザーの属性が認証後にセッションに追加されることがよくあるログインセッションなどの修正されたセッションだけを保存したい場合に便利です。この場合、すべてのセッションを無差別に保存するため、true に設定します。

最後に、storeをキャッシュに設定し、セッションキャッシュキーのプレフィックスをsession_に設定します。つまり、キャッシュ内のセッションアイテムのキーはsession_<セッションID>のようになります。

次に、強調表示された行を含むアプリレベルのデバッグミドルウェアを追加します。これにより、キャッシュされたセッションがどのように機能しているかを特定するのに役立ちます。

...

app.use(
  session({
    ...
  })
);

/**
 * Session sanity check middleware
 */
app.use(function (req, res, next) {
  console.log('Session ID:', req.session.id);

  // Get the item from the cache
  memcache.get(`session_${req.session.id}`, (err, val) => {
    if (err) console.log(err);

    if (val !== null) {
      console.log('Session from cache:', val.toString());
    }
  });

  next();
});

...

そのミドルウェアは各リクエストのセッションIDをログに記録します。次に、そのIDに対するセッションをキャッシュから取得し、その内容をログに記録します。このアプローチは、セッションが正常に動作し、キャッシュされていることを示しています。

ファイルを保存し、その後、変更をコミットしてデプロイメントにプッシュしてください。

  1. git add . && git commit -m "Add session caching" && git push

アプリ内で数字を提出し、その後、アプリプラットフォームダッシュボードのランタイムログを確認してデバッグメッセージにアクセスしてください。セッションIDとログに記録された値が見つかり、セッションが機能しキャッシュされていることを示します。

MemCachier ダッシュボードでは、ビューとセッションがキャッシュされると、ページをリフレッシュするたびに 3 ヒット数 が表示されます:ビューに対して 1、セッションに対して 1、デバッグミドルウェアでセッションを取得するために 1 です。

セッションキャッシングを実装しました。ここで止めてもよいですし、任意の最終ステップでアプリを整理することもできます。

(オプション)ステップ7 — リソースの整理

このチュートリアルで展開したアプリは料金が発生するので、作業が終わったらアプリとMemCachierアドオンをオプションで削除することができます。

アプリのダッシュボードから、アクションをクリックし、次にアプリを削除します。

MemCachierアドオンをクリーンアップするには、アドオンをクリックし、次にMemCachierアドオンの名前をクリックします。次に、設定をクリックし、破棄をクリックします。無料のMemCachierキャッシュは、30日間の非活動の後に無効になりますが、ツールをクリーンアップすることは良い習慣です。

結論

このチュートリアルでは、いいねボタンを使って素数を探すExpressアプリを作成しました。その後、そのアプリをGitHubにプッシュし、DigitalOcean App Platformにデプロイしました。最後に、リソース集約的な計算、レンダリングされたビュー、およびセッションをキャッシュするためにMemCachier Add-Onを使用して、Expressアプリをより速く、かつスケーラブルにしました。このチュートリアルのすべてのファイルをDigitalOcean Communityリポジトリで確認できます。

実装した各キャッシング戦略では、キーに接頭辞がありました:prime_、view_、およびsession_。名前空間の利点に加えて、接頭辞はキャッシュのパフォーマンスをプロファイリングする追加の利点を提供します。このチュートリアルではMemCachierの開発者プランを使用しましたが、個別の接頭辞のパフォーマンスを追跡できるインストロスペクション機能セットが付属した完全管理型プランを試すこともできます。例えば、任意の接頭辞のヒット率やヒット比率を監視することで、キャッシュのパフォーマンスに関する詳細な洞察を得ることができます。MemCachierを引き続き利用するには、彼らのドキュメントを確認できます。

DigitalOcean App Platformを使い続けるために、私たちのApp Platformハウツーガイドを試して、App Platformドキュメントをさらに読んでください。

関連記事: