بهینه‌ترین روش‌ خواندن فایل در Node.js


۲۶ اردیبهشت ۱۴۰۰
بهینه‌ترین روش‌ خواندن فایل در node.js

ممکن در موارد مختلفی مانند ثبت خطاهای برنامه در فایل Log یا بررسی عملکرد برنامه نیاز باشد تا با فایل‌ها کار کنید. صرف نظر از دلیل شما برای کار با فایل‌ها، خواندن یک فایل در برنامه‌ی Node.js بسیار ساده و آسان است اما اگر اندازه‌ی فایل مورد نظر بیشتر از مقدار RAM سرور باشد یا حتی محدودیت‌هایی بر روی سرور شما اعمال شده باشد با مشکل روبرو خواهید شد بنابراین باید به‌دنبال راه حلی بهینه‌تر برای خواندن فایل‌ها در Node.js باشید.

حال در این مقاله تصمیم داریم بهینه‌ترین روش برای خواندن فایل‌ها در Node.js را با بررسی راه حل‌های زیر پیدا کنیم:

  • fs.readFileSync
  • fs.createReadStream
  • fs.read

نگاهی به نمودارها

نمودار زیر از برنامه‌ی پیاده‌سازی شده‌‌ی Node.js که در یک Docker container اجرا شده و مسئولیت آن خواندن یک فایل یک گیگابایتی به‌صورت Chunkهای ده مگابایتی بوده، به‌دست آمده است. همچنین توجه داشته باشید که مقدار memory در نمودار زیر حاصل جمع memory استفاده شده توسط Node.js و تمام فرایند‌های Docker container است.

نمودار میزان ram استفاده شده توسط متدهای مختلف برای خواندن فایل در node.js

همان‌طور که مشاهده می‌کنید متد fs.readFileSync بیش از یک گیگابایت از حافظه را اشغال می‌کند. پس از آن میزان حافظه‌ی استفاده شده توسط متد fs.createReadStream را مشاهده می‌کنید و درنهایت میزان حافظه‌ی استفاده شده توسط متد fs.read وجود دارد که حدودا دو برابر اندازه‌ی هر Chunk از حافظه‌ی سرور استفاده می‌کند.

اولین انتخاب توسعه‌دهندگان برای خواندن فایل‌ها در Node.js

متد fs.readFileSync یا متد Asynchronous آن یعنی fs.readFile از اولین انتخاب‌های توسعه‌دهندگان برای خواندن فایل‌ها در Node.js است زیرا با نوشتن چند خط کد بسیار ساده و یک حلقه‌ی for به تمام داده‌های موجود در فایل دسترسی پیدا می‌کنند و با پیمایش آن می‌توانند کارهای مختلفی انجام دهند:

const CHUNK_SIZE = 10000000; // 10MB
const data = fs.readFileSync('./file');
for (let bytesRead = 0; bytesRead < data.length; bytesRead = bytesRead + CHUNK_SIZE) {
    // do something with data 
}

اما با مشاهده‌ی نمودارهای بخش قبل متوجه می‌شوید که این راه حل مقدار زیادی از RAM را اشغال می‌کند زیرا در این روش تمام داده‌های فایل در متغیر data ذخیره می‌شود بنابراین جای تعجب نیست که برای یک فایل یک گیگابایتی بیش از یک گیگابایت از RAM را اشغال کند.

خواندن فایل‌ها در Node.js با استفاده از fs.createReadStream

استفاده از متد fs.createReadStream به‌سادگی متد fs.readFile است اما این متد مقدار stream را return می‌کند بنابراین شما به یک فرایند پردازشی دیگر نیاز خواهید داشت تا به داده‌های واقعی دسترسی پیدا کنید و ما در مثال زیر از یک حلقه‌ی for await استفاده کرده‌ایم که پیمایش آرایه را برای ما آسان‌تر می‌کند:

