Node.jsアプリケーションのパフォーマンスを向上させる5つのヒント

NGINX | 2015年11月16日

「nodeサーバーの前段に#nginxを配置していないなら、おそらく構成を間違えています」— Bryan Hughes(Twitterへの投稿)

Node.jsは、世界で最も人気のあるプログラミング言語であるJavaScriptを使用してサーバーアプリケーションを作成するための主要なツールです。Webサーバーとアプリケーションサーバーの両方の機能を備えたNode.jsは、現在、あらゆる種類のマイクロサービスベースの開発とデリバリーに欠かせないツールと見なされています。(Forresterによる無料の「Node.jsとNGINXに関するレポート」をダウンロードしてください。)

Node.jsは、バックエンドアプリケーション開発において、Javaや.NETの代替として、またはそれらを補完するものとして利用できます。

Node.jsはシングルスレッドでノンブロッキングI/Oを使用するため、スケールして数万件の同時処理に対応できます。こうしたアーキテクチャ上の特性はNGINXと共通しており、NGINXも解決するために開発されました。その課題とは、10,000件を超える同時接続に対応するというC10K問題です。Node.jsは、高いパフォーマンスと開発者の生産性で広く知られています。

では、どのような問題が起こり得るのでしょうか?

Node.jsにはいくつかの弱点や脆弱性があり、Node.jsベースのシステムではパフォーマンスの低下やクラッシュが発生しやすくなる可能性があります。Node.jsベースのWebアプリケーションへのトラフィックが急増すると、問題がより頻繁に発生します。

また、Node.jsは、Webページの中核となる動的コンテンツを生成するアプリケーションロジックの作成と実行に非常に適しています。一方、画像やJavaScriptファイルなどの静的コンテンツの配信や、複数のサーバー間での負荷分散にはあまり適していません。

Node.jsを最大限に活用するには、静的コンテンツのキャッシュ、複数のアプリケーションサーバー間でのプロキシ処理と負荷分散、さらにクライアント、Node.js、Socket.IOを実行するサーバーなどの補助サービス間で発生するポート競合の管理が必要です。NGINXはこれらすべての用途に使用できるため、Node.jsのパフォーマンスチューニングに非常に役立つツールです。

以下のヒントを活用して、Node.jsアプリケーションのパフォーマンスを向上させましょう。

  1. リバースプロキシサーバーを導入する
  2. 静的ファイルをキャッシュする
  3. 複数のサーバー間でトラフィックを負荷分散
  4. WebSocket接続をプロキシ
  5. SSL/TLSとHTTP/2を実装する

注:Node.jsアプリケーションのパフォーマンスを手早く改善するには、最新のマルチコアサーバーを活用できるようにNode.jsの設定を変更します。詳しくは、Node.jsのドキュメントをご覧ください。WebサーバーのCPU数と同じ数の子プロセスをNode.jsで個別に生成する方法を確認できます。すると、各プロセスがまるで魔法のように、それぞれ1つのCPUに割り当てられ、パフォーマンスが大幅に向上します。

ヒント1:リバースプロキシサーバーを導入する

NGINX, Inc.では、高パフォーマンスなサイトの中核を担うアプリケーションサーバーが、インターネットからの受信トラフィックに直接さらされているのを見るたびに、少なからず危機感を覚えます。これには、たとえば多くのWordPressベースのサイトやNode.jsサイトが含まれます。

Node.jsは、ほとんどのアプリケーションサーバーよりもスケーラビリティを重視して設計されており、Webサーバー機能も大量のインターネットトラフィックを比較的適切に処理できます。しかし、Web配信はNode.jsの本来の目的ではなく、そのために設計されたものではありません。

トラフィック量の多いサイトでは、アプリケーションのパフォーマンスを向上させるための第一歩として、Node.jsサーバーの前段にリバースプロキシサーバーを配置します。これにより、Node.jsサーバーがインターネットトラフィックに直接さらされるのを防ぐとともに、複数のアプリケーションサーバーの利用、サーバー間の負荷分散、コンテンツのキャッシュを柔軟に行えるようになります。

既存のサーバー環境の前段にNGINXをリバースプロキシサーバーとして配置し、さらに他の用途にも活用する構成は、NGINXの中核的なユースケースであり、世界中の数千万ものWebサイトで採用されています。

