私たちのシステムを見ていただけますか?ソフトウェアを書いた人はもういませんし、私たちは多くの問題を抱えています。私たちは誰かがそれを調べて私たちのためにそれをきれいにする必要があります。
に行ったことがある人 ソフトウェア工学 かなりの時間、この一見無邪気な要求が、「災害がそこらじゅうに書かれている」プロジェクトの始まりであることが多いことを知っています。他の誰かのコードを継承することは、特にコードの設計が不十分でドキュメントが不足している場合、悪夢になる可能性があります。
それで、私が最近、私たちの顧客の1人から、彼の既存の socket.io チャットサーバーアプリケーション( Node.js )そしてそれを改善するために、私は非常に警戒していました。しかし、丘に向かって走る前に、私は少なくともコードを見ることに同意することにしました。
残念ながら、コードを見ると、私の懸念が再確認されただけです。このチャットサーバーは、単一の大きなJavaScriptファイルとして実装されていました。この単一のモノリシックファイルを、すっきりと設計され、保守が容易なソフトウェアに再設計することは、確かに課題です。しかし、私は挑戦を楽しんでいるので、同意しました。
既存のソフトウェアは、1,200行の文書化されていないコードを含む単一のファイルで構成されていました。うわぁ。さらに、いくつかのバグが含まれ、いくつかのパフォーマンスの問題があることが知られていました。
さらに、ログファイル(他の人のコードを継承する場合は常に開始するのに適した場所)を調べると、潜在的なメモリリークの問題が明らかになりました。ある時点で、プロセスは1GBを超えるRAMを使用していると報告されました。
これらの問題を考えると、ビジネスロジックのデバッグや拡張を試みる前に、コードを再編成してモジュール化する必要があることがすぐに明らかになりました。そのために、対処する必要のある初期の問題のいくつかは次のとおりです。
コードの再構築を開始する過程で、上記で特定された特定の問題に対処することに加えて、ソフトウェアシステムの設計に共通する(または少なくとも共通である必要がある)主要なアーキテクチャの目的のいくつかに対処し始めたいと思いました。 。これらには以下が含まれます:
私たちの目標は、単一のモノリシックmongoソースコードファイルから、モジュール化されたクリーンに設計されたコンポーネントのセットに移行することです。結果として得られるコードは、保守、拡張、およびデバッグが大幅に容易になるはずです。
このアプリケーションでは、コードを次の個別のアーキテクチャコンポーネントに編成することにしました。
logs
フォルダも含まれることに注意してください)この特定のアプローチには魔法はありません。コードを再構築するには、さまざまな方法があります。私は個人的に、この組織は過度に複雑になることなく、十分にクリーンでよく組織されていると感じました。
結果のディレクトリとファイルの編成を以下に示します。
ロギングパッケージは、今日のほとんどの開発環境と言語向けに開発されているため、今日では「独自の」ロギング機能が必要になることはめったにありません。
Node.jsを使用しているので、 log4js-node 、これは基本的にのバージョンです log4js Node.jsで使用するためのライブラリ。このライブラリには、いくつかのレベルのメッセージ(WARNING、ERRORなど)をログに記録する機能などのいくつかの優れた機能があり、たとえば毎日分割できるローリングファイルを作成できるため、その必要はありません。開くのに時間がかかり、分析や解析が難しい巨大なファイルを処理します。
私たちの目的のために、log4js-nodeの周りに小さなラッパーを作成して、特定の追加の必要な機能を追加しました。 log4js-nodeのラッパーを作成することを選択したことに注意してください。これは、コード全体で使用します。これにより、これらの拡張ロギング機能の実装が1つの場所にローカライズされるため、ロギングを呼び出すときにコード全体の冗長性と不要な複雑さが回避されます。
I / Oを使用していて、複数の接続(ソケット)を生成する複数のクライアント(ユーザー)が存在するため、ログファイルで特定のユーザーのアクティビティを追跡できるようにし、また知りたいと思います。各ログエントリのソース。したがって、アプリケーションのステータスに関するログエントリと、ユーザーアクティビティに固有のログエントリがいくつかあることを期待しています。
ロギングラッパーコードでは、ユーザーIDとソケットをマッピングできます。これにより、ERRORイベントの前後に実行されたアクションを追跡できます。ロギングラッパーを使用すると、イベントハンドラーに渡すことができるさまざまなコンテキスト情報を使用してさまざまなロガーを作成できるため、ログエントリのソースを知ることができます。
ロギングラッパーのコードが利用可能です ここに 。
多くの場合、システムのさまざまな構成をサポートする必要があります。これらの違いは、開発環境と本番環境の違いである場合もあれば、さまざまな顧客環境や使用シナリオを表示する必要性に基づく場合もあります。
これをサポートするためにコードの変更を要求するのではなく、一般的な方法は、構成パラメーターを使用してこれらの動作の違いを制御することです。私の場合、異なる設定を持つ可能性のある異なる実行環境(ステージングと本番)を持つ機能が必要でした。また、テストされたコードがステージングと本番の両方で適切に機能することを確認したかったので、この目的でコードを変更する必要があった場合、テストプロセスが無効になります。
Node.js環境変数を使用して、特定の実行に使用する構成ファイルを指定できます。したがって、以前にハードコードされたすべての構成パラメーターを構成ファイルに移動し、必要な設定で適切な構成ファイルをロードする単純な構成モジュールを作成しました。また、すべての設定を分類して、構成ファイルにある程度の編成を適用し、ナビゲートしやすくしました。
結果の構成ファイルの例を次に示します。
{ 'app': { 'port': 8889, 'invRepeatInterval':1000, 'invTimeOut':300000, 'chatLogInterval':60000, 'updateUsersInterval':600000, 'dbgCurrentStatusInterval':3600000, 'roomDelimiter':'_', 'roomPrefix':'/' }, 'webSite':{ 'host': 'mysite.com', 'port': 80, 'friendListHandler':'/MyMethods.aspx/FriendsList', 'userCanChatHandler':'/MyMethods.aspx/UserCanChat', 'chatLogsHandler':'/MyMethods.aspx/SaveLogs' }, 'logging': { 'appenders': [ { 'type': 'dateFile', 'filename': 'logs/chat-server', 'pattern': '-yyyy-MM-dd', 'alwaysIncludePattern': false } ], 'level': 'DEBUG' } }
これまで、さまざまなモジュールをホストするフォルダー構造を作成し、環境固有の情報を読み込む方法を設定し、ロギングシステムを作成したので、ビジネス固有のコードを変更せずにすべての要素を結び付ける方法を見てみましょう。
コードの新しいモジュラー構造のおかげで、エントリポイントapp.js
初期化コードのみを含む、十分に単純です。
var config = require('./config'); var logging = require('./logging'); var ioW = require('./ioW'); var obj = config.getCurrent(); logging.initialize(obj.logging); ioW.initialize(config);
コード構造を定義したとき、ioW
と言いましたフォルダには、businessおよびsocket.io関連のコードが含まれます。具体的には、次のファイルが含まれます(リストされているファイル名のいずれかをクリックすると、対応するソースコードが表示されます)。
index.js
– socket.ioの初期化と接続、イベントサブスクリプション、およびイベントの集中エラーハンドラーを処理しますeventManager.js
–すべてのビジネス関連ロジックをホストします(イベントハンドラー)webHelper.js
–Webリクエストを実行するためのヘルパーメソッド。linkedList.js
–リンクリストユーティリティクラスWebリクエストを作成するコードをリファクタリングして別のファイルに移動し、ビジネスロジックを同じ場所に変更せずに維持することができました。
1つの重要な注意: この段階で、eventManager.js
まだ別のモジュールに実際に抽出する必要があるいくつかのヘルパー関数が含まれています。ただし、この最初のパスの目的は、ビジネスロジックへの影響を最小限に抑えながらコードを再編成することであり、これらのヘルパー関数はビジネスロジックに複雑に関連付けられているため、これを後続のパスに延期して、コード。
Node.jsは定義上非同期であるため、コードのナビゲートとデバッグを特に困難にする「コールバック地獄」のネズミの巣に遭遇することがよくあります。この落とし穴を避けるために、私の新しい実装では、 パターンを約束します 特に活用しています 青い鳥 これは非常に素晴らしく、速い約束のライブラリです。 Promiseを使用すると、コードが同期しているかのようにコードを追跡できるようになり、エラー管理と、呼び出し間の応答を標準化するためのクリーンな方法も提供されます。コードには、一元化されたエラー処理とロギングを管理できるように、すべてのイベントハンドラーがpromiseを返す必要があるという暗黙のコントラクトがあります。
すべてのイベントハンドラーは、(非同期呼び出しを行うかどうかに関係なく)promiseを返します。これにより、エラー処理とログ記録を一元化でき、イベントハンドラー内に未処理のエラーが発生した場合に、そのエラーを確実にキャッチできます。
function execEventHandler(socket, eventName, eventHandler, data){ var sLogger = logging.createLogger(socket.id + ' - ' + eventName); sLogger.info(''); eventHandler(socket, data, sLogger).then(null, function(err){ sLogger.error(err.stack); }); };
ロギングの説明では、すべての接続にコンテキスト情報を含む独自のロガーがあると述べました。具体的には、ソケットIDとイベント名を作成時にロガーに関連付けているため、そのロガーをイベントハンドラーに渡すと、すべてのログ行にその情報が含まれます。
w9のc法人vss法人
var sLogger = logging.createLogger(socket.id + ' - ' + eventName);
イベント処理に関して言及する価値のあるもう1つのポイント:元のファイルにはsetInterval
がありました。 socket.io接続イベントのイベントハンドラー内にあった関数呼び出し。この関数を問題として識別しました。
io.on('connection', function (socket) { ... Several event handlers .... setInterval(function() { try { var date = Date.now(); var tmp = []; while (0 このコードは、指定された間隔(この場合は1分)でタイマーを作成しています。 すべての接続要求 私たちが得ること。したがって、たとえば、常に300のオンラインソケットがある場合、毎分300のタイマーが実行されます。上記のコードでわかるように、これに関する問題は、イベントハンドラーのスコープ内で定義されたソケットや変数が使用されていないことです。使用されている唯一の変数はmessageHub
です。モジュールレベルで宣言される変数。これは、すべての接続で同じであることを意味します。したがって、接続ごとに個別のタイマーはまったく必要ありません。そのため、これを接続イベントハンドラーから削除し、一般的な初期化コード(この場合はinitialize
)に含めました。関数。
最後に、応答の処理でwebHelper.js
に、デバッグプロセスに役立つ情報をログに記録する、認識されない応答の処理を追加しました。
if (!res || !res.d || !res.d.IsValid){ logger.debug(sendData); logger.debug(data); reject(new Error('Request failed. Path ' + params.path + ' . Invalid return data.')); return; }
最後のステップは、Node.jsの標準エラーのログファイルを設定することです。このファイルには、見逃した可能性のある未処理のエラーが含まれています。 Windowsのノードプロセス(理想的ではありませんが、ご存知のとおり…)をサービスとして設定するには、次のツールを使用します。 nssm これには、標準出力ファイル、標準エラーファイル、および環境変数を定義できるビジュアルUIがあります。
Node.jsのパフォーマンスについて
Node.jsは、シングルスレッドのプログラミング言語です。スケーラビリティを向上させるために、採用できるいくつかの選択肢があります。ノードクラスターモジュールがあるか、単にノードプロセスを追加し、それらの上にnginxを配置して、転送と負荷分散を行います。
ただし、この場合、すべてのノードクラスタサブプロセスまたはノードプロセスに独自のメモリスペースがあるため、これらのプロセス間で情報を簡単に共有することはできません。したがって、この特定のケースでは、外部データストア(など)を使用する必要があります 繰り返す )さまざまなプロセスでオンラインソケットを利用できるようにするため。
結論
これらすべてが整ったので、最初に渡されたコードの大幅なクリーンアップを達成しました。これは、コードを完璧にすることではなく、コードを再設計して、サポートと保守が容易になり、デバッグが容易になり、簡素化されるクリーンなアーキテクチャ基盤を作成することです。
以前に列挙した主要なソフトウェア設計原則(保守性、拡張性、モジュール性、スケーラビリティ)に準拠して、さまざまなモジュールの責任を明確かつ明確に識別するモジュールとコード構造を作成しました。また、元の実装で、パフォーマンスを低下させる高いメモリ消費につながるいくつかの問題を特定しました。
この記事を楽しんでいただけたでしょうか。さらにコメントや質問がある場合はお知らせください。