const CHUNK_SIZE = 10000000; // 10MB
async function start() {
    const stream = fs.createReadStream('./file', { highWaterMark: CHUNK_SIZE });
    for await (const data of stream) {
        // do something with data 
    }
}
start();

متغیر highWaterMark باعث می‌شود که فقط به‌اندازه‌ی تعداد بایت‌های تعریف شده از فایل مورد نظر خوانده شود و به این شکل شاهد کارکرد بهینه‌تر حافظه خواهیم بود زیرا اندازه‌ی محدودی از داده‌ها در حافظه نگهداری می‌شود.

کنترل بیشتر با استفاده از fs.read

این روش کمی پیچیده‌تر از دو روش قبلی است بااین‌‌حال کمترین میزان استفاده از RAM را در این روش شاهد هستیم اما قبل‌ از بررسی جزئیات باید بدانید که shared buffer چیست؟

مقایسه shared buffer و seprate buffer

shared buffer یک متغیر passed by refrence است و به‌جای ایجاد یک buffer جدید در هر فانکشن، یک buffer واحد در ابتدای برنامه ایجاد می‌شود. در نمودار زیر می‌توانید مقایسه‌ی بین دو برنامه‌ی مختلف را مشاهده کنید که در یکی از shared buffer استفاده شده است.

نمودار استفاده از حافظه با shared buffer و بدون آن

همان‌طور که مشاهده می‌کنید با استفاده از shared buffer شاهد استفاده‌ی بهینه‌تر حافظه هستیم و علاوه‌برآن سازگاری بیشتری در برنامه‌ی ما به‌وجود خواهد آمد.

حال که مفهوم shared buffer را متوجه شدید به‌سراغ پیاده‌سازی برنامه می‌رویم. در مرحله‌ی اول باید یک فانکشن با نام readBytes توسعه دهیم که یک Promise از fs.read را return می‌کند:

function readBytes(fd, sharedBuffer) {
    return new Promise((resolve, reject) => {
        fs.read(
            fd,
            sharedBuffer,
            0,
            sharedBuffer.length,
            null,
            (err) => {
                if (err) { return reject(err); }
                resolve();
            }
        );
    });
}

به دلیل اینکه نمی‌خواهیم به ریزجزئیات بپردازیم توصیه می‌شود که برای درک کدهای بالا به مستندات fs.read در سایت Node.js مراجعه کنید.

در مرحله‌ی بعد یک asynchronous generator با نام generateChunks را توسعه می‌دهیم:

async function* generateChunks(filePath, size) {
    const sharedBuffer = Buffer.alloc(size);
    const stats = fs.statSync(filePath); // file details
    const fd = fs.openSync(filePath); // file descriptor
    let bytesRead = 0; // how many bytes were read
    let end = size;

    for (let i = 0; i < Math.ceil(stats.size / size); i++) {
        await readBytes(fd, sharedBuffer);
        bytesRead = (i + 1) * size;
        if (bytesRead > stats.size) {
            // When we reach the end of file, 
            // we have to calculate how many bytes were actually read
            end = size - (bytesRead - stats.size);
        }
        yield sharedBuffer.slice(0, end);
    }
}

درنهایت داده‌ها را به‌کمک حلقه‌ی for پردازش خواهیم کرد:

const CHUNK_SIZE = 10000000; // 10MB

async function main() {
    for await (const chunk of generateChunks('./file', CHUNK_SIZE)) {
        // do someting with data       
    }
}

main();

البته باید بدانید که استفاده از این روش کمی ریسک به‌همراه دارد و امکان دارد Data leak یا Data malformation رخ دهد بنابراین باید در استفاده از این روش محتاط باشید.

منبع: https://betterprogramming.pub/a-memory-friendly-way-of-reading-files-in-node-js-a45ad0cc7bb6

برچسب‌ها:

خدمات رایگان لیارا

۲.۵ گیگابایت فضای ذخیره‌سازی ابری رایگان

