ms-dred
@ms-dred
Вечно что то не то и что то не так...

Утечка памяти при async/await?

Процесс работает 2 дня в итоге pm2 показывает 1350MB используемой памяти (трафик ~1500 хостов в сутки)
Структура основных файлов роутера
Не пойму где утечка, может быть и не тут, ищу уже неделю, предположения только из за saveCache(e), может ее на промисы подключить для параллельного выполнения с result?
const URL = require('url')
    , config = require('../configs')
    , score = require('../libs/score')
    , ObjectId = require('mongodb').ObjectId
    , HttpError = require('../libs/error').HttpError
    , Cache = require('../collections/cache').Cache
    , Posts = require('../collections/posts').Posts

exports.get = (request, response, next) => {

    request.href = `${request.protocol}://${request.get('host')}${request.originalUrl}`
    request.uid = request.user && request.user._id || ObjectId()

    const makeRequest = async () => {
        const e = {};
        const cache = await getCache(request, response)
        if(null !== cache) return
        e.params = await getParams(request)
        e.items = await getDocuments(e.params)
        return result(e, response), saveCache(e)
    }
      
    return makeRequest()
        .catch(e => {
            return next(new HttpError(404, e.message))
    })
}

/**
 * Get cache page href 
 * @param {*} request - href
 */
function getCache(request, response) {

    const href = new URL.URL(request.href)
    const format = +!!href.searchParams.get('format')

    href.searchParams.delete('seed')
    href.searchParams.delete('format')

    return Cache.findOne({ href: href }).exec()
        .then(e => {
            if(e) {
                e = JSON.parse(e.cache)
                e.score = score
                e.config = config
                e.params.sessionId = request.uid
            }
            return e ? (format && response.end(HTML(e)) || response.render('index', e)) : null
    }).catch(e => {
        throw new Error(e.message);
    })

}


/**
 * Сбор данных
 * @param {*} request 
 */
function getParams(request) {

    const cleanHref = new URL.URL(request.href)
    const format = +!!cleanHref.searchParams.get('format')

    cleanHref.searchParams.delete('seed')
    cleanHref.searchParams.delete('format')

    return {
        local: request.local,
        type: 'index',
        format: format,
        href: request.href,
        cleanHref: cleanHref,
        limit: config.count.photos,
        page: +cleanHref.searchParams.get('page') || 0,
        sessionId: request.user && request.user._id || ObjectId(),
        skip: !!parseInt(request.query.page) && (request.query.page - 1) * config.count.photos || 0
    }

}

/**
 * Get documents
 * @param {*} request 
 */
function getDocuments(request) {

    return Posts
        .aggregate([
            { $match: { catalog: { $nin: config.catalogs }, public: { $gte: 2 } } },
            { $sort: { indexAt: -1 } },
            { $skip: request.skip },
            { $limit: request.limit },
            {
                $lookup: {
                    from: 'users',
                    localField: 'owner',
                    foreignField: '_id',
                    as: 'owner'
                }
            }, {
                $project: {
                    _id: 1,
                    image: { $concat: ['$domain', 'o/', '$image', '?route=thumb&h=350'] },
                    href: { $concat: [config.domain.href, 'posts/', '$url', '.html'] },
                    tags: { $slice: ['$tags', 5] },
                    size: {
                        width: { $trunc: { $multiply: [ { $divide: ['$size.width', '$size.height'] }, 350] } },
                        height: { $trunc: 350 }
                    },
                    style: {
                        $concat: [
                            'background-color: rgb(',
                            { $toLower: { $arrayElemAt: [ '$pixels.rgb.r', 0 ] } }, ',',
                            { $toLower: { $arrayElemAt: [ '$pixels.rgb.g', 0 ] } }, ',',
                            { $toLower: { $arrayElemAt: [ '$pixels.rgb.b', 0 ] } }, ')'
                        ]
                    },
                    owner: { $arrayElemAt: [ '$owner', 0 ] },
                    //likes: { $in: [request.sessionId, '$likes'] }, // не работает правильно с кешированием
                    likes: 1,
                    indexAt: 1
                }
                
            }, {
                $group: {
                    _id: "$_id",
                    image: { $first: '$image' },
                    href: { $first: '$href' },
                    tags: { $first: '$tags' },
                    size: { $first: '$size' },
                    style: { $first: '$style' },
                    owner: { $first: {
                        _id: '$owner._id',
                        name: '$owner.name',
                        avatar: { $concat: ['background-image:url(', '$owner.avatar', ')'] },
                        href: { $concat: [config.domain.href, '$owner.username', '/'] }
                    } },
                    likes: { $first: '$likes' },
                    indexAt: { $first: '$indexAt' }
                }
            }, {
                $sort: { indexAt: -1 }
            }
            
        ])
        .exec()
        .then(e => {
            return e;
        })
        .catch(e => {
            throw new Error(e);
        })
}

