带有服务器端渲染的 React 应用程序因负载而崩溃

Posted

技术标签:

【中文标题】带有服务器端渲染的 React 应用程序因负载而崩溃【英文标题】:React app with Server-side rendering crashes with load 【发布时间】:2018-07-15 19:59:58 【问题描述】:

我在我的 React 应用程序中使用 react-boilerplate(带有 react-router、sagas、express.js),在它之上我添加了 s-s-r 逻辑,这样一旦它接收到 HTTP 请求,它就会将反应组件呈现给基于 URL 的字符串并将 html 字符串发送回客户端。

当服务器端发生 react 渲染时,它还会通过 sagas 向某些 API(基于 URL 最多 5 个端点)发出fetch 请求,以在将组件实际呈现为字符串之前获取组件的数据。

如果我同时只向 Node 服务器发出多个请求,一切都很好,但是一旦我模拟了 100 多个并发请求的负载并开始处理它,然后在某个时候它会崩溃而没有任何异常的迹象。

我在尝试调试应用程序时注意到的是,一旦 Node 服务器开始处理 100 多个传入请求,它就会同时向 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 端点生成更多请求以呈现反应组件。

我注意到它会在renderAppToString() 之后为每个客户端请求阻塞事件循环 300 毫秒,因此一旦有 100 个并发请求,它会阻塞它大约 10 秒。不过我不确定这是正常的还是坏的。

是否值得尝试限制对 Node 服务器的并发请求?

我找不到关于 s-s-r + 节点崩溃主题的太多信息。因此,如果有人过去遇到过类似问题,我将不胜感激有关在哪里寻找问题或寻找可能的解决方案的任何建议。

【问题讨论】:

为什么不在客户端的主 js 文件和用户把手中使用 ReactDOM.hydrate 来返回文件作为响应。为了清楚起见,我将在下面写一个答案 尝试使用PM2集群处理多个并发请求。 pm2.keymetrics.io/docs/usage/cluster-mode 这与我们现在正在处理的问题完全相同。即使 PM2 在集群模式下运行 7 台服务器,也是同样的问题。你有没有设法解决它? 【参考方案1】:

在上图中,我正在做 ReactDOM.hydrate(...) 我还可以加载我的初始状态和所需状态并将其发送到 hydrate 中。

我已经编写了中间件文件,我正在使用这个文件来决定基于哪个 URL 我应该发送哪个文件作为响应。

上面是我的中间件文件,我已经创建了基于 URL 请求的任何文件的 HTML 字符串。然后我添加这个 HTML 字符串并使用 express 的 res.render 返回它。

上图是我将请求的 URL 路径与路径文件关联字典进行比较的地方。一旦找到(即 URL 匹配),我使用 ReactDOMserver 渲染到字符串以将其转换为 HTML。如上所述,此 html 可用于使用 res.render 发送手柄栏文件。

通过这种方式,我设法在使用 MERN.io 堆栈构建的大多数 Web 应用程序上执行 s-s-r。

希望我的回答对您有所帮助,请写评论以供讨论

【讨论】:

【参考方案2】:

1.在集群中运行 express

Node.js 的单个实例在单个线程中运行。采取 多核系统的优势,用户有时会想要 启动一个 Node.js 进程集群来处理负载。

由于 Node 是单线程的,如果您正在初始化 express,问题也可能出在堆栈下方的文件中。

在运行节点应用时有许多最佳实践,但反应线程中通常不会提及。

提高运行多核服务器性能的简单解决方案是使用内置节点集群模块

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

这将在服务器的每个核心上启动应用的多个实例,从而显着提高并发请求的性能(如果您有多核服务器)

查看有关快递性能的更多信息 https://expressjs.com/en/advanced/best-practice-performance.html

您可能还想限制传入连接,因为当线程启动时,上下文切换响应时间会迅速下降,这可以通过在应用程序前面添加类似 nginx / HA 代理之类的东西来完成

2。等待 store 补水后再调用 render to string

您不希望在商店完成更新之前渲染您的布局,因为其他 cmets 注意到这是在渲染时阻塞线程。

以下是从 saga repo 中获取的示例,它展示了如何运行 saga 而无需渲染模板,直到它们全部解决

  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,并为此优化和响应

【讨论】:

据我所知,这允许 Node 应用程序在多核系统上并行处理请求。因此,假设我有 2 个内核,那么与当前方法相比,我将获得 x2 的性能。我认为他们不会完全解决问题,因为当前的性能非常低(最多 100 个并发请求)。 不,它不会,但无论哪种方式,100% 的性能提升都是好的 :)。更新答案以解决其他潜在问题 单个请求的 TTFB 是多少?如果它很大,这可能表明一个缓慢的端点阻塞了事情【参考方案3】:

renderToString() 的调用是同步的,因此它们在运行时会阻塞线程。因此,当您有 100 多个并发请求时,您有一个非常阻塞的队列会挂起约 10 秒,这并不奇怪。

编辑:有人指出 React v16 原生支持流式传输,但您需要使用 renderToNodeStream() 方法将 HTML 流式传输到客户端。它应该返回与renderToString() 完全相同的字符串,但改为流式传输,因此您不必等待完整的 HTML 呈现出来,然后再开始向客户端发送数据。

【讨论】:

由于 react v16 renderToString 支持流式传输,无需使用外部库。 reactjs.org/blog/2017/09/26/… 感谢您的更新,我会修改答案。实际上,该方法看起来支持流式传输,但renderToNodeStream() 是将 HTML 流式传输到客户端的必要方法。 在渲染 HTML 之前,如何确保所有请求都通过 sagas 返回了一些数据?即使我使第一个 renderToString 异步,它仍然会等到所有 saga 都已解决,然后会第二次执行 renderToString 以使用存储中的数据填充它。 您需要确保您的 sagas 在他们的请求得到解决之前不会返回。您已经在 sagasDone() 上使用 await 以确保在该函数解析之前不会发生标记,因此您只需要确保传奇 also 在他们的请求之前不会解析已经完成。您需要记住,它不是 HTTP 请求(无论如何都可能是异步的)。它对renderToStaticMarkup() 的调用导致进程在等待请求解决时被阻塞并挂起。 请注意,renderToNodeStream 不适用于 OP 中引用的 react Helemt

以上是关于带有服务器端渲染的 React 应用程序因负载而崩溃的主要内容,如果未能解决你的问题,请参考以下文章

带有服务器端渲染的 Webpack-React:使用哈希名称链接到服务器模板中的 css 文件

如何在 React/redux 中进行服务器端渲染?

react.js在服务器端渲染有啥好处?渲染是怎么个流程

使用 react-router-relay 和 react-rails 进行服务器端渲染

React 服务端渲染完美的解决方案

使用带有 NextJS 的 Apollo Client 时服务器端渲染数据?