Приложение React с рендерингом на стороне сервера аварийно завершает работу при нагрузке

Я использую react-boilerplate (с react-router, sagas, express.js) для своего приложения React, и поверх него я добавил логику SSR, чтобы после получения HTTP-запроса он отображал компоненты реакции в строку на основе URL и отправляет строку HTML обратно клиенту.

В то время как рендеринг реакции происходит на стороне сервера, он также делает fetch запрос через саги к некоторым API (до 5 конечных точек на основе URL-адреса), чтобы получить данные для компонентов, прежде чем он фактически отобразит компонент в строку.

Все работает отлично, если я делаю только несколько запросов к серверу Node одновременно, но как только я имитирую нагрузку из 100+ одновременных запросов, и он начинает их обрабатывать, то в какой-то момент он падает без каких-либо указаний на какое-либо исключение.

Когда я пытался отладить приложение, я заметил, что как только более 100 входящих запросов начинают обрабатываться сервером Node, он одновременно отправляет запросы к API, но не получает фактического ответа, пока не перестанет накапливать эти запросы.

Код, используемый для рендеринга на стороне сервера:

async function renderHtmlDocument({ store, renderProps, sagasDone, assets, webpackDllNames }) {
  // 1st render phase - triggers the sagas
  renderAppToString(store, renderProps);

  // send signal to sagas that we're done
  store.dispatch(END);

  // wait for all tasks to finish
  await sagasDone();

  // capture the state after the first render
  const state = store.getState().toJS();

  // prepare style sheet to collect generated css
  const styleSheet = new ServerStyleSheet();

  // 2nd render phase - the sagas triggered in the first phase are resolved by now
  const appMarkup = renderAppToString(store, renderProps, styleSheet);

  // capture the generated css
  const css = styleSheet.getStyleElement();

  const doc = renderToStaticMarkup(
    <HtmlDocument
      appMarkup={appMarkup}
      lang={state.language.locale}
      state={state}
      head={Helmet.rewind()}
      assets={assets}
      css={css}
      webpackDllNames={webpackDllNames}
    />
  );
  return `<!DOCTYPE html>\n${doc}`;
}

// The code that's executed by express.js for each request
function renderAppToStringAtLocation(url, { webpackDllNames = [], assets, lang }, callback) {
  const memHistory = createMemoryHistory(url);
  const store = createStore({}, memHistory);

  syncHistoryWithStore(memHistory, store);

  const routes = createRoutes(store);

  const sagasDone = monitorSagas(store);

  store.dispatch(changeLocale(lang));
  
  match({ routes, location: url }, (error, redirectLocation, renderProps) => {
    if (error) {
      callback({ error });
    } else if (renderProps) {
      renderHtmlDocument({ store, renderProps, sagasDone, assets, webpackDllNames })
        .then((html) => {
          callback({ html });
        })
        .catch((e) => callback({ error: e }));
    } else {
      callback({ error: new Error('Unknown error') });
    }
  });
}


Итак, я предполагаю, что что-то идет не так, когда он получает слишком много HTTP-запросов, которые, в свою очередь, генерируют еще больше запросов к конечным точкам API для рендеринга реагирующих компонентов.

Я заметил, что он блокирует цикл событий на 300 мс после renderAppToString() для каждого клиентского запроса, поэтому, когда есть 100 одновременных запросов, он блокирует его примерно на 10 секунд. Хотя я не уверен, что это нормально или плохо.

Стоит ли пытаться ограничить одновременные запросы к серверу Node?

Я не смог найти много информации на тему сбоев SSR + Node. Поэтому я был бы признателен за любые предложения относительно того, где искать, чтобы определить проблему или возможные решения, если кто-то сталкивался с подобной проблемой в прошлом.


person George Borunov    schedule 05.02.2018    source источник
comment
Почему бы не использовать ReactDOM.hydra в основном js-файле клиента и пользовательских рулях, чтобы вернуть файл в качестве ответа. Я напишу ответ ниже, чтобы было ясно   -  person Abhay Shiro    schedule 11.02.2018
comment
Попробуйте использовать кластер PM2 для обработки нескольких одновременных запросов. pm2.keymetrics.io/docs/usage/cluster-mode   -  person cauchy    schedule 14.02.2018
comment
Это точно такая же проблема, с которой мы имеем дело прямо сейчас. Даже с PM2 в кластерном режиме с 7 серверами та же проблема. Вам случайно не удалось это исправить?   -  person Hirad Roshandel    schedule 27.02.2020


Ответы (3)


введите здесь описание изображения

На приведенном выше изображении я выполняю ReactDOM.hydra(...). Я также могу загрузить свое начальное и требуемое состояние и отправить его в гидрат.

введите здесь описание изображения

Я написал файл промежуточного программного обеспечения, и я использую этот файл, чтобы решить, какой URL-адрес я должен отправить в ответ.

введите здесь описание изображения

Выше мой файл промежуточного программного обеспечения, я создал строку HTML для любого файла, который был запрошен на основе URL-адреса. Затем я добавляю эту строку HTML и возвращаю ее, используя res.render Express.

введите здесь описание изображения