/**
 * Result
 * @param {*} request 
 * @param {*} response 
 */
function result(request, response) {

    request.seed = new Date()
    request.cover = 1
    request.score = score
    request.config = config
    request.pagination = score.pagination({ count: request.items.length, href: request.params.href, limit: config.count.photos })
    request.page = {
        name: request.params.page ? request.params.local.pagination + request.params.page : request.params.local.name,
        description: request.params.local.description,
        keywords: request.params.local.keywords
    }
    return request.params.format && response.end(HTML(request)) || response.render('index', request)
    //return response.send(request)
}

/**
 * Write cache
 * @param {*} request 
 */
function saveCache(request) {
    return new Cache( { href: request.params.cleanHref, cache: JSON.stringify(request), type: request.params.type } ).save()
}



function HTML(request) {
    // Тут динамическая подгрузка данных
}
  • Вопрос задан
  • 423 просмотра
Решения вопроса 1
ms-dred
@ms-dred Автор вопроса
Вечно что то не то и что то не так...
Нашел где была утечка, но не тут.
Походе дело в npm sharp. Есть страницы где нужно предоставить картинку пользователю с нужным размером, по запросу скидывается все на sharp, для изменения размера изображения. Решил поставить GM для эксперимента и рост потребления памяти перестал увеличиваться.

До этого стоял sharp на калбеках и все было в порядке, после того как переписал на async/await начались проблемы.

Вот пример кода на sharp (async/await)
return sharp( path.join( config.path.dir, '../', request.document.replace(/^https?:\/\//, '') ) )
        .resize( request.params.width, request.params.height )
        .toFile(request.params.file)
        .then()
        .catch(e => {
            throw new Error(e)
    })


Версия sharp: "^0.18.4"

Переписал на GM так
function createFileGM(request) {

    return new Promise((resolve, reject) => {

        return gm( path.join( config.path.dir, '../', request.document.replace(/^https?:\/\//, '') ) )
            .resize(request.params.width, request.params.height, '^')
            .gravity('Center')
            .crop(request.params.width, request.params.height)
            .write(request.params.file, (e) => {
                if(e) return reject(e)
                return resolve()
        })
    })
}


Утечка пропала. Как вариант чтобы оставить sharp перепишу его на промисах и посмотрю что будет.
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
@Interface
Вы написали сюда довольно много кода, возможно вам кто-то сможет помочь в данном случае. Но я бы посоветовал вам следующие вещи:

1) попробуйте профилировщик памяти (https://www.toptal.com/nodejs/debugging-memory-lea... Это должно быть возможно во время отладки. По запросам 'node memory debug' / 'node heap dump'. Можно найти туториалы и пакеты (npm).

2) постарайтесь максимально локализовать проблему: чем меньше вы можете показать кода в котором воспроизводится проблема - тем проще ее найти, причем как вам, так и отвечающим (это существенно повысит шанс, что вам напишут рабочий ответ).

3) было бы очень кстати собрать эмулятор + работающее приложение, где есть исключительно эта утечка. А именно: минимальное приложение (которое можно выложить в публичный доступ не боясь нарушить NDA и подобное) + приложение эмулирующее запросы. Дебажить прямо в проде - долго и сложно. А так будет намного проще и "ответчики" смогут тоже подебажить.

Все эти советы облегчат работу как вам и вполне вероятно вы найдете ошибку в процессе, а также облегчат задачу отвечающим
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы