Компания 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);
};
};
Пример реализации автоподключения по таймауту
Также не стоит забывать выводить для пользователя понятные информационные сообщения в случае потери соединения с сервером или отсутствия данных от сервера дольше ожидаемого времени.
2. Сократить утечки памяти в браузере, связанные с работой клиента
Утечки памяти происходят, когда браузер по какой-то причине не до конца освобождает выделенную память. При непрерывной работе клиента утечки накапливаются и могут привести к потере производительности, зависанию интерфейса, а в худшем случае – к out of memory браузера. Поэтому при разработке клиента расход памяти необходимо оптимизировать.
Для детализации распределения памяти между JavaScript-объектами можно использовать, например, инструменты Memory - Heap Snapshots DevTools Google Chrome. Для просмотра использования процессорного времени, частоты смены кадров, для анализа смещения разметки и зон прорисовки - инструменты Performance DevTools Google Chrome.
3. Применять принудительную перезагрузку страницы
Не все утечки памяти могут быть связаны с работой клиента, они могут быть вызваны и ошибками работы самого браузера. Например, собственные утечки памяти того же Google Chrome - известная проблема. На этот случай рекомендуется применять принудительную перезагрузку страницы приложения по расписанию или по мере расходования выделенного странице объема памяти. Пример самого простого варианта – принудительная перезагрузка страницы раз в сутки.
// Перезагрузка страницы при изменении дня (clean tab memory)
setInterval(() => {
const currentDate = new Date();
// уведомляем о будущей перезагрузке
if (
!this._showReloadNotification &&
currentDate.getHours() === 23 &&
currentDate.getMinutes() == 59
) {
this._showReloadNotification = true;
this.notificationService.send('arm_status.arm_is_about_to_reload', 'info', {
options: {
timeout: 60000,
},
});
}
// день сменился
if (this._initiationMoment.getDate() !== currentDate.getDate()) {
location.reload();
}
}, 1000);
Пример реализации принудительной перезагрузки страницы. P.S. Как заметил @i86com в комментариях, надежнее проверять переход через день, чем точное время, например 00:00:00, т.к. точное время не всегда может сработать.
4. Изолировать обработку потоков данных на клиенте
На наши клиенты с высокой частотой идут потоки разнообразных данных, которые необходимо обработать и отобразить. Из-за этого основной поток исполнения терминала АРМ может быть перегружен, и, как следствие, в интерфейсе будут наблюдаться замирания картинки или отображения неактуальных данных. Для трудоемких вычислений и обработки больших объёмов поступающих данных рекомендуем применять веб-воркеры (Web Workers). Веб-воркеры - это средство для запуска скриптов в фоновом режиме. Поток веб-воркера может выполнять задачи без вмешательства в пользовательский интерфейс. Подробнее о работе с веб-воркерами можно почитать в официальной документации.
Применение этой технологии позволяет эффективно использовать аппаратные ресурсы за счет распараллеливания вычислений и, как следствие, разгрузить основной поток исполнения. Например, до выделения веб-воркеров из-за загрузки основного потока страница одного из наших клиентов справлялась с отрисовкой максимум 12 кадров в секунду (FPS), при этом наблюдались ощутимые подвисания интерфейса. После того, как обработка видеоданных была вынесена в отдельный веб-воркер, FPS стабильно поднялось до 20.
Дополнительные преимущества от применения веб-воркеров можно получить при использовании 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-канале. Присоединяйтесь!