Node.JSの複雑なアプリケーションの構造とスケーリングについて

ソフトウェアプロジェクトの構造は重要です。 作業の最初に下される決定は、製品のライフサイクル全体を通してこの作業がどのようなものになるかによって異なります。







この資料は、Node.jsの複雑なアプリケーションの構造化に関するよくある質問への回答に基づいています。 これは、自分の開発の構造を改善する必要性を感じているすべての人を対象としています。



ここで取り上げる主なトピックは次のとおりです。





ここでは、さまざまな概念を説明するために、サンプルアプリケーションを使用します。アプリケーションの完全なコードはGitHubにあります



デモプロジェクトの概要



このアプリケーションは、特定のキーワードの更新を購読することにより、Twitterからデータを受け取ります。 適切なツイートがRabbitMQキューに送信されます。 キューの内容が処理され、Redisデータベースに保存されます。 さらに、アプリケーションには、保存されたツイートへのアクセスを提供するREST APIがあります。



プロジェクトファイルの構造は次のようになります。



. |-- config |   |-- components |   |   |-- common.js |   |   |-- logger.js |   |   |-- rabbitmq.js |   |   |-- redis.js |   |   |-- server.js |   |   `-- twitter.js |   |-- index.js |   |-- social-preprocessor-worker.js |   |-- twitter-stream-worker.js |   `-- web.js |-- models |   |-- redis |   |   |-- index.js |   |   `-- redis.js |   |-- tortoise |   |   |-- index.js |   |   `-- tortoise.js |   `-- twitter |       |-- index.js |       `-- twitter.js |-- scripts |-- test |   `-- setup.js |-- web |   |-- middleware |   |   |-- index.js |   |   `-- parseQuery.js |   |-- router |   |   |-- api |   |   |   |-- tweets |   |   |   |   |-- get.js |   |   |   |   |-- get.spec.js |   |   |   |   `-- index.js |   |   |   `-- index.js |   |   `-- index.js |   |-- index.js |   `-- server.js |-- worker |   |-- social-preprocessor |   |   |-- index.js |   |   `-- worker.js |   `-- twitter-stream |       |-- index.js |       `-- worker.js |-- index.js `-- package.json
      
      





プロジェクトには3つのプロセスがあります。





web



プロセスとworker



プロセスの違いについては後で説明します。次に、ソリューションの構成データについて説明します。



さまざまなランタイムおよびアプリケーション構成のサポート



特定のアプリケーションインスタンスの構成データは、環境変数からロードする必要があります。 定数としてコードに追加する必要はありません。



アプリケーションの展開のさまざまなバリエーションや、さまざまなランタイム環境で一致しない可能性のあるパラメーターについて説明しています。 たとえば、開発環境、ビルドサーバー、可能な限り動作に近い環境、そして最終的に運用環境での起動です。 このアプローチにより、任意の条件で機能する単一のアプリケーションコードベースを使用できます。



構成データと内部アプリケーションメカニズムが正しく分離されていることを確認する良い方法は次のとおりです。 プロジェクトコードをいつでも作業できるように公開できる場合は、ロジックと設定を分離する必要があります。 これは、バージョン管理システムに侵入する秘密データまたはアカウント設定に対する保護を自動的に意味します。



環境変数は、 process.env



オブジェクトを使用しprocess.env



アクセスできます。 オブジェクトには文字列値のみが格納されるため、ここで型変換が必要になる場合があります。



 // config/config.js 'use strict' //    [ 'NODE_ENV', 'PORT' ].forEach((name) => { if (!process.env[name]) {   throw new Error(`Environment variable ${name} is missing`) } }) const config = {  env: process.env.NODE_ENV, logger: {   level: process.env.LOG_LEVEL || 'info',   enabled: process.env.BOOLEAN ? process.env.BOOLEAN.toLowerCase() === 'true' : false }, server: {   port: Number(process.env.PORT) } // ... } module.exports = config
      
      





構成データの検証



コードに設定を含めないでください-ソリューションは正しいですが、使用する前に環境変数を確認することも非常に便利です。 これは、作業の最初に構成エラーを検出し、アプリケーションが誤った設定や欠落している設定で作業しようとする状況を回避するのに役立ちます。 構成データのエラーを早期に検出する利点については、 こちらをご覧ください



joi



バリデーターを使用してデータ検証を追加することにより、 config.js



ファイルを改善した方法を次に示します。



 // config/config.js 'use strict' const joi = require('joi') const envVarsSchema = joi.object({  NODE_ENV: joi.string()   .allow(['development', 'production', 'test', 'provision'])   .required(), PORT: joi.number()   .required(), LOGGER_LEVEL: joi.string()   .allow(['error', 'warn', 'info', 'verbose', 'debug', 'silly'])   .default('info'), LOGGER_ENABLED: joi.boolean()   .truthy('TRUE')   .truthy('true')   .falsy('FALSE')   .falsy('false')   .default(true) }).unknown() .required() const { error, value: envVars } = joi.validate(process.env, envVarsSchema) if (error) {  throw new Error(`Config validation error: ${error.message}`) } const config = {  env: envVars.NODE_ENV, isTest: envVars.NODE_ENV === 'test', isDevelopment: envVars.NODE_ENV === 'development', logger: {   level: envVars.LOGGER_LEVEL,   enabled: envVars.LOGGER_ENABLED }, server: {   port: envVars.PORT } // ... } module.exports = config
      
      





構成データの分離



