استفاده از Streams در Node.js: کارایی در پردازش داده ها و کاربردهای عملی

معرفی
همه ما در مورد قدرت استریم ها در Node.js شنیده ایم و اینکه چگونه آنها در پردازش مقادیر زیادی داده به شیوه ای بسیار کارآمد، با حداقل منابع حافظه، تقریباً جادویی، برتری می یابند. اگر نه، در اینجا توضیح مختصری در مورد جریان ها ارائه شده است.
Node.js یک بسته/کتابخانه به نام دارد node:stream
. این بسته، در میان چیزهای دیگر، سه کلاس را تعریف می کند: Readable
، Writable
، و Transform
.
- خواندنی: داده ها را از یک منبع می خواند و رابط های همگام سازی را از طریق سیگنال ها فراهم می کند. می تواند داده های خوانده شده را به یک نمونه Writable یا Transform “ارسال” کند.
- قابل نوشتن: می تواند از یک نمونه Readable (یا Transform) بخواند و نتایج را در یک مقصد بنویسد. این مقصد می تواند یک فایل، یک جریان دیگر یا یک اتصال TCP باشد.
- تبدیل: می تواند هر کاری را که Readable و Writable می تواند انجام دهد انجام دهد و همچنین داده های گذرا را تغییر دهد. ما میتوانیم جریانها را برای پردازش مقادیر زیادی از دادهها هماهنگ کنیم، زیرا هر یک در یک زمان بر روی یک بخش عمل میکنند، بنابراین از حداقل منابع استفاده میکنند.
جریان در عمل
اکنون که تئوری های زیادی داریم، زمان آن رسیده است که به موارد استفاده واقعی نگاه کنیم که در آن جریان ها می توانند تفاوت ایجاد کنند. بهترین سناریوها آنهایی هستند که میتوانیم بخشی از دادهها را کمی کنیم، برای مثال، یک خط از یک فایل، یک تاپل از پایگاه داده، یک شی از یک سطل S3، یک پیکسل از یک تصویر یا هر شی مجزا.
تولید مجموعه داده های بزرگ
شرایطی وجود دارد که ما نیاز به تولید مقادیر زیادی داده داریم، به عنوان مثال:
- پر کردن پایگاه داده با اطلاعات تخیلی برای اهداف آزمایشی یا ارائه.
- تولید داده های ورودی برای انجام تست استرس بر روی یک سیستم.
- اعتبارسنجی عملکرد شاخص ها در پایگاه های داده رابطه ای.
- در نهایت با استفاده از آن دو هارد 2 ترابایتی که برای راه اندازی RAID خریدیم اما هرگز استفاده نکردیم (شوخی، اما جدی).
در این مورد، ما یک فایل با 1 میلیارد مشتری ایجاد خواهیم کرد تا آزمایشاتی را روی پایگاه داده یک شرکت خیالی انجام دهیم: “Meu Prego Pago” (ناخن پرداخت شده من). هر مشتری از “Meu Prego Pago” ویژگی های زیر را خواهد داشت:
- شناسه
- نام
- تاریخ ثبت نام
- وارد شدن
- کلمه عبور
چالش اصلی تولید یک فایل با حجم زیاد داده، انجام این کار بدون مصرف تمام RAM موجود است. ما نمی توانیم کل این فایل را در حافظه نگه داریم.
ابتدا یک جریان قابل خواندن برای تولید داده ها ایجاد می کنیم:
import { faker } from '@faker-js/faker';
import { Stream } from "node:stream"
function generateClients(amountOfClients) {
let numOfGeneratedClients = 0;
const generatorStream = new Stream.Readable({
read: function () {
const person = {
id: faker.string.uuid(),
nome: faker.person.fullName(),
dataCadastro: faker.date.past({ years: 3 }),
login: faker.internet.userName(),
senha: faker.internet.password()
};
if (numOfGeneratedClients >= amountOfClients) {
this.push(null);
} else {
this.push(Buffer.from(JSON.stringify(person)), 'utf-8');
numOfGeneratedClients++;
}
}
})
return generatorStream;
}
را generateClients
تابع یک جریان را تعریف می کند و آن را برمی گرداند. مهمترین بخش این تابع اجرای آن است read
روش.
را read
روش کنترل می کند که جریان چگونه داده ها را با استفاده از بازیابی می کند this.push
. هنگامی که دیگر داده ای برای خواندن وجود ندارد، روش خواندن فراخوانی می شود this.push(null)
.
از کتابخانه هم استفاده می کنیم '@faker-js/faker'
اینجا برای تولید داده های مشتری خیالی.
Node.js پیاده سازی های متعددی از کلاس های جریان دارد. یکی از آنها است fs.createWriteStream
، که یک جریان Writable ایجاد می کند که روی یک فایل می نویسد (همانطور که ممکن است از نام آن حدس زده باشید).
ما از این جریان برای ذخیره تمام کلاینت های تولید شده توسطgeneClients استفاده خواهیم کرد.
import fs from "node:fs"
import {generateClients} from "./generate-clients.js"
const ONE_BILLION = Math.pow(10, 9);
// output file
const outputFile = "./data/clients.csv"
// get the clients stream
const clients = generateClients(ONE_BILLION);
// erase the file (if it exists)
fs.writeFileSync(outputFile, '', { flag: 'w' })
// add new clients to the file
const writer = fs.createWriteStream(outputFile, { flags: 'a' });
clients.pipe(writer);
روش “لوله”.
میتوانیم ببینیم که برای اتصال جریان Readable و Writable، از آن استفاده میکنیم pipe
روش. این روش انتقال دادهها را بین جریانهای خواندن و نوشتن همگامسازی میکند و اطمینان میدهد که یک نویسنده کند توسط یک خواننده بسیار سریع غرق نمیشود و بنابراین از تخصیص بیش از حد حافظه به عنوان بافری برای انتقال داده بین جریانها جلوگیری میکند. جزئیات اجرایی بیشتری در اینجا وجود دارد، اما این موضوع برای زمان دیگری است.
نتایج
در اینجا میتوانیم ببینیم که چگونه این فرآیند هنگام تولید فایل، حافظه را مصرف میکند:
همانطور که نشان داده شد، این فرآیند تقریباً 106 مگابایت رم را به طور مداوم مصرف می کند. ما میتوانیم این مصرف حافظه را با ارائه پارامترهای اضافی به جریانها در حین ایجاد آنها یا با ایجاد جریانهای خودمان تغییر دهیم.
نتیجه
ما می توانیم از Node.js برای مدیریت حجم زیادی از داده ها استفاده کنیم. حتی هنگام ایجاد فایل هایی با گیگابایت اطلاعات و میلیون ها خط، فقط از مقدار کمی حافظه استفاده می کنیم.