次のような、NGINXをNode.jsのリバースプロキシサーバーとして使用する固有のメリットがあります。

  • 権限処理とポート割り当ての簡素化
  • 静的画像をより効率的に配信(次のヒントを参照)
  • Node.jsのクラッシュを適切に管理する
  • DoS攻撃の緩和

注記:これらのチュートリアルでは、Ubuntu 14.04またはCentOS環境でNGINXをリバースプロキシサーバーとして使用する方法を説明します。NGINXをNode.jsの前段に配置する方にとって、概要を把握するのに役立ちます。

ヒント2 – 静的ファイルをキャッシュする

Node.jsベースのサイトの利用が増えるにつれて、サーバーへの負荷が顕在化し始めます。この時点で行うべきことは2つあります。

  1. Node.jsサーバーを最大限に活用する
  2. アプリケーションサーバーを容易に追加し、サーバー間で負荷分散できるようにする

実際、これは簡単に実現できます。まず、前のヒントで説明したように、NGINXをリバースプロキシサーバーとして実装します。これにより、キャッシュや負荷分散(複数のNode.jsサーバーがある場合)などを簡単に実装できます。

アプリケーションコンテナプラットフォームであるModulusのWebサイトには、NGINXを使用してNode.jsアプリケーションのパフォーマンスを大幅に向上させる方法を紹介する有用な記事があります。Node.jsだけですべての処理を行った場合、著者のサイトが処理できたリクエスト数は平均で毎秒約900件でした。NGINXをリバースプロキシサーバーとして使用して静的コンテンツを配信したところ、同じサイトで毎秒1,600件を超えるリクエストを処理でき、パフォーマンスは約2倍に向上しました。

パフォーマンスを2倍に高めることで、サイト設計の見直し(および必要に応じた改善)、アプリケーションコードの最適化、アプリケーションサーバーの追加導入など、さらなる成長に対応するための追加対策を講じる時間を確保できます。

以下は、Modulus上で稼働するWebサイトで使用できる設定コードです。

server {    listen 80;
    server_name static-test-47242.onmodulus.net;

    root /mnt/app;
    index index.html index.htm;

    location /static/ {
        try_files $uri $uri/ =404;
    }

    location /api/ {
        proxy_pass http://node-test-45750.onmodulus.net;
    }
}

NGINX, Inc.のPatrick Nommensenによるこの詳細な記事では、Node.jsアプリケーションであるオープンソースのブログプラットフォームGhost上で運営する個人ブログの静的コンテンツをキャッシュする方法について説明しています。一部はGhost固有の内容ですが、コードの多くは他のNode.jsアプリケーションにも再利用できます。

たとえば、NGINXのlocationブロックでは、一部のコンテンツをキャッシュ対象から除外することが考えられます。通常、ブログプラットフォームの管理インターフェースはキャッシュしません。以下は、Ghostの管理インターフェースをキャッシュ対象から除外する設定コードです。

location ~ ^/(?:ghost|signout) {     proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $http_host;
    proxy_pass http://ghost_upstream;
    add_header Cache-Control "no-cache, private, no-store,
    must-revalidate, max-stale=0, post-check=0, pre-check=0";
}

静的コンテンツの配信に関する一般的な情報については、「NGINX Plus Admin Guide」を参照してください。この管理ガイドでは、設定手順、ファイル検索の成功時または失敗時の応答に関する複数のオプション、さらに高速なパフォーマンスを実現するための最適化手法について説明しています。

NGINXサーバーで静的ファイルをキャッシュすると、Node.jsアプリケーションサーバーの処理負荷が大幅に軽減され、パフォーマンスが大きく向上します。

ヒント3 – Node.jsロードバランサーを実装する

Node.jsアプリケーションで高い、つまりほぼ無制限のパフォーマンスを実現する真の鍵は、複数のアプリケーションサーバーを稼働させ、それらすべてに負荷を分散することです。

Node.jsのロードバランシングは、特に複雑になる場合があります。Node.jsでは、JSONオブジェクトをデータ交換の媒体として、Webブラウザで実行されるJavaScriptコードとNode.jsアプリケーションサーバーで実行されるJavaScriptコードが高度に連携するためです。つまり、特定のクライアントセッションは同じアプリケーションサーバー上で継続的に実行されるため、複数のアプリケーションサーバーでセッション永続性を実現することは本質的に困難です。

