Компания Datana занимается разработкой цифровых решений для оптимизации производственных процессов черной металлургии (подробнее в нашем блоге). Сейчас мы расскажем об опыте повышения стабильности и отказоустойчивости фронтендов наших систем или о том, как мы избавились от необходимости ночевать в цехе завода, чтобы вовремя нажать F5.

Специалист завода за работой
Специалист завода за работой

Клиентские части наших систем разрабатываются как программное обеспечение автоматизированных рабочих мест  (АРМ) специалистов заводов. 

АРМ имеют следующие особенности:

  • должны работать 24 часа, 7 дней в неделю, круглый год, без остановок;

  • аппаратное обеспечение часто недостаточно мощное, ограниченной производительности;  

  • должны работать полностью автономно - по требованиям безопасности доступа из Интернет к ним нет, для устранения проблем только физический доступ;

  • на UI должны выводиться данные с видеокамер и датчиков, поступающие в систему с высокой частотой;

  • вывод данных на UI с задержкой недопустим, потому что это исказит картину течения процесса для пользователя;

  • пользователи не будут самостоятельно устранять никакие проблемы с программным обеспечением - специалисты управляют техпроцессами в реальном времени и им некогда разбираться с состоянием системы, звонить в техподдержку или что-то самостоятельно обновлять.

Мы разрабатываем клиенты при помощи веб-технологий, с использованием фреймворка Angular. Веб-клиент открывается на странице в браузере и непрерывно работает с выводом данных в режиме реального времени и без взаимодействия с пользователем.

Особенности работы наших клиентов требуют особых подходов к обеспечению их стабильности и отказоустойчивости. При тестировании начальных версий наших систем были ситуации, когда из-за кратковременного сбоя сети или минимальных утечек памяти в браузере клиентская часть длительно работающей системы становилась неработоспособной. Решалась проблема простой перезагрузкой страницы. Только вот в боевых условиях для нажатия F5 в браузере сотруднику техподдержки пришлось бы выезжать на завод, возможно, даже ночью. 

Перечислим рекомендации, выполнение которых позволяет нашим сотрудникам техподдержки спать спокойно и может пригодиться при создании клиентов, подобных нашим.

1. Предусмотреть автоподключение клиента к серверу 

В условиях боевой эксплуатации соединение между клиентом и сервером может быть потеряно по многим причинам: сбой сети, разрыв соединения по инициативе сервера, длительное отсутствие данных и, как следствие, закрытие сокета. Поэтому необходимо реализовать автоподключение клиента к серверу вне зависимости от причин потери соединения и без ограничения попыток переподключения. Пример простой реализации - автоподключение по таймауту:

const connectWebsocket = endpoint => {
    const ws = new WebSocket(endpoint);
    ws.onclose = () => {
        console.warn(`Websocket ${currentWsId} disconnected.  url: ${ws.url} .
                      Reconnect will be attempted in 1 second`);
	      // Установка таймаута в 1 секунду
        setTimeout(() => {
            // Переподключение
            connectWebsocket(endpoint);
        }, 1000);
    };
};

Пример реализации автоподключения по таймауту

Также не стоит забывать выводить для пользователя понятные информационные сообщения в случае потери соединения с сервером или отсутствия данных от сервера дольше ожидаемого времени. 

Пример сообщения пользователю, UI одного из наших клиентов
Пример сообщения пользователю, UI одного из наших клиентов

2. Сократить утечки памяти в браузере, связанные с работой клиента

Утечки памяти происходят, когда браузер по какой-то причине не до конца освобождает выделенную память. При непрерывной работе клиента утечки накапливаются и могут привести к потере производительности, зависанию интерфейса, а в худшем случае – к out of memory браузера. Поэтому при разработке клиента расход памяти необходимо оптимизировать.

Out of memory в Google Chrome
Out of memory в Google Chrome

Для детализации распределения памяти между JavaScript-объектами можно использовать, например, инструменты Memory - Heap Snapshots DevTools Google Chrome. Для просмотра использования процессорного времени, частоты смены кадров, для анализа смещения разметки и зон прорисовки - инструменты Performance DevTools Google Chrome.

Применение DevTools Google Chrome
Применение DevTools Google Chrome

3. Применять принудительную перезагрузку страницы

Не все утечки памяти могут быть связаны с работой клиента, они могут быть вызваны и ошибками работы самого браузера. Например, собственные утечки памяти того же Google Chrome - известная проблема. На этот случай рекомендуется применять принудительную перезагрузку страницы приложения по расписанию или по мере расходования выделенного странице объема памяти. Пример самого простого варианта – принудительная перезагрузка страницы раз в сутки.