すべての構成データを1つのファイルに保存できますが、プロジェクトの成長および開発中に、そのようなファイルはサイズが大きくなり、作業するのに不便になります。 これらの問題を回避するために、たとえばアプリケーションのコンポーネントに基づいて設定を分離することは理にかなっています。 この例では、次のようになります。



 // config/components/logger.js 'use strict' const joi = require('joi') const envVarsSchema = joi.object({  LOGGER_LEVEL: joi.string()   .allow(['error', 'warn', 'info', 'verbose', 'debug', 'silly'])   .default('info'), LOGGER_ENABLED: joi.boolean()   .truthy('TRUE')   .truthy('true')   .falsy('FALSE')   .falsy('false')   .default(true) }).unknown() .required() const { error, value: envVars } = joi.validate(process.env, envVarsSchema) if (error) {  throw new Error(`Config validation error: ${error.message}`) } const config = {  logger: {   level: envVars.LOGGER_LEVEL,   enabled: envVars.LOGGER_ENABLED } } module.exports = config
      
      





その後、メインのconfig.js



ファイルで、コンポーネントのパラメーターを組み合わせるだけで済みます。



 // config/config.js 'use strict' const common = require('./components/common') const logger = require('./components/logger') const redis = require('./components/redis') const server = require('./components/server') module.exports = Object.assign({}, common, logger, redis, server)
      
      





作業環境に応じて構成データをグループ化しないでください。つまり、アプリケーションの製品バージョンの設定をconfig/production.js



ファイルに保持しないでください。 このアプローチにより、たとえば、長期にわたって同じ生産バージョンを異なる環境にデプロイする必要がある場合など、アプリケーションのスケーラビリティが妨げられます。



マルチプロセスアプリケーションの構成



このプロセスは、最新のアプリケーションの主要な構成要素です。 ソフトウェア製品は、自身の状態を監視しない多くのプロセスで構成される場合があります。 この例では、このようなプロセスが使用されます。 したがって、HTTP要求はweb



プロセスで処理でき、 worker



プロセスはスケジュールに従って何かを実行したり、長時間かかる特定の操作を実行したりできます。 保存される情報はデータベースに記録されます。 このアーキテクチャのおかげで、ソリューションは並列プロセスの起動によるスケーリングに適しています。 プロセスの数を増やす必要性の基準は、アプリケーションの負荷など、さまざまなメトリックになります。



上記では、構成データのコンポーネントへの分離について説明しました。 このアプローチは、プロジェクトにさまざまなタイプのプロセスがある場合に非常に役立ちます。 各タイプのプロセスは独自の設定を取得でき、以前に使用されていない環境変数の存在を期待せず、必要なコンポーネントのみを要求します。



config/index.js







 // config/index.js 'use strict' const processType = process.env.PROCESS_TYPE let config try {  config = require(`./${processType}`) } catch (ex) { if (ex.code === 'MODULE_NOT_FOUND') {   throw new Error(`No config for process type: ${processType}`) } throw ex } module.exports = config
      
      





ルートのindex.js



ファイルで、環境変数PROCESS_TYPE



して目的のプロセスを実行します。



 // index.js 'use strict' const processType = process.env.PROCESS_TYPE if (processType === 'web') {  require('./web') } else if (processType === 'twitter-stream-worker') { require('./worker/twitter-stream') } else if (processType === 'social-preprocessor-worker') { require('./worker/social-preprocessor') } else { throw new Error(`${processType} is an unsupported process type. Use one of: 'web', 'twitter-stream-worker', 'social-preprocessor-worker'!`) }
      
      





その結果、1つのアプリケーションがありますが、多くの独立したプロセスに分割されています。 必要に応じて、それぞれを個別に起動して、同じ種類の複数の並列プロセスを発生させることができますが、これはアプリケーションの他の部分には影響しません。 同時に、異なるプロセスがモデルなどのコードの一部を共有できるため、開発中のDRY原則の遵守に貢献します。



テスト付きファイルの構成



<module_name>.spec.js



<module_name>.e2e.spec.js



などの命名規則を使用して、テスト済みのファイルをテスト済みモジュールの隣に配置する必要があります。 テストは、テストするモジュールとともに進化する必要があります。 テストファイルがアプリケーションロジックファイルから分離されている場合、それらを見つけて最新の状態に保つことはより困難になります。



別のフォルダ/test



、アプリケーション自体で使用されない追加のテストとユーティリティをすべて保存する/test



は理にかなっています。



ビルドファイルとスクリプトファイルの配置



通常、 /scripts



フォルダーを作成します。このフォルダーには、bashスクリプト、Node.jsスクリプトを配置してデータベースを同期したり、フロントエンドを構築したりします。 このアプローチのおかげで、スクリプトはメインアプリケーションコードから分離され、プロジェクトのルートディレクトリはしばらくするとスクリプトファイルでいっぱいになりません。 このすべてを使いやすくするために、 package.json



ファイルのスクリプトセクションにスクリプトを登録できます。



結論



Node.jsの複雑なプロジェクトの構築とスケーリングに関するアイデアがあなたの役に立つことを願っています。 ところで、ここで、このトピックに関するより多くの資料があります。



Node.jsは非常に柔軟な環境であるため、アプリケーションの構造とスケーリングの分野での決定の一部が究極の真実であるとは言えませんが、他の決定は完全に受け入れられません。 おそらく、上記の推奨事項と根本的に異なる独自の開発を行っているか、同じ方向に進んでいる可能性があります。 そのような成果がある場合、それらを共有することは素晴らしいことです。



All Articles