インターネットとWebの大きな利点の1つは、ステートレス性が高いことです。これにより、要求されたファイルにアクセスできるサーバーであれば、どのサーバーでもクライアントのリクエストを処理できます。Node.jsはこのステートレス性を覆すものであり、特定のクライアントからのリクエストに常に同じサーバーが応答する、ステートフルな環境で最も効果的に機能します。

この要件を最も適切に満たせるのは、NGINX Open Sourceではなく、NGINX Plusです。NGINXの2つのバージョンは非常によく似ていますが、対応する負荷分散アルゴリズムが異なる点が大きな違いの1つです。

NGINXはステートレスな「負荷分散方式」をサポートしています:

  • ラウンドロビン – 新しいリクエストをリスト内の次のサーバーに送信します。
  • 最小接続数 – 新しいリクエストは、アクティブな接続数が最も少ないサーバーに送信されます。
  • IP Hash – 新しいリクエストは、クライアントのIPアドレスのハッシュ値に割り当てられたサーバーに送信されます。

これらの方式のうち、特定のクライアントからのリクエストを同じサーバーに確実に送信できるのはIP Hashだけであり、Node.jsアプリケーションに適しています。ただし、負荷分散手法に関するこちらの「ブログ記事」で説明しているように、IP Hashでは、他のサーバーに振り分けられるリクエストが減る一方で、1台のサーバーにリクエストが偏りやすくなります。この方式はステートフル性をサポートしますが、サーバーリソース間でのリクエストの割り当てが最適でなくなる可能性があります。

NGINXとは異なり、NGINX Plusはセッション維持をサポートしています。セッション維持を使用すると、特定のクライアントからのすべてのリクエストが確実に同じサーバーに送信されます。これにより、クライアントとサーバー間のステートフルな通信を実現するNode.jsと、高度な負荷分散機能を備えたNGINX Plusの利点を最大限に引き出せます。

そのため、NGINXまたはNGINX Plusを使用して、複数のNode.jsサーバー間の負荷分散を実現できます。ただし、負荷分散のパフォーマンスを最大限に高めながら、Node.jsに適したステートフル性も実現できる可能性が高いのは、NGINX Plusのみです。NGINX Plusに組み込まれているアプリケーションのヘルスチェックと監視機能も、ここで役立ちます。

NGINX Plusは、セッションドレイニングにも対応しています。これにより、アプリケーションサーバーは、自身をサービス対象から外すよう要求された後も、進行中のセッションを正常に完了できます。

ヒント4 – WebSocket接続のプロキシ

HTTPは、すべてのバージョンにおいて、クライアントがサーバーにファイルを要求する「プル」型通信向けに設計されています。WebSocketは、サーバーがクライアントから要求されていないファイルを能動的に送信できる「プッシュ」型および「プッシュ/プル」型通信を実現するための仕組みです。

The WebSocketプロトコルを使用すると、転送データ量を削減し、レイテンシーを最小限に抑えながら、クライアントとサーバー間のより堅牢なやり取りを容易に実現できます。必要に応じて、クライアントとサーバーの双方がリクエストを開始および受信できる全二重接続も実現可能です。

WebSocketプロトコルは堅牢なJavaScriptインターフェースを備えているため、Node.jsをアプリケーションサーバーとして使用する場合に適しています。また、トランザクション量が中程度のWebアプリケーションでは、Webサーバーとしても適しています。トランザクション量が増加した場合は、クライアントとNode.js Webサーバーの間にNGINXを配置し、NGINXまたはNGINX Plusを使用して、静的ファイルをキャッシュし、複数のアプリケーションサーバー間で負荷分散を行うのが合理的です。

Node.jsは、Node.jsアプリケーションと組み合わせて広く利用されているWebSocket APIであるSocket.IOと併用されることがよくあります。その結果、ポート80(HTTP用)またはポート443(HTTPS用)が混雑する場合があります。この問題は、Socket.IOサーバーへのプロキシを構成することで解決できます。前述のとおり、NGINXをプロキシサーバーとして使用でき、静的ファイルのキャッシュや負荷分散などの追加機能も利用できます。

以下は、ポート5000でリッスンするNodeアプリケーションファイルserver.jsのコードです。このアプリケーションはWebサーバーではなくプロキシサーバーとして動作し、リクエストを適切なポートにルーティングします。