// Перезагрузка страницы в 00:00:00 (clean tab memory)
setInterval(() => {
   const currentDate = new Date();
      if (currentDate.getHours() === 0 && currentDate.getMinutes() == 0 &&
    currentDate.getSeconds() == 0) {
        location.reload();
      }
}, 1000);

Пример реализации принудительной перезагрузки страницы

4. Изолировать обработку потоков данных на клиенте

На наши клиенты с высокой частотой идут потоки разнообразных данных, которые необходимо обработать и отобразить. Из-за этого основной поток исполнения терминала АРМ может быть перегружен, и, как следствие, в интерфейсе будут наблюдаться замирания картинки или отображения неактуальных данных. Для трудоемких вычислений и обработки больших объёмов поступающих данных рекомендуем применять веб-воркеры (Web Workers). Веб-воркеры - это средство для запуска скриптов в фоновом режиме. Поток веб-воркера может выполнять задачи без вмешательства в пользовательский интерфейс. Подробнее о работе с веб-воркерами можно почитать в официальной документации.

Применение этой технологии позволяет эффективно использовать аппаратные ресурсы за счет распараллеливания вычислений и, как следствие, разгрузить основной поток исполнения. Например, до выделения веб-воркеров из-за загрузки основного потока страница одного из наших клиентов справлялась с отрисовкой максимум 12 кадров в секунду (FPS), при этом наблюдались ощутимые подвисания интерфейса. После того, как обработка видеоданных была вынесена в отдельный веб-воркер, FPS стабильно поднялось до 20.

Примеры вывода видеоданных, UI одного из наших клиентов
Примеры вывода видеоданных, UI одного из наших клиентов

Дополнительные преимущества от применения веб-воркеров можно получить при использовании OffscreenCanvas. OffscreenCanvas позволяет работать с html-элементами canvas напрямую из веб-воркера, а это значит, что построение сложных графиков, может происходить без участия основного потока, тем самым разгружая его. Подробнее о работе с OffscreenCanvas - здесь.

ngAfterViewInit(): void {
    // Проверка поддержки веб-воркеров и OffscreenCanvas
    if (this._worker && window['OffscreenCanvas']) {
      const htmlCanvas = <HTMLCanvasElement>document.getElementById('canvas');
      // Перевод canvas в offscreen
      const offscreen = htmlCanvas.transferControlToOffscreen();
      this._worker.postMessage({
        type: 'canvas',
        payload: {
          canvas: offscreen
        }
      }, [offscreen]); // Обязательна передача в веб-воркер ссылки на canvas в 
                          transfer массиве
 
      // canvas передан, можно делать подключение  
      this._worker.postMessage({type: 'websocket', payload: this.frameWsApiUrl});
    }  else {
      // Логика на случай, если OffscreenCanvas не поддерживается
      this.connectToWsFrame();
    }
  }

Пример применения веб-воркеров и OffscreenCanvas

5. Ограничить частоту обрабатываемых на клиенте данных

Еще одна мера обеспечения стабильности клиента, когда данные поступают с высокой частотой и при этом необходимо перерисовывать интерфейс, - ограничение частоты обрабатываемых на клиенте данных с помощью троттлинга (throttle). Троттлинг функции означает, что функция вызывается не более одного раза в указанный период времени, период троттлинга. Это особенно актуально, если терминал слабый и не может полноценно справляться с неограниченной троттлингом обработкой данных: интерфейс подвисает, копится задержка, актуальность отображаемых данных теряется.

Например: пусть при фактическом поступлении данных каждые 200 мс настраивается период троттлинга в 1 секунду. В этом случае данные за 1 секунду будут накапливаться, а обрабатываться будет только последнее значение, прочие значения будут игнорироваться. Интерфейс также будет перерисовываться не чаще одного раза в секунду. 

Самый простой вариант реализации – жестко задать период троттлинга в виде константы. Однако предпочтительно определять период троттлинга в зависимости от производительности аппаратного обеспечения, на котором работает клиент. Например, один из вариантов – экспериментально определить несколько значений периода троттлинга и назначать их в зависимости от количества ядер терминала.

export const DEFAULT_ENV_PROD: Partial<IEnvironment> = {
    wsDebug: false,
   // Проверка количества логических ядер терминала для задания периода троттлинга
    wsThrottle: navigator.hardwareConcurrency >= 8 ? 200 : 400,
    noDataTimeout: 5000,
    videoOverlayFrameNumber: false,
    videoAspectRatio: '4/3',
};

Пример определения значения периода троттлинга в зависимости от количества ядер терминала

Для «тонкой настройки» обработки данных мы рекомендуем библиотеку RxJS. Библиотека позволяет манипулировать данными в функциональном стиле, в том числе ограничивать обработку данных с помощью троттлинга. А с помощью функции distinctUntilChanged библиотеки можно определять, что новые данные не отличаются от предыдущих, и за счет этого оптимизировать частоту обновления интерфейса и исключать «паразитные» перерисовки, когда данные не изменились.

this.wsService
     .on<FrontSystemState>('FrontSystemState')
     .pipe(
          // Если значение троттлинга задано в переменных окружения и является 
             числом (кроме нуля), то применяется троттлинг, иначе данные  
             пропускаются «как есть»
         typeof this.env.wsThrottle === 'number'
             ? throttleTime(this.env.wsThrottle, undefined, {leading: true, 
                            trailing: true})
             : identity,
         map(s => {
          // Проверка null или undefined значений
             s.integrationState = s.integrationState ?? {};
             return s;
         }),
         // Мы не хотим лишний раз обновлять интерфейс, особенно когда в данных 
         //   ничего не изменилось, поэтому проверяем, действительно ли значение 
         //   объекта поменялось по сравнению с предыдущим
         // distinctUntilChanged продолжит обработку пайпа, только если 
            значение изменилось
         distinctUntilChanged((x, y) => JSON.stringify(x) === JSON.stringify(y)),
         takeUntil(this._unsubscribe$),
         tap(data => {
             timeoutTimer.bounce();
             this._system$.next(data);
         }),
     )
     .subscribe();

Пример «тонкой» обработки данных

Например, после настройки троттлинга и исключения паразитных срабатываний отрисовки нам удалось поднять FPS страниц клиентов до 30 и избежать подвисаний интерфейса при высокой частоте данных, передаваемых с сервера. 

6. Защитить пользователя от самого себя

Если есть доступ к терминалам пользователей и возможность настраивать их до начала эксплуатации, рекомендуем настроить терминалы на режим киоска средствами операционной системы или с использованием стороннего программного обеспечения. Цель - обеспечить, чтобы клиент был открыт на странице в выбранном браузере и у пользователя не было возможности свернуть страницу или перейти на другую. Кроме того, необходимо отключить браузерные расширения, блокировать модальные окна и предложения обновлений. 

Один из вариантов реализации настройки терминала на режим киоска – использование специального дистрибутива Linux Porteus Kiosk на базе Gentoo. Это свободно распространяемый open source дистрибутив под лицензией GPL, который на текущий момент отвечает всем нашим требованиям к режиму киоска. 

Заключение

Описанные подходы просты и очевидны, но если их заранее не предусмотреть, то пользователи и служба поддержки обречены на совершенно лишние мучения. Поэтому если перед вами также стоит задача разработки непрерывно функционирующих веб-клиентов или ваши веб-клиенты должны справляться с большими потоками данных, то будем рады, если наш опыт вам пригодится. Также будем рады узнать, какие еще подходы вы бы рекомендовали использовать при создании подобных клиентов.

Свои производственные заметки мы, помимо Хабра, выкладываем в Telegram-канале. Присоединяйтесь!

Комментарии (13)


  1. monochromer
    17.05.2022 11:19
    +3

    Код - картинками, да ещё и serif-шрифтом...


    1. isakharov Автор
      17.05.2022 15:54
      +1

      Досадная ошибка при форматировании, исправились, спасибо вам.


  1. nin-jin
    17.05.2022 12:35
    +1

    Мы разрабатываем клиенты при помощи веб-технологий, с использованием фреймворка Angular.

    Не самый лучший выбор, если вам нужно отображение больших объёмов данных данных в реальном времени.

    При непрерывной работе клиента утечки накапливаются и могут привести к потере производительности, зависанию интерфейса, а в худшем случае – к out of memory браузера.

    Поэтому важно, чтобы используемый фреймворк поддерживал автоматический контроль времени жизни ресурсов. Опять же, Angular это не поддерживает.

    пусть при фактическом поступлении данных каждые 200 мс настраивается период троттлинга в 1 секунду. В этом случае данные за 1 секунду будут накапливаться, а обрабатываться будет только последнее значение, прочие значения будут игнорироваться. Интерфейс также будет перерисовываться не чаще одного раза в секунду.

    Не знаю, что вы там такого рисуете, но не вижу проблемы перерисовывать интерфейс по мере поступления данных, то есть каждые 200мс в данном случае.

    Для «тонкой настройки» обработки данных мы рекомендуем библиотеку RxJS. Библиотека позволяет манипулировать данными в функциональном стиле, в том числе ограничивать обработку данных с помощью троттлинга. А с помощью функции distinctUntilChanged библиотеки можно определять, что новые данные не отличаются от предыдущих, и за счет этого оптимизировать частоту обновления интерфейса и исключать «паразитные» перерисовки, когда данные не изменились.

    Лучше всё же использовать системы реактивности, которые занимаются этим самостоятельно, а не требуют постоянной "тонкой настройки".


    1. Desprit
      17.05.2022 14:36
      +4

      Опять реклама $mol? Непонятно только зачем. Ибо что ни тезис, то какая-то белиберда.


    1. isakharov Автор
      17.05.2022 16:29
      +2

      В статье не утверждается, что Angular единственно верный вариант решения.
      Мы стремились описать такие кейсы, которые были бы применимы вне зависимости от используемой библиотеки или фреймворка.

      Если ваш стек позволяет реализовать их из коробки, отлично!

      А если вы работаете с Angular и вам потребовалось реализовать промышленный АРМ, то не унывайте. Мы собрали основные проблемы и вы можете закрыть их за 5 минут, пользуясь примерами выше.


      1. nin-jin
        17.05.2022 17:16
        +1

        Я работал с Angular в связках с RxJS, NgRx, MobX. Занимался исправлением его детски болезней и оптимизациями. И могу определённо сказать: Angular - просто неверный вариант решения. Особенно для условий, когда надо работать быстро, долго и стабильно.


        1. CAHKEIIIOH
          18.05.2022 09:55
          +1

          Зато можно долго и стабильно продавать сервис.


  1. Refridgerator
    18.05.2022 06:00
    +1

    Занимаюсь тем же самым. Только не JavaScript, а C#, все серверные задачи оформлены как службы, все логи с утечками памяти мониторятся централизовано в реальном времени. Переподключение при потере связи — само собой, конечное или бесконечное количество попыток зависит от контекста, запись в лог пишется однократно при попытках>2. Все логи пишутся в один и тот журнал подсистемы EventLog Windows, а не раскиданы по разным текстовым файлам или консолям. В некоторых задачах параметры из .ini файла подгружаются динамически, без необходимости перезапуска приложения или службы.


    1. isakharov Автор
      18.05.2022 11:47
      +1

      Для логирования мы используем ELK и это очень удобно, посмотрите, может вам тоже подойдет.

      Особенно, когда много сервисов, данных и логов.

      В Kibana быстро набрасываются дашборды и отчеты.

      Анализ ошибок и сопровод сильно упрощаются.

      Бизнесовые метрики также можно завернуть на Elastic.


      1. Refridgerator
        19.05.2022 05:54

        Я остановился на стандартном журнале событий потому, что в него пишет и сама Windows, и её драйвера, и СУБД Sybase ASE, у нас используемая. Сложно представить, как это всё принудительно перевести на другую систему логирования.

        И в целом я стремлюсь к минимизации сторонних фреймворков до предела. Потому что одно дело — быстро накидать работающее решение, и совсем другое — обеспечить ему надёжность и кувалдоустойчивость. Ну и события последних дней показали, что импортозамещение — не такая уж и бредовая идея.


        1. isakharov Автор
          19.05.2022 22:46

          Журнал Windows однажды тоже придется импортозаместить :)


  1. maynoz
    19.05.2022 14:00

    Ничего личного, но для систем реального времени есть специализированный софт и разрабатывать вот это все, с моей точки зрения и опыта, смотрится как - "Мы выиграли тендер в России, но бюджет был недостаточный для покупки SCADA-системы и решили сделать это все на web'е". Может я конечно и утрирую, но в нефтянке вот это все бы точно не прокатило...


    1. isakharov Автор
      19.05.2022 22:34

      Дело в том, что просто взять и внедрить SCADA никто не даст, даже если бюджет такое позволит.

      У предприятия есть безопасность, существующие тех процессы и тех инструкции, системы всех уровней автоматизации от L2 до L4. Получить разрешение на изменение этих систем или внедрение новых это проект совершенно другого уровня и бюджета, как вы верно заметили.

      К тому же в статье речь об оптимизации работы лишь нескольких агрегатов одного цеха, это не автоматизация производства или даже отдельного участка. На соседнем агрегате работает аналогичная система китайских "коллег", сделанная на .Net и мы конкурируем.

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

      P.S. Я бы не сказал, что у нас система реального времени. Да, мы выводим медиа на фронт в реальном времени, но это вершина айсберга.