۲.۵ گیگابایت Object Storage سازگار با پروتکل S3 با دیسک‌های SSD به‌صورت رایگان دریافت کنید.

هاست رایگان برای دیتابیس‌

دیتابیس‌های MariaDB، PostgreSQL و Redis را فقط با یک کلیک و به‌صورت رایگان تهیه کنید.

سرویس DNS رایگان

به سادگی دامنه‌تان را اضافه کنید و به صورت رایگان رکورد‌های آن را مدیریت کنید.

۱۰۰ هزار تومان اعتبار اولیه

بعد از ثبت نام در لیارا مبلغ ۱۰۰ هزار تومان اعتبار هدیه دریافت می‌کنید که با توجه به ساعتی بودن هزینه سرویس‌ها، می‌توانید تمامی خدمات پولی را برای چندین هفته رایگان استفاده کنید.

ارسال ۱۰۰ ایمیل تراکنشی رایگان در هر ماه

در سرویس ایمیل لیارا شما می‌توانید تا ۱۰۰ ایمیل رایگان در هر ماه ارسال کنید و فقط برای بیش از آن هزینه پرداخت کنید. (به‌همراه دسترسی SMTP)

هاست رایگان برای انواع وبسایت

تفاوتی ندارد برای وبسایت خود از Node استفاده می‌کنید یا Laravel و Django، در لیارا می‌توانید به صورت کاملا رایگان آن را میزبانی کنید.

توسعه‌دهندگان درباره‌ی ما چه می‌گویند

تجربه کار باliara_cloud@امروز خیلی خوب بود. یکی از سرویس هام رو منتقل کردم روش و راضیم. انقد سریع و جذاب کارم راه افتادم اصن باورم نمیشد! برعکس سرویس های PaaS دیگه با اون همه پیچیدگیشون. دمتون گرم
...

MohammadReza
liara testimonial
keikaavousi

بعد از بسته شدن @fandoghpaas و ناراحتی همه‌مون از اینکه یه سرویس خوب و صادق نمی‌تونه از پس هزینه‌ها بر بیاد، سرویسم رو منتقل کردم به پاس لیارا (https://liara.ir @liara_cloud) . تجربه راحت و خوب. تفاوت‌هایی داشت که کمی کار می‌خواست ولی تا الان کاملا راضی.

jadi
liara testimonial
jadi

یه خسته نباشید باید به تصمیمliara_cloud@بگم،
بعد از چندین روز سرکله زدن با سرویس های مشابه بالاخره تصمیم گرفتم لیارا رو امتحان کنم و باور نمیشه ۱۰ دقیقه بیشتر وقت نبرد،
دمتون گرم.

Arch
liara testimonial
EbadiDev

واسه سرویس PaaS با اختلاف لیارا بهترین رابط کاربری داره و یکی از مزیت‌های سرویس دیتابیس‌شون اینه که خودشون به صورت دوره‌ای بکآپ میگیرن.
...

Ali Najafi
liara testimonial
me_ali_najafi

یکی از کارهای خوبی که جدیداً میکنم اینه که یه دیتابیس روی لیارا میسازم و به پروژه وصل میکنم اینطوری هم خونه و هم محل کار دیتابیس بروز رو دارم و راحت میتونم ادامه بدم کار روliara_cloud@

Navid
liara testimonial
1navid

عاشقliara_cloud@شدم درسته در حد AWS نیست ولی خب تجربه خوبی واسه پروژه های داخل ایران ارائه میده، میتونم رو CD هم اجراش کنم

Amir H Shekari
liara testimonial
vanenshi

همراه شما هستیم

در خصوص سفارش یا استفاده از سرویس‌ها سوالی دارید؟
تلفن واحد فروش:
۰۲۵-۳۳۵۵۷۶۱۹ (روزهای کاری ۹ الی ۱۷)
تلفن واحد فروش: ۳۳۵۵۷۶۱۹-۰۲۵ (روزهای کاری ۹ الی ۱۷)