var io = require('socket.io').listen(5000);
io.sockets.on('connection', function (socket) {
  socket.on('set nickname', function (name) {
    socket.set('nickname', name, function () {
      socket.emit('ready');
    });
  });

  socket.on('msg', function () {
    socket.get('nickname', function (err, name) {
      console.log('Chat message by ', name);
    });
  });
});

Within your index.htmlファイルに次のコードを追加して、サーバーアプリケーションに接続し、アプリケーションとユーザーのブラウザー間にWebSocketを作成します。

<script src="/socket.io/socket.io.js"></script><script>// <![CDATA[
     var socket = io(); // your initialization code here.
// ]]>
</script>

NGINXの設定を含む詳しい手順については、NGINXおよびNGINX PlusをNode.jsおよびSocket.IOと組み合わせて使用する方法に関するブログ記事をご覧ください。この種のWebアプリケーションで生じる可能性があるアーキテクチャおよびインフラストラクチャ上の問題について詳しくは、リアルタイムWebアプリケーションとWebSocketに関するブログ記事をご覧ください。

ヒント5:SSL/TLSとHTTP/2を実装する

サイト上でのすべてのユーザー操作を保護するために、SSL/TLSを使用するサイトが増えています。SSL/TLSに移行するかどうか、またいつ移行するかはお客さまの判断ですが、移行する場合、NGINXは次の2つの方法で移行を支援します。

  1. NGINXをリバースプロキシとして設定すると、クライアントとのSSL/TLS接続をNGINXで終端できます。Node.jsサーバーとNGINXリバースプロキシサーバーの間では、暗号化されていないリクエストとコンテンツが送受信されます。
  2. 初期の検証結果では、HTTPプロトコルの新しいバージョンであるHTTP/2を使用することで、SSL/TLSの使用に伴うパフォーマンス低下の大部分またはすべてを相殺できる可能性があります。NGINXはHTTP/2をサポートしており、SSL/TLSとともにHTTP/2を終端できるため、Node.jsアプリケーションサーバーを変更する必要もありません。

必要な実装手順には、Node.js構成ファイル内のURLの更新、NGINX構成でのセキュアな接続の確立と最適化、必要に応じたSPDYまたはHTTP/2の使用が含まれます。HTTP/2のサポートを追加すると、HTTP/2に対応するバージョンのブラウザーは新しいプロトコルを使用してアプリケーションと通信し、古いバージョンのブラウザーは引き続きHTTP/1.xを使用します。

サーバーレベルで実装されたHTTP/2は、HTTP/2とHTTP/1.xの両方に対応するブラウザーをサポート

以下の設定コードは、こちらで説明している、SPDYを使用するGhostブログ向けのものです。OCSPステープリングなどの高度な機能が含まれています。OCSPステープリングのオプションを含め、SSL終端にNGINXを使用する際の考慮事項については、こちらをご覧ください。同じトピックの概要については、こちらをご覧ください。

Node.jsアプリケーションを構成し、SPDYからHTTP/2へアップグレードするために必要な変更はわずかです。今すぐアップグレードすることも、2016年初頭にSPDYのサポートが終了する際に行うこともできます。

server {    server_name domain.com;
    listen 443 ssl spdy;
    spdy_headers_comp 6;
    spdy_keepalive_timeout 300;
    keepalive_timeout 300;
    ssl_certificate_key /etc/nginx/ssl/domain.key;
    ssl_certificate /etc/nginx/ssl/domain.crt;
    ssl_session_cache shared:SSL:10m;  
    ssl_session_timeout 24h;           
    ssl_buffer_size 1400;              
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/nginx/ssl/trust.crt;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains';
    add_header X-Cache $upstream_cache_status;
 
    location / {
        proxy_cache STATIC;
        proxy_cache_valid 200 30m;
        proxy_cache_valid 404 1m;
        proxy_pass http://ghost_upstream;
        proxy_ignore_headers X-Accel-Expires Expires Cache-Control;
        proxy_ignore_headers Set-Cookie;
        proxy_hide_header Set-Cookie;
        proxy_hide_header X-powered-by;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header Host $http_host;
        expires 10m;
    }

    location /content/images {
        alias /path/to/ghost/content/images;
        access_log off;
        expires max;
    }

    location /assets {
        alias /path/to/ghost/themes/uno-master/assets;
        access_log off;
        expires max;
    }

    location /public {
        alias /path/to/ghost/built/public;
        access_log off;
        expires max;
    }

    location /ghost/scripts {
        alias /path/to/ghost/core/built/scripts;
        access_log off;
        expires max;
    }

    location ~ ^/(?:ghost|signout) { 
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://ghost_upstream;
        add_header Cache-Control "no-cache, private, no-store,
        must-revalidate, max-stale=0, post-check=0, pre-check=0";
        proxy_set_header X-Forwarded-Proto https;
    }
}

まとめ

このブログ記事では、Node.jsアプリケーションで実施できる、特に重要なパフォーマンス改善策をいくつか紹介します。Node.jsと併用するアプリケーション構成にNGINXを追加し、リバースプロキシサーバーとしての利用、静的ファイルのキャッシュ、負荷分散、WebSocket接続のプロキシ、SSL/TLSおよびHTTP/2プロトコルの終端に活用する方法を中心に説明します。

NGINXとNode.jsの組み合わせは、広く認知されており、マイクロサービスに適した新しいアプリケーションを構築する方法として、またJavaやMicrosoft .NETを使用する既存のSOAベースのアプリケーションに柔軟性と機能を追加する方法として活用されています。この記事では、Node.jsアプリケーションを最適化し、必要に応じてNode.jsとNGINXの連携を実現する方法を紹介します。


共有

著者について

Floyd Smith
Floyd Smithコンテンツマーケティングディレクター

Floyd Smithのその他のブログ記事

関連するブログ記事

チュートリアル:NGINXを使用してアプリケーション向けにOpenTelemetryを構成する
NGINX | 2024/01/18

チュートリアル:NGINXを使用してアプリケーション向けにOpenTelemetryを構成する

OpenTelemetryを使用してNGINXサーバーの状態と健全性をより明確に把握することで、問題のトラブルシューティングを行い、Webアプリケーションのパフォーマンスを向上できます。

AkitaのNGINX Plus認定モジュールでAPIエンドポイントの問題を特定・修正
NGINX | 2023/05/31

AkitaのNGINX Plus認定モジュールでAPIエンドポイントの問題を特定・修正

Akita SoftwareのAPIオブザーバビリティ向け新しいNGINX Plus Certified Moduleを活用して、APIエンドポイントの問題をより迅速に特定し、修正できます。

JavaプロジェクトをスキャフォールディングしてKubernetesネイティブアプリを構築する方法
NGINX | 2023/02/09

JavaプロジェクトをスキャフォールディングしてKubernetesネイティブアプリを構築する方法

Red HatのシニアプリンシパルデベロッパーアドボケイトであるDaniel Oh氏が、Javaプロジェクトをゼロから構築するためのひな形を作成し、Kubernetesネイティブアプリケーションとしてビルドする方法を解説します。

F5 NGINXaaS for Azureのご紹介:Microsoft Azureでネイティブに利用できるSaaS型ロードバランシング
NGINX | 2022/05/24

F5 NGINXaaS for Azureのご紹介:Microsoft Azureでネイティブに利用できるSaaS型ロードバランシング

NGINXaaS for Azureは、NGINX PlusをAzureネイティブのSaaSとして利用できるサービスです。NGINXの設定を容易にリフト&シフトできるため、クラウド導入とモダンアプリの展開を加速できます。

SELinuxでNGINXおよびNGINX Plusを使用する
NGINX | 2018/08/17

SELinuxでNGINXおよびNGINX Plusを使用する

Red Hat Enterprise Linux(RHEL)および関連ディストリビューションでSecurity-Enhanced Linux(SELinux)を有効にすると、デフォルト設定によりNGINXとNGINX Plusの一部の操作が制限されます。この記事では、すべての機能を利用できるようにSELinuxの設定を変更する方法について説明します。

NGINX PlusによるAWS Auto Scalingグループの負荷分散
NGINX | 2017/03/06

NGINX PlusによるAWS Auto Scalingグループの負荷分散

新しいnginx-asg-sync統合ソフトウェアにより、NGINX PlusはAWS Auto Scalingグループの負荷を直接分散できるため、クラウドでの負荷分散がさらに柔軟になります