注册
登录
新闻动态
其他科技
返回
大规模服务器端渲染
作者:
糖果
发布时间:
2024-08-13 09:04:01 (18天前)
来源:
https://engineeringblog.yelp.com
在 Yelp,我们使用服务器端渲染 (SSR) 来提高基于 React 的前端页面的性能。在 2021 年初发生一系列生产事件后,我们意识到我们现有的 SSR 系统无法扩展,因为我们将更多页面从基于 Python 的模板迁移到 React。在这一年的剩余时间里,我们致力于重新构建我们的 SSR 系统,以提高稳定性、降低成本并提高功能团队的可观察性。 背景 什么是 SSR? 服务器端渲染是一种用于提高 JavaScript 模板系统(例如 React)性能的技术。我们无需等待客户端下载 JavaScript 包并根据其内容呈现页面,而是在服务器端呈现页面的 HTML,并在下载后在客户端附加动态挂钩。这种方法以增加的传输大小换取渲染速度的提高,因为我们的服务器通常比客户端机器快。在实践中,我们发现它显着改善了我们的LCP时序。 现状 我们通过将它们与入口点函数和任何其他依赖项捆绑到一个自包含的 .js 文件中来为 SSR 准备组件。然后入口点使用ReactDOMServer,它接受组件道具并生成呈现的 HTML。作为我们持续集成过程的一部分,这些 SSR 捆绑包被上传到 S3。 我们的旧 SSR 系统将在启动时下载并初始化每个 SSR 捆绑包的最新版本,以便它准备好呈现任何页面,而无需在关键路径中等待 S3。然后,根据传入的请求,将选择并调用适当的入口点函数。这种方法给我们带来了一些问题: 下载和初始化每个捆绑包显着增加了服务启动时间,这使得快速响应扩展事件变得困难。 让服务管理所有捆绑包会产生大量内存需求。每次我们水平扩展并启动一个新的服务实例时,我们必须分配的内存等于每个包的源代码和运行时使用量的总和。为来自同一实例的所有捆绑包提供服务也使得衡量单个捆绑包的性能特征变得困难。 如果在服务重新启动之间上传了新版本的捆绑包,则服务将没有它的副本。我们通过根据需要动态下载丢失的包来解决这个问题,并使用 LRU 缓存来确保我们不会同时在内存中保存太多动态包。 旧系统基于 Airbnb 的Hypernova。Airbnb 已经写了关于 Hypernova 问题的博客文章,但核心问题是渲染组件会阻塞事件循环,并可能导致多个 Node API 以意想不到的方式中断。我们遇到的一个关键问题是阻塞事件循环会破坏 Node 的 HTTP 请求超时功能,这会在系统已经过载时显着加剧请求延迟。任何 SSR 系统都必须设计为尽量减少因渲染而阻塞事件循环的影响。 随着 Yelp 的 SSR 捆绑包数量持续增加,这些问题在 2021 年初达到了顶点: 启动时间变得如此缓慢,以至于 Kubernetes 开始将实例标记为不健康并自动重新启动它们,从而阻止它们变得健康。 该服务的巨大堆大小导致了严重的垃圾收集问题。在旧系统生命周期结束时,我们为其分配了近 12GB 的旧堆空间。在一项实验中,我们确定由于浪费在垃圾收集上的时间,我们无法每秒处理超过 50 个请求。 ![](/user/files/hCYxInXUzeBrxfatJzQr80a55B5soQTPfpcV8ePovMc.png) 由于频繁的bundle eviction和re-initialization而破坏动态bundle缓存会造成很大的CPU负担,开始影响在同一主机上运行的其他服务。 所有这些问题都降低了 Yelp 的前端性能,并导致了几起事件。 重构目标 在处理完这些事件后,我们着手重新构建我们的 SSR 系统。我们选择稳定性、可观察性和简单性作为我们的设计目标。新系统应该在没有太多人工干预的情况下运行和扩展。不仅对于基础设施团队,而且对于拥有捆绑包的功能团队,都应该很容易观察到。新系统的设计应该很容易让未来的开发人员理解。 我们还选择了一些具体的功能性目标: 尽量减少阻塞事件循环的影响,以便请求超时等功能正常工作。 通过 bundle 对服务实例进行分片,这样每个 bundle 都有自己独特的资源分配。这减少了我们的整体资源占用,并使特定于捆绑包的性能更易于观察。 能够快速失败我们预计无法快速服务的请求。如果我们知道渲染请求需要很长时间,系统应该立即回退到客户端渲染,而不是先等待 SSR 超时。这为我们的最终用户提供了最快的用户体验。 执行 语言选择 在实现 SSR 服务 (SSRS) 时,我们评估了几种语言,包括 Python 和 Rust。从内部生态系统的角度来看,使用 Python 本来是理想的,但是,我们发现 Python 的 V8 绑定状态还没有准备好生产,并且需要大量投资才能使用 SSR。 接下来,我们评估了 Rust,它具有高质量的 V8 绑定,已经在流行的生产就绪项目(如Deno )中使用。但是,我们所有的 SSR 包都依赖于 Node 运行时 API,它不是裸 V8 的一部分;因此,我们必须重新实现它的重要部分来支持 SSR。除了在 Yelp 的开发者生态系统中普遍缺乏对 Rust 的支持之外,这也阻止了我们使用它。 最后,我们决定在 Node 中重写 SSRS,因为 Node 提供了一个V8 VM API,允许开发人员在沙盒 V8 上下文中运行 JS,在 Yelp 开发人员生态系统中有高质量的支持,并且允许我们重用来自其他内部 Node 的代码服务以减少实施工作。 算法 SSRS 由一个主线程和许多工作线程组成。节点工作线程不同于操作系统线程,因为每个线程都有自己的事件循环,并且内存不能在线程之间随意共享。 当主线程收到 HTTP 请求时,会执行以下步骤: 检查请求是否应该基于“超时因素”快速失败。目前,这个因素包括平均渲染运行时间和当前队列大小,但可以扩展以包含更多指标,如 CPU 负载和吞吐量。 将请求推送到渲染工作池队列。 当工作线程收到请求时,它会执行以下步骤: 执行服务器端渲染。这会阻塞事件循环,但仍然是允许的,因为工作人员一次只处理一个请求。在这种 CPU 密集型工作发生时,不应使用事件循环。 将渲染的 HTML 返回到主线程。 当主线程收到来自工作线程的响应时,它会将呈现的 HTML 返回给客户端。 ![](/user/files/8bGxt61Faq_8ujNal-aM5CeidCPBO7i3h-gzPGLSl5I.png) 这种方法为我们提供了两个重要的保证来帮助我们满足我们的要求: 事件循环永远不会在主 Web 服务器线程中被阻塞。 当它在工作线程中被阻塞时,永远不需要事件循环。 我们使用了Piscina,一个提供上述功能的第三方库。它管理线程池,支持任务队列、任务取消和许多其他有用的功能。选择Fastify来支持主线程 Web 服务器是因为它既高性能又对开发人员友好。 Fastify 服务器: const workerPool = new Piscina({...}); app.post('/batch', opts, async (request, reply) => { if ( Math.min(avgRunTime.movingAverage(), RENDER_TIMEOUT_MSECS) * (workerPool.queueSize + 1) > RENDER_TIMEOUT_MSECS ) { // Request is not expected to complete in time. throw app.httpErrors.tooManyRequests(); } try { const start = performance.now(); currentPendingTasks += 1; const resp = await workerPool.run(...); const stop = performance.now(); const runTime = resp.duration; const waitTime = stop - start - runTime; avgRunTime.push(Date.now(), runTime); reply.send({ results: resp, }); } catch (e) { // Error handling code } finally { currentPendingTasks -= 1; } }); 缩放 水平缩放的自动缩放 SSRS 建立在 PaaSTA 之上,它提供了开箱即用的自动缩放机制。我们决定构建一个自定义的自动缩放信号来获取工作池的利用率: Math.min(currentPendingTasks, WORKER_COUNT) / WORKER_COUNT; 该值与我们在移动时间窗口内的目标利用率(设定点)进行比较,以进行水平缩放调整。我们发现,与基本的容器 CPU 使用扩展相比,此信号有助于我们将每个工作人员的负载保持在更健康、更准确的配置状态,确保在合理的时间内处理所有请求,而不会导致工作人员超载或过度扩展服务。 垂直缩放的自动调谐 Yelp 由许多不同流量负载的页面组成;因此,支持这些页面的 SSRS 分片具有截然不同的资源需求。我们没有为每个 SSRS 分片静态定义资源,而是利用动态资源自动调整来自动调整容器资源,如 CPU 和分片内存。 这两种扩展机制确保每个分片都有它需要的实例和资源,无论它接收到多少流量或多少流量。最大的好处是在不同的页面集上高效地运行 SSRS,同时保持成本效益。 获胜 用 Piscina 和 Fastify 重写 SSRS 让我们避免了我们之前的实现遇到的阻塞事件循环问题。结合分片方法和更好的扩展信号,我们可以压缩更多性能,同时降低云计算成本。一些亮点包括: 服务器端渲染包时平均减少 125 毫秒 p99。 通过减少启动时初始化的捆绑包的数量,将旧系统中的服务启动时间从几分钟缩短到几秒钟。 通过使用自定义缩放因子和更有效地调整每个分片的资源,将云计算成本降低到之前系统的三分之一。 由于每个分片现在只负责渲染一个包,因此提高了可观察性,使团队能够更快地了解哪里出了问题。 创建了一个更具可扩展性的系统,允许将来进行改进,例如 CPU 分析和捆绑源映射支持![](/user/files/8bGxt61Faq_8ujNal-aM5CeidCPBO7i3h-gzPGLSl5I.png)
收藏
举报
1 条回复
动动手指,沙发就是你的了!
登录
后才能参与评论