Node.JS и выгрузка каталога из 1С на сайт

в 7:52, , рубрики: javascript, json, node.js, parser, Веб-разработка, метки: , , ,

На днях закрыли очередной проект. Суть: создание новой версии интернет-каталога. Старая версия сайта, в силу ряда причин, клиента не устраивала. Особенностью проекта была его номенклатурная база. Объём номенклатуры каталога составлял ~26000 позиций раскиданных по дереву из 513 узлов + характеристики товара. Почти каждая номенклатурная позиция имела описание на 1-2К текста.

Файл выгрузки каталога в формате ComerceML 2 для старого сайта весил 104 MB. Формировался на стороне 1С 10 минут и после передачи на хостинг, парсился на стороне сайта полтора часа (!) со 100% загрузкой CPU.

Выход из ситуации

В качестве альтернативы XML формату решили выгрузить в JSON. Идея была в том, чтобы попробовать разобрать JSON чем-то, что имеет нативную реализацию парсера, а именно node.js с его JSON.parse().

Наш 1С-ник, разобравшись с новым для него форматом, в несколько итераций добился того, что выгрузка 1С формировала валидный JSON. Время формирования выгрузки сократилось с 10 до 3.5 минут. Те же данные которые в XML формате занимали 104 мегабайта, уместились в 58 мегабайт JSON. Но это было ожидаемо, неожиданностью стало другое…

Для тестирования времени разбора выгрузки я набросал тестовый код:

// Node.js

var fs = require('fs');

function parser(filename, callback){

    fs.readFile(filename, { encoding:'utf8' }, function(err, dataFromFile){
        var parsedData;

        if(err){
            callback(err);
        } else {
            try {
                console.time('parse data'); // парсинг - синхронная операция...
                parsedData = JSON.parse(dataFromFile.toString().trim()); // <- собственно сам парсинг.
                console.timeEnd('parse data'); // ... поэтому измеряем задержку "в лоб".

                callback(null, parsedData ); 
            }
            catch (e){
                callback(e)
            }
        }

    });
}

parser('../import/import.json', function(err, data){

    if(err){
        throw (err);
    }

    console.log('groups', data.groups.length);
    console.log('items', data.items.length);
    console.log('properties', data.properties.length);
});

Запустив его на своей машине (CPU 3.3GHz), я даже не успел привстать чтобы пойти за чаем. Результат и скорость с которой он был выведен в консоль, заставил меня предположить, что в коде баг и он не отработал корректно…

> node parse.js

parse data: 718ms
groups 513
items 26098
properties 149

Но это был не баг. Данные действительно были разобраны и помещены в память за ⅔ секунды. Количество элементов в коллекциях полностью совпадало с заявленным количеством в 1С. Оставалось только найти под столом отпавшую челюсть и написать сервис с полным циклом обработки данных.

Общая архитектура сервиса обработки выгрузки

В целом выгрузка на сайт отрабатывает по стандартной схеме:

  • формирование выгрузки из 1С и его упаковка архиватором;
  • загрузка сформированных файлов по FTP;
  • вызов обработчика выгрузки по HTTP;

Сервис обработчика выгрузки реализован по схеме:

  1. распаковывать архив;
  2. распарсить JSON;
  3. сообщить в HTTP ответе что всё впорядке или произошла ошибка;
  4. если всё хорошо — выставить флаг занятости и заливать данные в базу до победного конца;
  5. умереть очистив память.
  6. Возродиться новым процессом — за это отвечает Monit.

На продакшене (DigitalOcean, тариф за $10), с момента вызова и до пункта 3 сервис отрабатывает в целом через 3-4 секунды, после чего повторный вызов сервиса будет возвращать флаг занятости пока заливается база. Весь цикл обработки выгрузки с занесением данных в базу составляет 80 — 90 секунд. Загрузка процессора в момент парсинга выглядит как единичный пик до 70% с основанием в 10 — 30%.

В итоге:

  • время формирования выгрузки сократилось с 10 до 3.5 минут;
  • объём выгрузки сократился со 104 до 58 мегабайт (1.5 мегабайта после архивации);
  • полное время обработки выгрузки на стороне сервера сократилось с полутора часов до полутора минут;
  • ???????
  • PROFIT

P.S. Средство от головной боли при отладке.

При всей своей скорости JSON.parse() очень неудобен для отладки. В случае наличия ошибки в структуре JSON, вы получаете практически ноль отладочной информации. На этапе пока ваш 1С специалист осваивает JSON очень выручает модуль JSON Lint. Он может быть использован как отдельная утилита или в качестве библиотеки. В отличии от штатного парсера он сообщает в объект исключения номер строки JSON-файла где произошло недоразумение, что при разборе косяков в файле в десятки мегабайт кардинально облегчает жизнь. Цена за такое удобство — скорость. Она упадёт в 5-7 раз по сравнению с нативным JSON.parse().

Тот же тестовый код с JSON Lint будет выглядеть так.

// Node.js

var fs = require('fs'),
    jsonlint = require("jsonlint"); // Очень полезен на этапе отладки

function parser(filename, callback){

    fs.readFile(filename, { encoding:'utf8' }, function(err, dataFromFile){
        var parsedData;

        if(err){
            callback(err);
        } else {
            try {
                console.time('parse data'); // парсинг - синхронная операция...

                /* Используем Jsonlint если нам нужна более подробная информация о месте ошибки в структуре JSON.
                * Работает медленнее в 5-7 раз чем нативный JSON.parse().
                 */
                parsedData = jsonlint.parse(dataFromFile);  // тот же парсинг, но с дебагом и поэтессами.

                console.timeEnd('parse data'); // ... поэтому измеряем задержку "в лоб".

                callback(null, parsedData );
            }
            catch (e){
                callback(e)
            }
        }

    });
}

parser('../import/import.json', function(err, data){

    if(err){
        throw (err);
    }

    console.log('groups', data.groups.length);
    console.log('items', data.items.length);
    console.log('properties', data.properties.length);
});

В заключение хочу традиционно пожелать, чтобы этот материал был ещё кому-нибудь полезен также как был полезен нам.

Автор: Staltec

Источник

Поделиться

* - обязательные к заполнению поля