На изображении выше я сравниваю запрошенный путь URL со словарем ассоциаций пути и файла. Как только он найден (т. е. совпадает с URL-адресом), я использую ReactDOMserver для рендеринга в строку, чтобы преобразовать его в HTML. Этот HTML-код можно использовать для отправки с файлом панели управления с помощью res.render, как обсуждалось выше.

Таким образом, мне удалось выполнить SSR в большинстве моих веб-приложений, созданных с использованием стека MERN.io.

Надеюсь, мой ответ помог вам, и, пожалуйста, напишите комментарий для обсуждения

person Abhay Shiro    schedule 11.02.2018

<сильный>1. Запустить экспресс в кластере

Один экземпляр Node.js запускается в одном потоке. Чтобы воспользоваться преимуществами многоядерных систем, пользователю иногда нужно запустить кластер процессов Node.js для обработки нагрузки.

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

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

Простое решение для повышения производительности на сервере с несколькими ядрами — использовать встроенный модуль кластера узлов.

https://nodejs.org/api/cluster.html

Это запустит несколько экземпляров вашего приложения на каждом ядре вашего сервера, что значительно улучшит производительность (если у вас многоядерный сервер) для одновременных запросов.

Дополнительную информацию об экспресс-производительности см. на странице https://expressjs.com/en/advanced/best-practice-performance.html

Вы также можете захотеть ограничить входящие соединения, так как когда поток начинает время отклика на переключение контекста, время отклика быстро сокращается, это можно сделать, добавив что-то вроде NGINX / HA Proxy перед вашим приложением.

<сильный>2. Подождите, пока хранилище будет заполнено водой, прежде чем вызывать рендеринг в строку

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

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

  store.runSaga(rootSaga).done.then(() => {
    console.log('sagas complete')
    res.status(200).send(
      layout(
        renderToString(rootComp),
        JSON.stringify(store.getState())
      )
    )
  }).catch((e) => {
    console.log(e.message)
    res.status(500).send(e.message)
  })

https://github.com/redux-saga/redux-saga/blob/master/examples/real-world/server.js

<сильный>3. Убедитесь, что среда узла настроена правильно

Также убедитесь, что вы правильно используете NODE_ENV=production при сборке/запуске кода, поскольку и экспресс, и реакция оптимизируются для этого.

person user1095118    schedule 08.02.2018
comment
Насколько мне известно, это позволяет приложению Node обрабатывать запросы параллельно в многоядерных системах. Итак, допустим, если у меня 2 ядра, то я получу производительность x2 по сравнению с текущим подходом. Я не думаю, что они решат проблему полностью, так как текущая производительность довольно низкая (до 100 одновременных запросов). - person George Borunov; 12.02.2018
comment
Нет, не будет, но 100% прирост производительности в любом случае хорош :). Обновление ответа для решения других потенциальных проблем - person user1095118; 14.02.2018
comment
Что такое TTFB для одного запроса? если он большой, это может указывать на то, что медленная конечная точка забивает все. - person user1095118; 14.02.2018

Вызовы renderToString() являются синхронными, поэтому они блокируют поток во время выполнения. Поэтому неудивительно, что когда у вас есть более 100 одновременных запросов, у вас есть чрезвычайно заблокированная очередь, висящая примерно на 10 секунд.

Изменить: было указано, что React v16 изначально поддерживает потоковую передачу, но вам необходимо использовать метод renderToNodeStream() для потоковой передачи HTML клиенту. Он должен возвращать ту же самую строку, что и renderToString(), но вместо этого передавать ее в потоковом режиме, поэтому вам не нужно ждать полной обработки HTML-кода, прежде чем вы начнете отправлять данные клиенту.

person mootrichard    schedule 08.02.2018
comment
Поскольку react v16 renderToString поддерживает потоковую передачу, нет необходимости использовать внешние библиотеки. reactjs.org/blog/ 26.09.2017/ - person zarcode; 10.02.2018
comment
Спасибо за обновление, я исправлю ответ. На самом деле похоже, что этот метод не поддерживает потоковую передачу, а скорее renderToNodeStream() является необходимым методом для потоковой передачи HTML клиенту. - person mootrichard; 12.02.2018
comment
Как убедиться, что все запросы вернули некоторые данные через саги, прежде чем я отрисую HTML? Даже если я сделаю первый renderToString асинхронным, он все равно будет ждать, пока все саги не будут разрешены, а затем выполнит renderToString второй раз, чтобы заполнить его данными в хранилище. - person George Borunov; 12.02.2018
comment
Вы должны убедиться, что ваши саги не вернутся, пока их запросы не будут разрешены. Вы уже используете await для sagasDone(), чтобы гарантировать, что разметка не произойдет, пока эта функция не разрешится, поэтому вам просто нужно убедиться, что сага также не разрешится, пока их запросы не будут выполнены. Вы должны помнить, что это не HTTP-запросы (которые в любом случае, вероятно, асинхронны). Это вызов renderToStaticMarkup(), из-за которого процесс блокируется и зависает в ожидании разрешения запросов. - person mootrichard; 13.02.2018
comment
в качестве примечания renderToNodeStream не работает с реакцией Helemt, на которую ссылается OP - person user1095118; 14.02.2018