译者 | 刘汪洋
审校 | 重楼
在现代数字化环境下,单纯构建一个具备基本功能的系统已无法满足更高的应用需求。我们需要开发在高负载环境下能够稳定且高效扩展的系统。
众多开发者和架构师的实践证明,系统可扩展性的提升往往伴随着独特的挑战。即使是微小的效率问题,在放大到百万倍的负载下,也可能导致系统陷入瘫痪。那么,怎样才能确保你的应用程序在任何负载下都能快速响应呢?
本文将详细介绍构建可扩展系统时的性能优化策略。我们会探讨一些适用于各种代码库的通用策略,无论是前端还是后端,也不论使用何种编程语言。这些策略不仅限于理论层面;它们已在全球一些最具挑战性的技术环境中经过实际应用和验证。作为 Facebook 团队的一员,我亲自参与了将这些优化技术应用于多个项目中,包括 Facebook 的轻量级广告创建体验和 Meta 商务套件。
因此,无论你是在打造下一个大型社交网络、企业级软件套件,还是仅仅想要优化个人项目,我们在此讨论的策略都将成为你工具箱中的宝贵资产。现在,让我们开始探索吧。
预取是一种基于预测用户行为的性能优化技术。设想用户正在与应用程序交互,系统能够预测用户的下一步操作,并提前获取相关数据。这种方法能够创造一种无缝体验:当数据被需要时,它几乎能够即刻被获取,从而使应用程序显得更加迅速和响应灵敏。主动在需求出现之前获取数据能够显著提升用户体验,但如果过度使用,可能会导致资源浪费,如带宽、内存甚至处理能力的浪费。Facebook 在其需要依赖机器学习的复杂操作中大量使用预取,例如在“好友建议”功能中。
预取涉及在用户明确表达需求之前,主动向服务器发送请求以检索数据。尽管这看起来很有吸引力,但开发者必须确保在效率和资源使用之间取得平衡。
在实施预取之前,首先应确保服务器响应时间已经得到优化。后端代码优化可以通过以下方式实现更佳的服务器响应时间:
预取的核心是对用户下一步操作的预测。然而,预测有时可能不准确。如果系统为用户从未访问的页面或功能预获取数据,就会造成资源的浪费。因此,开发者应采用机制来评估用户意图,例如跟踪用户行为模式或检查用户的活跃参与度,以确保数据仅在有高概率被使用的情况下被获取。
预取可以在任何编程语言或框架中实现。以 React 为例,来展示预取的实现方法。
考虑一个简单的 React 组件。该组件一旦完成渲染,就会触发一个 AJAX 调用来预先获取数据。当用户点击该组件中的按钮时,第二个组件会使用这些预先获取的数据:
import React, { useState, useEffect } from 'react';import axios from 'axios';function PrefetchComponent() { const [data, setData] = useState(null); const [showSecondComponent, setShowSecondComponent] = useState(false); // 组件渲染完成后立即预取数据 useEffect(() => { axios.get('https://api.example.com/data-to-prefetch') .then(response => { setData(response.data); }); }, []); return ( <div> <button onClick={() => setShowSecondComponent(true)}>Show Next Component </button>{showSecondComponent && <SecondComponent data={data} />} </div> );}function SecondComponent({ data }) { // 在这个组件中使用预取的数据 return ( <div> {data ? <div>Here is the prefetched data: {data}</div> : <div>Loading...</div>}</div>);}export default PrefetchComponent;
在上述代码示例中,PrefetchComponent组件在渲染之后立刻进行数据获取。当用户点击按钮时,SecondComponent组件会展示,使用的是之前预先获取的数据。
在计算机科学中,“不要重复自己”原则是优秀编码习惯的核心。此原则也是性能优化的有效手段,正是记忆化技术的基础。记忆化建立在这样一个观点上:重复执行某些操作可能会消耗大量资源,尤其是当这些操作的结果不经常发生变化时。那么,为什么要重复执行已经完成的工作呢?
记忆化通过缓存计算结果来提升应用程序的性能。当同一计算再次被请求时,系统会先检查结果是否已在缓存中。如果已缓存,就直接从缓存中提取结果,省去了实际计算的步骤。从本质上讲,记忆化涉及到对之前结果的存储(由此得名)。这对于计算成本高且经常被同样的输入调用的函数来说尤为有效。这就好比一个学生解决了一个复杂的数学问题,并在书的边缘记下了答案。如果未来的考试中出现了同样的问题,学生可以简单地查看书边的笔记,而不必重新解决这个问题。
记忆化并非适用于所有情况。在某些场景下,记忆化可能会导致更多的内存消耗。因此,正确识别何时使用这种技术至关重要:
在 React 中,我们可以利用 useCallback 和useMemo等钩子来实现记忆化。让我们来看一个简单的例子:
import React, { useState, useCallback, useMemo } from 'react';function ExpensiveOperationComponent() { const [input, setInput] = useState(0); const [count, setCount] = useState(0); // 模拟一个计算开销很大的操作 const expensiveOperation = useCallback((num) => { console.log('Computing...'); // 模拟耗时长的计算 for(let i = 0; i < 1000000000; i++) {} return num * num; }, []); const memoizedResult = useMemo(() => expensiveOperation(input), [input, expensiveOperation]); return ( <div> <input value={input} onChange={e => setInput(e.target.value)} /> <p>Result of Expensive Operation: {memoizedResult}</p> <button onClick={() => setCount(count + 1)}>Re-render component</button> <p>Component re-render count: {count}</p> </div> );}export default ExpensiveOperationComponent;
在这个示例中,expensiveOperation函数模拟了一个计算密集型任务。我们使用useCallback钩子来确保在每次组件渲染时,这个函数不会被重新定义。此外,useMemo钩子被用来存储expensiveOperation的结果,这样,即使组件重新渲染,如果输入没有变化,就不会重复执行这个计算。
并行数据获取是指同时获取多个数据集,而非逐个获取。这就好比在超市结账时,有多个收银员同时服务,而不仅仅是一个:顾客能更快得到服务,排队时间缩短,整体效率得到提升。在数据处理领域,鉴于很多数据集之间互不相关,因此并行获取能显著加快页面加载速度,尤其适用于检索复杂数据所需时间较长的场景。
在 PHP 中,随着现代扩展和工具的发展,实现并行处理变得更为简便。以下是一个使用concurrent {}代码块的基本示例:
<?phpuse Concurrent/TaskScheduler;require 'vendor/autoload.php';// 假设这些是一些从各种来源获取数据的函数function fetchDataA() { // 模拟延迟 sleep(2); return "Data A";}function fetchDataB() { // 模拟延迟 sleep(3); return "Data B";}$scheduler = new TaskScheduler();$result = concurrent { "a" => fetchDataA(), "b" => fetchDataB(),};echo $result["a"]; // Outputs: Data Aecho $result["b"]; // Outputs: Data B?>
在此示例中,fetchDataA 和 fetchDataB 分别代表两个数据检索函数。通过运用concurrent {}代码块,这两个函数可同时执行,从而缩短了获取这两个数据集的总耗时。
延迟加载是一种设计模式,其核心思想是仅在真正需要时才加载数据或资源。与预先加载所有内容不同,延迟加载只载入初始视图所需的必要内容,随后根据需求加载额外资源。这类似于一家餐厅仅在顾客点特定菜品时才开始烹饪,而非预先准备所有菜肴。例如,在网页中,模态框的数据只有在用户点击按钮打开模态框时才被加载。通过这种方式,可以将数据的获取推迟到实际需要的时刻。
有效实现延迟加载的关键在于,要确保在数据获取过程中向用户提供清晰的反馈,以优化用户体验。常见的做法是在数据检索时展示一个旋转的加载动画,这样用户就能明白他们的请求正在被处理,即便数据暂时还不可用。
以下是一个 React 组件中实现延迟加载的示例。此组件只在用户点击按钮以查看模态框内容时获取数据:
import React, { useState } from 'react';function LazyLoadedModal() { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); const fetchDataForModal = async () => { setIsLoading(true); // 模拟一次 AJAX 获取数据的调用 const response = await fetch('https://api.example.com/data'); const result = await response.json(); setData(result); setIsLoading(false); setIsModalOpen(true); }; return ( <div> <button onClick={fetchDataForModal}> Open Modal </button> {isModalOpen && ( <div className="modal"> {isLoading ? ( <p>Loading...</p> // 这里可以使用旋转圈或加载动画 ) : ( <p>{data}</p> )} </div> )} </div> );}export default LazyLoadedModal;
在这个例子中,只有当用户点击“打开模态框”按钮后,才会开始获取模态框的数据。在此之前,不会发起不必要的网络请求。一旦开始获取数据,便会显示加载信息(或旋转器),以示用户请求正在处理。
在当今快速的数字时代,响应时间的每一毫秒都十分重要。用户寻求快速响应,而企业无法承受让用户等待的后果。性能优化已成为提供优质数字体验的必要条件,而不仅仅是一种优化。
通过预取、记忆化、并行获取和延迟加载等技术,开发者能有效提升应用性能。虽然这些策略在应用和方法上有所不同,但它们共同的目标是确保应用程序能够尽可能高效和快速地运行。
重要的一点是,不存在一劳永逸的解决方案或“银弹”。每个应用程序都有其独特之处,性能优化应结合对应用程序需求的深入理解、对用户期望的认识,以及正确技术的有效应用。这是一个持续改进和学习的过程。
刘汪洋,51CTO社区编辑,昵称:明明如月,一个拥有 5 年开发经验的某大厂高级 Java 工程师,拥有多个主流技术博客平台博客专家称号。
原文标题:Performance Optimization Strategies in Highly Scalable Systems,作者:Hemanth Murali
本文链接:http://www.28at.com/showinfo-26-43323-0.html高度可扩展系统中的性能优化策略
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: 深入了解Java的GC原理,掌握JVM 性能调优!
下一篇: JS小知识,分享一些让我迷惑的前端面试题