<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/rss.xsl" type="text/xsl"?>
<rss version="2.0">
  <channel>
    <title>IT瘾优化推荐</title>
    <link>https://itindex.net/categories/优化</link>
    <description>IT社区推荐资讯 - ITIndex.net</description>
    <language>zh</language>
    <copyright>https://itindex.net/</copyright>
    <generator>https://itindex.net/</generator>
    <docs>http://backend.userland.com/rss</docs>
    <image>
      <url>https://itindex.net/images/logo.gif</url>
      <title>IT社区推荐资讯 - ITIndex.net</title>
      <link>https://itindex.net/categories/优化</link>
    </image>
    <item>
      <title>阿里巴巴发布优化运行国产大模型的 RISC-V 服务器芯片</title>
      <link>https://itindex.net/detail/63186-%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4-%E4%BC%98%E5%8C%96-%E5%9B%BD%E4%BA%A7</link>
      <description>阿里巴巴发布了优化运行国产大模型的 RISC-V 服务器芯片玄铁 C950，原生支持 Qwen3、DeepSeek V3 等千亿参数大模型。阿里巴巴称玄铁 C950 单核通用性能在 Specint2006 基准测试中突破 70 分，刷新了全球 RISC-V 性能纪录。Google 研究员 Laurie Kirk 称玄铁 C950 的性能与苹果在 2020 年发布的 M1 芯片差不多。玄铁 C950  实现了 2025 年发布的 RISC-V RVA v23.1。该芯片使用 5 纳米工艺制造。
 &lt;p&gt;&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/63186-%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4-%E4%BC%98%E5%8C%96-%E5%9B%BD%E4%BA%A7</guid>
      <pubDate>Wed, 25 Mar 2026 15:53:08 CST</pubDate>
    </item>
    <item>
      <title>拥抱 GitFlow，优化开发流程：团队协作的最佳实践</title>
      <link>https://itindex.net/detail/62876-%E6%8B%A5%E6%8A%B1-gitflow-%E4%BC%98%E5%8C%96</link>
      <description>&lt;h2&gt;一. 引言&lt;/h2&gt;
 &lt;p&gt;在软件开发领域，良好的团队协作和高效的版本控制流程对项目的成功至关重要。在过去的几年里，GitFlow 成为了一种备受推崇的工作流模式，为团队提供了一种清晰而灵活的方式来管理代码库和开发过程。无论是小型团队还是大规模项目，GitFlow 都被证明是优化团队协作和版本控制流程的理想选择。&lt;/p&gt;
 &lt;p&gt;GitFlow 相比于传统的版本控制流程，如单一分支或简单分支管理，具有一些明显的优势。它采用了分支管理的策略，将开发过程划分为多个独立的分支，并定义了明确的分支命名规范和合并策略。这种模式提供了更好的代码隔离和并行开发的能力，使团队能够更好地处理复杂项目和功能开发。&lt;/p&gt;
 &lt;p&gt;在本篇文章中，我们将深入探讨 GitFlow 的各个方面，包括基础命令、使用流程和最佳实践，让我们对 GitFlow 工作流有一个更加清晰的认识&lt;/p&gt;
 &lt;h2&gt;二. 认识 GitFlow&lt;/h2&gt;
 &lt;h3&gt;1. GitFlow 的介绍&lt;/h3&gt;
 &lt;p&gt;GitFlow 是一种 Git 分支管理的模型，由 Vincent Driessen 发布于 2010 年，它基于分支的概念，通过合理的分支规划和管理，帮助团队更好地组织开发流程，并保证代码的稳定性和版本控制的可靠性。&lt;/p&gt;
 &lt;h3&gt;2. GitFlow 的基本原则&lt;/h3&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c7e65acb184147e4bd68ee8ba3aa3ebc~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2078&amp;h=984&amp;s=643674&amp;e=png&amp;b=fefefe"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;GitFlow 的核心思想是将软件开发过程分为几个不同的分支，并定义了每个分支的具体作用和规则。下面是 GitFlow 的基本分支和其作用：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;主分支&lt;/strong&gt;(master)：用于存放稳定的、发布的版本代码，永远处于可发布状态。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;开发分支&lt;/strong&gt;(develop)：用于集成各个特性分支的最新开发进展，也是团队进行日常开发的主要分支。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;功能分支&lt;/strong&gt;(feature)：用于开发新的功能特性，从 develop 分支创建，开发完成后合并回 develop 分支。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;补丁分支&lt;/strong&gt;(hotfix)：用于修复线上版本的紧急 bug，在 master 分支上创建，并在修复完成后合并到 master 和 develop 分支。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;发布分支&lt;/strong&gt;(release)：用于发布新的版本，在 develop 分支上创建，经过测试后合并到 master 分支，并且合并回 develop 分支。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;3. GitFlow 的优点&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;严格的分支管理，使得开发工作独立，减少冲突和错误的合并。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;明确的分工和版本控制流程，提高团队协作和沟通效率。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;稳定的版本控制，保证生产环境的稳定性和代码质量。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;可控的版本发布，便于回滚和修复线上问题。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;三. 安装使用&lt;/h2&gt;
 &lt;p&gt;GitFlow 并不需要单独的安装过程，它是在现有的 Git 仓库中使用的一种工作流程。所以要使用 GitFlow，首先需要确保已经安装了 Git。&lt;/p&gt;
 &lt;p&gt;如果你已经安装了 Git，请跳过以下步骤。&lt;/p&gt;
 &lt;p&gt;安装 Git 的步骤如下：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;
   &lt;p&gt;在官网下载 Git 安装程序，地址为：https://git-scm.com/downloads 。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;根据你的操作系统选择对应的安装程序进行下载。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;双击下载的安装程序，并按照提示进行安装。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;安装完成后，打开命令行界面（终端或命令提示符）。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;运行以下命令，检查 Git 安装是否成功：&lt;/p&gt;
   &lt;pre&gt;    &lt;code&gt;git --version
&lt;/code&gt;&lt;/pre&gt;
   &lt;p&gt;如果成功安装，你将看到类似以下的输出：&lt;/p&gt;
   &lt;pre&gt;    &lt;code&gt;git version x.x.x
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;确保 Git 安装成功后，你可以在已有的 Git 仓库中使用 GitFlow。&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;请注意，GitFlow 并非是 Git 的官方组件，而是一种 Git 的应工作流程。因此，你不需要单独安装 GitFlow。只需要按照前面所提到的 GitFlow 的使用流程，在已有的 Git 仓库中运行相应的命令即可使用 GitFlow。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;h2&gt;四. 常用命令&lt;/h2&gt;
 &lt;p&gt;下面是一些常用的 GitFlow 命令:&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;
   &lt;p&gt;初始化 GitFlow:&lt;/p&gt;
   &lt;pre&gt;    &lt;code&gt;git flow init
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;开始一个新的功能分支:&lt;/p&gt;
   &lt;pre&gt;    &lt;code&gt;git flow feature start &amp;lt;feature-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;结束一个功能分支:&lt;/p&gt;
   &lt;pre&gt;    &lt;code&gt;git flow feature finish &amp;lt;feature-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;开始一个 bug 修复分支:&lt;/p&gt;
   &lt;pre&gt;    &lt;code&gt;git flow bugfix start &amp;lt;bugfix-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;结束一个 bug 修复分支:&lt;/p&gt;
   &lt;pre&gt;    &lt;code&gt;git flow bugfix finish &amp;lt;bugfix-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;开始一个发布分支:&lt;/p&gt;
   &lt;pre&gt;    &lt;code&gt;git flow release start &amp;lt;release-version&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;完成一个发布分支:&lt;/p&gt;
   &lt;pre&gt;    &lt;code&gt;git flow release finish &amp;lt;release-version&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;开始一个热修复分支:&lt;/p&gt;
   &lt;pre&gt;    &lt;code&gt;git flow hotfix start &amp;lt;hotfix-version&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;完成一个热修复分支:&lt;/p&gt;
   &lt;pre&gt;    &lt;code&gt;git flow hotfix finish &amp;lt;hotfix-version&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;查看当前 GitFlow 状态:&lt;/p&gt;
   &lt;pre&gt;    &lt;code&gt;git flow status
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;以上命令是 GitFlow 的基本命令，使用这些命令可以方便地管理不同类型的分支，并协同开发团队进行代码版本控制。你可以根据项目的需要和实际情况来选择使用哪些命令。&lt;/p&gt;
 &lt;h2&gt;五. GitFlow 的使用流程&lt;/h2&gt;
 &lt;p&gt;GitFlow 是一种流行的 Git 分支管理工作流。它定义了一套基于分支的模型，用于规范开发团队在项目中的代码管理和版本控制操作。下面是 GitFlow 的基本使用流程，以及相应的代码说明：&lt;/p&gt;
 &lt;h3&gt;1. 初始化 GitFlow&lt;/h3&gt;
 &lt;ol&gt;
  &lt;li&gt;在项目根目下，运行以下命令来初始化 GitFlow：   &lt;pre&gt;    &lt;code&gt;git flow init
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
 &lt;h3&gt;2. 开发新功能&lt;/h3&gt;
 &lt;ol&gt;
  &lt;li&gt;创建新功能分支，基于    &lt;code&gt;develop&lt;/code&gt; 分支：   &lt;pre&gt;    &lt;code&gt;git flow feature start feature-name
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
  &lt;li&gt;在该分支上进行开发和修改代码，完成后提交代码：   &lt;pre&gt;    &lt;code&gt;git add .
git commit -m &amp;quot;Add new feature&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
  &lt;li&gt;完成开发后，结束功能分支并将其合并到    &lt;code&gt;develop&lt;/code&gt; 分支：   &lt;pre&gt;    &lt;code&gt;git flow feature finish feature-name
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
 &lt;h3&gt;3. 修复 bug&lt;/h3&gt;
 &lt;ol&gt;
  &lt;li&gt;创建修复分支，基于    &lt;code&gt;develop&lt;/code&gt; 分支：   &lt;pre&gt;    &lt;code&gt;git flow bugfix start bugfix-name
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
  &lt;li&gt;修复 bug 并提交代码：   &lt;pre&gt;    &lt;code&gt;git add .
git commit -m &amp;quot;Fix bug&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
  &lt;li&gt;修复完成后，结束修复分支并将其合并到    &lt;code&gt;develop&lt;/code&gt; 分支和    &lt;code&gt;master&lt;/code&gt; 分支：   &lt;pre&gt;    &lt;code&gt;git flow bugfix finish bugfix-name
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
 &lt;h3&gt;4. 发布新版本&lt;/h3&gt;
 &lt;ol&gt;
  &lt;li&gt;创建发布分支，基于    &lt;code&gt;develop&lt;/code&gt; 分支：   &lt;pre&gt;    &lt;code&gt;git flow release start release-version
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
  &lt;li&gt;在发布分支上进行版本相关的修改、测试等操作，完成后提交代码：   &lt;pre&gt;    &lt;code&gt;git add .
git commit -m &amp;quot;Release version&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
  &lt;li&gt;发布完成后，结束发布分支并将其合并到    &lt;code&gt;develop&lt;/code&gt; 分支和    &lt;code&gt;master&lt;/code&gt; 分支，同时标记该版本号：   &lt;pre&gt;    &lt;code&gt;git flow release finish release-version
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
 &lt;blockquote&gt;
  &lt;p&gt;备注：上述命令中的    &lt;code&gt;feature-name&lt;/code&gt;、   &lt;code&gt;bugfix-name&lt;/code&gt; 以及    &lt;code&gt;release-version&lt;/code&gt; 分别表示分支名称、修复分支名称和版本号，可以根据实际需求进行替换。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;请注意，GitFlow 对于项目来说是一种推荐的工作流，但在实际使用中也可以据团队要求和项目需要进行适度的调整和定制。&lt;/p&gt;
 &lt;h2&gt;六. 最佳实践和经验总结&lt;/h2&gt;
 &lt;p&gt;从以前开发的几个项目来看，以下是一些 GitFlow 的最佳实践和经验总结：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;
   &lt;p&gt;分支命名规范：&lt;/p&gt;
   &lt;ul&gt;
    &lt;li&gt;feature 分支：feature/&amp;lt;feature-name&amp;gt;&lt;/li&gt;
    &lt;li&gt;bug 修复分支：bugfix/&amp;lt;bugfix-name&amp;gt;&lt;/li&gt;
    &lt;li&gt;发布分支：release/&amp;lt;release-version&amp;gt;&lt;/li&gt;
    &lt;li&gt;热修复分支：hotfix/&amp;lt;hotfix-version&amp;gt;&lt;/li&gt;
    &lt;li&gt;主分支：master&lt;/li&gt;
    &lt;li&gt;开发分支：develop&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;使用 GitFlow 工作流模式：&lt;/p&gt;
   &lt;ul&gt;
    &lt;li&gt;开发新功能或解决问题时，创建一个 feature 或 bugfix 分支，基于 develop 分支，完成后合并回 develop 分支。&lt;/li&gt;
    &lt;li&gt;预发布版本之前，创建一个 release 分支，基于 develop 分支，进行测试和准备发布，完成后合并回 develop 分支，并合并到 master 分支。&lt;/li&gt;
    &lt;li&gt;遇到紧急 bug 修复，可以创建一个 hotfix 分支，基于 master 分支，完成后合并回 develop 和 master 分支。&lt;/li&gt;
    &lt;li&gt;master 分支始终保持稳定的生产版本，develop 分支是主要开发分支。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;切换分支前先进行提交或保存工作区的改动，以免丢失修改。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;使用合适的工具和插件来支持 GitFlow，如 Git 版本管理工具（如 GitKraken、SourceTree）、命令行工具（如 Git Bash）和 IDE 集成工具。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;使用版本标签来标记发布版本，方便回溯和管理。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;团队合作时，确保团队成员了解和遵循 GitFlow 的使用和流程规范，以便协同工作和保持代码库的整洁和可维护。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;定期进行代码合并和分支删除，以保持代码仓库的整洁性，并减少潜在的冲突和问题。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;这些实践和经验可以帮助你更好地使用 GitFlow 工作流模式，提高团队协作效率，并保持代码库的稳定和可控性。但请根据你的具体项目和团队需求做出适当的调整和改进。&lt;/p&gt;
 &lt;h2&gt;七. 可选的工具和插件推荐&lt;/h2&gt;
 &lt;p&gt;在平常开发中，我主要使用以下的方式来使用 GitFlow，辅助我在使用 GitFlow 工作流模式时更高效地操作和管理代码库：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;SourceTree&lt;/strong&gt;：Atlassian 公司开发的免费 Git 和 Mercurial 客户端，支持 Windows 和 macOS，提供直观的界面和图形化工具，对 GitFlow 有很好的支持。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;IDE 集成工具&lt;/strong&gt;：如果你使用特定的集成开发环境（如 IntelliJ IDEA、WebStorm、Visual Studio Code、 等），可以安装相应的 GitFlow 插件或扩展来支持 GitFlow 工作流模式。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;以上只是列举了我平常项目中常用的，还有其他的工具，比如：  &lt;code&gt;GitKraken&lt;/code&gt;、  &lt;code&gt;GitExtensions&lt;/code&gt;、  &lt;code&gt;GitFlow AVH Edition&lt;/code&gt;、  &lt;code&gt;Git-Flow Completion&lt;/code&gt;等等，其实他们的使用方式大相径庭，可以根据你的个人喜好和项目需求选择使用，以提升你的 GitFlow 工作流体验和效率。&lt;/p&gt;
 &lt;h2&gt;八. 总结&lt;/h2&gt;
 &lt;p&gt;通过本篇文章，我们了解了 GitFlow 工作流模式，它是优化团队协作和版本控制流程的一种有效方法。GitFlow 的核心理念是将开发过程划分为多个独立的分支，并定义了明确的分支命名规范和合并策略。样做可以提供好的代码隔离和行开发能力，团队能够更好地处理复杂项目和功能开发。&lt;/p&gt;
 &lt;p&gt;在实际应用中，我们可以遵循 GitFlow 的步骤和最佳实践，从功能开发分支、发布分支到维护分支的管理，确保团队在不同阶段的开发和合并过程中高效而序地工作。同时，可以借助一些可选的工具和插件，如 GitKraken、SourceTree 等，提升我们在使用 GitFlow 过程中的效率和便利性。&lt;/p&gt;
 &lt;p&gt;然而，要充分发挥 GitFlow 的优势，团队成员需要深入了解和适应这种工作流模式。每个人都需要遵循一致的分支命名规范，严格按照合并策略进行代码合并，尽量避免分支的混乱和代码冲突的产生。只有这样，才能真正体验到 GitFlow 带来的效益和改进。&lt;/p&gt;
 &lt;p&gt;GitFlow 是一个强大的工作流模式，它已经在许多项目中得到广泛应用并取得了显著的成果。通过学习和掌握 GitFlow，我们将可以更好地管理和迭代我们的代码库，从而提高团队的生产力和项目的成功率。&lt;/p&gt;
 &lt;h2&gt;参考资料&lt;/h2&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://nvie.com/posts/a-successful-git-branching-model/"&gt;A successful Git branching model&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow"&gt;Understanding the GitFlow Workflow&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62876-%E6%8B%A5%E6%8A%B1-gitflow-%E4%BC%98%E5%8C%96</guid>
      <pubDate>Sat, 04 Nov 2023 14:31:04 CST</pubDate>
    </item>
    <item>
      <title>面试官：MySQL 上亿大表，如何深度优化？</title>
      <link>https://itindex.net/detail/62850-%E9%9D%A2%E8%AF%95-mysql-%E5%A4%A7%E8%A1%A8</link>
      <description>&lt;ul&gt;  &lt;li&gt;   &lt;a href="https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&amp;mid=2247487551&amp;idx=1&amp;sn=18f64ba49f3f0f9d8be9d1fdef8857d9&amp;chksm=fa496f8ecd3ee698f4954c00efb80fe955ec9198fff3ef4011e331aa37f55a6a17bc8c0335a8&amp;scene=21&amp;token=899450012&amp;lang=zh_CN#wechat_redirect"&gt;背景&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&amp;mid=2247487551&amp;idx=1&amp;sn=18f64ba49f3f0f9d8be9d1fdef8857d9&amp;chksm=fa496f8ecd3ee698f4954c00efb80fe955ec9198fff3ef4011e331aa37f55a6a17bc8c0335a8&amp;scene=21&amp;token=899450012&amp;lang=zh_CN#wechat_redirect"&gt;分析&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&amp;mid=2247487551&amp;idx=1&amp;sn=18f64ba49f3f0f9d8be9d1fdef8857d9&amp;chksm=fa496f8ecd3ee698f4954c00efb80fe955ec9198fff3ef4011e331aa37f55a6a17bc8c0335a8&amp;scene=21&amp;token=899450012&amp;lang=zh_CN#wechat_redirect"&gt;测试&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&amp;mid=2247487551&amp;idx=1&amp;sn=18f64ba49f3f0f9d8be9d1fdef8857d9&amp;chksm=fa496f8ecd3ee698f4954c00efb80fe955ec9198fff3ef4011e331aa37f55a6a17bc8c0335a8&amp;scene=21&amp;token=899450012&amp;lang=zh_CN#wechat_redirect"&gt;实施&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&amp;mid=2247487551&amp;idx=1&amp;sn=18f64ba49f3f0f9d8be9d1fdef8857d9&amp;chksm=fa496f8ecd3ee698f4954c00efb80fe955ec9198fff3ef4011e331aa37f55a6a17bc8c0335a8&amp;scene=21&amp;token=899450012&amp;lang=zh_CN#wechat_redirect"&gt;索引优化后&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&amp;mid=2247487551&amp;idx=1&amp;sn=18f64ba49f3f0f9d8be9d1fdef8857d9&amp;chksm=fa496f8ecd3ee698f4954c00efb80fe955ec9198fff3ef4011e331aa37f55a6a17bc8c0335a8&amp;scene=21&amp;token=899450012&amp;lang=zh_CN#wechat_redirect"&gt;delete大表优化为小批量删除&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&amp;mid=2247487551&amp;idx=1&amp;sn=18f64ba49f3f0f9d8be9d1fdef8857d9&amp;chksm=fa496f8ecd3ee698f4954c00efb80fe955ec9198fff3ef4011e331aa37f55a6a17bc8c0335a8&amp;scene=21&amp;token=899450012&amp;lang=zh_CN#wechat_redirect"&gt;总结&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;hr&gt;&lt;/hr&gt; &lt;p&gt;  &lt;strong&gt;前段时间刚入职一家公司，就遇上这事！&lt;/strong&gt;&lt;/p&gt; &lt;h2&gt;  &lt;a href="https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&amp;mid=2247487551&amp;idx=1&amp;sn=18f64ba49f3f0f9d8be9d1fdef8857d9&amp;scene=21#wechat_redirect"&gt;背景&lt;/a&gt;&lt;/h2&gt; &lt;p&gt;XX实例（一主一从）xxx告警中每天凌晨在报SLA报警，该报警的意思是存在一定的主从延迟（若在此时发生主从切换，需要长时间才可以完成切换，要追延迟来保证主从数据的一致性）&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;XX实例的慢查询数量最多（执行时间超过1s的sql会被记录），XX应用那方每天晚上在做删除一个月前数据的任务&lt;/p&gt;&lt;/blockquote&gt; &lt;blockquote&gt;  &lt;p&gt;基于 Spring Boot + MyBatis Plus + Vue &amp;amp; Element 实现的后台管理系统 + 用户小程序，支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能&lt;/p&gt;  &lt;ul&gt;   &lt;li&gt;项目地址：https://github.com/YunaiV/ruoyi-vue-pro&lt;/li&gt;   &lt;li&gt;视频教程：https://doc.iocoder.cn/video/&lt;/li&gt;&lt;/ul&gt;&lt;/blockquote&gt; &lt;h2&gt;  &lt;a href="https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&amp;mid=2247487551&amp;idx=1&amp;sn=18f64ba49f3f0f9d8be9d1fdef8857d9&amp;scene=21#wechat_redirect"&gt;分析&lt;/a&gt;&lt;/h2&gt; &lt;p&gt;使用pt-query-digest工具分析最近一周的mysql-slow.log&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;pt-query-digest --since=148h mysql-slow.log | less   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;结果第一部分  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;最近一个星期内，总共记录的慢查询执行花费时间为25403s，最大的慢sql执行时间为266s，平均每个慢sql执行时间5s，平均扫描的行数为1766万&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;结果第二部分&lt;/strong&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;select arrival_record操作记录的慢查询数量最多有4万多次，平均响应时间为4s，delete arrival_record记录了6次，平均响应时间258s。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;select xxx_record语句&lt;/strong&gt;&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;select arrival_record 慢查询语句都类似于如下所示，where语句中的参数字段是一样的，传入的参数值不一样
select count(*) from arrival_record where product_id=26 and receive_time between &amp;apos;2019-03-25 14:00:00&amp;apos; and &amp;apos;2019-03-25 15:00:00&amp;apos; and receive_spend_ms&amp;gt;=0\G&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;select arrival_record 语句在mysql中最多扫描的行数为5600万、平均扫描的行数为172万，推断由于扫描的行数多导致的执行时间长&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;查看执行计划&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;explain select count(*) from arrival_record where product_id=26 and receive_time between &amp;apos;2019-03-25 14:00:00&amp;apos; and &amp;apos;2019-03-25 15:00:00&amp;apos; and receive_spend_ms&amp;gt;=0\G;   &lt;br /&gt;*************************** 1. row ***************************   &lt;br /&gt;id: 1   &lt;br /&gt;select_type: SIMPLE   &lt;br /&gt;table: arrival_record   &lt;br /&gt;partitions: NULL   &lt;br /&gt;type: ref   &lt;br /&gt;possible_keys: IXFK_arrival_record   &lt;br /&gt;key: IXFK_arrival_record   &lt;br /&gt;key_len: 8   &lt;br /&gt;ref: const   &lt;br /&gt;rows: 32261320   &lt;br /&gt;filtered: 3.70   &lt;br /&gt;Extra: Using index condition; Using where   &lt;br /&gt;1 row in set, 1 warning (0.00 sec)   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;  &lt;strong&gt;用到了索引IXFK_arrival_record，但预计扫描的行数很多有3000多w行&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;show index from arrival_record;   &lt;br /&gt;+----------------+------------+---------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+   &lt;br /&gt;| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |   &lt;br /&gt;+----------------+------------+---------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+   &lt;br /&gt;| arrival_record | 0 | PRIMARY | 1 | id | A | 107990720 | NULL | NULL | | BTREE | | |   &lt;br /&gt;| arrival_record | 1 | IXFK_arrival_record | 1 | product_id | A | 1344 | NULL | NULL | | BTREE | | |   &lt;br /&gt;| arrival_record | 1 | IXFK_arrival_record | 2 | station_no | A | 22161 | NULL | NULL | YES | BTREE | | |   &lt;br /&gt;| arrival_record | 1 | IXFK_arrival_record | 3 | sequence | A | 77233384 | NULL | NULL | | BTREE | | |   &lt;br /&gt;| arrival_record | 1 | IXFK_arrival_record | 4 | receive_time | A | 65854652 | NULL | NULL | YES | BTREE | | |   &lt;br /&gt;| arrival_record | 1 | IXFK_arrival_record | 5 | arrival_time | A | 73861904 | NULL | NULL | YES | BTREE | | |   &lt;br /&gt;+----------------+------------+---------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+   &lt;br /&gt;show create table arrival_record;   &lt;br /&gt;..........   &lt;br /&gt;arrival_spend_ms bigint(20) DEFAULT NULL,   &lt;br /&gt;total_spend_ms bigint(20) DEFAULT NULL,   &lt;br /&gt;PRIMARY KEY (id),   &lt;br /&gt;KEY IXFK_arrival_record (product_id,station_no,sequence,receive_time,arrival_time) USING BTREE,   &lt;br /&gt;CONSTRAINT FK_arrival_record_product FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE NO ACTION ON UPDATE NO ACTION   &lt;br /&gt;) ENGINE=InnoDB AUTO_INCREMENT=614538979 DEFAULT CHARSET=utf8 COLLATE=utf8_bin |   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;ul&gt;  &lt;li&gt;该表总记录数约1亿多条，表上只有一个复合索引，product_id字段基数很小，选择性不好&lt;/li&gt;  &lt;li&gt;传入的过滤条件 where product_id=26 and receive_time between &amp;apos;2019-03-25 14:00:00&amp;apos; and &amp;apos;2019-03-25 15:00:00&amp;apos; and receive_spend_ms&amp;gt;=0 没有station_nu字段，使用不到复合索引 IXFK_arrival_record的   &lt;code&gt;product_id&lt;/code&gt;,   &lt;code&gt;station_no&lt;/code&gt;,   &lt;code&gt;sequence&lt;/code&gt;,   &lt;code&gt;receive_time&lt;/code&gt;这几个字段&lt;/li&gt;  &lt;li&gt;根据最左前缀原则，select arrival_record只用到了复合索引IXFK_arrival_record的第一个字段product_id，而该字段选择性很差，导致扫描的行数很多，执行时间长&lt;/li&gt;  &lt;li&gt;receive_time字段的基数大，选择性好，可对该字段单独建立索引，select arrival_record sql就会使用到该索引&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;现在已经知道了在慢查询中记录的select arrival_record where语句传入的参数字段有 product_id，receive_time，receive_spend_ms，还想知道对该表的访问有没有通过其它字段来过滤了？&lt;/p&gt; &lt;hr&gt;&lt;/hr&gt; &lt;p&gt;  &lt;strong&gt;神器tcpdump出场的时候到了&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;使用tcpdump抓包一段时间对该表的select语句&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;tcpdump -i bond0 -s 0 -l -w - dst port 3316 | strings | grep select | egrep -i &amp;apos;arrival_record&amp;apos; &amp;gt;/tmp/select_arri.log   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;获取select 语句中from 后面的where条件语句&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;IFS_OLD=$IFS   &lt;br /&gt;IFS=$&amp;apos;\n&amp;apos;   &lt;br /&gt;for i in `cat /tmp/select_arri.log `;do echo ${i#*&amp;apos;from&amp;apos;}; done | less   &lt;br /&gt;IFS=$IFS_OLD   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;  &lt;code&gt;arrival_record arrivalrec0_ where arrivalrec0_.sequence=&amp;apos;2019-03-27 08:40&amp;apos; and arrivalrec0_.product_id=17 and arrivalrec0_.station_no=&amp;apos;56742&amp;apos;   &lt;br /&gt;arrival_record arrivalrec0_ where arrivalrec0_.sequence=&amp;apos;2019-03-27 08:40&amp;apos; and arrivalrec0_.product_id=22 and arrivalrec0_.station_no=&amp;apos;S7100&amp;apos;   &lt;br /&gt;arrival_record arrivalrec0_ where arrivalrec0_.sequence=&amp;apos;2019-03-27 08:40&amp;apos; and arrivalrec0_.product_id=24 and arrivalrec0_.station_no=&amp;apos;V4631&amp;apos;   &lt;br /&gt;arrival_record arrivalrec0_ where arrivalrec0_.sequence=&amp;apos;2019-03-27 08:40&amp;apos; and arrivalrec0_.product_id=22 and arrivalrec0_.station_no=&amp;apos;S9466&amp;apos;   &lt;br /&gt;arrival_record arrivalrec0_ where arrivalrec0_.sequence=&amp;apos;2019-03-27 08:40&amp;apos; and arrivalrec0_.product_id=24 and arrivalrec0_.station_no=&amp;apos;V4205&amp;apos;   &lt;br /&gt;arrival_record arrivalrec0_ where arrivalrec0_.sequence=&amp;apos;2019-03-27 08:40&amp;apos; and arrivalrec0_.product_id=24 and arrivalrec0_.station_no=&amp;apos;V4105&amp;apos;   &lt;br /&gt;arrival_record arrivalrec0_ where arrivalrec0_.sequence=&amp;apos;2019-03-27 08:40&amp;apos; and arrivalrec0_.product_id=24 and arrivalrec0_.station_no=&amp;apos;V4506&amp;apos;   &lt;br /&gt;arrival_record arrivalrec0_ where arrivalrec0_.sequence=&amp;apos;2019-03-27 08:40&amp;apos; and arrivalrec0_.product_id=24 and arrivalrec0_.station_no=&amp;apos;V4617&amp;apos;   &lt;br /&gt;arrival_record arrivalrec0_ where arrivalrec0_.sequence=&amp;apos;2019-03-27 08:40&amp;apos; and arrivalrec0_.product_id=22 and arrivalrec0_.station_no=&amp;apos;S8356&amp;apos;   &lt;br /&gt;arrival_record arrivalrec0_ where arrivalrec0_.sequence=&amp;apos;2019-03-27 08:40&amp;apos; and arrivalrec0_.product_id=22 and arrivalrec0_.station_no=&amp;apos;S8356&amp;apos;   &lt;br /&gt;select 该表 where条件中有product_id,station_no,sequence字段，可以使用到复合索引IXFK_arrival_record的前三个字段   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;综上所示，优化方法为，删除复合索引IXFK_arrival_record，建立复合索引idx_sequence_station_no_product_id，并建立单独索引indx_receive_time&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;delete xxx_record语句&lt;/strong&gt;&lt;/p&gt; &lt;img&gt;&lt;/img&gt; &lt;blockquote&gt;  &lt;p&gt;该delete操作平均扫描行数为1.1亿行，平均执行时间是262s&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;delete语句如下所示，每次记录的慢查询传入的参数值不一样&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;delete from arrival_record where receive_time &amp;lt; STR_TO_DATE(&amp;apos;2019-02-23&amp;apos;, &amp;apos;%Y-%m-%d&amp;apos;)\G   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;执行计划&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;explain select * from arrival_record where receive_time &amp;lt; STR_TO_DATE(&amp;apos;2019-02-23&amp;apos;, &amp;apos;%Y-%m-%d&amp;apos;)\G   &lt;br /&gt;*************************** 1. row ***************************   &lt;br /&gt;id: 1   &lt;br /&gt;select_type: SIMPLE   &lt;br /&gt;table: arrival_record   &lt;br /&gt;partitions: NULL   &lt;br /&gt;type: ALL   &lt;br /&gt;possible_keys: NULL   &lt;br /&gt;key: NULL   &lt;br /&gt;key_len: NULL   &lt;br /&gt;ref: NULL   &lt;br /&gt;rows: 109501508   &lt;br /&gt;filtered: 33.33   &lt;br /&gt;Extra: Using where   &lt;br /&gt;1 row in set, 1 warning (0.00 sec)   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;  &lt;p&gt;该delete语句没有使用索引（没有合适的索引可用），走的全表扫描，导致执行时间长&lt;/p&gt;  &lt;p&gt;优化方法也是 建立单独索引indx_receive_time(receive_time)&lt;/p&gt;&lt;/blockquote&gt; &lt;blockquote&gt;  &lt;p&gt;基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue &amp;amp; Element 实现的后台管理系统 + 用户小程序，支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能&lt;/p&gt;  &lt;ul&gt;   &lt;li&gt;项目地址：https://github.com/YunaiV/yudao-cloud&lt;/li&gt;   &lt;li&gt;视频教程：https://doc.iocoder.cn/video/&lt;/li&gt;&lt;/ul&gt;&lt;/blockquote&gt; &lt;h2&gt;  &lt;a href="https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&amp;mid=2247487551&amp;idx=1&amp;sn=18f64ba49f3f0f9d8be9d1fdef8857d9&amp;scene=21#wechat_redirect"&gt;测试&lt;/a&gt;&lt;/h2&gt; &lt;p&gt;拷贝arrival_record表到测试实例上进行删除重新索引操作  &lt;strong&gt;XX实例arrival_record表信息&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;du -sh /datas/mysql/data/3316/cq_new_cimiss/arrival_record*   &lt;br /&gt;12K /datas/mysql/data/3316/cq_new_cimiss/arrival_record.frm   &lt;br /&gt;48G /datas/mysql/data/3316/cq_new_cimiss/arrival_record.ibd   &lt;br /&gt;select count() from cq_new_cimiss.arrival_record;   &lt;br /&gt;+-----------+   &lt;br /&gt;| count() |   &lt;br /&gt;+-----------+   &lt;br /&gt;| 112294946 |   &lt;br /&gt;+-----------+   &lt;br /&gt;1亿多记录数   &lt;br /&gt;   &lt;br /&gt;SELECT   &lt;br /&gt;table_name,   &lt;br /&gt;CONCAT(FORMAT(SUM(data_length) / 1024 / 1024,2),&amp;apos;M&amp;apos;) AS dbdata_size,   &lt;br /&gt;CONCAT(FORMAT(SUM(index_length) / 1024 / 1024,2),&amp;apos;M&amp;apos;) AS dbindex_size,   &lt;br /&gt;CONCAT(FORMAT(SUM(data_length + index_length) / 1024 / 1024 / 1024,2),&amp;apos;G&amp;apos;) AS table_size(G),   &lt;br /&gt;AVG_ROW_LENGTH,table_rows,update_time   &lt;br /&gt;FROM   &lt;br /&gt;information_schema.tables   &lt;br /&gt;WHERE table_schema = &amp;apos;cq_new_cimiss&amp;apos; and table_name=&amp;apos;arrival_record&amp;apos;;   &lt;br /&gt;+----------------+-------------+--------------+------------+----------------+------------+---------------------+   &lt;br /&gt;| table_name | dbdata_size | dbindex_size | table_size(G) | AVG_ROW_LENGTH | table_rows | update_time |   &lt;br /&gt;+----------------+-------------+--------------+------------+----------------+------------+---------------------+   &lt;br /&gt;| arrival_record | 18,268.02M | 13,868.05M | 31.38G | 175 | 109155053 | 2019-03-26 12:40:17 |   &lt;br /&gt;+----------------+-------------+--------------+------------+----------------+------------+---------------------+   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;磁盘占用空间48G，mysql中该表大小为31G，存在17G左右的碎片，大多由于删除操作造成的（记录被删除了，空间没有回收)&lt;/p&gt; &lt;hr&gt;&lt;/hr&gt; &lt;p&gt;  &lt;strong&gt;备份还原该表到新的实例中，删除原来的复合索引，重新添加索引进行测试&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;mydumper并行压缩备份&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;user=root   &lt;br /&gt;passwd=xxxx   &lt;br /&gt;socket=/datas/mysql/data/3316/mysqld.sock   &lt;br /&gt;db=cq_new_cimiss   &lt;br /&gt;table_name=arrival_record   &lt;br /&gt;backupdir=/datas/dump_$table_name   &lt;br /&gt;mkdir -p $backupdir   &lt;br /&gt;  nohup echo `date +%T` &amp;amp;&amp;amp; mydumper -u $user -p $passwd -S $socket -B $db -c -T $table_name -o $backupdir -t 32 -r 2000000 &amp;amp;&amp;amp; echo `date +%T` &amp;amp;   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;并行压缩备份所花时间（52s）和占用空间（1.2G，实际该表占用磁盘空间为48G，mydumper并行压缩备份压缩比相当高！）&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;Started dump at: 2019-03-26 12:46:04   &lt;br /&gt;........   &lt;br /&gt;   &lt;br /&gt;Finished dump at: 2019-03-26 12:46:56   &lt;br /&gt;   &lt;br /&gt;du -sh   /datas/dump_arrival_record/   &lt;br /&gt;1.2G  /datas/dump_arrival_record/   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;  &lt;strong&gt;拷贝dump数据到测试节点&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;scp -rp /datas/dump_arrival_record root@10.230.124.19:/datas   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;  &lt;strong&gt;多线程导入数据&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;time myloader -u root -S /datas/mysql/data/3308/mysqld.sock -P 3308 -p root -B test -d /datas/dump_arrival_record -t 32   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;real 126m42.885s
user 1m4.543s
sys 0m4.267s&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;逻辑导入该表后磁盘占用空间&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;du -h -d 1 /datas/mysql/data/3308/test/arrival_record.*   &lt;br /&gt;12K /datas/mysql/data/3308/test/arrival_record.frm   &lt;br /&gt;30G /datas/mysql/data/3308/test/arrival_record.ibd   &lt;br /&gt;没有碎片，和mysql的该表的大小一致   &lt;br /&gt;cp -rp /datas/mysql/data/3308 /datas   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;分别使用online DDL和 pt-osc工具来做删除重建索引操作
先删除外键，不删除外键，无法删除复合索引，外键列属于复合索引中第一列&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;nohup bash /tmp/ddl_index.sh &amp;amp;   &lt;br /&gt;2019-04-04-10:41:39 begin stop mysqld_3308   &lt;br /&gt;2019-04-04-10:41:41 begin rm -rf datadir and cp -rp datadir_bak   &lt;br /&gt;2019-04-04-10:46:53 start mysqld_3308   &lt;br /&gt;2019-04-04-10:46:59 online ddl begin   &lt;br /&gt;2019-04-04-11:20:34 onlie ddl stop   &lt;br /&gt;2019-04-04-11:20:34 begin stop mysqld_3308   &lt;br /&gt;2019-04-04-11:20:36 begin rm -rf datadir and cp -rp datadir_bak   &lt;br /&gt;2019-04-04-11:22:48 start mysqld_3308   &lt;br /&gt;2019-04-04-11:22:53 pt-osc begin   &lt;br /&gt;2019-04-04-12:19:15 pt-osc stop   &lt;br /&gt;online ddl 花费时间为34 分钟，pt-osc花费时间为57 分钟，使用onlne ddl时间约为pt-osc工具时间的一半   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;*  &lt;strong&gt;做DDL 参考&lt;/strong&gt;*&lt;/p&gt; &lt;hr&gt;&lt;/hr&gt; &lt;hr&gt;&lt;/hr&gt; &lt;img&gt;&lt;/img&gt; &lt;h2&gt;  &lt;a href="https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&amp;mid=2247487551&amp;idx=1&amp;sn=18f64ba49f3f0f9d8be9d1fdef8857d9&amp;scene=21#wechat_redirect"&gt;实施&lt;/a&gt;&lt;/h2&gt; &lt;p&gt;由于是一主一从实例，应用是连接的vip，删除重建索引采用online ddl来做。停止主从复制后，先在从实例上做（不记录binlog），主从切换，再在新切换的从实例上做（不记录binlog)&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;function red_echo () {   &lt;br /&gt;   &lt;br /&gt;        local what=&amp;quot;$*&amp;quot;   &lt;br /&gt;        echo -e &amp;quot;$(date +%F-%T)  ${what}&amp;quot;   &lt;br /&gt;}   &lt;br /&gt;   &lt;br /&gt;function check_las_comm(){   &lt;br /&gt;    if [ &amp;quot;$1&amp;quot; != &amp;quot;0&amp;quot; ];then   &lt;br /&gt;        red_echo &amp;quot;$2&amp;quot;   &lt;br /&gt;        echo &amp;quot;exit 1&amp;quot;   &lt;br /&gt;        exit 1   &lt;br /&gt;    fi   &lt;br /&gt;}   &lt;br /&gt;   &lt;br /&gt;red_echo &amp;quot;stop slave&amp;quot;   &lt;br /&gt;mysql -uroot -p$passwd --socket=/datas/mysql/data/${port}/mysqld.sock -e&amp;quot;stop slave&amp;quot;   &lt;br /&gt;check_las_comm &amp;quot;$?&amp;quot; &amp;quot;stop slave failed&amp;quot;   &lt;br /&gt;   &lt;br /&gt;red_echo &amp;quot;online ddl begin&amp;quot;   &lt;br /&gt; mysql -uroot -p$passwd --socket=/datas/mysql/data/${port}/mysqld.sock -e&amp;quot;set sql_log_bin=0;select now() as  ddl_start;ALTER TABLE $db_.\`${table_name}\` DROP FOREIGN KEY FK_arrival_record_product,drop index IXFK_arrival_record,add index idx_product_id_sequence_station_no(product_id,sequence,station_no),add index idx_receive_time(receive_time);select now() as ddl_stop&amp;quot; &amp;gt;&amp;gt;${log_file} 2&amp;gt;&amp;amp; 1   &lt;br /&gt; red_echo &amp;quot;onlie ddl stop&amp;quot;   &lt;br /&gt; red_echo &amp;quot;add foreign key&amp;quot;   &lt;br /&gt; mysql -uroot -p$passwd --socket=/datas/mysql/data/${port}/mysqld.sock -e&amp;quot;set sql_log_bin=0;ALTER TABLE $db_.${table_name} ADD CONSTRAINT _FK_${table_name}_product FOREIGN KEY (product_id) REFERENCES cq_new_cimiss.product (id) ON DELETE NO ACTION ON UPDATE NO ACTION;&amp;quot; &amp;gt;&amp;gt;${log_file} 2&amp;gt;&amp;amp; 1   &lt;br /&gt; check_las_comm &amp;quot;$?&amp;quot; &amp;quot;add foreign key error&amp;quot;   &lt;br /&gt; red_echo &amp;quot;add foreign key stop&amp;quot;   &lt;br /&gt;   &lt;br /&gt;red_echo &amp;quot;start slave&amp;quot;   &lt;br /&gt;mysql -uroot -p$passwd --socket=/datas/mysql/data/${port}/mysqld.sock -e&amp;quot;start slave&amp;quot;   &lt;br /&gt;check_las_comm &amp;quot;$?&amp;quot; &amp;quot;start slave failed&amp;quot;   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;*  &lt;strong&gt;执行时间&lt;/strong&gt;*&lt;/p&gt; &lt;p&gt;2019-04-08-11:17:36 stop slave
mysql: [Warning] Using a password on the command line interface can be insecure.
ddl_start
2019-04-08  &lt;strong&gt;11:17:36&lt;/strong&gt;ddl_stop
2019-04-08  &lt;strong&gt;11:45:13&lt;/strong&gt;2019-04-08-11:45:13 onlie ddl stop
2019-04-08-  &lt;strong&gt;11:45:13&lt;/strong&gt;add foreign key
mysql: [Warning] Using a password on the command line interface can be insecure.
2019-04-08-12:33:48 add foreign key stop
2019-04-08-  &lt;strong&gt;12:33:48&lt;/strong&gt;start slave&lt;/p&gt; &lt;p&gt;*  &lt;strong&gt;再次查看delete 和select语句的执行计划&lt;/strong&gt;*&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;explain select count(*) from arrival_record where receive_time &amp;lt; STR_TO_DATE(&amp;apos;2019-03-10&amp;apos;, &amp;apos;%Y-%m-%d&amp;apos;)\G   &lt;br /&gt;*************************** 1. row ***************************   &lt;br /&gt;id: 1   &lt;br /&gt;select_type: SIMPLE   &lt;br /&gt;table: arrival_record   &lt;br /&gt;partitions: NULL   &lt;br /&gt;type: range   &lt;br /&gt;possible_keys: idx_receive_time   &lt;br /&gt;key: idx_receive_time   &lt;br /&gt;key_len: 6   &lt;br /&gt;ref: NULL   &lt;br /&gt;rows: 7540948   &lt;br /&gt;filtered: 100.00   &lt;br /&gt;Extra: Using where; Using index   &lt;br /&gt;explain select count(*) from arrival_record where product_id=26 and receive_time between &amp;apos;2019-03-25 14:00:00&amp;apos; and &amp;apos;2019-03-25 15:00:00&amp;apos; and receive_spend_ms&amp;gt;=0\G;   &lt;br /&gt;*************************** 1. row ***************************   &lt;br /&gt;id: 1   &lt;br /&gt;select_type: SIMPLE   &lt;br /&gt;table: arrival_record   &lt;br /&gt;partitions: NULL   &lt;br /&gt;type: range   &lt;br /&gt;possible_keys: idx_product_id_sequence_station_no,idx_receive_time   &lt;br /&gt;key: idx_receive_time   &lt;br /&gt;key_len: 6   &lt;br /&gt;ref: NULL   &lt;br /&gt;rows: 291448   &lt;br /&gt;filtered: 16.66   &lt;br /&gt;Extra: Using index condition; Using where   &lt;br /&gt;都使用到了idx_receive_time 索引，扫描的行数大大降低   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;  &lt;a href="https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&amp;mid=2247487551&amp;idx=1&amp;sn=18f64ba49f3f0f9d8be9d1fdef8857d9&amp;scene=21#wechat_redirect"&gt;索引优化后&lt;/a&gt;&lt;/h2&gt; &lt;p&gt;delete 还是花费了77s时间&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;delete from arrival_record where receive_time &amp;lt; STR_TO_DATE(&amp;apos;2019-03-10&amp;apos;, &amp;apos;%Y-%m-%d&amp;apos;)\G   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;img&gt;&lt;/img&gt; &lt;blockquote&gt;  &lt;p&gt;delete 语句通过receive_time的索引删除300多万的记录花费77s时间*&lt;/p&gt;&lt;/blockquote&gt; &lt;h2&gt;  &lt;a href="https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&amp;mid=2247487551&amp;idx=1&amp;sn=18f64ba49f3f0f9d8be9d1fdef8857d9&amp;scene=21#wechat_redirect"&gt;delete大表优化为小批量删除&lt;/a&gt;&lt;/h2&gt; &lt;p&gt;*  &lt;strong&gt;应用端已优化成每次删除10分钟的数据（每次执行时间1s左右），xxx中没在出现SLA（主从延迟告警）&lt;/strong&gt;*  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;*  &lt;strong&gt;另一个方法是通过主键的顺序每次删除20000条记录&lt;/strong&gt;*&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;#得到满足时间条件的最大主键ID   &lt;br /&gt;#通过按照主键的顺序去 顺序扫描小批量删除数据   &lt;br /&gt;#先执行一次以下语句   &lt;br /&gt; SELECT MAX(id) INTO @need_delete_max_id FROM `arrival_record` WHERE receive_time&amp;lt;&amp;apos;2019-03-01&amp;apos; ;   &lt;br /&gt; DELETE FROM arrival_record WHERE id&amp;lt;@need_delete_max_id LIMIT 20000;   &lt;br /&gt; select ROW_COUNT();  #返回20000   &lt;br /&gt;   &lt;br /&gt;   &lt;br /&gt;#执行小批量delete后会返回row_count(), 删除的行数   &lt;br /&gt;#程序判断返回的row_count()是否为0，不为0执行以下循环，为0退出循环，删除操作完成   &lt;br /&gt; DELETE FROM arrival_record WHERE id&amp;lt;@need_delete_max_id LIMIT 20000;   &lt;br /&gt; select ROW_COUNT();   &lt;br /&gt;#程序睡眠0.5s   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;  &lt;a href="https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&amp;mid=2247487551&amp;idx=1&amp;sn=18f64ba49f3f0f9d8be9d1fdef8857d9&amp;scene=21#wechat_redirect"&gt;总结&lt;/a&gt;&lt;/h2&gt; &lt;ul&gt;  &lt;li&gt;表数据量太大时，除了关注访问该表的响应时间外，还要关注对该表的维护成本（如做DDL表更时间太长，delete历史数据）。&lt;/li&gt;  &lt;li&gt;对大表进行DDL操作时，要考虑表的实际情况（如对该表的并发表，是否有外键）来选择合适的DDL变更方式。&lt;/li&gt;  &lt;li&gt;对大数据量表进行delete，用小批量删除的方式，减少对主实例的压力和主从延迟。&lt;/li&gt;&lt;/ul&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>dev</category>
      <guid isPermaLink="true">https://itindex.net/detail/62850-%E9%9D%A2%E8%AF%95-mysql-%E5%A4%A7%E8%A1%A8</guid>
      <pubDate>Mon, 18 Sep 2023 00:00:00 CST</pubDate>
    </item>
    <item>
      <title>最新进展：Serverless 冷启动优化技术</title>
      <link>https://itindex.net/detail/62812-serverless-%E5%86%B7%E5%90%AF%E5%8A%A8-%E4%BC%98%E5%8C%96</link>
      <description>作者 | 华为云 Serverless 团队策划 | 褚杏娟问题背景 &lt;p&gt;Serverless 计算也称服务器无感知计算或函数计算，是近年来一种新兴的云计算编程模式。其致力于大幅简化云业务开发流程，使得应用开发者从繁杂的服务器运维工作中解放出来（例如自动伸缩、日志和监控等）。借助 Serverless 计算，开发者仅需上传业务代码并进行简单的资源配置便可实现服务的快速构建部署，云服务商则按照函数服务调用量和实际资源使用收费，从而帮助用户实现业务的快速交付 (fast built &amp;amp; Relia. Deliv.) 和低成本运行。&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;在当前阶段，业界公认的 Serverless 计算的准确定义应该为“FaaS+BaaS”，即 Function-as-a-Service 同 Backend-as-a-service 的组合。Serverless 计算改变了用户使用云的方式，为了实现细粒度的资源供给和高度弹性的扩缩容能力，Serverless 计算提供了无状态函数的编程抽象，即将用户的应用程序构建为多个无状态的函数集合，并通过中间存储、BaaS 等业务组件交互，继而构建完整的云应用。&lt;/p&gt; &lt;p&gt;然而，Serverless 计算的无状态函数编程在带来高度弹性和灵活性的同时，也导致了不可避免的冷启动问题。由于函数通常在执行完请求后被释放，当请求到达时，如果没有可用实例则需要从零开始启动新的实例处理请求（即冷启动）。当冷启动调用发生时，Serverless 平台需要执行实例调度、镜像分发、实例创建、资源配置、运行环境初始化以及代码加载等一系列操作，这一过程引发的时延通常可达请求实际执行时间的数倍。相对于冷启动调用，热调用（即请求到达时有可用实例）的准备时间可以控制在亚毫秒级。&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;在特定领域例如 AI 推理场景，冷启动调用导致的高时延问题则更为突出，例如，使用 TensorFlow 框架的启动以及读取和加载模型可能需要消耗数秒或数十秒。&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;因此，如何缓解 Serverless 函数的冷启动问题，改善函数性能是当前 Serverless 领域面临的主要挑战之一。&lt;/p&gt; &lt;p&gt;从研究思路上看，目前工业界和学术界主要从两个方面入手解决冷启动问题：&lt;/p&gt; &lt;ol&gt;  &lt;li&gt;   &lt;p&gt;加快实例启动速度：当冷启动调用发生时，通过加速实例的初始化过程来减少启动时延。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;降低冷启动发生率：通过函数预热、复用或实例共享等方法提高实例的利用效率，减少冷启动调用的发生。&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;业界最新进展 &lt;p&gt;在本篇文章中，我们首先从提高实例启动速度方面介绍下业界的前沿进展。&lt;/p&gt;提高实例启动速度 &lt;p&gt;当冷启动发生时，Serverless 平台内部实例的初始化过程可以划分为准备和加载两个阶段。其中，准备阶段主要包括控制面决策调度 / 镜像获取、Runtime 运行时初始化、应用数据 / 代码传输几个部分。而加载阶段位于实例内部，包括用户应用框架和代码的初始化过程。在工业界和学术界公开的研究成果中，针对实例启动过程中的每个阶段都有大量的技术手段和优化方法。如下图所示，经过优化，实例冷启动的准备阶段和加载阶段时间可被极大地缩短。&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;下面列举了一些近年来发表在计算机系统领域知名会议的相关工作，主要可以分为五个方面：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;调度优化 / 镜像快速分发 / 本地池化：例如利用文件级镜像缓存复用机制和并行化镜像构建技术，加快容器构建速度 FAST-BUILD [MSST&amp;apos;19]；阿里云基于树结构的跨节点快速镜像分发 FaasNet [ATC&amp;apos;21]；AWS 基于 Block 和分布式多级缓存的镜像分发策略 [Arxiv&amp;apos;23]；Pod 池 + 特化实例跳过镜像传输 [华为云 FunctionGraph]。其中，快速镜像分发依赖于 VM 节点的上 / 下行网络带宽，Pod 池特化技术则是典型的以空间换时间的做法。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;轻量级虚拟化 / 安全容器：例如针对传统容器 Docker 的精简优化工作 SOCK [ATC&amp;apos;21]；更侧重安全性的轻量级虚拟化技术（Kata Containers, gVisor 等)；基于安全容器的进一步的精简优化工作 (Catalyzer [ASPLOS&amp;apos;20], REAP[ASPLOS&amp;apos;21])。通过裁剪优化，安全容器的启动时延最快可以被压缩至亚毫秒级。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;数据共享 / 跨节点传输优化：例如基于 RDMA 共享内存减少跨节点启动过程的数据拷贝 RemoteFork [OSDI&amp;apos;23]；或者利用本地代码缓存跳过代码传输 [华为 FunctionGraph, 字节 ByteFaaS 等]。基于 RDMA 技术的跨节点数据传输时延可降低至微妙级。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;用户代码精简 / 快速加载：例如针对 Java 语言的 JVM（Java Virtual Machine）运行时优化技术 [FunctionGraph]；以及针对 Python 运行时库的裁剪优化工作 FaasLight [arxiv&amp;apos;23]。通过特定的优化，JVM 启动时间可由数秒降低至数十毫秒，而 Python 代码的启动加载时延可降低约 1/3。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;其它非容器运行时技术：例如 WASM（即 WebAssembly）技术以及针对 WASM 的内存隔离方面的优化工作 Faasm [ATC&amp;apos;20]。相比容器化技术，直接以进程和线程方式组织运行函数，可在保证低开销函数运行的同时具备高度灵活性。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;接下来，针对上述 5 个方面的优化工作进行详细介绍。&lt;/p&gt;镜像压缩 / 快速分发 / 本地池化 &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;快速请求 / 实例调度。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;在 Serverless 场景下，由于函数的体积更小、动态性更高，Serverless 平台在负载高峰时也会面临较大压力。因此主流的 Serverless 服务商通常采用“大小流区分的负载均衡 + 本地优先调度”的设计来实现请求的快速分发，降低调度时延。即在流量较小的时候，采用工作负载聚合的方式来分发请求，减少集群资源使用；在流量较大的时候，通过一致性 hash 或轮询等方法将请求快速映射到后端节点，从而降低排队时延。这种“自顶向下”的调度设计简单，易于实现，但是在超大规模的函数并发场景下仍可能面临顶层控制器的性能瓶颈问题，因此，根据实际场景架构适当地探索“自下而上”的调度框架设计将有助于缓解该问题。&lt;/p&gt; &lt;p&gt;此外，也有相关研究人员提出可将 Serverless 集群划分为多个小型的 worker 池，每个 worker 池由一个单独的 semi-global 子调度器管理，降低各个调度器的压力，从而改善调度时延[1] 。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;镜像压缩与快速分发。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;在 Serverless 平台中，用户的函数镜像通常被存储在 cluster-level 的镜像仓库，节点（通常指虚拟机 Virtual Machine, 简称 VM）创建实例时首先需要将镜像文件通过网络传输到对应的节点（1 个 100MB 的镜像文件从传输到容器创建完成需要耗费约 20 秒 [阿里云，Borg]）。在这个过程中，影响镜像传输时延的因素主要为镜像体积和节点网络带宽。因此，Serverless 平台通常会采用镜像压缩技术缩小镜像体积，例如开源工具 fastfreeze[2] 。除此之外，为了解决节点网络带宽受限导致的拉取镜像时间过长的问题，阿里云的 FaasNet[29] 工作提出了一种基于树状网络的跨节点镜像分发策略，即通过根节点的 VM 将镜像下发给子节点（通常为 2），子节点获取到镜像后以同样方式扩展到更多的节点，从而充分利用节点的上 / 下行带宽实现短时间内快速扩容大量实例的目的。&lt;/p&gt; &lt;p&gt;此外，在节点中设立镜像缓存也是一种有效的方法，可以在一定程度上减少镜像传输操作，例如 AWS Lambda 采用了一种基于 Block 的镜像切分和缓存方法[30]，相比于基于 Layer 的镜像构建方法，基于 Block 的镜像切分粒度更细，可以极大地提高镜像复用度，同时这些镜像 Block 被存储在由本地缓存，分布式缓存和 S3 组成的三级缓存系统中，从而实现快速镜像分发和实例创建速度。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;Pod 池与实例特化。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;类似于本地镜像缓存的概念，在集群中预留包含函数运行时基础镜像的实例（即具备用户代码运行环境的热容器）也是一种常用的“以时间换空间”的做法[3]。这种方法在集群中建立 Pod 池，预先启动一定数量的具有不同资源规格和语言运行时的实例（例如 Java, Nodejs, Python 等）。当冷启动调用发生时，Serverless 平台可以快速从 Pod 池中取出所需要的实例进行特化（挂载代码文件并加载执行），从而跳过镜像获取和运行时环境启动阶段，实现快速创建实例的目的。&lt;/p&gt; &lt;p&gt;生产环境下的数据测算显示，Node.js 实例特化时的启动时间仅需 15ms。然而，在 Pod 池中预留实例会占用大量集群资源，增加云供应商的服务成本。如何权衡用户函数性能和 Pod 池预留成本也是该类方法所面临的一个问题。&lt;/p&gt;轻量级虚拟化 / 安全容器加速 &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;传统容器加速。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;相比于传统的 VM 动辄数十秒至数分钟的创建时间，容器的启动速度和运行时开销更小，因此 Serverless 平台通常使用容器技术来隔离用户的函数实例，例如 Docker[4] 。然而在考虑函数的实际运行性能时，Docker 之类的传统容器设计包含了很多不必要的运行开销，因此并不适合直接作为函数沙盒环境。为此，SOCK 工作[5] 基于 Linux container 的设计分析了 Docker 文件系统和网络模块的性能瓶颈，通过裁剪不必要的启动项并对网络模块进行特定优化，大幅降低了 Docker 容器的内核扩展开销（超过 18 倍）。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;兼顾安全性的轻量化容器或 microVM。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;在非 Serverless 的云场景中，VM 的强安全隔离性使其在云租户的服务部署中扮演着重要角色。在 Serverless 场景中，租户函数间的安全隔离性也同样重要。为了弥补传统容器的安全性不足的问题，研究学者尝试将 VM 的高安全性同容器技术进行融合，例如 Intel 和 HyperHQ 团队主导的 Kata Containers 项目[6] ，Kata Containers 的本质是一个精简后的轻量级虚拟机，在其上面运行着用户的容器进程。为了减少暴露给容器进程的攻击面，Kata Containers 基于传统的虚拟化技术通过硬件虚拟出一个 microVM，而 microVM 里则运行了一个裁剪后的 Linux 内核来实现进程间的强隔离（  &lt;em&gt;注：裁剪即屏蔽掉非必须的系统调用接口&lt;/em&gt;）。AWS Lambda 的 Firecracker[7] 也采用了类似 microVM 的技术路线来实现多租户隔离性和函数性能间的权衡。&lt;/p&gt; &lt;p&gt;除此之外，gVisor[8] 则是谷歌推出的轻量级安全容器项目。gVisor 采用了另一条技术路线，其核心原理是给容器进程配置一个类似“Guest Kernel”的极小的“独立内核”（即守护进程），通过拦截容器进程的系统调用来向宿主机 OS 发起有限的、可控的系统调用，从而提高系统安全性。由于这个独立内核运行在用户态，免去了大量地址空间转换和内核态切换的操作，因此具有较小的性能开销和更好的灵活性，同时又达到了将容器和宿主机 OS（Operation System）隔离的目的。&lt;/p&gt; &lt;p&gt;无论是 Kata Containers 类容器技术，还是 gVisor 安全容器技术，它们实现安全容器的方法在本质上是殊途同归的。这两种技术都采用了“系统分层”的设计思想，即向容器进程分配了一个独立的“操作系统内核”，从而避免了让租户的容器直接共享宿主机的内核。这样，容器进程的攻击面就从整个宿主机内核变成了一个极小的、独立的、以容器为单位的内核，从而有效解决了容器进程发生“逃逸”或者夺取整个宿主机控制权限的问题。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;针对安全容器的进一步加速。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;尽管 gVisor 之类的安全容器极大地弥补了传统容器的安全隔离性不足的问题，然而，安全性提高的同时也带来了额外的性能损失，例如拦截调用会增大启动和运行开销。为此，Catalyzer[9] 基于快照技术和状态重用技术针对 gVisor 安全容器的启动过程进行了进一步优化，大幅缩短了 gVisor 的启动时间。Catalyzer 的设计思想是“避免从零开始启动”，即通过检查点和快照恢复机制跳过启动过程关键路径上的初始化（零初始化），从而实现快速启动的目的。此外，Catalyzer 还提出了一个新的操作系统原语 sfork 技术，通过直接重用正在运行的沙箱实例的状态来进一步减少启动延迟。评估表明，Catalyzer 在最佳情况下可将 gVisor 的启动延迟降低至&amp;lt;1ms。&lt;/p&gt; &lt;p&gt;值得注意的是，尽管 Catalyzer 实现了“用户角度”的快速启动，但是其并不能保证内部所有的系统状态都为最新可用状态，为此，Catalyzer 采用了按需加载机制作为补救措施，即通过在后续运行过程中逐步恢复用户级内存状态和系统状态来最终恢复函数性能。该领域的另一项研究工作 REAP[10] 发现 Catalyzer 的设计机制带来的开销并不是可忽略的，运行过程中频繁的系统缺页在最坏的情况下会导致时延提高接近一倍。为此，REAP 提出了一种页面预加载机制，通过识别函数运行过程中所需的必要工作集并提前载入内存，从而显著改善了 Catalyzer 的缺页开销问题。&lt;/p&gt; &lt;p&gt;  &lt;em&gt;注：该部分涉及的一些术语可以参考   &lt;strong&gt;扩展知识部分&lt;/strong&gt;进行了解。&lt;/em&gt;&lt;/p&gt;数据共享 / 传输优化 &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;基于 RDMA 的跨节点分发。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;在 Serverless 平台内部，借助网络进行跨节点数据、代码或者状态的传输也是一个较为耗时的操作，尤其是在大量实例并发创建的场景中，远程容器初始化的频繁需求进一步加剧了这种现象。为了解决该问题，MITOSIS[11] 工作在支持 RDMA 的操作系统中实现了一个新的操作系统原语，即 remote fork，remote fork 允许不同节点的操作系统之间通过 RDMA 技术进行内存共享映射，从而实现快速远程内存读取和跨节点容器间的数据传输。MITOSIS 可以大幅降低跨节点的实例初始化时间，从单个实例跨节点复制 10,000 个新实例的时间小于 1 秒。然而，MITOSIS 的实现涉及对操作系统内核代码的大量修改，同时还依赖于 RDMA 硬件的支持，这些都使得它在实际部署中会面临着挑战。&lt;/p&gt; &lt;p&gt;除此之外，新型网络设备例如 DPU 硬件等也提供了基于 RDMA 的高速跨节点互联的机制，即通过内存映射和共享来加快网络数据传输速度，这些硬件都可以被探索用于改善 Serverless 平台跨节点数据传输的效率。&lt;/p&gt;代码加载延迟优化 &lt;p&gt;前面章节提到的加速技术都集中在系统层和框架层，在应用层，容器运行时环境的启动和函数代码的加载也在冷启动延迟中占据了相当的比例，尤其是对于机器学习等应用，运行库的加载和框架的启动时间可达数秒[12]。值得注意的是，即便调度时延和容器初始化时间再短，长的用户侧启动时延仍然会使得冷启动问题变得棘手。&lt;/p&gt; &lt;p&gt;为此，Serverless 平台通常会针对函数的运行时进行优化，例如华为的 FunctionGraph 内部针对 JVM 的优化技术可将简单 Java 应用的初始化时间降低至数十毫秒。在学术研究领域，也有针对 Python 函数运行时库的裁剪优化工作例如 FaasLight[13]，FaasLight 的核心思想是“在函数启动过程中尽可能地只加载少量运行必需的代码”。为了实现这一目标，FaaSLight 构建了函数级调用图来识别和筛选与应用程序功能相关的代码，并对非强相关的代码（即可选代码）进行按需加载，避免必要代码识别不准确导致应用失败。实验结果显示 FaaSLight 可最高降低 78% 的 Python 代码加载延迟，平均延迟降低约 28%。&lt;/p&gt;非容器化运行时技术 &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;WASM 运行时。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;WSAM（即 WebAssembly）[14]技术也是近年来一种新兴的编程运行时，可以用在云场景中代替部分容器运行时来部署用户函数。WSAM 的运行原理类似于 JVM，可直接运行在宿主机 OS 上，其支持多种语言编码，用户函数代码在运行前需要编译成 WASM 运行时，继而实现跨平台运行。WASM 同样提供了高性能沙盒环境，并具备高灵活性和一定的安全性。&lt;/p&gt; &lt;p&gt;尽管 WASM 有诸多优势，但是也存在着诸如内存隔离性弱这样的问题，因此并不能完全替代现有的容器技术，而是在大多数情况下与 Docker 容器技术互相配合使用。据悉，基于 WASM 的函数计算平台目前在国内著名的云计算厂商内部都有布局，有的平台已经推出了初步的商用版本。&lt;/p&gt; &lt;p&gt;在研究领域，也有针对 WASM 的内存隔离性开展优化的工作，例如 Faasm[15]。Faasm 提出了一个新的隔离抽象“faaslets”，并基于 WebAssembly 的 software-default isolation (SFI) 机制实现了函数间的内存隔离。此外，在 faaslets 的设计中，来自不同租户的多个函数可共享相同的内存地址空间，这样就可以避免函数间通信时大量的数据交换所导致的性能开销。faaslets 还可以与 Linux cgroups 机制配合使用，继而实现例如 CPU, 网络资源等资源的隔离。这些优化措施可以使得 Faasm 获得接近 2 倍的性能加速同时降低内存使用 10 倍以上。&lt;/p&gt; &lt;p&gt;值得注意的是，上述的这些技术手段，无论是容器技术、VM 技术还是 WASM 技术，它们本质上都是以进程方式运行在服务器内部，因此都可以借助快照技术（例如 Linux CRIU[16]）实现更快的系统状态保存和恢复，从而进一步加快初始化速度。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;Unikernal 技术。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;从前面列举的一些技术的特征可以看到，函数运行环境的启动速度和安全性之间总是存在一个折中，即为了改善安全性必须添加一些中间层来增强系统的控制能力，然而中间层的引入势必会降低系统启动速度。那么是否存在能够同时满足快速启动和高安全性的技术方案呢？答案就是 Unikernel[17]。Unikernel 顾名思义，是指定制化的操作系统内核。它的核心思想是将“用户程序和运行必需的库文件统一编译成为一个特殊的、单地址空间的内核镜像”。由于只有一个操作系统进程（这里不再有用户态和内核态的区分），同时编译时去掉了不必要的运行库，所以 Unikernel 运行速度可以非常快。如果将 Unikernel 加载到 VM 运行，也可以实现高安全性和隔离性。&lt;/p&gt; &lt;p&gt;但是 Unikernel 的缺点也十分明显，即灵活性差。由于 Unikernel 是与某种语言紧密相关的，所以一个 Unikernel 只能用一种语言编写，如果用户程序有所变动，则需要重新编译 Unikernel，之后加载并重启 VM。同时，Unikernel 的编码门槛也很高，这与 Serverless 计算的简化编程的设计初衷相悖，也是其难以被广泛应用的原因之一。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;扩展知识介绍&lt;/strong&gt;：&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;传统裸金属、VM 和容器技术的区别&lt;/strong&gt;（在体系结构中所处的层级图、用户态和内核态概念）&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;在计算机的体系结构中，各种物理设备例如 CPU 处理器、磁盘、网卡等首先组成计算机的物理硬件，也成为“裸金属服务器”。裸金属服务器的运行需要操作系统的支持，比如我们熟知的 Linux 操作系统。借助操作系统就可以编写运行各种软件例如网页服务器、数据库等来提供外部服务。&lt;/p&gt; &lt;p&gt;在云计算场景中，通过虚拟机共享资源是早期云服务商的主要做法。借助虚拟化技术例如 Microsoft hyper-V、Citrix Xen 等，可以将裸金属服务器虚拟为多个单独的 VM，VM 间共享裸金属服务器（也称宿主机）的物理资源，但是在每个 VM 内对租户来说是独占资源的（例如 CPU，内存和磁盘）。实现这一目的的手段就是借助 Hypervisor（即虚拟机监视器）来管理划分各个 VM 使用的物理资源，实现高安全性和隔离性。Hypervisor 的内部比较复杂，涉及到系统调用、物理地址划分等操作，这里不再展开。&lt;/p&gt; &lt;p&gt;在 Hypervisor 划分出来的每个 VM 内部都可以运行一个独立的操作系统（也称 Guest OS），相比于 VM，宿主机的操作系统则称为 Host OS。Guest OS 对于租户来讲是一个类似本机的操作系统，包含运行程序所需要的各种系统调用库和可执行文件，具体的层级划分如左侧子图所示。&lt;/p&gt; &lt;p&gt;容器则是相比 VM 更为轻量级的虚拟化技术，例如 Linux Container（LXC）和基于 LXC 实现的 Docker 等技术（右侧子图所示）。容器的本质是一个用户态进程，因此它可以直接运行在 Host OS 上，我们称之为 Container Engine。Container Engine 可以创建出多个容器给租户使用，并通过 Linux cgroups 等机制实现 namespace 和租户资源（例如 CPU 和内存）的隔离。不同于 VM 的运作方式，容器内部并没有 Guest OS，而是复用底层的宿主机的操作系统，通过创建文件系统和加载用户程序运行所需要的库来实现运作。因此，相比 VM 来讲，容器的运行方式更加轻便灵活，但是安全性较差（例如某个租户的程序可能导致宿主机 OS 被攻击，继而使得其他租户的程序也无法运行）。为了解决这个问题，早期的云厂商做法通常是将容器部署在 VM 中，从而实现安全性和灵活性的折中取舍。&lt;/p&gt; &lt;p&gt;  &lt;em&gt;注：操作系统通常运行在内核态，权限最高，而租户程序则运行在用户态。用户态程序执行系统调用需要切换至内核态，执行完后再切换回用户态。&lt;/em&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;VM、容器和 Unikernel 对比 （灵活性，启动速度和隔离性）&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;前面我们提到，Serverless 平台中函数运行的技术路线有 VM，容器和 Unikernal 几种或是它们的组合。无论哪种方式，基本都在围绕技术方案的灵活性（Flexibility），启动速度（Startup Latency）和隔离性（Isolation Level）进行权衡。&lt;/p&gt; &lt;p&gt;下图总结并列出了这几种虚拟化方案的对比：&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;可以看到，VM 具有高隔离性，灵活性较好，但是启动速度最慢。&lt;/p&gt; &lt;p&gt;而传统容器以用户态进程运行，具有高度灵活性，较快的启动速度，但是隔离性较差。&lt;/p&gt; &lt;p&gt;安全容器是针对传统容器进行了裁剪和进一步控制，具有较高的隔离性，但是启动速度和灵活性有所降低。&lt;/p&gt; &lt;p&gt;WASM 技术类似 JVM 的设计理念，编译完成后即可运行，因此具有更快的启动速度和较高的灵活性，但是隔离性较差。&lt;/p&gt; &lt;p&gt;Unikernel 采用了定制化 OS 和单地址空间，因此可以兼顾快的启动速度和高隔离性，但是灵活性很差。&lt;/p&gt; &lt;p&gt;不难发现，这三个要素的关系非常类似于分布式系统的 CAP 原理，即一致性 (Consistency)，可用性 (Availability) 和分区容忍性 (Partitiontolerance) 在分布式系统中最多只能同时实现两点，不可能三者兼顾。&lt;/p&gt; &lt;p&gt;下表展示了各虚拟化技术的启动延迟等指标的具体对比：&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;注：表格数据引用出处 The serverless computing survey[31]。&lt;/p&gt;小结 &lt;p&gt;在实际场景中，不同的函数计算厂商往往有自己的虚拟化技术方案，往往根据自家产品的业务线，技术积累或者客户群体需求进行综合考虑。在出于商业意图或者个人需要搭建函数计算平台进行研究时，可以根据安全性需求制定自己的技术路线。例如个人或者高校团体研究可以归类为非安全性场景，基于目前的&amp;quot;VM+Kuberneters (k8s)+Docker+runtime 优化&amp;quot;技术栈就可以实现一个可用版本，其中 VM 可以提供资源强隔离性，基于多个 VM 节点组建 k8s 集群，函数计算框架例如 OpenWhisk 或 OpenFaaS 可以依托 k8s 进行部署。尽管同一个 VM 内的多租户共享会导致面临一定的安全隐患，但对于实验环境来说是足够的。&lt;/p&gt; &lt;p&gt;在注重租户间隔离性和强调安全的场景下，AWS Lambda 采用的“裸金属 +Hypervisor+microVM+ 容器”技术路线通过 microVM 实现强隔离性，并且 VM 内一般只允许部署同一个租户的函数来提高安全性。而谷歌采用的“裸金属 +Hypervisor+VM+gVisor”则是另一种技术方案，Hypervisor 和 VM 负责提供硬件虚拟化及资源强隔离性，安全性则由 gVisor 提供。其中，Firecracker 和 gVisor 均已开源。&lt;/p&gt; &lt;p&gt;讨论：VM 在每种技术方案都属于必需品的地位，但是这样势必会导致高开销的存在。那么能不能采用“裸金属 + 轻量级 Hypervisor+Host OS+ 安全容器”或者直接“裸金属 +Host OS+ 安全容器”的技术方案减少中间层的存在呢？其实这种方式并非不可以，比如剑桥大学就提出了 CHERI[18] 指令集扩展从而实现在硬件层面进行内存隔离，通过卸载掉虚拟化软件的部分职责，使得 Hypervisor“变薄”。尽管这项工作只基于模拟器进行了实现，但是也验证了未来更轻量化的 Serverless 计算底座的可行性。&lt;/p&gt;降低冷启动发生率 &lt;p&gt;在文章的上半部分，我们主要介绍了如何加速函数实例初始化过程的一些技术方案，包括通过优化调度和镜像分发策略，采用更轻量级的虚拟化技术，或者借助 RDMA 硬件改善跨节点数据传输等方法，尽管这些方法已经可以将实例运行时环境的初始化的时间压缩至数十毫秒甚至是数毫秒，然而用户侧的延迟却仍然存在，例如程序状态的恢复，变量或者配置文件的重新初始化，相关库和框架的启动。具体来讲，在机器学习应用中，TensorFlow 框架的启动过程往往需要花费数秒，即使实例运行时环境的启动时间再短，应用整体的冷启动时延对用户而言依然是无法接受的（注：通常大于 200ms 的时延可被用户察觉）。&lt;/p&gt; &lt;p&gt;在这种情况下，可以从另一个角度入手解决冷启动问题，即降低冷启动调用的发生率。例如，通过缓存完整的函数实例，请求到达时可以快速恢复并处理请求，从而实现近乎零的初始化时延（例如 Docker unpause 操作时延小于 0.5ms）。&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;降低冷启动发生率的相关研究可以分为如下几个方面：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;实例保活 / 实例预留：例如基于 Time-to-Live 的 keepalive 保活机制 [AWS Lambda, OpenWhisk]；或者通过并发配置接口预留一定数量的实例 [AWS Labmda 等]；这些方法原理简单，易于实现，但是在面对负载变化时缓存效率较低。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;基于负载特征学习的动态缓存：例如基于请求到达间隔预测的动态缓存方案 Serverless in the Wild [ASPLOS&amp;apos;20]；学习长短期负载变化特征的动态缓存方案 INFless [ASPLOS&amp;apos;22]；基于优先级的可替换缓存策略 FaasCache [ATC&amp;apos;21]；面向异构服务器集群的低成本缓存方案 IceBreaker [ASPLOS&amp;apos;22]。这些动态缓存方案根据负载特征学习决定实例缓存数量或时长，从而在降低冷启动调用率的同时改善缓存资源消耗。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;优化请求分发提高命中率：例如兼顾节点负载和本地化执行的请求调度算法 CH-RLU [HPDC&amp;apos;22]。通过权衡节点负载压力和缓存实例的命中率来对请求的分发规则进行优化设计，避免节点负载过高导致性能下降，同时兼顾冷启动率。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;改善并发 / 实例共享或复用：例如允许同一函数工作流的多个函数共享 Sandbox 环境 SAND [ATC&amp;apos;18]；使用进程或线程编排多个函数到单个实例中运行 Faastlane [ATC&amp;apos;21]；提高实例并发处理能力减少实例创建 Fifer [Middle&amp;apos;20]; 允许租户复用其它函数的空闲实例减少冷启动时间 Pagurus [ATC&amp;apos;22]。这些实例共享或者复用技术可以同缓存方案结合使用，降低冷启动带来的性能影响。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;接下来，针对上述 4 个方面的研究工作进行详细介绍。&lt;/p&gt;实例保活 / 实例预留 &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;实例并发设置和预留。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;商用的 Serverless 平台例如 AWS Lambda[19]、华为云 FunctionGraph[20]都内置了 Time-to-live 实例保活机制即 keepalive 方法。其原理是将执行完请求的实例内存中保活一段时间再释放。如果保活期间有新的请求到达，则可以立即执行并重置剩余的保活时间。除此之外，云服务器通常会向用户提供实例并发设置的接口，用户在部署函数后可以设置预留一定数量的实例（需额外支付费用），从而降低由于突发负载导致的冷启动调用率。然而，这类静态的缓存方法易于实现，但是实例预留数量或保活时长难以根据负载变化进行实时调整，从而导致性能下降或者资源超分浪费。&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt;基于负载特征学习的动态缓存 &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;基于直方图统计的动态实例缓存。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;微软的研究团队通过分析 Azure 的生产环境 trace，发现请求的到达间隔普遍分布在秒级、数分钟乃至数小时，基于 Time-to-live 的静态保活策略会导致大量的资源浪费。因此，作者提出了基于混合直方图的策略来统计函数请求的调用间隔，并根据这些信息动态的释放或提前拉取函数实例，从而在减少冷启动调用率的同时减少 Serverless 平台内部的缓存资源浪费[21] 。然而，这套设计方案只针对 0-1 的函数扩缩场景进行了评估，因为它只控制实例的缓存时长，而无法感知实例数量的变化，如何将这套机制运用在 1- 多的实例扩缩场景中还面临着一些挑战。&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;基于长短期直方图的动态实例缓存。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;直方图策略通过生成函数请求到达间隔的分布来决定缓存实例的何时创建和缓存时长，然而，在初期历史统计数据不足或者负载发生短期突变的场景下，得到的直方图分布往往会不具有代表性，从而导致缓存策略的实际效果下降。为了解决该问题，INFless[22] 提出了一个长短期结合的混合直方图策略来改善预测精度，通过分别统计长期的直方图分布特征和短期的直方图分布特征，来共同决定缓存实例的创建时间点和保活时长，从而改善缓存效率（冷启动率 / 缓存资源成本）。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;基于优先级的可替换缓存策略。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;无论是 Time-to-live 的 keepalive 还是基于直方图的动态缓存方案，都是倾向于减少缓存资源的消耗，从而降低函数服务提供商的运营成本。然而，FaasCache  &lt;sup&gt;[23] &lt;/sup&gt;的作者认为函数计算集群的内存资源与其与闲置，不如用来缓存更多的函数实例，从而最大程度地降低冷启动调用发生率。为此，FaasCache 借鉴了 CPU 缓存的替换算法，通过函数的最近访问时间，函数内存消耗以及冷启动时长为每个实例定义优先级。请求执行结束后的函数实例都会被长期缓存在节点中，仅当节点无可用资源时才驱逐较低优先级的实例来缓存新的函数。此外，FaasCache 还设计了一个反馈控制机制来调整缓存容量的大小，通过权衡冷启动率和缓存资源成本来实现较好的缓存效率。&lt;/p&gt; &lt;p&gt;面向异构集群的低成本缓存策略。&lt;/p&gt; &lt;p&gt;云服务供应商通常会提供多种具有不同 CPU 型号的服务器给租户使用，这些 VM 的处理能力和价格有所差异。IceBreaker[24] 针对异构函数计算集群设计了一个成本优先的缓存策略，其核心思想是“可以使用价格更为便宜的低端服务器缓存冷启动开销较低的函数，从而节省总体成本”。为了实现该目的，IceBreaker 基于傅里叶变化预测函数的负载到达率，并结合实例的使用率和节点加速前亲和性来决定将函数缓存在高端服务器，低端服务器或者不进行缓存，从而以损失部分函数性能的代价降低整体缓存消耗的资源成本。&lt;/p&gt;优化请求分发提高缓存命中率 &lt;p&gt;此外，还有一些工作通过优化或重新设计 Serverless 平台的请求分发策略来改善函数实例的命中率，从而改善缓存效率。在 Serverless 平台内部，前端组件收到请求后，通常以 hash 或负载均衡的策略分发到后端节点进行处理。将同一函数的请求分发到相对固定的节点可以有效地提高节点内实例的使用率（即缓存命中率），这也称为缓存的本地性原理。但是，由于函数负载中通常存在大量的热点函数，当热点函数聚集在后端节点时，则可能导致节点资源使负载压力升高，从而引发性能下降。&lt;/p&gt; &lt;p&gt;因此，CH-RLU[25] 工作提出了一个负载感知的请求调度策略，作者基于一致性 Hash 的分发规则在调度过程中考虑后端节点的压力，通过将高负载的函数分发到不同的节点来避免局部热点， 从而缓解资源竞争导致的函数性能下降问题。可以看到，优化负载分发的研究工作可以同现有的缓存方案结合使用，通过协同设计可以进一步改善 Serverless 平台的缓存效率。&lt;/p&gt;改善并发 / 实例共享或复用 &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;多函数共享 Sandbox。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;SAND[26] 工作在函数工作流场景下提出了一种多函数共享实例 Sandbox 的方案，它允许同一个函数工作流内的不同函数共享同样的 Sandbox 环境，从而减少请求执行时创建的函数实例数量，这将有助于减少请求调用的冷启动发生率。然而，这在实际场景中也有很多局限性，例如租户的多个函数可能由不同的语言运行时编写，这些函数往往无法从实例共享中获益。此外，多个函数共享 Sandbox 会增加实例的体积，而工作流中的函数可能被多条路径所调用，在函数扩容时则会导致不必要的资源浪费。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;进 / 线程混合的函数编排。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;传统的函数实例以容器为单位，每个函数运行在一个单独的容器实例内部，容器间的数据传输需要借助网络或第三方存储实现，以 AWS S3 为例，通过 S3 进行函数间数据交换会导致较高时延。为了改善函数实例间的通信效率，Faastlane[27] 工作提出了一个新的函数编排组织方式，即将工作流中的不同函数以进程或线程的形式在容器实例中执行，同时采用 IPC 进程间通信来提高函数交互效率。对于包含敏感数据操作的函数工作流，Faastlane 使用英特尔内存保护密钥 (MPK) 提供轻量级线程级隔离域。在 Apache OpenWhisk 上的评测结果表明，Faastlane 可以将函数工作流的处理时间降低 15 倍，并将函数间的交互延迟减少 99.95%。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;提高实例并发处理能力。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;在 Serverless 平台中，函数实例通常被配置较少的资源容量，例如内存以 128MB 为单位递增，而 CPU 的配额同内存大小成比例。当请求到达后，平台通常以“一对一”的请求 - 实例映射方式处理请求。低的实例并发处理能力可能导致 Serverless 平台在面临突发负载时创建出大量的实例，不仅降低函数的服务性能，也会增加系统调度压力。为了解决该问题，Fifer [12] 工作提出了一种支持多并发处理的函数实例运行方案，通过为实例分配多个独立的 CPU 处理线程来提高实例的吞吐。同时，通过计算实例的排队延迟，并发处理数和未来请求到达率来提前预置对应数量的实例，从而减少冷启动调用的产生。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;允许租户间实例抢占 / 复用。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;通常 Serverless 平台中的租户函数运行在单独的实例中，不同函数间的实例有独立的资源配置和运行环境。由于不同租户的函数可能具有相同的运行时语言环境，而租户的函数实例也不一定在同一时间都处于忙碌状态，因此，Pagurus[28] 工作另辟蹊径，设计了一个类似“寄居蟹”的租户间实例复用机制，其核心思想是“当 A 租户函数的冷启动发生时，可以迅速抢占 B 租户的空闲实例的运行时环境，从而减少冷启动的时延”，通过实例抢占和复用，从而避免冷启动发生时从零启动的长时延问题。同时，可以看到该做法同 Pod 池特化技术有异曲同工之妙。&lt;/p&gt;小结 &lt;p&gt;可以看到，改善实例的并发能力或是通过实例共享 / 复用都是为了提高已有实例的利用效率，而缓存的方案则是尽可能利用少的资源来预置更多的实例，从而降低冷启动调用的产生。上述这些方法从不同的角度缓解 Serverless 函数冷启动的问题，可以结合使用来改善缓存效率。&lt;/p&gt; &lt;p&gt;在主动缓存的方案中，由于突发流量或者负载的无规律性，基于负载预测来进行缓存还存在较大难度。在实际场景中，通过主动预测 + 优先级缓存调整来实现缓存方案将更有潜力。受限于各家函数计算平台内部的架构，函数共享实例或者抢占复用的实施成本较高，因此开发人员通常会选择更加易于实现的缓存方案例如预热池来改善服务质量。&lt;/p&gt;总    结 &lt;p&gt;Serverless 的无状态设计赋予了函数计算高度弹性化的扩展能力，然而也带来了难以避免的冷启动问题。消除 Serverless 函数的冷启动开销还是从降低函数冷启动率和加速实例启动过程两个角度综合入手。&lt;/p&gt; &lt;p&gt;对于冷启动开销比较大的函数，在函数计算框架的设计机制中进行优化，尽量避免冷启动发生；当冷启动发生时，采用一系列启动加速技术来缩短整个过程进行补救。在 Serverless 平台的内部，冷启动的管理在实践中可以做进一步精细的划分，例如针对 VIP 大客户、针对有规律的负载，或是针对冷启动开销小的函数，通过分类做定制化、有目的的管理可以进一步改善系统效率。&lt;/p&gt; &lt;p&gt;在未来，基于 WASM+ 进程 fork 的技术可能会有助于彻底消除冷启动问题，但是考虑到实际可用性和安全性等问题，短期内的结果可能也是 WASM 和 Container-based 函数并存的折中方案，互相取长补短，因此在当前阶段研究冷启动问题还是有较大价值的。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;参考文献&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;[1] Arjun Singhvi, Kevin Houck, Arjun Balasubramanian, Mohammed Danish Shaikh, Shivaram Venkataraman, Aditya Akella. Archipelago: A Scalable Low-Latency Serverless Platform. arXiv:1911.09849, 2019.  &lt;br /&gt;[2] Fastfreeze. https://github.com/twosigma/fastfreeze. 2023.  &lt;br /&gt;[3] 刘方明, 李林峰, 王磊. 华为 Serverless 核心技术与实践 [M]. 北京: 电子工业出版社, 2021.11.  &lt;br /&gt;[4] Docker. https://www.docker.com/. 2023.  &lt;br /&gt;[5] Edward Oakes, Leon Yang, Dennis Zhou, Kevin Houck, Tyler Harter, Andrea C. Arpaci-Dusseau, Remzi H. Arpaci-Dusseau: SOCK: Rapid Task Provisioning with Serverless-Optimized Containers. USENIX Annual Technical Conference 2018: 57-70.  &lt;br /&gt;[6] Kata Containers. https://katacontainers.io/. 2023.  &lt;br /&gt;[7] Alexandru Agache, Marc Brooker, Alexandra Iordache, Anthony Liguori, Rolf Neugebauer, Phil Piwonka, Diana-Maria Popa: Firecracker: Lightweight Virtualization for Serverless Applications. NSDI 2020: 419-434.  &lt;br /&gt;[8] Open-sourcing gVisor, a sandboxed contaienr runtime. https://github.com/google/gvisor. 2023.  &lt;br /&gt;[9] Dong Du, Tianyi Yu, Yubin Xia, Binyu Zang, Guanglu Yan, Chenggang Qin, Qixuan Wu, Haibo Chen: Catalyzer: Sub-millisecond Startup for Serverless Computing with Initialization-less Booting. ASPLOS 2020: 467-481.  &lt;br /&gt;[10] Dmitrii Ustiugov, Plamen Petrov, Marios Kogias, Edouard Bugnion, Boris Grot: Benchmarking, analysis, and optimization of serverless function snapshots. ASPLOS 2021: 559-572.  &lt;br /&gt;[11] Xingda Wei, Fangming Lu, Tianxia Wang, Jinyu Gu, Yuhan Yang, Rong Chen, Haibo Chen. No Provisioned Concurrency: Fast RDMA-codesigned Remote Fork for Serverless Computing. OSDI 2023.  &lt;br /&gt;[12] Jashwant Raj Gunasekaran, Prashanth Thinakaran, Nachiappan Chidambaram Nachiappan, Mahmut Taylan Kandemir, Chita R. Das. Fifer: Tackling Resource Underutilization in the Serverless Era. Middleware 2020: 280-295.  &lt;br /&gt;[13] Xuanzhe Liu, Jinfeng Wen, Zhenpeng Chen, Ding Li, Junkai Chen, Yi Liu, Haoyu Wang, Xin Jin. FaaSLight: General Application-Level Cold-Start Latency Optimization for Function-as-a-Service in Serverless Computing. arXiv:2207.08175. 2022.  &lt;br /&gt;[14] WebAssembly. https://webassembly.org. 2023.  &lt;br /&gt;[15] Simon Shillaker, Peter R. Pietzuch: Faasm: Lightweight Isolation for Efficient Stateful Serverless Computing. USENIX Annual Technical Conference 2020: 419-433.  &lt;br /&gt;[16] Linux CRIU, stands for checkpoint and resotre in userspace. https://github.com/checkpoint-restore/criu. 2023.  &lt;br /&gt;[17] Anil Madhavapeddy, Richard Mortier, Charalampos Rotsos, David J. Scott, Balraj Singh, Thomas Gazagnaire, Steven Smith, Steven Hand, Jon Crowcroft: Unikernels: library operating systems for the cloud. ASPLOS 2013: 461-472.  &lt;br /&gt;[18] Robert N. M. Watson, Jonathan Woodruff, Peter G. Neumann, Simon W. Moore, Jonathan Anderson, David Chisnall, Nirav H. Dave, Brooks Davis, Khilan Gudka, Ben Laurie, Steven J. Murdoch, Robert M. Norton, Michael Roe, Stacey D. Son, Munraj Vadera: CHERI: A Hybrid Capability-System Architecture for Scalable Software Compartmentalization. IEEE Symposium on Security and Privacy 2015: 20-37.  &lt;br /&gt;[19] AWS Lambda. https://aws.amazon.com/lambda/. 2023.  &lt;br /&gt;[20] 华为云 FunctionGraph. https://www.huaweicloud.com/product/functiongraph.html. 2023.  &lt;br /&gt;[21] Mohammad Shahrad, Rodrigo Fonseca, Iñigo Goiri, Gohar Chaudhry, Paul Batum, Jason Cooke, Eduardo Laureano, Colby Tresness, Mark Russinovich, Ricardo Bianchini: Serverless in the Wild: Characterizing and Optimizing the Serverless Workload at a Large Cloud Provider. USENIX Annual Technical Conference 2020: 205-218.  &lt;br /&gt;[22] Yanan Yang, Laiping Zhao, Yiming Li, Huanyu Zhang, Jie Li, Mingyang Zhao, Xingzhen Chen, Keqiu Li: INFless: a native serverless system for low-latency, high-throughput inference. ASPLOS 2022: 768-781.  &lt;br /&gt;[23] Alexander Fuerst, Prateek Sharma: FaasCache: keeping serverless computing alive with greedy-dual caching. ASPLOS 2021: 386-400.  &lt;br /&gt;[24] Rohan Basu Roy, Tirthak Patel, Devesh Tiwari: IceBreaker: warming serverless functions better with heterogeneity. ASPLOS 2022: 753-767.  &lt;br /&gt;[25]Alexander Fuerst, Prateek Sharma: Locality-aware Load-Balancing For Serverless Clusters. HPDC 2022: 227-239.  &lt;br /&gt;[26] Istemi Ekin Akkus, Ruichuan Chen, Ivica Rimac, Manuel Stein, Klaus Satzke, Andre Beck, Paarijaat Aditya, Volker Hilt: SAND: Towards High-Performance Serverless Computing. USENIX Annual Technical Conference 2018: 923-935.  &lt;br /&gt;[27] Swaroop Kotni, Ajay Nayak, Vinod Ganapathy, Arkaprava Basu: Faastlane: Accelerating Function-as-a-Service Workflows. USENIX Annual Technical Conference 2021: 805-820.  &lt;br /&gt;[28] Zijun Li, Quan Chen, Minyi Guo: Pagurus: Eliminating Cold Startup in Serverless Computing with Inter-Action Container Sharing. USENIX Annual Technical Conference 2022.  &lt;br /&gt;[29] Ao Wang, Shuai Chang, Huangshi Tian, Hongqi Wang, Haoran Yang, Huiba Li, Rui Du, Yue Cheng: FaaSNet: Scalable and Fast Provisioning of Custom Serverless Container Runtimes at Alibaba Cloud Function Compute. USENIX Annual Technical Conference 2021: 443-457.  &lt;br /&gt;[30] Marc Brooker, Mike Danilov, Chris Greenwood, Phil Piwonka: On-demand Container Loading in AWS Lambda. CoRR abs/2305.13162 (2023).  &lt;br /&gt;[31] Zijun Li, Linsong Guo, Jiagan Cheng, Quan Chen, Bingsheng He, Minyi Guo: The Serverless Computing Survey: A Technical Primer for Design Architecture. ACM Comput. Surv. 54(10s): 220:1-220:34 (2022).&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;   &lt;strong&gt;    &lt;strong&gt;     &lt;strong&gt;      &lt;strong&gt;       &lt;strong&gt;        &lt;strong&gt;         &lt;strong&gt;          &lt;strong&gt;           &lt;strong&gt;            &lt;strong&gt;             &lt;strong&gt;              &lt;strong&gt;               &lt;strong&gt;                &lt;strong&gt;                 &lt;strong&gt;                  &lt;strong&gt;                   &lt;strong&gt;                    &lt;strong&gt;                     &lt;strong&gt;点击底部                      &lt;strong&gt;阅读原文&lt;/strong&gt;访问 InfoQ 官网，获取更多精彩内容！&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;今日好文推荐 &lt;p&gt;  &lt;a href="http://mp.weixin.qq.com/s?__biz=MjM5MDE0Mjc4MA==&amp;mid=2651175282&amp;idx=1&amp;sn=408fd28a8918b1ca2b2947d3988e1c63&amp;chksm=bdb841218acfc8378ce20f6dafc92397120d9bc423166d5642248b57c1724af3fb2b38be67c3&amp;scene=21#wechat_redirect" target="_blank"&gt;OpenAI 遭遇离职潮：员工对 ChatGPT 进展缓慢失望，痛批 CEO 不务正业&lt;/a&gt;  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;  &lt;a href="http://mp.weixin.qq.com/s?__biz=MjM5MDE0Mjc4MA==&amp;mid=2651175273&amp;idx=1&amp;sn=88f6bceb13587812efca90a4a8abc02d&amp;chksm=bdb8413a8acfc82c8dae649dcdd8dfbdd58ea07513eab661fc1394604b2ae531f982c4d17cf9&amp;scene=21#wechat_redirect" target="_blank"&gt;阿里改革，P8 成为历史；GPT-4 模型架构泄露；OpenAI 面临最严调查，Altman 惊慌连发 3 推｜Q 资讯&lt;/a&gt;&lt;/p&gt; &lt;p&gt;  &lt;a href="http://mp.weixin.qq.com/s?__biz=MjM5MDE0Mjc4MA==&amp;mid=2651175154&amp;idx=1&amp;sn=c1340f2a375abea1147791b592a963d1&amp;chksm=bdb840a18acfc9b7e65a7bad85cd7068d5b4b0705c892b66c192217c9bbe7a9a8c80cc6d6ac8&amp;scene=21#wechat_redirect" target="_blank"&gt;神器还是垃圾？那些用 AIGC 编程的人，实践得怎么样了&lt;/a&gt;  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;  &lt;a href="http://mp.weixin.qq.com/s?__biz=MjM5MDE0Mjc4MA==&amp;mid=2651174818&amp;idx=1&amp;sn=4ce6a97c182527b1ec4fddf4c723290d&amp;chksm=bdb84ff18acfc6e78a72b57421fea9e6f82c25c7d21f9c4e225c26a9c108d1b43f699584fc98&amp;scene=21#wechat_redirect" target="_blank"&gt;&lt;/a&gt;  &lt;a href="http://mp.weixin.qq.com/s?__biz=MjM5MDE0Mjc4MA==&amp;mid=2651175003&amp;idx=1&amp;sn=c166987c764421ab81eab0f43c5f6d6f&amp;chksm=bdb840088acfc91e919af6a4770cd056ddd51269f99d6a089747a5f3dd9134807930ac8b681b&amp;scene=21#wechat_redirect" target="_blank"&gt;甲骨文火上浇油、SUSE投入1000万美元，多方“围剿”红帽：“红帽负担不起？那我们来！”&lt;/a&gt;&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>dev</category>
      <guid isPermaLink="true">https://itindex.net/detail/62812-serverless-%E5%86%B7%E5%90%AF%E5%8A%A8-%E4%BC%98%E5%8C%96</guid>
      <pubDate>Sun, 23 Jul 2023 00:00:00 CST</pubDate>
    </item>
    <item>
      <title>OpenAI 发布 GPT-4，有哪些技术上的优化或突破？</title>
      <link>https://itindex.net/detail/62679-openai-gpt-%E6%8A%80%E6%9C%AF</link>
      <description>&lt;p&gt;在这个历史性的时刻，回答个问题，留下自己作为历史见证人的足迹。&lt;/p&gt; &lt;p&gt;GPT4的技术报告里很明确地指出了三个新的方向：&lt;/p&gt; &lt;p&gt;第一，LLM最前沿研究的封闭化或小圈子化。技术报告里说了，出于竞争以及安全等方面的考虑，未公布模型规模等技术细节。从GPT 2.0的开源，到GPT 3.0的只有论文，再到ChatGPT连论文也没有，直到GPT 4.0的技术报告更像效果评测报告。一个很明显的趋势是，OpenAI做实了CloseAI的名号，之后OpenAI的LLM最前沿研究不会再放出论文。&lt;/p&gt; &lt;p&gt;在这个情形下，其它技术相对领先的公司有两种选择。一种是做更极致的LLM开源化，比如Meta貌似选择了这条道路，这一般是竞争处于劣势的公司作出的合理选择，但是往往相关技术不会是最前沿的技术；另外一种选择是跟进OpenAI，也选择技术封闭化。Google之前算是LLM的第二梯队，但在“微软+OpenAI”的一顿组合拳下，现在局面有点狼狈不堪。GPT 4.0去年8月就做好了，估计现在GPT 5.0正在炼丹过程中，这么长的时间窗口，结果Google都能落到目前这个局面，想想Transformer、CoT等非常关键的一些研究都是自己做出来的，竟沦落至此，不知一众高层作何感想。Google在后面能快速跟进，维持在第二梯队应该问题不大，很可能比第三名技术也领先很多。出于竞争关系考虑，我猜Google大概率会跟进OpenAI走技术封闭的路线，最先进的LLM技术优先用来炼属于自己的丹，而不是写成论文放出来普惠大众尤其是普惠OpenAI。而这很可能导致LLM最前沿研究的封闭化。&lt;/p&gt; &lt;p&gt;从现在算起，国内在经过一阵时间后（要做到ChatGPT的6到7折应该比较快，要追平估计要较长时间），必然被迫进入自主创新的局面。从最近三个月国内的各种情况看，将来会如何？大概率不太乐观。当然，这个关肯定很难，但必须得过，只能祝愿有能力且有初心者尽力加油了。&lt;/p&gt; &lt;p&gt;第二，GPT 4技术报告里提到的LLM模型的“能力预测（Capability Prediction）”是个非常有价值的新研究方向（其实之前也有个别其它资料，我记得看过，但是具体哪篇记不起来了）。用小模型来预测某些参数组合下对应大模型的某种能力，如果预测足够精准，能够极大缩短炼丹周期，同时极大减少试错成本，所以无论理论价值还是实际价值巨大，这个绝对是非常值得认真研究具体技术方法的。&lt;/p&gt; &lt;p&gt;第三，GPT 4开源了一个LLM评测框架，这也是后面LLM技术快速发展非常重要的方向。尤其对于中文，构建实用的中文LLM评测数据和框架具备特别重要的意义，好的LLM评测数据可以快速发现LLM目前存在的短板和改进方向，意义重大，但是很明显目前这块基本处于空白状态。这个对于资源要求其实没那么高，适合很多机构去做，不过确实是个辛苦活。&lt;/p&gt; &lt;p&gt;除了GPT 4技术报告里明确指出的三个方向，因为最近LLM各方面新闻比较多，顺手再写两个其它技术方向。&lt;/p&gt; &lt;p&gt;首先，斯坦福大学最近在Meta的7B 开源LLaMA基础上，加上Self Instruct技术构造的Alpaca，也代表了一个技术方向。如果归纳下，这个方向可以被称为“低成本复现ChatGPT”的方向。所谓Self Instruct，就是采取一定技术手段，不用人工标注Instruct，而是从OpenAI的接口里，好听点叫“蒸馏”出Instruct，也就是不用人标注，而是ChatGPT作为teacher，给你的Instruct打上标注结果。这个把Instruct标注成本直接打到了几百美金的基准，时间成本就更短了。再加上模型7B规模也不大，所以可以看成一种“低成本复现ChatGPT”的技术路线。&lt;/p&gt; &lt;p&gt;我估计国内早就有不少人采取这个技术路线了。毫无疑问，这是一条捷径，但是走捷径有好处有坏处，具体不展开谈了。在追赶ChatGPT的过程中，先把成本打下来去复现ChatGPT到七八成，我个人还是觉得可行也支持的，毕竟穷人有穷人的玩法。当然，追求把模型做小，效果又不怎么往下掉，如果能扎扎实实去做，是非常具有价值的。&lt;/p&gt; &lt;p&gt;另外，具身智能毫无疑问会是LLM下一阶段的重点研究方向。这方面的代表就是前阵子Google放出来的PaLM-E了。目前的GPT 4，我们可以认为人类创造出了一个超级大脑，但还是把它封锁在GPU集群里。而这个超级大脑需要一个身体，GPT 4要和物理世界发生联系、交流和互动，并在物理世界中获得真实的反馈，来学会在真实世界里生存，并根据真实世界的反馈，利用比如强化学习来学习在世界游走的能力。这个肯定是最近的将来最热门的LLM研究方向。&lt;/p&gt; &lt;p&gt;多模态LLM给予了GPT 4以眼睛和耳朵，而具身智能给予GPT 4身体、脚和手。GPT 4和你我发生一些联系，而依托于GPT 4本身强大的学习能力，这个事情估计很快会出现在我们身边。&lt;/p&gt; &lt;p&gt; 如果你细想，其实还有很多其它有前途的方向。我的个人判断是：未来5到10年，将会是AGI最快速发展的黄金十年。如果我们站在未来30年的时间节点，当我们回顾这10年时，我们中一定会有人，想起下面的诗句：“懂得，但为时太晚，他们使太阳在途中悲伤， 也并不温和地走进那个良夜。”&lt;/p&gt;
 &lt;br /&gt; &lt;br /&gt;
来源：知乎 www.zhihu.com &lt;br /&gt;
    
作者： &lt;a href="http://www.zhihu.com/people/zhang-jun-lin-76?utm_campaign=rss&amp;utm_medium=rss&amp;utm_source=rss&amp;utm_content=author"&gt;张俊林&lt;/a&gt; &lt;br /&gt;
            
 &lt;br /&gt;
【知乎日报】千万用户的选择，做朋友圈里的新鲜事分享大牛。
         &lt;a href="http://daily.zhihu.com?utm_source=rssyanwenzi&amp;utm_campaign=tuijian&amp;utm_medium=rssnormal" target="_blank"&gt;点击下载&lt;/a&gt; &lt;br /&gt;
 &lt;br /&gt;
此问题还有  &lt;a href="http://www.zhihu.com/question/589639535/answer/2937928726?utm_campaign=rss&amp;utm_medium=rss&amp;utm_source=rss&amp;utm_content=title" target="_blank"&gt;587 个回答，查看全部。&lt;/a&gt; &lt;br /&gt;
                延伸阅读： &lt;br /&gt;
 &lt;a href="http://www.zhihu.com/question/589743557?utm_campaign=rss&amp;utm_medium=rss&amp;utm_source=rss&amp;utm_content=title" target="_blank"&gt;OpenAI 发布 GPT-4，该产品都有哪些值得关注的亮点？&lt;/a&gt; &lt;br /&gt;
            
 &lt;a href="http://www.zhihu.com/question/589698290?utm_campaign=rss&amp;utm_medium=rss&amp;utm_source=rss&amp;utm_content=title" target="_blank"&gt;OpenAI 发布 GPT-4，有哪些可以辅助我们的技术？&lt;/a&gt; &lt;br /&gt;
            &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62679-openai-gpt-%E6%8A%80%E6%9C%AF</guid>
      <pubDate>Wed, 15 Mar 2023 21:09:44 CST</pubDate>
    </item>
    <item>
      <title>一文读懂数据库优化之分库分表</title>
      <link>https://itindex.net/detail/62586-%E6%96%87%E8%AF%BB-%E6%95%B0%E6%8D%AE%E5%BA%93-%E4%BC%98%E5%8C%96</link>
      <description>&lt;div&gt;  &lt;blockquote&gt;   &lt;p&gt;    &lt;strong&gt;来自公众号：腾讯技术工程&lt;/strong&gt;    &lt;br /&gt;&lt;/p&gt;   &lt;p&gt;作者：tayroctang，腾讯 PCG 后台开发工程师&lt;/p&gt;&lt;/blockquote&gt;  &lt;blockquote&gt;   &lt;p&gt;本文从 5W1H 角度介绍了分库分表手段，其在解决如 IO 瓶颈、读写性能、物理存储瓶颈、内存瓶颈、单机故障影响面等问题的同时也带来如事务性、主键冲突、跨库 join、跨库聚合查询等问题。anyway，在综合业务场景考虑，正如缓存的使用一样，本着非必须勿使用原则。如数据库确实成为性能瓶颈时，在设计分库分表方案时也应充分考虑方案的扩展性，或者考虑采用成熟热门的分布式数据库解决方案，如 TiDB。&lt;/p&gt;&lt;/blockquote&gt;  &lt;p&gt;阅读此文你将了解：&lt;/p&gt;  &lt;ul&gt;   &lt;li&gt;什么是分库分表以及为什么分库分表&lt;/li&gt;   &lt;li&gt;如何分库分表&lt;/li&gt;   &lt;li&gt;分库分表常见几种方式以及优缺点&lt;/li&gt;   &lt;li&gt;如何选择分库分表的方式&lt;/li&gt;&lt;/ul&gt;  &lt;br /&gt;  &lt;h3&gt;   &lt;strong&gt;数据库常见优化方案&lt;/strong&gt;&lt;/h3&gt;  &lt;p&gt;对于后端程序员来说，绕不开数据库的使用与方案选型，那么随着业务规模的逐渐扩大，其对于存储的使用上也需要随之进行升级和优化。&lt;/p&gt;  &lt;p&gt;随着规模的扩大，数据库面临如下问题：&lt;/p&gt;  &lt;ul&gt;   &lt;li&gt;读压力：并发 QPS、索引不合理、SQL 语句不合理、锁粒度&lt;/li&gt;   &lt;li&gt;写压力：并发 QPS、事务、锁粒度&lt;/li&gt;   &lt;li&gt;物理性能：磁盘瓶颈、CPU 瓶颈、内存瓶颈、IO 瓶颈&lt;/li&gt;   &lt;li&gt;其他：宕机、网络异常&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt;面对上述问题，常见的优化手段有：&lt;/p&gt;  &lt;img&gt;&lt;/img&gt;  &lt;p&gt;索引优化、主从同步、缓存、分库分表每个技术手段都可以作为一个专题进行讲解，本文主要介绍分库分表的技术方案实现。&lt;/p&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;h3&gt;   &lt;strong&gt;什么是分库分表？&lt;/strong&gt;&lt;/h3&gt;  &lt;p&gt;对于阅读本文的读者来说，分库分表概念应该并不会陌生，其拆开来讲是分库和分表两个手段：&lt;/p&gt;  &lt;ul&gt;   &lt;li&gt;分表：将一个表中的数据按照某种规则分拆到多张表中，    &lt;strong&gt;降低锁粒度以及索引树&lt;/strong&gt;，提升数据查询效率。&lt;/li&gt;   &lt;li&gt;分库：将一个数据库中的数据按照某种规则分拆到多个数据库中，以    &lt;strong&gt;缓解单服务器的压力&lt;/strong&gt;（CPU、内存、磁盘、IO）。&lt;/li&gt;&lt;/ul&gt;  &lt;br /&gt;  &lt;h3&gt;   &lt;strong&gt;为什么分库分表？&lt;/strong&gt;&lt;/h3&gt;  &lt;ul&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;性能角度：CPU、内存、磁盘、IO 瓶颈&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;   &lt;ul&gt;    &lt;li&gt;随着业务体量扩大，数据规模达到百万行，数据库索引树庞大，查询性能出现瓶颈。     &lt;br /&gt;&lt;/li&gt;    &lt;li&gt;用户并发流量规模扩大，由于单库(单服务器)物理性能限制也无法承载大流量。&lt;/li&gt;&lt;/ul&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;可用性角度：单机故障率影响面&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;   &lt;ul&gt;    &lt;li&gt;     &lt;p&gt;如果是单库，数据库宕机会导致 100%服务不可用，N 库则可以将影响面降低 N 倍。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;   &lt;p&gt;    &lt;br /&gt;&lt;/p&gt;&lt;/ul&gt;  &lt;h3&gt;   &lt;strong&gt;分库分表带来的问题？&lt;/strong&gt;&lt;/h3&gt;  &lt;ul&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;事务性问题&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;   &lt;ul&gt;    &lt;li&gt;方案一：在进行分库分表方案设计过程中，从业务角度出发，尽可能保证一个事务所操作的表分布在一个库中，从而实现数据库层面的事务保证。     &lt;br /&gt;&lt;/li&gt;    &lt;li&gt;方案二：方式一无法实现的情况下，业务层引入分布式事务组件保证事务性，如事务性消息、TCC、Seata 等分布式事务方式实现数据最终一致性。&lt;/li&gt;    &lt;li&gt;     &lt;p&gt;分库      &lt;strong&gt;可能&lt;/strong&gt;导致执行一次事务所需的数据分布在不同服务器上，数据库层面无法实现事务性操作，需要更上层业务引入分布式事务操作，难免会给业务带来一定复杂性，那么要想解决事务性问题一般有两种手段：&lt;/p&gt;     &lt;br /&gt;&lt;/li&gt;&lt;/ul&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;主键(自增 ID)唯一性问题&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;   &lt;ul&gt;    &lt;li&gt;在数据库表设计时，经常会使用自增 ID 作为数据主键，这就导致后续在迁库迁表、或者分库分表操作时，会因为主键的变化或者主键不唯一产生冲突，要解决主键不唯一问题，有如下方案：     &lt;br /&gt;&lt;/li&gt;    &lt;li&gt;方案一：自增 ID 做主键时，设置自增步长，采用等差数列递增，避免各个库表的主键冲突。但是这个方案仍然无法解决迁库迁表、以及分库分表扩容导致主键 ID 变化问题&lt;/li&gt;    &lt;li&gt;方案二：主键采用全局统一 ID 生成机制：如 UUID、雪花算法、数据库号段等方式。&lt;/li&gt;&lt;/ul&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;跨库多表 join 问题&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;   &lt;ul&gt;    &lt;li&gt;首先来自大厂 DBA 的建议是，线上服务尽可能不要有表的 join 操作，join 操作往往会给后续的分库分表操作带来各种问题，可能导致数据的死锁。可以采用多次查询业务层进行数据组装(需要考虑业务上多次查询的事务性的容忍度)     &lt;br /&gt;&lt;/li&gt;&lt;/ul&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;跨库聚合查询问题&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt;分库分表会导致常规聚合查询操作，如 group by，order by 等变的异常复杂。需要复杂的业务代码才能实现上述业务逻辑，其常见操作方式有：&lt;/p&gt;  &lt;p&gt;§ 方案一：赛道赛马机制，每次从 N 个库表中查询出 TOP N 数据，然后在业务层代码中进行聚合合并操作。&lt;/p&gt;  &lt;pre&gt;   &lt;code&gt;§  假设： 以2库1表为例，每次分页查询N条数据。    &lt;br /&gt;§    &lt;br /&gt;§  第一次查询：    &lt;br /&gt;§  ① 每个表中分别查询出N条数据：    &lt;br /&gt;§  SELECT * FROM db1_table1 where $col &amp;gt; 0 order by $col   LIMITT  0,N    &lt;br /&gt;§  SELECT * FROM db2_table1 where $col &amp;gt; 0 order by $col   LIMITT  0,N    &lt;br /&gt;§  ② 业务层代码对上述两者做归并排序，假设最终取db1数据K1条，取db2数据K2条，则K1+K2 = N    &lt;br /&gt;§  此时的DB1 可以计算出OffSet为K1 ，DB2计算出Offset为K2    &lt;br /&gt;§  将获取的N条数据以及相应的Offset  K1/K2返回给 端上。    &lt;br /&gt;§    &lt;br /&gt;§  第二次查询：    &lt;br /&gt;§  ① 端上将上一次查询对应的数据库的Offset  K1/K2 传到后端    &lt;br /&gt;§  ② 后端根据Offset构造查询语句查询分别查询出N条语句    &lt;br /&gt;§  SELECT * FROM db1_table1 where $col &amp;gt; 0 order by $col   LIMITT  $K1,N    &lt;br /&gt;§  SELECT * FROM db2_table1 where $col &amp;gt; 0 order by $col   LIMITT  $K2,N    &lt;br /&gt;§  ③ 再次使用归并排序，获取TOP N数据，将获取的N条数据以及相应的Offset  K1/K2返回给 端上。    &lt;br /&gt;§    &lt;br /&gt;§  第三次查询:    &lt;br /&gt;依次类推.......    &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;  &lt;p&gt;§ 方案二：可以将经常使用到 groupby,orderby 字段存储到一个单一库表(可以是 REDIS、ES、MYSQL)中，业务代码中先到单一表中根据查询条件查询出相应数据，然后根据查询到的主键 ID，到分库分表中查询详情进行返回。2 次查询操作难点会带来接口耗时的增加，以及极端情况下的数据不一致问题。&lt;/p&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;h3&gt;   &lt;strong&gt;什么是好的分库分表方案？&lt;/strong&gt;&lt;/h3&gt;  &lt;ul&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;满足业务场景需要&lt;/strong&gt;：根据业务场景的不同选择不同分库分表方案：比如按照时间划分、按照用户 ID 划分、按照业务能力划分等&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;方案可持续性&lt;/strong&gt;：&lt;/p&gt;&lt;/li&gt;   &lt;ul&gt;    &lt;li&gt;何为可持续性？其实就是：业务数据量级和流量量级未来进一步达到新的量级的时候，我们的分库分表方案可以持续灵活扩容处理。&lt;/li&gt;&lt;/ul&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;最小化数据迁移&lt;/strong&gt;：扩容时一般涉及到历史数据迁移，其扩容后需要迁移的数据量越小其可持续性越强，理想的迁移前后的状态是（同库同表&amp;gt;同表不同库&amp;gt;同库不同表&amp;gt;不同库不同表）&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;数据偏斜&lt;/strong&gt;：数据在库表中分配的均衡性，尽可能保证数据流量在各个库表中保持等量分配，避免热点数据对于单库造成压力。&lt;/p&gt;&lt;/li&gt;   &lt;ul&gt;    &lt;li&gt;最大数据偏斜率：（数据量最大样本 - 数据量最小样本）/ 数据量最小样本。一般来说，如果我们的最大数据偏斜率在 5%以内是可以接受的。&lt;/li&gt;&lt;/ul&gt;&lt;/ul&gt;  &lt;br /&gt;  &lt;h3&gt;   &lt;strong&gt;如何分库分表&lt;/strong&gt;&lt;/h3&gt;  &lt;img&gt;&lt;/img&gt;  &lt;h4&gt;垂直拆分：&lt;/h4&gt;  &lt;ul&gt;   &lt;li&gt;    &lt;p&gt;垂直拆表&lt;/p&gt;&lt;/li&gt;   &lt;ul&gt;    &lt;li&gt;即大表拆小表，将一张表中数据不同”字段“分拆到多张表中，比如商品库将商品基本信息、商品库存、卖家信息等分拆到不同库表中。&lt;/li&gt;    &lt;li&gt;考虑因素有将     &lt;strong&gt;不常用&lt;/strong&gt;的，     &lt;strong&gt;数据较大&lt;/strong&gt;，     &lt;strong&gt;长度较长&lt;/strong&gt;（比如 text 类型字段）的拆分到“扩展表“，表和表之间通过”主键外键“进行关联。&lt;/li&gt;    &lt;li&gt;好处：降低表数据规模，提升查询效率，也避免查询时数据量太大造成的“跨页”问题。&lt;/li&gt;&lt;/ul&gt;   &lt;li&gt;    &lt;p&gt;垂直拆库&lt;/p&gt;&lt;/li&gt;   &lt;ul&gt;    &lt;li&gt;垂直拆库则在垂直拆表的基础上，将一个系统中的不同业务场景进行拆分，比如订单表、用户表、商品表。&lt;/li&gt;    &lt;li&gt;好处：降低单数据库服务的压力（物理存储、内存、IO 等）、降低单机故障的影响面&lt;/li&gt;&lt;/ul&gt;&lt;/ul&gt;  &lt;h4&gt;水平拆分：&lt;/h4&gt;  &lt;ul&gt;   &lt;li&gt;    &lt;p&gt;操作：将总体数据按照某种维度(时间、用户)等分拆到多个库中或者表中，典型特征不同的库和表结构完全一下，如订单按照(日期、用户 ID、区域)分库分表。&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;水平拆表&lt;/p&gt;&lt;/li&gt;   &lt;ul&gt;    &lt;li&gt;将数据按照某种维度拆分为多张表，但是由于多张表还是从属于     &lt;strong&gt;一个库&lt;/strong&gt;，其降低     &lt;strong&gt;锁粒度&lt;/strong&gt;，一定程度提升查询性能，但是仍然会有 IO 性能瓶颈。&lt;/li&gt;&lt;/ul&gt;   &lt;li&gt;    &lt;p&gt;水平拆库&lt;/p&gt;&lt;/li&gt;   &lt;ul&gt;    &lt;li&gt;将数据按照某种维度分拆到多个库中，降低单机单库的压力，提升读写性能。&lt;/li&gt;&lt;/ul&gt;&lt;/ul&gt;  &lt;h4&gt;   &lt;strong&gt;常见水平拆分手段&lt;/strong&gt;&lt;/h4&gt;  &lt;h5&gt;range 分库分表&lt;/h5&gt;  &lt;p&gt;顾名思义，该方案根据数据范围划分数据的存放位置。&lt;/p&gt;  &lt;h6&gt;   &lt;strong&gt;思路一：时间范围分库分表&lt;/strong&gt;&lt;/h6&gt;  &lt;p&gt;举个最简单例子，我们可以把订单表按照年份为单位，每年的数据存放在单独的库（或者表）中。&lt;/p&gt;  &lt;p&gt;时下非常流行的分布式数据库：TiDB 数据库，针对 TiKV 中数据的打散，也是基于 Range 的方式进行，将不同范围内的[StartKey,EndKey)分配到不同的 Region 上。&lt;/p&gt;  &lt;p&gt;缺点：&lt;/p&gt;  &lt;ul&gt;   &lt;li&gt;需要提前建库或表。&lt;/li&gt;   &lt;li&gt;数据热点问题：当前时间的数据会集中落在某个库表。&lt;/li&gt;   &lt;li&gt;分页查询问题：涉及到库表中间分界线查询较复杂。&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt;例子：交易系统流水表则是按照天级别分表。&lt;/p&gt;  &lt;h5&gt;hash 分库分表&lt;/h5&gt;  &lt;p&gt;hash 分表是使用最普遍的使用方式，其根据“主键”进行 hash 计算数据存储的库表索引。原理可能大家都懂，但有时拍脑袋决定的分库分表方案可能会导致严重问题。&lt;/p&gt;  &lt;h6&gt;思路一：独立 hash&lt;/h6&gt;  &lt;p&gt;对于分库分表，最常规的一种思路是通过主键计算 hash 值，然后 hash 值分别对库数和表数进行取余操作获取到库索引和表索引。比如：电商订单表，按照用户 ID 分配到 10 库 100 表中。&lt;/p&gt;  &lt;pre&gt;   &lt;code&gt;const (    &lt;br /&gt;        // DbCnt 库数量    &lt;br /&gt;        DbCnt = 10    &lt;br /&gt;        // TableCnt 表数量    &lt;br /&gt;        TableCnt = 100    &lt;br /&gt;)    &lt;br /&gt;    &lt;br /&gt;// GetTableIdx 根据用户 ID 获取分库分表索引    &lt;br /&gt;func GetTableIdx(userID int64) (int64, int64) {    &lt;br /&gt;    hash := hashCode(userID)    &lt;br /&gt;        return hash % DbCnt, hash % TableCnt    &lt;br /&gt;}    &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;  &lt;p&gt;上述是伪代码实现，大家可以先思考一下上述代码可能会产生什么问题？&lt;/p&gt;  &lt;p&gt;比如 1000? 1010?，1020 库表索引是多少？&lt;/p&gt;  &lt;p&gt;思考一下........&lt;/p&gt;  &lt;p&gt;思考一下........&lt;/p&gt;  &lt;p&gt;思考一下........&lt;/p&gt;  &lt;p&gt;思考一下........&lt;/p&gt;  &lt;p&gt;思考一下........&lt;/p&gt;  &lt;p&gt;思考一下........&lt;/p&gt;  &lt;p&gt;答：数据偏斜问题。&lt;/p&gt;  &lt;img&gt;&lt;/img&gt;  &lt;p&gt;非互质关系导致的数据偏斜问题证明：&lt;/p&gt;  &lt;pre&gt;   &lt;code&gt;假设分库数分表数最大公约数为a，则分库数表示为 m*a , 分表数为 n*a （m,n为正整数）    &lt;br /&gt;    &lt;br /&gt;某条数据的hash规则计算的值为H，    &lt;br /&gt;    &lt;br /&gt;若某条数据在库D中，则H mod (m*a) == D 等价与  H=M*m*a+D （M为整数）    &lt;br /&gt;    &lt;br /&gt;则表序号为 T = H % (n*a) = (M*m*a+D)%(n*a)    &lt;br /&gt;    &lt;br /&gt;如果D==0 则T= [(M*m)%n]*a    &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;  &lt;h6&gt;思路二：统一 hash&lt;/h6&gt;  &lt;p&gt;思路一中，由于库和表的 hash 计算中存在公共因子，导致数据偏斜问题，那么换种思考方式：10 个库 100 张表，一共 1000 张表，那么从 0 到 999 排序，根据 hash 值对 1000 取余，得到[0,999]的索引，似乎就可以解决数据偏斜问题：&lt;/p&gt;  &lt;pre&gt;   &lt;code&gt;// GetTableIdx 根据用户 ID 获取分库分表索引    &lt;br /&gt;// 例子：1123011 -&amp;gt; 1,1    &lt;br /&gt;func GetTableIdx(userID int64) (int64, int64) {    &lt;br /&gt;    hash := hashCode(userID)    &lt;br /&gt;    slot := DbCnt * TableCnt    &lt;br /&gt;        return hash % slot % DbCnt, hash % slot / DbCnt    &lt;br /&gt;}    &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;  &lt;p&gt;上面会带来的问题？&lt;/p&gt;  &lt;p&gt;比如 1123011 号用户，扩容前是 1 库 1 表，扩容后是 0 库 11 表&lt;/p&gt;  &lt;img&gt;&lt;/img&gt;  &lt;p&gt;扩展性问题证明。&lt;/p&gt;  &lt;pre&gt;   &lt;code&gt;某条数据的hash规则计算的值为H，分库数为D，分表数为T    &lt;br /&gt;    &lt;br /&gt;扩容前：    &lt;br /&gt;分片序号K1 = H % (D*T),则H = M*DT + K1 ，且K1 一定是小于（D*T）    &lt;br /&gt;D1 = K1 % D    &lt;br /&gt;T1 = K1 / D    &lt;br /&gt;    &lt;br /&gt;扩容后：    &lt;br /&gt;如果M为偶数，即M= 2*N    &lt;br /&gt;K2 = H% (2DT) = (2NDT+K1)%(2DT) = K1%(2DT) ,K1 一定小于（2DT）,所以K2=K1    &lt;br /&gt;D2 = K2%（2D） = K1 %（2D）    &lt;br /&gt;T2 = K2/(2D) = K1 / （2D）    &lt;br /&gt;    &lt;br /&gt;如果M为奇数，即M = 2*N+1    &lt;br /&gt;K2 = H%（2DT） = (2NDT +DT +K1)%(2DT) = (DT+K1)%(2DT) = DT + K1    &lt;br /&gt;D2 = K2 %(2D) = (DT+K1) % (2D)    &lt;br /&gt;T2 = K2 /(2D) = (DT+K1) / (2D)    &lt;br /&gt;    &lt;br /&gt;结论：扩容后库序号和表序号都变化    &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;  &lt;h6&gt;思路三：二次分片法&lt;/h6&gt;  &lt;p&gt;思路二中整体思路正确，只是最后计算库序号和表序号的时候，使用了库数量作为影响表序号的因子，导致扩容时表序号偏移而无法进行。事实上，我们只需要换种写法，就能得出一个比较大众化的分库分表方案。&lt;/p&gt;  &lt;pre&gt;   &lt;code&gt;func GetTableIdx(userId int64){    &lt;br /&gt;        //①算Hash    &lt;br /&gt;        hash:=hashCode(userId)    &lt;br /&gt;        //②分片序号    &lt;br /&gt;        slot:=hash%(DbCnt*TableCnt)    &lt;br /&gt;        //③重新修改二次求值方案    &lt;br /&gt;        dbIdx:=slot/TableCnt    &lt;br /&gt;        tblIdx:=slot%TableCnt    &lt;br /&gt;        return dbIdx,tblIdx    &lt;br /&gt;}    &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;  &lt;p&gt;从上述代码中可以看出，其唯一不同是在计算库索引和表索引时，采用 TableCnt 作为基数（注：扩容操作时，一般采用库个数 2 倍扩容），这样在扩容时，表个数不变，则表索引不会变。&lt;/p&gt;  &lt;p&gt;可以做简要的证明：&lt;/p&gt;  &lt;pre&gt;   &lt;code&gt;某条数据的hash规则计算的值为H，分库数为D，分表数为T    &lt;br /&gt;    &lt;br /&gt;扩容前：    &lt;br /&gt;分片序号K1 = H % (D*T),则H =  M*DT + K1 ，且K1 一定是小于（D*T）    &lt;br /&gt;D1 = K1 / T    &lt;br /&gt;T1 = K1 % T    &lt;br /&gt;    &lt;br /&gt;扩容后：    &lt;br /&gt;如果M为偶数，即M= 2*N    &lt;br /&gt;K2 =  H% (2DT) = (2NDT+K1)%(2DT) = K1%(2DT) ,K1 一定小于（2DT）,所以K2=K1    &lt;br /&gt;D2 = K2/T  = K1 /T = D1    &lt;br /&gt;T2 = K2%T = K1 % T = T1    &lt;br /&gt;    &lt;br /&gt;如果M为奇数，即M = 2*N+1    &lt;br /&gt;K2 = H%（2DT） = (2NDT +DT +K1)%(2DT) = (DT+K1)%(2DT) = DT + K1    &lt;br /&gt;D2 = K2 /T = (DT+K1) / T = D + K1/T = D + D1    &lt;br /&gt;T2 = K2 %T = (DT+K1) % T = K1 %T = T1    &lt;br /&gt;    &lt;br /&gt;结论：    &lt;br /&gt;M为偶数时，扩容前后库序号和表序号都不变    &lt;br /&gt;M为奇数时，扩容前后表序号不变，库序号会变化。    &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;  &lt;h6&gt;思路四：基因法&lt;/h6&gt;  &lt;p&gt;由思路二启发，我们发现案例一不合理的主要原因，就是因为库序号和表序号的计算逻辑中，有公约数这个因子在影响库表的独立性。那么我们是否可以换一种思路呢？我们使用相对独立的 Hash 值来计算库序号和表序号呢？&lt;/p&gt;  &lt;pre&gt;   &lt;code&gt;func GetTableIdx(userID int64)(int64,int64){    &lt;br /&gt;        hash := hashCode(userID)    &lt;br /&gt;        return atoi(hash[0:4]) % DbCnt,atoi(hash[4:])%TableCnt    &lt;br /&gt;}    &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;  &lt;p&gt;这也是一种常用的方案，我们称为基因法，即使用原分片键中的某些基因（例如前四位）作为库的计算因子，而使用另外一些基因作为表的计算因子。&lt;/p&gt;  &lt;p&gt;在使用基因法时，要主要计算 hash 值的片段保持充分的随机性，避免造成严重数据偏斜问题。&lt;/p&gt;  &lt;h6&gt;思路五：关系表冗余&lt;/h6&gt;  &lt;p&gt;按照索引的思想，可以通过分片的键和库表索引建立一张索引表，我们把这张索引表叫做“路由关系表”。每次查询操作，先去路由表中查询到数据所在的库表索引，然后再到库表中查询详细数据。同时，对于写入操作可以采用随机选择或者顺序选择一个库表进入写入。&lt;/p&gt;  &lt;p&gt;那么由于路由关系表的存在，我们在数据扩容时，无需迁移历史数据。同时，我们可以为每个库表指定一个权限，通过权重的比例调整来调整每个库表的写入数据量。从而实现库表数据偏斜率调整。&lt;/p&gt;  &lt;p&gt;此种方案的缺点是每次查询操作，需要先读取一次路由关系表，所以请求耗时可能会有一定增加。本身由于写索引表和写库表操作是不同库表写操作，需要引入分布式事务保证数据一致性，极端情况可能带来数据的不一致。&lt;/p&gt;  &lt;p&gt;且索引表本身没有分库分表，自身可能会存在性能瓶颈，可以通过存储在 redis 进行优化处理。&lt;/p&gt;  &lt;img&gt;&lt;/img&gt;  &lt;h6&gt;思路六：分段索引关系表&lt;/h6&gt;  &lt;p&gt;思路五中，需要将全量数据存在到路由关系表中建立索引，再结合 range 分库分表方案思想，其实有些场景下完全没有必要全部数据建立索引，可以按照号段式建立区间索引，我们可以将分片键的区间对应库的关系通过关系表记录下来，每次查询操作，先去路由表中查询到数据所在的库表索引，然后再到库表中查询详细数据。&lt;/p&gt;  &lt;img&gt;&lt;/img&gt;  &lt;h6&gt;思路七：一致性 Hash 法&lt;/h6&gt;  &lt;p&gt;一致性 Hash 算法也是一种比较流行的集群数据分区算法，比如 RedisCluster 即是通过一致性 Hash 算法，使用 16384 个虚拟槽节点进行每个分片数据的管理。关于一致性 Hash 的具体原理这边不再重复描述，读者可以自行翻阅资料。&lt;/p&gt;  &lt;p&gt;其思想和思路五有异曲同工之妙。&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;本文从 5W1H 角度介绍了分库分表手段，其在   &lt;strong&gt;解决&lt;/strong&gt;如 IO 瓶颈、读写性能、物理存储瓶颈、内存瓶颈、单机故障影响面等   &lt;strong&gt;问题的同时&lt;/strong&gt;，   &lt;strong&gt;也带来&lt;/strong&gt;如事务性、主键冲突、跨库 join、跨库聚合查询   &lt;strong&gt;等问题&lt;/strong&gt;。anyway，在综合业务场景考虑，正如缓存的使用一样，非必须使用分库分表，则不应过度设计采用分库分表方案。如数据库确实成为性能瓶颈时，在设计分库分表方案时也应充分考虑方案的扩展性。或者说可以考虑采用成熟热门的分布式数据库解决方案，如 TiDB。&lt;/p&gt;  &lt;p&gt;---END---&lt;/p&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;strong&gt;推荐↓↓↓&lt;/strong&gt;  &lt;p&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>dev</category>
      <guid isPermaLink="true">https://itindex.net/detail/62586-%E6%96%87%E8%AF%BB-%E6%95%B0%E6%8D%AE%E5%BA%93-%E4%BC%98%E5%8C%96</guid>
      <pubDate>Thu, 12 Jan 2023 00:00:00 CST</pubDate>
    </item>
    <item>
      <title>服务端性能优化--最大QPS推算及验证 - huangyingsheng - 博客园</title>
      <link>https://itindex.net/detail/62539-%E6%9C%8D%E5%8A%A1-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96-qps</link>
      <description>&lt;div&gt;    &lt;h1&gt;服务端性能优化--最大QPS推算及验证&lt;/h1&gt;    &lt;p&gt;影响QPS（即吞吐量）的因素有哪些？每个开发都有自己看法，一直以为众说纷纭，例如：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;QPS受编程语言的影响。（PHP是最好的语言？）&lt;/li&gt;      &lt;li&gt;QPS主要受编程模型的影响，比如不是coroutine、是不是NIO、有没有阻塞？&lt;/li&gt;      &lt;li&gt;QPS主要由业务逻辑决定，业务逻辑越复杂，QPS越低。&lt;/li&gt;      &lt;li&gt;QPS受数据结构和算法的影响。&lt;/li&gt;      &lt;li&gt;QPS受线程数的影响。&lt;/li&gt;      &lt;li&gt;QPS受系统瓶颈的影响。&lt;/li&gt;      &lt;li&gt;QPS和RT关系非常紧密。&lt;/li&gt;      &lt;li&gt;more...&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;嗯，这些说法好像都对，但是好像又有点不对，好像总是不太完整，有没有一个系统点的说法能让人感觉一听就豁然开朗？      &lt;br /&gt;今天我们就这个话题来阐述一下，将一些现有的理论作为依据，把上方这些看起来比较零碎的看法总结归纳起来，希望能为服务端的性能提升进行一点优化，这也是一个优化的起点，未来才有可能做更多的优化，例如TCP、DNS、CDN、系统监控、多级缓存、多机房部署等等优化的手段。&lt;/p&gt;    &lt;img height="100" src="https://summer-blog-images.oss-cn-shanghai.aliyuncs.com/generic/v2-bc309f8a7eb0de9dd0c7277be07e9a46_1440w.jpg" width="100"&gt;&lt;/img&gt;    &lt;p&gt;      &lt;strong&gt;好了，废话不多说，直接开聊。&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;我们经常再做优化的时候，例如电商的促销秒杀等活动页，一开始可能会认为说Gzip并不是影响CPU的最大因子，直到拿出一次又一次的实验数据，研发们才开始慢慢接受（尬不尬），这是为什么？难道说Gzip真的是影响CPU的最大因子吗？那我们就拿出一点数据来验证一下对吧，接下来我们从RT着手开始慢慢了解，看到文章结尾就知道为什么Gzip和CPU的关系，同事也会发现，性能优化的相关知识其实也是体系化的，并不是分散零碎的。&lt;/p&gt;    &lt;h3&gt;RT&lt;/h3&gt;    &lt;p&gt;什么是 RT ？是概念还是名词还是理论？      &lt;br /&gt;      &lt;br /&gt;RT其实也没那么玄乎，就是 Response Time （就是响应时间嘛，哈哈哈），只不过看你目前在什么场景下，也许你是c端（app、pc等）的用户，响应时间是你请求服务器到服务器响应你的时间间隔，对于我们后端优化来说，就是接受到请求到响应用户的时间间隔。这听起来怎么感觉这不是在说废话吗？这说的不都是服务端的处理时间吗？不同在哪里？其实这里有个容易被忽略的因素，叫做网络开销。      &lt;br /&gt;所以服务端RT ≈ 网络开销 + 客户端RT。也就是说，一个差的网络环境会导致两个RT差距的悬殊（比如，从深圳访问上海的请求RT，远大于上海本地内的请求RT）&lt;/p&gt;    &lt;p&gt;客户端的RT则会直接影响客户体验，要降低客户端RT，提升用户的体验，必须考虑两点，第一点是服务端的RT，第二点是网络。对于网络来说常见的有CDN、AND、专线等等，分别适用于不同的场景，有机会写个blog聊一下这个话题。&lt;/p&gt;    &lt;p&gt;对于服务端RT来说，主要看服务端的做法。      &lt;br /&gt;有个公式：RT = Thread CPU Time + Thread Wait Time      &lt;br /&gt;从公式中可以看出，要想降低RT，就要降低 Thread CPU Time 或者 Thread Wait Time。这也是马上要重点深挖的一个知识点。&lt;/p&gt;    &lt;p&gt;      &lt;strong&gt;Thread CPU Time（简称CPU Time）        &lt;br /&gt;        &lt;br /&gt;Thread Wait Time（简称Wait Time）&lt;/strong&gt;&lt;/p&gt;    &lt;h3&gt;单线程QPS&lt;/h3&gt;    &lt;p&gt;我们都知道 RT 是由两部分组成 CPU Time + Wait Time 。那如果系统里只有一个线程或者一个进程并且进程中只有一个线程的时候，那么最大的 QPS 是多少呢？      &lt;br /&gt;假设 RT 是 199ms （CPU Time 为 19ms ，Wait Time 是 180ms ），那么 1000s以内系统可以接收的最大请求就是      &lt;br /&gt;1000ms/(19ms+180ms)≈5.025。&lt;/p&gt;    &lt;p&gt;所以得出单线程的QPS公式：&lt;/p&gt;    &lt;p&gt;&lt;/p&gt;    &lt;div&gt;\[单线程QPS = 1000ms/RT
\]&lt;/div&gt;    &lt;p&gt;&lt;/p&gt;    &lt;h3&gt;最佳线程数&lt;/h3&gt;    &lt;p&gt;还是上面的那个话题 （CPU Time 为 19ms ，Wait Time 是 180ms ），假设CPU的核数1。假设只有一个线程，这个线程在执行某个请求的时候，CPU真正花在该线程上的时间就是CPU Time，可以看做19ms，那么在整个RT的生命周期中，还有 180ms 的 Wait Time，CPU在做什么呢？抛开系统层面的问题（这里不考虑什么时间片轮循、上下文切换等等），可以认为CPU在这180ms里没做什么，至少对于当前的业务来说，确实没做什么。&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;一核的情况        &lt;br /&gt;由于每个请求的接收，CPU只需要工作19ms，所以在180ms的时间内，可以认为系统还可以额外接收180ms/19ms≈9个的请求。由于在同步模型中，一个请求需要一个线程来处理，因此，我们需要额外的9个线程来处理这些请求。这样，总的线程数就是：&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;&lt;/p&gt;    &lt;div&gt;\[（180ms + 19ms）/19ms ≈ 10个
\]&lt;/div&gt;    &lt;p&gt;&lt;/p&gt;    &lt;p&gt;        多线程之后，CPU Time从19ms变成了20ms，这1ms的差值代表多线程之后上下文切换、GC带来的额外开销（对于我们java来说是jvm，其他语言另外计算），这里的1ms只是代表一个概述，你也可以把它看做n。&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;p&gt;两核的情况          &lt;br /&gt;一核的情况下可以有10个线程，那么两核呢？在理想的情况下，可以认为最佳线程数为：2 x ( 180ms + 20ms )/20ms = 20个&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;CPU利用率          &lt;br /&gt;我们之前说的都是CPU满载下的情况，有时候由于某个瓶颈，导致CPU不得不有效利用，比如两核的CPU，因为某个资源，只能各自使用一半的能效，这样总的CPU利用率就变成了50%，再这样的情况下，最佳线程数应该是：50% x 2 x( 180ms + 20ms )/20ms = 10个          &lt;br /&gt;这个等式转换成公式就是：最佳线程数 = (RT/CPU Time) x CPU 核数 x CPU利用率          &lt;br /&gt;当然，这不是随便推测的，在收集到的很多的一些著作或者论坛的文档里都有这样的一些实验去论述这个公式或者这个说法是正确的。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;h3&gt;最大QPS&lt;/h3&gt;    &lt;h4&gt;1.最大QPS公式推导&lt;/h4&gt;    &lt;p&gt;假设我们知道了最佳线程数，同时我们还知道每个线程的QPS，那么线程数乘以每个线程的QPS既这台机器在最佳线程数下的QPS。所以我们可以得到下图的推算。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="image" src="https://summer-blog-images.oss-cn-shanghai.aliyuncs.com/qps_optimization/image_001.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;我们可以把分子和分母去约数，如下图。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="image" src="https://summer-blog-images.oss-cn-shanghai.aliyuncs.com/qps_optimization/image_002.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;于是简化后的公式如下图.&lt;/p&gt;    &lt;p&gt;      &lt;img alt="image" src="https://summer-blog-images.oss-cn-shanghai.aliyuncs.com/qps_optimization/image_003.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;从公式可以看出，决定QPS的时CPU Time、CPU核数和CPU利用率。CPU核数是由硬件做决定的，很难操纵，但是CPU Time和CPU利用率与我们的代码息息相关。&lt;/p&gt;    &lt;p&gt;虽然宏观上是正确的，但是推算的过程中还是有一点小小的不完美，因为多线程下的CPU Time（比如高并发下的GC次数增加消耗更多的CPU Time、线程上下文切换等等）和单线程的CPU Time是不一样的，所以会导致推算出来的结果有误差。&lt;/p&gt;    &lt;p&gt;尤其是在同步模型下的相同业务逻辑中，单线程时的CPU Time肯定会比大量多线程的CPU Time小，但是对于异步模型来说，切换的开销会变得小很多，为什么？这里先卖个葫芦吧，看完本篇就知道了。&lt;/p&gt;    &lt;p&gt;既然决定QPS的是CPU Time和CPU核数，那么这两个因子又是由谁来决定的呢？（越看越懵哈）&lt;/p&gt;    &lt;h4&gt;2.CPU Time&lt;/h4&gt;    &lt;p&gt;终于讲到了 CPU Time，CPU Time不只是业务逻辑所消耗的CPU时间，而是一次请求中所有环节上消耗的CPU时间之和。比如在web应用中，一个请求过来的HTTP的解析所消耗的CPU时间，是CPU Time的一部分。另外，这个请求中请求RPC的encode和decode所消耗的CPU时间也是CPU Time的一部分。&lt;/p&gt;    &lt;p&gt;那么CPU Time是由哪些因素决定的呢？两个关键字：数据结构+算法。      &lt;br /&gt;举一些例子吧&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;均摊问题&lt;/li&gt;      &lt;li&gt;hash问题&lt;/li&gt;      &lt;li&gt;排序和查找问题&lt;/li&gt;      &lt;li&gt;状态机问题&lt;/li&gt;      &lt;li&gt;序列化问题&lt;/li&gt;&lt;/ul&gt;    &lt;h4&gt;3.CPU利用率&lt;/h4&gt;    &lt;p&gt;CPU利用率不高的情况时常发生，一下因素都会影响CPU的利用率，从而影响系统可以支持的最大QPS。&lt;/p&gt;    &lt;h5&gt;1) IO能力&lt;/h5&gt;    &lt;ul&gt;      &lt;li&gt;磁盘IO&lt;/li&gt;      &lt;li&gt;网络IO        &lt;br /&gt;·带宽，比如某大促压力测试时，由于某个应用放在Tair中的数据量大，导致Tair的机器网卡跑满。        &lt;br /&gt;·网路链路，还是这次大促，借用了其他核心交换机下的机器，导致客户端RT明显增加。&lt;/li&gt;&lt;/ul&gt;    &lt;h5&gt;2) 数据库连接池（并发能力=PoolWaitTime/RT(Client) x PoolSize）。&lt;/h5&gt;    &lt;h5&gt;3) 内存不足，GC大量占用CPU，导致给业务逻辑使用的CPU利用率下降，而且GC时还满足Amdahl定律锁定义的场景。&lt;/h5&gt;    &lt;h5&gt;4) 共享资源的竞争，比如各种锁策略(读写锁、锁分离等)，各种阻塞队列，等等。&lt;/h5&gt;    &lt;h5&gt;5) 所依赖的其他后端服务QPS低造成的瓶颈。&lt;/h5&gt;    &lt;h5&gt;6) 线程数或者进程数，甚至编程模型(同步模型，异步模型)。&lt;/h5&gt;    &lt;p&gt;在压力测试过程中，出现最多的就是网络IO层面的问题，GC大量占用CPU Time之类的问题也经常出现。&lt;/p&gt;    &lt;h4&gt;4.CPU核数，Amdahl定律，Gustafson定律&lt;/h4&gt;    &lt;h5&gt;1)Amdahl定律(安达尔定律，不是达尔文定律！！！)&lt;/h5&gt;    &lt;p&gt;Amdahl定律是用来描述可伸缩性的，什么是可伸缩性？说白了就是比如增加计算机资源，如CPU、内存、宽带等，QPS能够相应的进行改进。&lt;/p&gt;    &lt;p&gt;既然Amdahl定律是描述可伸缩性的，那它是如何描述的呢？&lt;/p&gt;    &lt;p&gt;Amdahl在自己的论文中指出，可伸缩性是指在一个系统中，基于可并行化和串行化的组件各自所占的比例，当程序获得额外的计算资源(如CPU或者内存等)时，系统理论上能够获得的加速值(QPS或者其他指标可以翻几倍)。用一个公式来表达，如果F表示必须串行化执行的比例，那么在一个N核处理器的机器中，加速：&lt;/p&gt;    &lt;p&gt;&lt;/p&gt;    &lt;div&gt;\[Speedup \leq \frac{1}{F+\frac{1-F}{N}}
\]&lt;/div&gt;    &lt;p&gt;&lt;/p&gt;    &lt;p&gt;这个公式代表的意义是比较广泛的，在项目管理中也有一句类似的话：&lt;/p&gt;    &lt;p&gt;一个女人生一个孩子要9个月，但是永远不可能让9个女人在一个月内就生一个孩子。      &lt;br /&gt;我们根据这个例子套一个公式先，这里设F=100%，9个女人表示N=9，于是就有1/(100%+(1-100%)/9)=1，所以9个女人的加速比为1，等于没有加速。&lt;/p&gt;    &lt;p&gt;到这里，其实这个公式还只是描述了在增加资源的情况下系统的加速比，而不是在资源不变的情况下优化数据结构和算法之后带来的提升。优化数据结构和算法带来的提升要看前文中最大的QPS公式。不过这两个公式也不是完全没有联系的，在增加资源的情况下，它们的联系还是比较紧密的。&lt;/p&gt;    &lt;h5&gt;2) Gustafson定律（古斯塔夫森定律）&lt;/h5&gt;    &lt;p&gt;这个定律名字有点长，但这不是关键，关键的是，它是Amdahl定律的补充，公式为：&lt;/p&gt;    &lt;p&gt;&lt;/p&gt;    &lt;div&gt;\[S(P) = P-α·(P-1)
\]&lt;/div&gt;    &lt;p&gt;&lt;/p&gt;    &lt;p&gt;P是处理器的个数，α是串行时间占总执行时间的比例。      &lt;br /&gt;生孩子的案例再次套上这个公式，P为女人的个数，等于9，串行比例是100%。Speedup=9-100%x(9-1)=1，也就是9个女人是无法在一个月内把孩子生出来的……&lt;/p&gt;    &lt;p&gt;之所以说是Amdahl定律的补充，是因为两个定律的关系是相辅相成的关系。前者从串行和并行执行时间的角度来推导，后者从串行和并行的计算量角度来推导，不管是哪个角度，最终的结果其实是一样的。&lt;/p&gt;    &lt;h5&gt;3)CPU核数和Amdahl定律的关系&lt;/h5&gt;    &lt;p&gt;通过最大QPS公式，我们发现，在CPU Time和CPU利用率不变的情况下，核数越多，QPS就越大。比如核数从1到4，在CPU Time和CPU利用率不变的情况下，加速比应该是4，所以QPS应该也是增加4倍。&lt;/p&gt;    &lt;p&gt;这是资源增加（CPU核数增加）的情况下的加速比，也可以通过Amdahl定律来衡量，考虑串行和并行的比例在增加资源的情况下是否会改变。也就是要考虑在N增加的情况下，F受哪些因素的影响：&lt;/p&gt;    &lt;p&gt;&lt;/p&gt;    &lt;div&gt;\[Speedup \leq \frac{1}{F+\frac{1-F}{N}}
\]&lt;/div&gt;    &lt;p&gt;&lt;/p&gt;    &lt;p&gt;只要F大于0，最大QPS就不会翻4倍。      &lt;br /&gt;      &lt;br /&gt;一个公式说要增加4被倍，一个定理说 没有4倍，互相矛盾？      &lt;br /&gt;      &lt;br /&gt;其实事情是这样的，通过最大QPS公式，我们可以发现，如果CPU Time和CPU 利用率不变，核数从1增加到4，QPS会相应的增加4倍。但是在实际情况下，当核数增加时，CPU Time和CPU 利用率大部分时候是变化的，所以前面的假设不成立，即一般场景下QPS不能增加4倍。&lt;/p&gt;    &lt;p&gt;而Amdahl定律中的N变化时，F也可能会变化，即一般场景下，最大QPS并不能增加4被，所以这其实并不矛盾。相反它们是相辅相成的。这里一定要注意，这里说的是一般场景，如果你的场景完全没有串行（程序没有串行，系统没有串行，上下文切换没有串行，什么串行都没有），那么理论上是可以增加4倍的。&lt;/p&gt;    &lt;p&gt;为什么增加计算机资源时，最大QPS公式中的CPU Time和CPU利用率会变化，F也会变化呢？我们可以从宏观上分析一下，增加计算机资源时，达到满载:&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;p&gt;QPS会更高，单位时间内产生的对象会更多。在同等条件下，minor GC被触发的次数增加，还有些场景发生过对象多到响应没返回它们就进了“老年代”，从而fullGC被触发。宏观上，这是属于串行的部分，对于Amdahl公式来说F会受到影响，对于最大QPS公式来说，CPU Time和CPU利用率也受到影响.&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;在同步模型下大量的线程在完成一次请求中，上下文被切换的次数大大增加。&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;尤其是在有 串行模块的时候，串行的执行和等待时间增加，F会变化，某些场景下CPU          &lt;br /&gt;利用率也达不到理想效果，这取决于你的代码。这也是要做锁分离、为什么要缩小同步          &lt;br /&gt;块的原因。当然还有锁自身的优化，比如偏向、自旋、读写分离等技术，都是为了不断          &lt;br /&gt;地减少Amdahl定律中的F，也是为了减少CPU Time ( 锁本身的优化)，提高CPU利          &lt;br /&gt;用率(使用锁的方法的优化)。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;。锁本身的优化最为津津乐道的是自旋、偏向、自适应，synchronized分析，还有reetrantLock的代码及AQS等等。&lt;/p&gt;    &lt;p&gt;。使用锁的优化方法最常见的是缩小锁区间、锁分离、无锁化、volatile。&lt;/p&gt;    &lt;p&gt;所以在增加计算资源时，更高的并发产生，会引起最大QPS公式中两个参数的变化，也会      &lt;br /&gt;引起Amdahl定律中F值的变化，同时公式和定律变化的趋势是相同的。Amdahl定律是得到广      &lt;br /&gt;泛认可的，也是得到数据验证的。最大QPS公式好像没有人验证过，这里引用一个比较有名的      &lt;br /&gt;测试结果，如下图.&lt;/p&gt;    &lt;p&gt;      &lt;img alt="image" src="https://summer-blog-images.oss-cn-shanghai.aliyuncs.com/qps_optimization/image_005.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;当计算资源(处理器数量)增加时，在串行部分比例不变的情况下，CPU利用率下降。&lt;/li&gt;      &lt;li&gt;当计算资源(处理器数量)增加时，串行占的比例越大，CPU利用率下降得越多。&lt;/li&gt;&lt;/ul&gt;    &lt;h3&gt;实验数据验证公式&lt;/h3&gt;    &lt;p&gt;所以其实到现在我们一直在说理论，带了一点点的公式，听起来好像是那么回事，但是公式到底怎么用？准不准确？可以精准测试还是概要测试即可？&lt;/p&gt;    &lt;p&gt;我们接下来实验一下吧。&lt;/p&gt;    &lt;p&gt;!!! 接下来会涉及到CPU Time 包含了操作系统对CPU的消耗，比如进程，线程调度等。&lt;/p&gt;    &lt;h4&gt;1.数据准备&lt;/h4&gt;    &lt;p&gt;这里就用之前的一个电商活动页面的优化来说吧，在这个过程中，我们做了大量测试，由于测试中使用了localhost方式，所以Java进程在IO上的Wait Time是非常小的。接下来，由于最佳线程数接近CPU核数,      &lt;br /&gt;所以在两核的机器上使用了10个Java进程，客户端发起了10个并发请求,这是在最佳线程数下(最佳线程数在一个区间里，在这个区间里QPS总体变化不大,并且也用了5、15个并发测试效果，发现QPS值基本相同)得出的大量结果，接下来就分析一下这些测试结果，见下表。&lt;/p&gt;    &lt;h5&gt;1)测试QPS结果&lt;/h5&gt;    &lt;table&gt;      &lt;tr&gt;        &lt;th&gt;原始页面大小&lt;/th&gt;        &lt;th&gt;压缩后的大小&lt;/th&gt;        &lt;th&gt;优化前QPS&lt;/th&gt;        &lt;th&gt;优化后QPS&lt;/th&gt;        &lt;th&gt;优化前RT&lt;/th&gt;        &lt;th&gt;优化后RT&lt;/th&gt;&lt;/tr&gt;      &lt;tr&gt;        &lt;td&gt;92kb&lt;/td&gt;        &lt;td&gt;17kb&lt;/td&gt;        &lt;td&gt;164&lt;/td&gt;        &lt;td&gt;2024&lt;/td&gt;        &lt;td&gt;60.7ms&lt;/td&gt;        &lt;td&gt;4.9ms&lt;/td&gt;&lt;/tr&gt;      &lt;tr&gt;        &lt;td&gt;138kb&lt;/td&gt;        &lt;td&gt;8.7kb&lt;/td&gt;        &lt;td&gt;143&lt;/td&gt;        &lt;td&gt;1859&lt;/td&gt;        &lt;td&gt;69.8ms&lt;/td&gt;        &lt;td&gt;3.3ms&lt;/td&gt;&lt;/tr&gt;      &lt;tr&gt;        &lt;td&gt;182kb&lt;/td&gt;        &lt;td&gt;11.4kb&lt;/td&gt;        &lt;td&gt;121&lt;/td&gt;        &lt;td&gt;2083&lt;/td&gt;        &lt;td&gt;82.3ms&lt;/td&gt;        &lt;td&gt;4.8ms&lt;/td&gt;&lt;/tr&gt;      &lt;tr&gt;        &lt;td&gt;248kb&lt;/td&gt;        &lt;td&gt;32kb&lt;/td&gt;        &lt;td&gt;77&lt;/td&gt;        &lt;td&gt;1977&lt;/td&gt;        &lt;td&gt;129.6ms&lt;/td&gt;        &lt;td&gt;5.0ms&lt;/td&gt;&lt;/tr&gt;      &lt;tr&gt;        &lt;td&gt;295kb&lt;/td&gt;        &lt;td&gt;34.4kb&lt;/td&gt;        &lt;td&gt;70&lt;/td&gt;        &lt;td&gt;1722&lt;/td&gt;        &lt;td&gt;141.1ms&lt;/td&gt;        &lt;td&gt;5.8ms&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;    &lt;p&gt;      &lt;strong&gt;我们其实只要关注各项优化前后的QPS变化即可。&lt;/strong&gt;&lt;/p&gt;    &lt;h5&gt;2） CPU利用率&lt;/h5&gt;    &lt;p&gt;由于Apache Bench和Java部署在同一台机器.上，所以CPU利用率应该减去Apache Bench      &lt;br /&gt;的CPU资源消耗。根据观察，优化前Apache Bench 的CPU消耗在1.7%到2%之间，优化后      &lt;br /&gt;Apache Bench 的CPU资源消耗在20%左右。为什么优化前后有这么大的差距呢?因为优化后      &lt;br /&gt;响应能够及时返回，所以导致Apache Bench使用的CPU资源多了。&lt;/p&gt;    &lt;p&gt;在接下来的计算中，我们将优化前的CPU利用率设置为98%，优化后的CPU利用率设置为80%。&lt;/p&gt;    &lt;h5&gt;3）CPU Time 计算公式&lt;/h5&gt;    &lt;p&gt;根据QPS的计算方法，把QPS挪到右边的分母中，CPU  Time移到等号左边，就会得到下图的公式。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="image" src="https://summer-blog-images.oss-cn-shanghai.aliyuncs.com/qps_optimization/image_004.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;h5&gt;4) CPU Time计算示例&lt;/h5&gt;    &lt;p&gt;根据上方列出的三点（CPU利用率、QPS和CPU核数），接下里我们就详细的描述一下推算方法了。&lt;/p&gt;    &lt;h4&gt;计算得到的CPU Time&lt;/h4&gt;    &lt;p&gt;根据上方的表格计算方法，利用QPS计算出各页面理论上的CPU Time，计算后的结果如下表：&lt;/p&gt;    &lt;table&gt;      &lt;tr&gt;        &lt;th&gt;原始页面92kb&lt;/th&gt;        &lt;th&gt;计算公式&lt;/th&gt;&lt;/tr&gt;      &lt;tr&gt;        &lt;td&gt;优化前CPU Time计算&lt;/td&gt;        &lt;td&gt;1000 / 164 x 2 x 0.98 = 12ms&lt;/td&gt;&lt;/tr&gt;      &lt;tr&gt;        &lt;td&gt;优化后CPU Time计算&lt;/td&gt;        &lt;td&gt;1000 / 2024 x 2 x 0.8 = 0.8ms&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;    &lt;p&gt;      &lt;br /&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;table&gt;      &lt;tr&gt;        &lt;th&gt;原始页面大小&lt;/th&gt;        &lt;th&gt;压缩后的大小&lt;/th&gt;        &lt;th&gt;优化前QPS&lt;/th&gt;        &lt;th&gt;优化后QPS&lt;/th&gt;        &lt;th&gt;优化前CPU Time&lt;/th&gt;        &lt;th&gt;优化后CPU Time&lt;/th&gt;&lt;/tr&gt;      &lt;tr&gt;        &lt;td&gt;92kb&lt;/td&gt;        &lt;td&gt;17kb&lt;/td&gt;        &lt;td&gt;164&lt;/td&gt;        &lt;td&gt;2024&lt;/td&gt;        &lt;td&gt;12ms&lt;/td&gt;        &lt;td&gt;0.8ms&lt;/td&gt;&lt;/tr&gt;      &lt;tr&gt;        &lt;td&gt;138kb&lt;/td&gt;        &lt;td&gt;8.7kb&lt;/td&gt;        &lt;td&gt;143&lt;/td&gt;        &lt;td&gt;1859&lt;/td&gt;        &lt;td&gt;13.7ms&lt;/td&gt;        &lt;td&gt;0.86ms&lt;/td&gt;&lt;/tr&gt;      &lt;tr&gt;        &lt;td&gt;182kb&lt;/td&gt;        &lt;td&gt;11.4kb&lt;/td&gt;        &lt;td&gt;121&lt;/td&gt;        &lt;td&gt;2083&lt;/td&gt;        &lt;td&gt;16.2ms&lt;/td&gt;        &lt;td&gt;0.77ms&lt;/td&gt;&lt;/tr&gt;      &lt;tr&gt;        &lt;td&gt;248kb&lt;/td&gt;        &lt;td&gt;32kb&lt;/td&gt;        &lt;td&gt;77&lt;/td&gt;        &lt;td&gt;1977&lt;/td&gt;        &lt;td&gt;25.5ms&lt;/td&gt;        &lt;td&gt;0.81ms&lt;/td&gt;&lt;/tr&gt;      &lt;tr&gt;        &lt;td&gt;295kb&lt;/td&gt;        &lt;td&gt;34.4kb&lt;/td&gt;        &lt;td&gt;70&lt;/td&gt;        &lt;td&gt;1722&lt;/td&gt;        &lt;td&gt;28ms&lt;/td&gt;        &lt;td&gt;0.93ms&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;    &lt;p&gt;这里主要看一下各项的CPU Time优化前后的变化，接下来，我们把两个值做减法，然后和开篇中提到过的实测程序中Gzip的CPU Time进行对比。&lt;/p&gt;    &lt;h4&gt;实测CPU Time&lt;/h4&gt;    &lt;h5&gt;1） 5个页面的Gzip所消耗的CPU Time&lt;/h5&gt;    &lt;p&gt;实测5个页面做Gzip所消耗的时间，然后跟公式计算出来的CPU Time做一个对比，如下表：&lt;/p&gt;    &lt;table&gt;      &lt;tr&gt;        &lt;th&gt;原始页面大小&lt;/th&gt;        &lt;th&gt;CPU Time公式差值(上表的CPU Time差值)&lt;/th&gt;        &lt;th&gt;Gzip CPU Time测量值(10次平均值)&lt;/th&gt;        &lt;th&gt;差值&lt;/th&gt;&lt;/tr&gt;      &lt;tr&gt;        &lt;td&gt;92kb&lt;/td&gt;        &lt;td&gt;11.2ms&lt;/td&gt;        &lt;td&gt;8ms&lt;/td&gt;        &lt;td&gt;3.2ms&lt;/td&gt;&lt;/tr&gt;      &lt;tr&gt;        &lt;td&gt;138kb&lt;/td&gt;        &lt;td&gt;12.8ms&lt;/td&gt;        &lt;td&gt;7ms&lt;/td&gt;        &lt;td&gt;5.8ms&lt;/td&gt;&lt;/tr&gt;      &lt;tr&gt;        &lt;td&gt;182kb&lt;/td&gt;        &lt;td&gt;15.4ms&lt;/td&gt;        &lt;td&gt;9ms&lt;/td&gt;        &lt;td&gt;6.4ms&lt;/td&gt;&lt;/tr&gt;      &lt;tr&gt;        &lt;td&gt;248kb&lt;/td&gt;        &lt;td&gt;24.7ms&lt;/td&gt;        &lt;td&gt;21ms&lt;/td&gt;        &lt;td&gt;3.0ms&lt;/td&gt;&lt;/tr&gt;      &lt;tr&gt;        &lt;td&gt;295kb&lt;/td&gt;        &lt;td&gt;27.1ms&lt;/td&gt;        &lt;td&gt;23ms&lt;/td&gt;        &lt;td&gt;4.1ms&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;    &lt;p&gt;可以看到，计算出来的CPU Time值要比测出来的要多一点，多了几毫秒，这是为什么呢?      &lt;br /&gt;      &lt;br /&gt;      &lt;br /&gt;其实是因为在优化前，有两个消耗CPU Time的阶段，一个是执行Java代码时，另一个是执行Gzip时。而优化后，整个逻辑变成了从缓存获取数据后直接返回，只有非常少的Java代码在消耗CPU Time(10行以内)。&lt;/p&gt;    &lt;h5&gt;2） Java页面执行消耗的CPU Time&lt;/h5&gt;    &lt;p&gt;大体上可以认为：&lt;/p&gt;    &lt;p&gt;优化前的CPU Time - 优化后的CPU Time = Gzip CPU Time + 全页面Java代码的CPU Time&lt;/p&gt;    &lt;p&gt;在实验中，一开始只统计了 Gzip 本身的消耗，而在 Java 文件中 Java代码执行的时间并没有包含在内，所以两者差距比较大。于是，我们单独统计了5个页面的Java代码的执行时间，发现文件中Java码执行的时间为3~6ms。实际测量的Gzip CPU Time 加上3~6ms的Java代码执行时间，和使用公式计算出来的CPUTime基本吻合。&lt;/p&gt;    &lt;p&gt;根据上面的计算和测量结果，我们发现 Gzip 的  CPU Time消耗加上Java代码的CPU Time消耗，与公式测量出来的总的CPU Time消耗非常接近，误差为1~2ms。考虑到CPU Time测量是单线程测量，而压力测试QPS是并发情况下(会多出进程切换的开销和GC等的开销)，我们认为这点误差是合理的，测试结果说明公式在宏观上是正确的。&lt;/p&gt;    &lt;h3&gt;压力测试最佳线程数和QPS临界点&lt;/h3&gt;    &lt;p&gt;前面讲到了公式的推导，并在一个固定的条件下验证了公式在该场景下的正确性。&lt;/p&gt;    &lt;p&gt;假设在一个thread-per-client的场景，有一一个Ajax请求，这个请求返回一个Json字符串，每个请求的CPU Time为1ms，WaitTime为300ms(比如读写Socket和线程调度的等待开销)。那么最佳线程数是&lt;/p&gt;    &lt;p&gt;&lt;/p&gt;    &lt;div&gt;\[(300+1/1 )x4x 100%=1204
\]&lt;/div&gt;    &lt;p&gt;&lt;/p&gt;    &lt;p&gt;尤其在广域网上，Wait Time=300ms是正常的数值。在国际环境下，300ms就更加常见了。这意味着如果是4核的机器，需要1204个线程，如果是8核的机器则需要2408个线程。实际上，有些HTTP服务的CPU Time是远小于1ms的，比如上面的场景中将页面压缩并缓存起来之后,CPU Time基本为0.8ms,如果WaitTime还是300ms,那么需要数以千计的线程啊!当线程数不断增加的时候，到达某个临界点之后对系统就开始产生负面影响了。&lt;/p&gt;    &lt;p&gt;(1) 大量线程上下文切换的开销，引起CPU Time的增加及QPS的减少。所以，有时候还没有达到最佳线程数，而QPS已经开始略微下降了。因为CPU Time发生变化、线程多了之后，调度引起的CPU Time提升的百分比和QPS下降的百分比成正比(上方的QPS公式)，上下文切换带来的开销如下。&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;上下文切换(微妙级别)&lt;/li&gt;      &lt;li&gt;JVM本身的开销&lt;/li&gt;      &lt;li&gt;CPU Cache加载&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;(2)线程的栈空间会占用大量的内存，假设每个线程的栈空间是1MB，这么多的线程就要占用数GB的内存。&lt;/p&gt;    &lt;p&gt;(3)在CPU Time不变的情况下，因为线程上下文切换和操作系统想尽力为线程在宏观上平均分配时间片的行为，导致每个线程的Wait Time都增加了，于是每个请求的RT也增加了，最终就会产生用户体验下降的情况。&lt;/p&gt;    &lt;p&gt;可以 用一张图来表示一下临界点的概念。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="image" src="https://summer-blog-images.oss-cn-shanghai.aliyuncs.com/qps_optimization/image_006.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;由于线程数增加超过某个临界点会影响CPU Time、QPS和RT，所以很难精确测量高并发下的CPU Time,它随着机器硬件、操作系统、线程数等因素不断变化。我们能做的就是压力测试QPS，并在压力测试的过程中调整线程数，使QPS达到临界点，这个临界点是QPS的一个峰值点。这个峰值点的线程数即当前系统的最佳线程数，当然如果这个时候CPU利用率没有达到100%，那么证明系统中可能存在瓶颈，应该在找到并处理瓶颈之后继续压力测试，并且重新找到这个临界点。&lt;/p&gt;    &lt;p&gt;当数据结构发生改变、算法改进或者业务逻辑发生改变时，最佳线程数有可能会跟着变化。&lt;/p&gt;    &lt;h3&gt;总结：&lt;/h3&gt;    &lt;p&gt;这篇 blog的例子中，在CPU Time下降到1ms左右而Wait Time需要数百毫秒的场景下，我们需要很多线程。但是当达到这个线程数的时候，有可能早就达到了临界点。所以系统整体已经不是最健康的状态了，但是现有的编程模型已经阻碍了我们前进，那么应该怎么办呢?为使某个系统达到最优状态?&lt;/p&gt;    &lt;p&gt;所以下一篇blog我们来说一下编程中的同步模型和异步模型问题，以及为什么异步模型只需要这么少的线程，是不是公式在异步模型下失效了。&lt;/p&gt;    &lt;br /&gt;    &lt;br /&gt;    &lt;br /&gt;    &lt;br /&gt;    &lt;br /&gt;    &lt;p&gt;我的个人站点      &lt;br /&gt;SUMMER      &lt;a href="https://www.huangyingsheng.com/about" rel="noopener" target="_blank"&gt;https://www.huangyingsheng.com/about&lt;/a&gt;&lt;/p&gt;    &lt;br /&gt;    &lt;br /&gt;    &lt;br /&gt;    &lt;br /&gt;    &lt;br /&gt;&lt;/div&gt;
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62539-%E6%9C%8D%E5%8A%A1-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96-qps</guid>
      <pubDate>Mon, 12 Dec 2022 15:48:36 CST</pubDate>
    </item>
    <item>
      <title>美团外卖搜索基于Elasticsearch的优化实践</title>
      <link>https://itindex.net/detail/62495-%E7%BE%8E%E5%9B%A2%E5%A4%96%E5%8D%96-%E6%90%9C%E7%B4%A2-elasticsearch</link>
      <description>&lt;blockquote&gt;
  &lt;p&gt;美团外卖搜索工程团队在Elasticsearch的优化实践中，基于Location-Based Service（LBS）业务场景对Elasticsearch的查询性能进行优化。该优化基于Run-Length Encoding（RLE）设计了一款高效的倒排索引结构，使检索耗时（TP99）降低了84%。本文从问题分析、技术选型、优化方案等方面进行阐述，并给出最终灰度验证的结论。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;h2&gt;1. 前言&lt;/h2&gt;
 &lt;p&gt;最近十年，Elasticsearch 已经成为了最受欢迎的开源检索引擎，其作为离线数仓、近线检索、B端检索的经典基建，已沉淀了大量的实践案例及优化总结。然而在高并发、高可用、大数据量的 C 端场景，目前可参考的资料并不多。因此，我们希望通过分享在外卖搜索场景下的优化实践，能为大家提供 Elasticsearch 优化思路上的一些借鉴。&lt;/p&gt;
 &lt;p&gt;美团在外卖搜索业务场景中大规模地使用了 Elasticsearch 作为底层检索引擎。其在过去几年很好地支持了外卖每天十亿以上的检索流量。然而随着供给与数据量的急剧增长，业务检索耗时与 CPU 负载也随之上涨。通过分析我们发现，当前检索的性能热点主要集中在倒排链的检索与合并流程中。针对这个问题，我们基于 Run-length Encoding（RLE）  &lt;sup&gt;[1]&lt;/sup&gt; 技术设计实现了一套高效的倒排索引，使倒排链合并时间（TP99）降低了 96%。我们将这一索引能力开发成了一款通用插件集成到 Elasticsearch 中，使得 Elasticsearch 的检索链路时延（TP99）降低了 84%。&lt;/p&gt;
 &lt;h2&gt;2. 背景&lt;/h2&gt;
 &lt;p&gt;当前，外卖搜索业务检索引擎主要为 Elasticsearch，其业务特点是具有较强的 Location Based Service（LBS） 依赖，即用户所能点餐的商家，是由商家配送范围决定的。对于每一个商家的配送范围，大多采用多组电子围栏进行配送距离的圈定，一个商家存在多组电子围栏，并且随着业务的变化会动态选择不同的配送范围，电子围栏示意图如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;1 &amp;#30005;&amp;#23376;&amp;#22260;&amp;#26639;&amp;#31034;&amp;#24847;&amp;#22270;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a40da921da3e46cca29865bf169c8586~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;考虑到商家配送区域动态变更带来的问题，我们没有使用 Geo Polygon  &lt;sup&gt;[2]&lt;/sup&gt; 的方式进行检索，而是通过上游一组 R-tree 服务判定可配送的商家列表来进行外卖搜索。因此，LBS 场景下的一次商品检索，可以转化为如下的一次 Elasticsearch 搜索请求：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;POST food/_search
{
   &amp;quot;query&amp;quot;: {
      &amp;quot;bool&amp;quot;: {
         &amp;quot;must&amp;quot;:{
            &amp;quot;term&amp;quot;: { &amp;quot;spu_name&amp;quot;: { &amp;quot;value&amp;quot;: &amp;quot;烤鸭&amp;quot;} }
           //...
         },
         &amp;quot;filter&amp;quot;:{
           &amp;quot;terms&amp;quot;: {
              &amp;quot;wm_poi_id&amp;quot;: [1,3,18,27,28,29,...,37465542] // 上万
            }
         }
      }
   }
  //...
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;对于一个通用的检索引擎而言，Terms 检索非常高效，平均到每个 Term 查询耗时不到0.001 ms。因此在早期时，这一套架构和检索 DSL 可以很好地支持美团的搜索业务——耗时和资源开销尚在接受范围内。然而随着数据和供给的增长，一些供给丰富区域的附近可配送门店可以达到 20000~30000 家，这导致性能与资源问题逐渐凸显。这种万级别的 Terms 检索的性能与耗时已然无法忽略，仅仅这一句检索就需要 5~10 ms。&lt;/p&gt;
 &lt;h2&gt;3. 挑战及问题&lt;/h2&gt;
 &lt;p&gt;由于 Elasticsearch 在设计上针对海量的索引数据进行优化，在过去的 10 年间，逐步去除了内存支持索引的功能（例如 RAMDirectory 的删除）。为了能够实现超大规模候选集的检索，Elasticsearch/Lucene 对 Term 倒排链的查询流程设计了一套内存与磁盘共同处理的方案。&lt;/p&gt;
 &lt;p&gt;一次 Terms 检索的流程分为两步：分别检索单个 Term 的倒排链，多个 Term 的倒排链进行合并。&lt;/p&gt;
 &lt;h3&gt;3.1 倒排链查询流程&lt;/h3&gt;
 &lt;ol&gt;
  &lt;li&gt;从内存中的 Term Index 中获取该 Term 所在的 Block 在磁盘上的位置。&lt;/li&gt;
  &lt;li&gt;从磁盘中将该 Block 的 TermDictionary 读取进内存。&lt;/li&gt;
  &lt;li&gt;对倒排链存储格式的进行 Decode，生成可用于合并的倒排链。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;可以看到，这一查询流程非常复杂且耗时，且各个阶段的复杂度都不容忽视。所有的 Term 在索引中有序存储，通过二分查找找到目标 Term。这个有序的 Term 列表就是 TermDictionary ，二分查找 Term 的时间复杂度为 O(logN) ，其中 N 是 Term 的总数量 。Lucene 采用 Finite State Transducer  &lt;sup&gt;[3]&lt;/sup&gt;（FST）对 TermDictionary 进行编码构建 Term Index。FST 可对 Term 的公共前缀、公共后缀进行拆分保存，大大压缩了 TermDictionary 的体积，提高了内存效率，FST 的检索速度是 O(len(term))，其对于 M 个 Term 的检索复杂度为 O(M * len(term))。&lt;/p&gt;
 &lt;h3&gt;3.2 倒排链合并流程&lt;/h3&gt;
 &lt;p&gt;在经过上述的查询，检索出所有目标 Term 的 Posting List 后，需要对这些 Posting List 求并集（OR 操作）。在 Lucene 的开源实现中，其采用 Bitset 作为倒排链合并的容器，然后遍历所有倒排链中的每一个文档，将其加入 DocIdSet 中。&lt;/p&gt;
 &lt;p&gt;伪代码如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;Input:  termsEnum: 倒排表；termIterator：候选的term
Output: docIdSet : final docs set
for term in termIterator:
  if termsEnum.seekExact(term) != null:
     docs = read_disk()  // 磁盘读取
     docs = decode(docs) // 倒排链的decode流程
     for doc in docs:
        docIdSet.or(doc) //代码实现为DocIdSetBuilder.add。
end for
docIdSet.build()//合并，排序，最终生成DocIdSetBuilder，对应火焰图最后一段。
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;假设我们有 M 个 Term，每个 Term 对应倒排链的平均长度为 K，那么合并这 M 个倒排链的时间复杂度为：O(K * M + log(K * M))。 可以看出倒排链合并的时间复杂度与 Terms 的数量 M 呈线性相关。在我们的场景下，假设一个商家平均有一千个商品，一次搜索请求需要对一万个商家进行检索，那么最终需要合并一千万个商品，即循环执行一千万次，导致这一问题被放大至无法被忽略的程度。&lt;/p&gt;
 &lt;p&gt;我们也针对当前的系统做了大量的调研及分析，通过美团内部的 JVM Profile 系统得到 CPU 的火焰图，可以看到这一流程在 CPU 热点图中的反映也是如此：无论是查询倒排链、还是读取、合并倒排链都相当消耗资源，并且可以预见的是，在供给越来越多的情况下，这三个阶段的耗时还会持续增长。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;2 profile &amp;#28779;&amp;#28976;&amp;#22270;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/897ba0cfa43f40cd8ee9b853313f246a~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;可以明确，我们需要针对倒排链查询、倒排链合并这两个问题予以优化。&lt;/p&gt;
 &lt;h2&gt;4. 技术探索与实践&lt;/h2&gt;
 &lt;h3&gt;4.1 倒排链查询优化&lt;/h3&gt;
 &lt;p&gt;通常情况下，使用 FST 作为 Term 检索的数据结构，可以在内存开销和计算开销上取得一个很好的平衡，同时支持前缀检索、正则检索等多种多样的检索 Query，然而在我们的场景之下，FST 带来的计算开销无法被忽视。&lt;/p&gt;
 &lt;p&gt;考虑到在外卖搜索场景有以下几个特性：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;Term 的数据类型为 long 类型。&lt;/li&gt;
  &lt;li&gt;无范围检索，均为完全匹配。&lt;/li&gt;
  &lt;li&gt;无前缀匹配、模糊查找的需求，不需要使用前缀树相关的特性。&lt;/li&gt;
  &lt;li&gt;候选数量可控，每个商家的商品数量较多，即 Term 规模可预期，内存可以承载这个数量级的数据。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;因此在我们的应用场景中使用空间换取时间是值得的。&lt;/p&gt;
 &lt;p&gt;对于 Term 查询的热点：可替换 FST 的实现以减少 CPU 开销，常见的查找数据结构中，哈希表有 O(1) 的查询复杂度，将 Term 查找转变为对哈希表的一次查询。&lt;/p&gt;
 &lt;p&gt;对于哈希表的选取，我们主要选择了常见的 HashMap 和 LongObjectHashMap。&lt;/p&gt;
 &lt;p&gt;我们主要对比了 FST、HashMap 和 LongObjectHashMap（哈希表的一种高性能实现）的空间和时间效率。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;strong&gt;在内存占用上&lt;/strong&gt;：FST 的内存效率极佳。而 HashMap/LongObjectHashMap 都有明显的劣势；&lt;/li&gt;
  &lt;li&gt;   &lt;strong&gt;在查询时间上&lt;/strong&gt;：FST 的查询复杂度在 O (len(term))，而 Hash/LongObjectHashMap 有着 O(1) 的查询性能；&lt;/li&gt;
&lt;/ul&gt;
 &lt;blockquote&gt;
  &lt;p&gt;注：检索类型虽然为 Long，但是我们将底层存储格式进行了调整，没有使用开源的 BKD Tree 实现，使用 FST 结构，仅与 FST 进行对比。BKD Tree 在大批量整数 terms 的场景下劣势更为明显。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;我们使用十万个 &amp;lt;Long, Long&amp;gt; 的键值对来构造数据，对其空间及性能进行了对比，结果如下：&lt;/p&gt;
 &lt;table&gt;

  &lt;tr&gt;
   &lt;th&gt;&lt;/th&gt;
   &lt;th&gt;内存占用&lt;/th&gt;
   &lt;th&gt;10 万个 Key 的查询时间&lt;/th&gt;
&lt;/tr&gt;


  &lt;tr&gt;
   &lt;td&gt;FST&lt;/td&gt;
   &lt;td&gt;481kB&lt;/td&gt;
   &lt;td&gt;63ms&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;HashMap&lt;/td&gt;
   &lt;td&gt;9048kB&lt;/td&gt;
   &lt;td&gt;3.5ms&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;LongObjectHashMap&lt;/td&gt;
   &lt;td&gt;5545kB&lt;/td&gt;
   &lt;td&gt;1ms&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;结论&lt;/td&gt;
   &lt;td&gt;FST &amp;gt;&amp;gt; LongObjectHashMap &amp;gt; HashMap&lt;/td&gt;
   &lt;td&gt;LongObjectHashMap &amp;gt; HashMap &amp;gt;&amp;gt; FST&lt;/td&gt;
&lt;/tr&gt;

&lt;/table&gt;
 &lt;p&gt;可以看到， 在内存占用上 FST 要远优于 LongObjectHashMap 和 HashMap。而在查询速度上 LongObjectHashMap 最优。&lt;/p&gt;
 &lt;p&gt;我们最终选择了 LongObjectHashMap 作为倒排链查询的数据结构。&lt;/p&gt;
 &lt;h3&gt;4.2 倒排链合并&lt;/h3&gt;
 &lt;p&gt;基于上述问题，我们需要解决两个明显的 CPU 热点问题：倒排链读取 &amp;amp; 倒排链合并。我们需要选择合适的数据结构缓存倒排链，不再执行磁盘 /page cache 的 IO。数据结构需要必须满足以下条件：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;支持批量 Merge，减少倒排链 Merge 耗时。&lt;/li&gt;
  &lt;li&gt;内存占用少，需要处理千万数量级的倒排链。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;在给出具体的解决方案之前，先介绍一下 Lucene 对于倒排合并的原生实现、RoaringBitMap、Index Sorting。&lt;/p&gt;
 &lt;h4&gt;4.2.1 原生实现&lt;/h4&gt;
 &lt;p&gt;Lucene在不同场景上使用了不同的倒排格式，提高整体的效率（空间/时间），通过火焰图可以发现，在我们的场景上，TermInSetQuery 的倒排合并逻辑开销最大。&lt;/p&gt;
 &lt;p&gt;TermInSetQuery 的倒排链合并操作分为两个步骤：倒排链读取和合并。&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;
   &lt;p&gt;倒排链读取：&lt;/p&gt;
   &lt;p&gt;Lucene 倒排链压缩存储在索引文件中，倒排链读取需要实时解析，其对外暴露的 API 为迭代器结构。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;倒排链合并：&lt;/p&gt;
   &lt;p&gt;倒排链合并主要由 DocIdSetBuilder 合并生成倒排链，先使用稀疏结构存储 Doc ID，当  Doc ID 个数超过一定阈值时，升级到稠密结构（FixedBitSet）存储，实现方式如下（对应代码 IntArrayDocIdSet/BitDocIdSet）：&lt;/p&gt;
   &lt;ul&gt;
    &lt;li&gt;稀疏数据：存储采用 List&amp;lt;int[]&amp;gt; array 方式存储 Doc ID，最终经过 Merge 和排序形成一个有序数组 int[]，耗时主要集中在数组申请和排序。&lt;/li&gt;
    &lt;li&gt;稠密数据：基于 long[] 实现的 bitmap 结构（FixedBitSet），耗时主要集中在 FixedBitSet 的插入过程，由于倒排链需要实时 Decode 以及 FixedBitSet 的底层实现，无法实现批量 Merge，只能循环单个 Doc ID 插入，数据量大的情况下，耗时明显。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
 &lt;blockquote&gt;
  &lt;p&gt;我们采用线上流量和数据压测发现该部分平均耗时约 7 ms。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;h4&gt;4.2.2 RoaringBitmap&lt;/h4&gt;
 &lt;p&gt;当前 Elasticsearch 选择 RoaringBitMap 做为 Query Cache 的底层数据结构缓存倒排链，加快查询速率。&lt;/p&gt;
 &lt;p&gt;RoaringBitmap 是一种压缩的位图，相较于常规的压缩位图能提供更好的压缩，在稀疏数据的场景下空间更有优势。以存放 Integer 为例，Roaring Bitmap 会对存入的数据进行分桶，每个桶都有自己对应的 Container。在存入一个32位的整数时，它会把整数划分为高 16 位和低 16 位，其中高 16 位决定该数据需要被分至哪个桶，我们只需要存储这个数据剩余的低 16 位，将低 16 位存储到 Container 中，若当前桶不存在数据，直接存储 null 节省空间。 RoaringBitmap有不同的实现方式，下面以 Lucene 实现（RoaringDocIdSet）进行详细讲解：&lt;/p&gt;
 &lt;p&gt;如原理图中所示，RoaringBitmap 中存在两种不同的 Container：Bitmap Container 和 Array Container。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;3 Elasticsearch&amp;#20013;Roaringbitmap&amp;#30340;&amp;#31034;&amp;#24847;&amp;#22270;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8fbe6c0df21f47459b5611bd6d4d657a~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;这两种 Container 分别对应不同的数据场景——若一个 Container 中的数据量小于 4096 个时，使用 Array Container 来存储。当 Array Container 中存放的数据量大于 4096 时，Roaring Bitmap 会将 Array Container 转为 Bitmap Container。即 Array Container 用于存放稀疏数据，而 Bitmap Container 用于存放稠密数据，这样做是为了充分利用空间。下图给出了随着容量增长 Array Container 和 Bitmap Container 的空间占用对比图，当元素个数达到 4096 后（每个元素占用 16 bit ），Array Container 的空间要大于 Bitmap Container。&lt;/p&gt;
 &lt;p&gt;备注：Roaring Bitmap 可参考官方博客  &lt;sup&gt;[4]&lt;/sup&gt;。&lt;/p&gt;
 &lt;h4&gt;4.2.3 Index Sorting&lt;/h4&gt;
 &lt;p&gt;Elasticsearch 从 6.0 版本开始支持 Index Sorting  &lt;sup&gt;[5]&lt;/sup&gt; 功能，在索引阶段可以配置多个字段进行排序，调整索引数据组织方式，可以调整文档所对应的 Doc ID。以 city_id，poi_id 为例：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;4 Index Sorting &amp;#31034;&amp;#24847;&amp;#22270;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c52912322f8e4e14bae7f1865d93e3af~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;如上示例所示：Index Sorting 会将给定的排序字段（如上图的 city_id 字段）的文档排序在一起，相同排序值的文档的 Doc ID 严格自增，对该字段建立倒排，那么其倒排链为自增数列。&lt;/p&gt;
 &lt;h3&gt;4.3 基于 RLE 的倒排格式设计&lt;/h3&gt;
 &lt;p&gt;基于以上的背景知识以及当前 Elasticsearch/Lucene 的解决方案，可以明确目前有 2 个改造点需要考虑。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;合适的倒排结构，用于存储每个 Term 的倒排链。&lt;/li&gt;
  &lt;li&gt;合适的中间结构，用于存储多个 Term 合并后的倒排链。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;对于索引倒排格式 PostingsEnum，支持接口为：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;public abstract class DocIdSetIterator {
  public abstract int docID();
  public abstract int nextDoc();
  public abstract int advance(int target);
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;倒排仅支持单文档循环调用，不支持批量读取，因此需要为倒排增加批量顺序读取的功能。&lt;/p&gt;
 &lt;p&gt;对于多倒排链的合并，由于原结构 DocIdSetBuilder 的实现也不支持批量对数据进行合并，我们探索了评估了 Elasticsearch 用于缓存 Query Cache 的数据结构 RoaringBitMap，然而其实现 RoaringDocIdSet 也无法满足我们对缓存数据结构特性需求，主要问题：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;原生 RoaringDocIdSet 在构建时，只能支持递增的添加 Doc ID。而在实际生产中每一个商家的商品的 Doc ID 都是离散的。这就限制了其使用范围。&lt;/li&gt;
  &lt;li&gt;原生 RoaringDocIdSet 的底层存储结构 Bitmap Container 和 Array Container 均不支持批量合并，这就无法满足我们对倒排链合并进行优化的需求。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;在明确这个问题的场景下，我们可以考虑最简单的改造，支持索引倒排格式 PostingsEnum 的批量读取。并考虑了如下几种场景：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;在支持批量读取倒排的情况下，直接使用原结构 DocIdSetBuilder 进行批量的合并。&lt;/li&gt;
  &lt;li&gt;在支持批量读取倒排的情况下，使用 RoaringBitMap 进行批量合并。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;然而我们发现即使对 bitset 进行分段合并，直接对数据成段进行 OR 操作，整体开销下降并不明显。其原因主要在于：对于读取的批量结果，均为稀疏分布的 Doc ID，仅减少倒排的循环调用无法解决性能开销问题。&lt;/p&gt;
 &lt;p&gt;那么问题需要转化为如何解决 Doc ID 分布稀疏的问题。在上文提及的 Index Sorting 即一个绝佳的解决方案。并且由于业务 LBS 的特点，一次检索的全部结果集均集中在某个地理位置附近，以及我们检索仅针对门店列表 ID 的特殊场景，我们最终选择对城市 ID、 Geohash、门店 ID 进行排序，从而让稀疏分布的 Doc ID 形成连续分布。在这样的排序规则应用之后，我们得到了一组绝佳的特性：每一个商家所对应的商品，其 Doc ID 完全连续。&lt;/p&gt;
 &lt;h4&gt;4.3.1 Run-Length Encoding&lt;/h4&gt;
 &lt;p&gt;Run-Length Encoding  &lt;sup&gt;[3]&lt;/sup&gt;（RLE）技术诞生于50年前，最早应用于连续的文本压缩及图像压缩。在 2014 年，第一个开源在 GitHub 的 RoaringBitmap 诞生  &lt;sup&gt;[6]&lt;/sup&gt;，2016年，在 RoaringBitMap 的基础上增加了对于自增序列的 RLE 实现  &lt;sup&gt;[7]&lt;/sup&gt;，并应用在 bitmap 这一场景上。&lt;/p&gt;
 &lt;p&gt;在 bitmap 这一场景之下，主要通过压缩连续区间的稠密数据，节省内存开销。以数组 [1, 2, 3, ..., 59, 60, 89, 90, 91, ..., 99, 100] 为例（如下图上半部分）：使用 RLE 编码之后就变为 [1, 60, 89, 12]——形如 [start1, length1, start2, length2, ...] 的形式，其中第一位为连续数字的起始值，第二位为其长度。&lt;/p&gt;
 &lt;p&gt;在数组完全连续场景下中，对 32768 个 id (short) 进行存储，数组存储需要 64 kB，Bitmap 存储需要使用 4 kB，而 RLE 编码后直接存储仅需要 4 byte。在这一特性下，如果商家倒排链完全有序，那么商家的倒排链，可被压缩到最低仅需要两个整数即可表示。&lt;/p&gt;
 &lt;p&gt;当然 RLE 并不适用所有情况，在目标序列完全不连续的场景下，如 [1, 3, 5, 7, ... , M]，RLE 编码存储需要使用 2 * M byte的空间，比数组直接存储的空间效率差一倍。&lt;/p&gt;
 &lt;p&gt;为了和 Elasticsearch 的实现保持一致，我们决定使用 RoaringBitMap 作为倒排存储的结构，以及中间结果合并的数据结构。针对 RoaringDocIdSet 我们进行了如下改造。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;5 &amp;#20498;&amp;#25490;&amp;#38142;Merge&amp;#26041;&amp;#24335;&amp;#30340;&amp;#28436;&amp;#36827;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4835e9579d414b24a59057f20998830a~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h4&gt;4.3.2 RLE Container 的实现&lt;/h4&gt;
 &lt;p&gt;在对商家 ID 字段开启 Index Sorting 之后，同商家的商品 ID 已经连续分布。那么对于商家字段的倒排链就是严格自增且无空洞的整数序列。我们采用RLE编码对倒排链进行编码存储。由于将倒排链编码为 [start1, length1, start2, length2, ...]，更特殊的，在我们场景下，一个倒排链的表示为  [start, length]，RLE编码做到了对倒排链的极致压缩，假设倒排链为 [1, 2, ...., 1000]， 用 ArrayContainer 存储，内存空间占用为 16 bit * 100 = 200 Byte, RLE 编码存储只需要 16 bit * 2 = 4 Byte。考虑到具体的场景分布，以及其他场景可能存在多段有序倒排的情况，我们最终选择了 [start1, length1, start2, length2, ...] 这样的存储格式，且 [start,  start + length] 之间两两互不重叠。&lt;/p&gt;
 &lt;p&gt;对于多个商家的倒排合并流程，对于该格式的合并，我们并不需要对 M 个倒排链长度为 K 进行循环处理，这个问题转变为：如何对多组分段 [start, length] 进行排序，并将排序后的结果合并为一个数组。那么我们将原时间复杂度为 $O(K * M + log(K * M))$ 的合并流程，改造为复杂度为 O(M * logM) 的合并流程，大大降低了合并的计算耗时，减少了 CPU 的消耗。&lt;/p&gt;
 &lt;h4&gt;4.3.3 SparseRoaringDocIdSet 实现&lt;/h4&gt;
 &lt;p&gt;我们在 RoaringDocIdSet 的基础上增加了 RLE Container 后，性能已经得到了明显的提升，加速了 50%，然而依然不符合我们的预期。我们通过对倒排链的数据分析发现：倒排链的平均长度不大，基本在十万内。但是其取值范围广 [ 0, Integer.MAX-1 ]。这些特征说明，如果以 RoaringDocIdSet 按高 16 位进行分桶的话，大部分数据将集中在其中连续的几个桶中。&lt;/p&gt;
 &lt;p&gt;在 Elasticsearch 场景上，由于无法预估数据分布，RoaringDocIdSet 在申请 bucket 容器的数组时，会根据当前 Segment 中的最大 Doc ID 来申请，计算公式为：(maxDoc + (1 &amp;lt;&amp;lt; 16) -  1) &amp;gt;&amp;gt;&amp;gt; 16。这种方式可以避免每次均按照 Integer.MAX-1 来创建容器带来的无谓开销。然而，当倒排链数量偏少且分布集中时，这种方式依然无法避免大量 bucket 被空置的空间浪费；另一方面，在对倒排链进行合并时，这些空置的 bucket 也会参与到遍历中，即使它被置为了空。这就又造成了性能上的浪费。我们通过压测评估证实了这一推理，即空置的 bucket 在合并时也会占用大量的 CPU 资源。&lt;/p&gt;
 &lt;p&gt;针对这一问题，我们设计了一套用于稀疏数据的方案，实现了 SparseRoaringDocIdSet，同时保持接口与 RoaringDocIdSet 一致，可在各场景下进行复用，其结构如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;class SparseRoaringDocIdSet {
   int[] index;// 记录有 container 的 bucket Index
   Container[] denseSets;// 记录紧密排列的倒排链
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;保存倒排链的过程与 RoaringDocIDSet 保持一致，在确认具体的 Container 的分桶时，我们额外使用一组索引记录所有有值的 bucket 的 location。&lt;/p&gt;
 &lt;p&gt;下图是一组分别使用 RLE based RoaringDocIdSet 和 SparseRoaringDocIdSet 对 [846710, 100, 936858, 110] 倒排链（RLE 编码）进行存储的示意图：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;6 SparseRoaringDocIdSet &amp;#32534;&amp;#25490;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a2d5ae0003934a148e9c91596dfa4bd6~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;可以看到：在 SparseRoaringDocIdSet 实现下，所有不为空的 bucket 被紧密的排列在了一起，并在 index [] 中记录了其原始 bucket 的索引，这就避免了大量 bucket 被空置的情况。另外，在进行倒排链的合并时，就可以直接对紧密排列的 denseSet 进行遍历，并从 index [] 获得其对应的原始 bucket location，这就避免了大量的空置 bucket 在合并时带来的性能浪费。&lt;/p&gt;
 &lt;p&gt;我们分别对以下4个场景进行了压测：原生的 TermInSetQuery 对倒排链的合并逻辑、基于 FixedBitSet 的批量合并、RLE based  RoaringBitmap、RLE based Dense RoaringBitmap。对 10000 个平均长度为 100 的倒排链进行合并压测，压测结果如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;7 &amp;#20498;&amp;#25490;&amp;#38142;Merge&amp;#24615;&amp;#33021;&amp;#27604;&amp;#23545;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/64c3b31624244d3db48585cd5d82306e~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;我们实现的 RLE based Dense RoaringBitmap，相比官方的基准实现耗时降低了 96%（TP99 13 ms 下降至 0.5 ms）。&lt;/p&gt;
 &lt;h3&gt;4.4 功能集成&lt;/h3&gt;
 &lt;p&gt;至此，核心的倒排索引问题已经解决，后续主要为工程问题：如何在 Elasticsearch 系统中集成基于 RLE 的倒排格式。对于高吞吐高并发的C端在线场景，我们希望尽可能保障线上的稳定，对开源数据格式的兼容，保障前向的兼容，做到随时可降级。&lt;/p&gt;
 &lt;p&gt;工程部分主要分为两部分：倒排索引的集成和在线检索链路。以下主要介绍全量索引部分的链路设计。&lt;/p&gt;
 &lt;h4&gt;4.4.1 倒排索引集成&lt;/h4&gt;
 &lt;p&gt;倒排索引格式的改造，一般情况下，需要实现一套 PostingsFormat，并实现对应的 Reader、Writer。为了保证对原有检索语句的兼容，支持多种场景的检索，以及为了未来能够无缝的配合 Elasticsearch 的版本升级，我们并没有选择直接实现一组新的 PostingsFormat，避免出现不兼容的情况导致无法升级版本。我们选择了基于现有的倒排格式，在服务加载前后初始化 RLE 倒排，并考虑到业务场景，我们决定将 RLE 倒排全量放入内存中，以达到极致的性能。具体的解决方案为：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;索引加载过程中增加一组 Hook，用于获取现有的 InternalEngine（ Elasticsearch中负责索引增删改查的主要对象 ）。&lt;/li&gt;
  &lt;li&gt;对所有的 semgents 遍历读取数据，解析倒排数据。&lt;/li&gt;
  &lt;li&gt;对所有配置了 RLE 倒排优化的字段，生成 RLE 倒排表。&lt;/li&gt;
  &lt;li&gt;将 RLE 倒排表与 segment 做关联，保证后续的检索链路中能获取到倒排表。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;为了避免内存泄漏，我们也将索引删除，segment 变更的场景进行了相应的处理。&lt;/p&gt;
 &lt;h4&gt;4.4.2 在线检索链路&lt;/h4&gt;
 &lt;p&gt;在线检索链路也采用了无侵入兼容的实现，我们实现了一套新的检索语句，并且在索引无 RLE 倒排的情况下，可以降级回原生的检索类型，更加安全。&lt;/p&gt;
 &lt;p&gt;我们基于 Elasticsearch 的插件机制，生成一组新的 Query，实现了其 AbstractQueryBuilder，实现对 Query 的解析与改写，并将 Query 与 RLE 倒排进行关联，我们通过改写具体的检索实现，将整个链路集成到 Elasticsearch 中。&lt;/p&gt;
 &lt;h2&gt;5. 性能收益&lt;/h2&gt;
 &lt;p&gt;对于 Elasticsearch 而言，一次检索分为这么几个阶段，可参考下图  &lt;sup&gt;[8]&lt;/sup&gt;。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;8 Elasticsearch&amp;#30340;&amp;#26816;&amp;#32034;&amp;#36807;&amp;#31243;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/67072d5340684282b0089c5f962a7a99~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;由协调节点进行请求的分发，发送到各个检索节点上。&lt;/li&gt;
  &lt;li&gt;每个数据节点的各自进行检索，并返回检索结果给协调节点，这一段各个数据节点的耗时即“数据节点查询耗时”。&lt;/li&gt;
  &lt;li&gt;协调节点等待所有数据节点的返回，协调节点选取 Top K 后进行 fetch 操作。1～3 步的完整耗时为“完整链路查询耗时”。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;我们将上述改动（Index Sorting + Dense Roaring Bitmap + RLE）上线到生产环境的商品索引后，性能如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;9 &amp;#25968;&amp;#25454;&amp;#33410;&amp;#28857;&amp;#26597;&amp;#35810;&amp;#32791;&amp;#26102;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a308001cdcea4431880e2db03d5aa5c3~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;10 &amp;#23436;&amp;#25972;&amp;#38142;&amp;#36335;&amp;#26597;&amp;#35810;&amp;#32791;&amp;#26102;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bcd12c11e90a4ae9b694322be97e03cb~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;至此，我们成功将全链路的检索时延（TP99）降低了 84%（100 ms 下降至 16 ms），解决了外卖搜索的检索耗时问题，并且线上服务的 CPU 也大大降低。&lt;/p&gt;
 &lt;h2&gt;6. 总结与展望&lt;/h2&gt;
 &lt;p&gt;本文主要针对搜索业务场景中遇到的问题，进行问题分析、技术选型、压测、选择合适的解决方案、集成、灰度验证。我们最终实现了一套基于 RLE 倒排格式，作为一种新型的倒排格式，彻底解决了这个场景上的性能瓶颈，从分析至上线的流程长达数月。本文希望能提供一个思路，让其他同学在遇到 Elasticsearch 相关的性能问题时，也能遵循相同的路径，解决业务上的问题。&lt;/p&gt;
 &lt;p&gt;一般的，我们分析问题可以遵循这样的路径：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;明确性能问题后，首先通过流量录制，获得一个用于后续基准压测的测试集合。&lt;/li&gt;
  &lt;li&gt;通过相关的性能分析工具，先明确是否存在 CPU 的热点或 IO 问题，对于 Java 技术栈，有很多常见的可用于分析性能的工具，美团内部有 Scaple 分析工具，外部可以使用 JProfiler、Java Flight Recorder、Async Profiler、Arthas、perf 这些工具。&lt;/li&gt;
  &lt;li&gt;对分析火焰图进行分析，配合源代码，进行数据分析和验证。&lt;/li&gt;
  &lt;li&gt;此外在 Elasticsearch 中还可以通过 Kibana 的 Search Profiler 用于协助定位问题。在录制大量的流量，抽样分析后，以我们的场景为例，进行 Profiler 后可以明确 TermInSetQuery 占用了一半以上的耗时。&lt;/li&gt;
  &lt;li&gt;明确问题后从索引、检索链路两侧进行分析，评估问题，进行多种解决方案的设计与尝试，通过 Java Microbenchmark Harness（JMH）代码基准测试工具，验证解决方案的有效性。&lt;/li&gt;
  &lt;li&gt;集成验证最终效果。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;11 Kibana&amp;#20013;&amp;#30340;Search Profiler&amp;#31034;&amp;#20363;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f1560090a54e471db6e3cdb3c7cb97ab~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;我们最终实现的关键点：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;使用哈希表来实现索引 Term 的精确查找，以此减少倒排链的查询与读取的时间。&lt;/li&gt;
  &lt;li&gt;选取 RoaringBitmap 作为存储倒排链的数据结构，并与 RLE Container 相结合，实现对倒排链的压缩存储。当然，最重要的是，RLE 编码将倒排链的合并问题转换成了排序问题，实现了批量合并，从而大幅度减少了合并的性能消耗。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;当然，我们的方案也还具有一些可以继续探索优化的地方。我们进行具体方案开发的时候，主要考虑解决我们特定场景的问题，做了一些定制化，以取得最大的性能收益。在一些更通用的场景上，也可以通过 RLE 倒排获得收益，例如根据数据的分布，自动选择 bitmap/array/RLE 容器，支持倒排链重叠的情况下的数据合并。&lt;/p&gt;
 &lt;p&gt;我们在发现也有论文与我们的想法不谋而合，有兴趣了解可以参考具体论文  &lt;sup&gt;[9]&lt;/sup&gt;。另外，在增量索引场景下，如果增量索引的变更量非常大，那么势必会遇到频繁更新内存 RLE 倒排的情况，这对内存和性能的消耗都不小，基于性能的考量，也可以直接将 RLE 倒排索引的结构固化到文件中，即在写索引时就完成对倒排链的编码，这样就能避免这一问题。&lt;/p&gt;
 &lt;h2&gt;7. 作者简介&lt;/h2&gt;
 &lt;p&gt;泽钰、张聪、晓鹏等，均来自美团到家事业群/搜索推荐技术部-搜索工程团队。&lt;/p&gt;
 &lt;h2&gt;8. 参考文献&lt;/h2&gt;
 &lt;p&gt;[1] https://en.wikipedia.org/wiki/Run-length_encoding&lt;/p&gt;
 &lt;p&gt;[2] https://www.elastic.co/guide/en/elasticsearch/reference/7.10/query-dsl-geo-polygon-query.html&lt;/p&gt;
 &lt;p&gt;[3] https://en.wikipedia.org/wiki/Finite-state_transducer&lt;/p&gt;
 &lt;p&gt;[4] Frame of Reference and Roaring Bitmaps&lt;/p&gt;
 &lt;p&gt;[5] https://www.elastic.co/cn/blog/index-sorting-elasticsearch-6-0&lt;/p&gt;
 &lt;p&gt;[6] Chambi S, Lemire D, Kaser O, et al. Better bitmap performance with roaring bitmaps[J]. Software: practice and experience, 2016, 46(5): 709-719.&lt;/p&gt;
 &lt;p&gt;[7] Lemire D, Ssi‐Yan‐Kai G, Kaser O. Consistently faster and smaller compressed bitmaps with roaring[J]. Software: Practice and Experience, 2016, 46(11): 1547-1569.&lt;/p&gt;
 &lt;p&gt;[8] 检索两阶段流程：https://www.elastic.co/guide/cn/elasticsearch/guide/current/_fetch_phase.html#_fetch_phase&lt;/p&gt;
 &lt;p&gt;[9] Arroyuelo D, González S, Oyarzún M, et al. Document identifier reassignment and run-length-compressed inverted indexes for improved search performance[C]//Proceedings of the 36th international ACM SIGIR conference on Research and development in information retrieval. 2013: 173-182.&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;阅读美团技术团队更多技术文章合集&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&amp;mid=2651765958&amp;idx=1&amp;sn=8201546812e5a95a2bee9dffc6d12f00&amp;chksm=bd12658b8a65ec9de2f5be1e96796dfb3c8f1a374d4b7bd91266072f557caf8118d4ddb72b07#rd"&gt;前端&lt;/a&gt; |   &lt;a href="http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&amp;mid=2651765957&amp;idx=1&amp;sn=c4014e8901b62257b5275567f407abc8&amp;chksm=bd1265888a65ec9e6c6f4f2dc5206d727e5fe91f580f91c326da3dbab381e3da58f75cd52204#rd"&gt;算法&lt;/a&gt; |   &lt;a href="http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&amp;mid=2651765966&amp;idx=1&amp;sn=24485355d27894e8a1b5a1746f6e3235&amp;chksm=bd1265838a65ec95dbb6e54e12170aeb9f6643b9fa0abf221c0f301c03e84543f67294785b77#rd"&gt;后端&lt;/a&gt; |   &lt;a href="http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&amp;mid=2651765964&amp;idx=1&amp;sn=ab6d8db147234fe57f27dd46eec40fef&amp;chksm=bd1265818a65ec9749246dd1a2eb3bf7798772cc4d5b4283b15eae2f80bc6db63a1471a9e61e#rd"&gt;数据&lt;/a&gt; |   &lt;a href="http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&amp;mid=2651765965&amp;idx=1&amp;sn=37e0c56c8b080146ce5249243bfd84d8&amp;chksm=bd1265808a65ec96d3a2b2c87c6e27c910d49cb6b149970fb2db8bf88045a0a85fed2e6a0b84#rd"&gt;安全&lt;/a&gt; |   &lt;a href="http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&amp;mid=2651765963&amp;idx=1&amp;sn=a3de9ef267d07d94118c1611776a4b28&amp;chksm=bd1265868a65ec906592d25ad65f2a8516338d07ec3217059e6975fc131fc0107d66a8cd2612#rd"&gt;运维&lt;/a&gt; |   &lt;a href="http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&amp;mid=2651765973&amp;idx=1&amp;sn=32a23bf1d278dda0398f993ab60a697e&amp;chksm=bd1265988a65ec8e630ef4d24b4946ab6bd7e66702c1d712481cf3c471468a059c470a14c30d#rd"&gt;iOS&lt;/a&gt; |   &lt;a href="http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&amp;mid=2651765972&amp;idx=1&amp;sn=afe02ec92762c1ce18740d03324c4ac3&amp;chksm=bd1265998a65ec8f10d5f58d0f3681ddfc5325137218e568e1cda3a50e427749edb5c6a7dcf5#rd"&gt;Android&lt;/a&gt; |   &lt;a href="http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&amp;mid=2651765974&amp;idx=1&amp;sn=763c1e37d04acffd0142a2852ecfb000&amp;chksm=bd12659b8a65ec8dfcfeb2028ef287fae7c38f134a665375ba420556ce5d2e4cf398147bd12e#rd"&gt;测试&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;|&lt;/strong&gt;  在公众号菜单栏对话框回复【2021年货】、【2020年货】、【2019年货】、【2018年货】、【2017年货】等关键词，可查看美团技术团队历年技术文章合集。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9fb8f0b78f2948d4b590a00df546d1a2~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;| 本文系美团技术团队出品，著作权归属美团。欢迎出于分享和交流等非商业目的转载或使用本文内容，敬请注明“内容转载自美团技术团队”。本文未经许可，不得进行商业性转载或者使用。任何商用行为，请发送邮件至tech@meituan.com申请授权。&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62495-%E7%BE%8E%E5%9B%A2%E5%A4%96%E5%8D%96-%E6%90%9C%E7%B4%A2-elasticsearch</guid>
      <pubDate>Fri, 18 Nov 2022 15:52:59 CST</pubDate>
    </item>
    <item>
      <title>网站速度终极优化</title>
      <link>https://itindex.net/detail/62494-%E7%BD%91%E7%AB%99-%E9%80%9F%E5%BA%A6-%E7%BB%88%E6%9E%81</link>
      <description>&lt;p&gt;  &lt;img alt="" src="https://cdn.pixabay.com/photo/2018/05/20/10/13/racing-3415413_960_720.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;好久没有折腾网站速度了。最近尝试再次优化提升网站的访问速度。&lt;/p&gt;
 &lt;p&gt;利用了CDN厂商Cloudflare的Page rules，创建了三条页面规则，将后台登录页面免除缓存外，其它页面全部都缓存在Cloudflare的全球CDN数据中心。&lt;/p&gt;
 &lt;p&gt;这样全球访客访问本网站的时候，除了第一次要从源主机上索取生成页面，其它都不再需要，直接从最近的CDN数据中心获取，这样既减轻了网站主机的资源消耗压力，又直接提升了访客的访问速度。&lt;/p&gt;
 &lt;p&gt;鉴于Google Anlytics统计数据越来越不好用（Cloudflare本身自带Web Analytics），再加上我也没有动机去使用它，索性把统计代码取消了，这样一来既减少了网页页面请求数量，也没有了cookies记录，因此访问过的网页会直接缓存在访客的浏览器中一段时间（设置为1天）。&lt;/p&gt;
 &lt;p&gt;去除了Google Anlytics统计功能后，访客在本网站上来回浏览的时候，短时间内去CDN上下载网页都不必了——CDN那边会返回304告诉浏览器页面没有改动，所以直接调用原缓存页面，进一步提升网站访问速度。&lt;/p&gt;
 &lt;p&gt;这样的网站速度终极优化方案，把本网站的加载速度基本控制在500毫秒以内，比秒开还要快。&lt;/p&gt;
 &lt;p&gt;这么多年后，再一次化繁为简、返朴归真，回归到博客网站写作的初心，其它的东西都不必要。&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>信息技术 博客 网站优化</category>
      <guid isPermaLink="true">https://itindex.net/detail/62494-%E7%BD%91%E7%AB%99-%E9%80%9F%E5%BA%A6-%E7%BB%88%E6%9E%81</guid>
      <pubDate>Fri, 18 Nov 2022 16:05:30 CST</pubDate>
    </item>
    <item>
      <title>优化 Kubernetes 中的 Java 无服务器函数</title>
      <link>https://itindex.net/detail/62472-%E4%BC%98%E5%8C%96-kubernetes-java</link>
      <description>&lt;blockquote&gt;
  &lt;p&gt;在 Kubernetes 中运行无服务器函数时，实现更快的启动速度和更小的内存占用。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;  &lt;img height="720" src="https://img.linux.net.cn/data/attachment/album/202210/26/151603a4a44w1a71zk8b11.jpg" title="Ship captain sailing the Kubernetes seas" width="1280"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;由于运行上千个应用程序容器荚Pod所耗费的资源多，令它实现较少工作节点和资源占用所需成本也较高，所以在使用   &lt;a href="https://opensource.com/article/19/6/reasons-kubernetes"&gt;Kubernetes&lt;/a&gt; 时，快速启动和较少的内存占用是至关重要的。在 Kubernetes 平台运行容器化微服务时，内存占用是比吞吐量更重要的考量因素，这是因为：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;由于需要持续运行，所以耗费资源更多（不同于 CPU 占用）&lt;/li&gt;
  &lt;li&gt;微服务令开销成本成倍增加&lt;/li&gt;
  &lt;li&gt;一个单体应用程序变为若干个微服务的情况（例如 20 个微服务占用的存储空间约有 20GB）&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;这些情况极大影响了无服务器函数的发展和 Java 部署模型。到目前为止，许多企业开发人员选择 Go、Python 或 Node.js 这些替代方案来解决性能瓶颈，直到出现了   &lt;a href="https://quarkus.io/"&gt;Quarkus&lt;/a&gt; 这种基于 kubernetes 的原生 Java 堆栈，才有所改观。本文介绍如何在使用了 Quarkus 的 kubernetes 平台上进行性能优化，以便运行无服务器函数。&lt;/p&gt;
 &lt;h3&gt;容器优先的设计理念&lt;/h3&gt;
 &lt;p&gt;由于 Java 生态系统中传统的框架都要进行框架的初始化，包括配置文件的处理、  &lt;code&gt;classpath&lt;/code&gt; 的扫描、类加载、注解的处理以及构建元模型，这些过程都是必不可少的，所以它们都比较耗费资源。如果使用了几种不同的框架，所耗费的资源也是成倍增加。&lt;/p&gt;
 &lt;p&gt;Quarkus 通过“左移shifting left”，把所有的资源开销大的操作都转移到构建阶段，解决了这些 Java 性能问题。在构建阶段进行代码和框架分析、字节码转换和动态元模型生成，而且只有一次，结果是：运行时可执行文件经过高度优化，启动非常快，不需要经过那些传统的启动过程，全过程只在构建阶段执行一次。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="Quarkus Build phase" src="https://img.linux.net.cn/data/attachment/album/202210/26/151634r3l3gzbp27bkke0f.png" title="Quarkus Build phase"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;更重要的是：Quarkus 支持构建原生可执行文件，它具有良好性能，包括快速启动和极小的驻留集大小resident set size（RSS）内存占用，跟传统的云原生 Java 栈相比，具备即时扩展的能力和高密度的内存利用。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="Quarkus RSS and Boot Time Metrics" src="https://img.linux.net.cn/data/attachment/album/202210/26/151635p95d33596ilju4rd.png" title="Quarkus RSS and Boot Time Metrics"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;这里有个例子，展示如何使用 Quarkus 将一个   &lt;a href="https://opensource.com/article/21/5/what-serverless-java"&gt;Java 无服务器&lt;/a&gt; 项目构建为本地可执行文件。&lt;/p&gt;
 &lt;h3&gt;1、使用 Quarkus 创建无服务器 Maven 项目&lt;/h3&gt;
 &lt;p&gt;以下命令生成一个 Quarkus 项目，（例如   &lt;code&gt;quarkus-serverless-native&lt;/code&gt;）以此创建一个简单的函数：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;$ mvn io.quarkus:quarkus-maven-plugin:1.13.4.Final:create \
       -DprojectGroupId=org.acme \
       -DprojectArtifactId=quarkus-serverless-native \
       -DclassName=&amp;quot;org.acme.getting.started.GreetingResource&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;2、构建一个本地可执行文件&lt;/h3&gt;
 &lt;p&gt;你需要使用 GraalVM 为 Java 程序构建一个本地可执行文件。你可以选择 GraalVM 的任何发行版，例如   &lt;a href="https://www.graalvm.org/community/"&gt;Oracle GraalVM Community Edition (CE)&lt;/a&gt; 或   &lt;a href="https://github.com/graalvm/mandrel"&gt;Mandrel&lt;/a&gt;（Oracle GraalVM CE 的下游发行版）。Mandrel 是为支持 OpenJDK 11 上的 Quarkus-native 可执行文件的构建而设计的。&lt;/p&gt;
 &lt;p&gt;打开   &lt;code&gt;pom.xml&lt;/code&gt;，你将发现其中的   &lt;code&gt;native&lt;/code&gt; 设置。你将使用它来构建本地可执行文件。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;profiles&amp;gt;
    &amp;lt;profile&amp;gt;
        &amp;lt;id&amp;gt;native&amp;lt;/id&amp;gt;
        &amp;lt;properties&amp;gt;
            &amp;lt;quarkus.package.type&amp;gt;native&amp;lt;/quarkus.package.type&amp;gt;
        &amp;lt;/properties&amp;gt;
    &amp;lt;/profile&amp;gt;
&amp;lt;/profiles&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
 &lt;blockquote&gt;
  &lt;p&gt;   &lt;strong&gt;注意：&lt;/strong&gt; 你可以在本地安装 GraalVM 或 Mandrel 发行版。你也可以下载 Mandrel 容器映像来构建它（像我那样），因此你还需要在本地运行一个容器引擎（例如 Docker）。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;假设你已经打开了容器运行时，此时需要运行一下 Maven 命令：&lt;/p&gt;
 &lt;p&gt;使用   &lt;a href="https://www.docker.com/"&gt;Docker&lt;/a&gt; 作为容器引擎：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;$ ./mvnw package -Pnative \
  -Dquarkus.native.container-build=true \
  -Dquarkus.native.container-runtime=docker
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;使用   &lt;a href="https://podman.io/"&gt;Podman&lt;/a&gt; 作为容器引擎：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;$ ./mvnw package -Pnative \
  -Dquarkus.native.container-build=true \
  -Dquarkus.native.container-runtime=podman
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;输出信息结尾应当是   &lt;code&gt;BUILD SUCCESS&lt;/code&gt;。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="Native Build Logs" src="https://img.linux.net.cn/data/attachment/album/202210/26/151635iuy2m9l5zul5zu5i.png" title="Native Build Logs"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;不借助 JVM 直接运行本地可执行文件：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;$ target/quarkus-serverless-native-1.0.0-SNAPSHOT-runner
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;输出信息类似于：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,&amp;lt; / /_/ /\ \  
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/  
INFO  [io.quarkus] (main) quarkus-serverless-native 1.0.0-SNAPSHOT native
(powered by Quarkus xx.xx.xx.) Started in 0.019s. Listening on: http://0.0.0.0:8080
INFO [io.quarkus] (main) Profile prod activated.
INFO [io.quarkus] (main) Installed features: [cdi, kubernetes, resteasy]
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;简直是超音速！启动只花了 19 毫秒。你的运行时间可能稍有不同。&lt;/p&gt;
 &lt;p&gt;使用 Linux 的   &lt;code&gt;ps&lt;/code&gt; 工具检测一下，结果内存占用还是很低。检测的方法是：在应用程序运行期间，另外打开一个终端，运行如下命令：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;$ ps -o pid,rss,command -p $(pgrep -f runner)
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;输出结果类似于：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;  PID    RSS COMMAND
10246  11360 target/quarkus-serverless-native-1.0.0-SNAPSHOT-runner
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;该进程只占 11MB 内存。非常小！&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;   &lt;strong&gt;注意:&lt;/strong&gt; 各种应用程序（包括 Quarkus）的驻留集大小和内存占用，都因运行环境而异，并随着应用程序载入而上升。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;你也可以使用 REST API 访问这个函数。输出结果应该是   &lt;code&gt;Hello RESTEasy&lt;/code&gt;:&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;$ curl localhost:8080/hello
Hello RESTEasy
&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;3、把函数部署到 Knative 服务&lt;/h3&gt;
 &lt;p&gt;如果你还没有创建命名空间，现在就在   &lt;a href="https://docs.okd.io/latest/welcome/index.html"&gt;OKD&lt;/a&gt;（OpenShift Kubernetes 发行版）  &lt;a href="https://docs.okd.io/latest/applications/projects/configuring-project-creation.html"&gt;创建一个命名空间&lt;/a&gt;（例如   &lt;code&gt;quarkus-serverless-native&lt;/code&gt;），进而把这个本地可执行文件部署为无服务器函数。然后添加   &lt;code&gt;quarkus-openshift&lt;/code&gt; 扩展：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;$ ./mvnw -q quarkus:add-extension -Dextensions=&amp;quot;openshift&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;向   &lt;code&gt;src/main/resources/application.properties&lt;/code&gt; 文件中添加以下内容，配置 Knative 和 Kubernetes 的相关资源：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;quarkus.container-image.group=quarkus-serverless-native
quarkus.container-image.registry=image-registry.openshift-image-registry.svc:5000
quarkus.native.container-build=true
quarkus.kubernetes-client.trust-certs=true
quarkus.kubernetes.deployment-target=knative
quarkus.kubernetes.deploy=true
quarkus.openshift.build-strategy=docker
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;构建本地可执行文件，并把它直接部署到 OKD 集群：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;$ ./mvnw clean package -Pnative
&lt;/code&gt;&lt;/pre&gt;
 &lt;blockquote&gt;
  &lt;p&gt;   &lt;strong&gt;注意：&lt;/strong&gt; 提前使用    &lt;code&gt;oc login&lt;/code&gt; 命令，确保登录的是正确的项目（例如    &lt;code&gt;quarkus-serverless-native&lt;/code&gt;）。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;输出信息结尾应当是   &lt;code&gt;BUILD SUCCESS&lt;/code&gt;。完成一个本地二进制文件的构建并部署为 Knative 服务需要花费几分钟。成功创建服务后，使用   &lt;code&gt;kubectl&lt;/code&gt; 或   &lt;code&gt;oc&lt;/code&gt; 命令工具，可以查看 Knative 服务和版本信息：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;$ kubectl get ksvc
NAME                        URL   [...]
quarkus-serverless-native   http://quarkus-serverless-native-[...].SUBDOMAIN  True

$ kubectl get rev
NAME                              CONFIG NAME                 K8S SERVICE NAME                  GENERATION   READY   REASON
quarkus-serverless-native-00001   quarkus-serverless-native   quarkus-serverless-native-00001   1            True
&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;4、访问本地可执行函数&lt;/h3&gt;
 &lt;p&gt;运行   &lt;code&gt;kubectl&lt;/code&gt; 命令，搜索无服务器函数的节点：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;$ kubectl get rt/quarkus-serverless-native
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;输出信息类似于：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;NAME                         URL                                                                                                          READY   REASON
quarkus-serverless-native   http://quarkus-serverless-restapi-quarkus-serverless-native.SUBDOMAIN   True
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;用   &lt;code&gt;curl&lt;/code&gt; 命令访问上述信息中的   &lt;code&gt;URL&lt;/code&gt; 字段：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;$ curl http://quarkus-serverless-restapi-quarkus-serverless-native.SUBDOMAIN/hello
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;过了不超过一秒钟，你也会得到跟本地操作一样的结果：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;Hello RESTEasy
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;当你在 OKD 群集中访问 Quarkus 运行中的节点的日志，你会发现本地可执行文件正在以 Knative 服务的形式运行。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="Native Quarkus Log" src="https://img.linux.net.cn/data/attachment/album/202210/26/151635hrov9o2fwvfwlrwl.png" title="Native Quarkus Log"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;下一步呢？&lt;/h3&gt;
 &lt;p&gt;你可以借助 GraalVM 发行版优化 Java 无服务器函数，从而在 Knative 中使用 Kubernetes 将它们部署为无服务器函数。Quarkus 支持在普通的微服务中使用简易配置进行性能优化。&lt;/p&gt;
 &lt;p&gt;本系列的下一篇文章将指导你在不更改代码的情况下跨多个无服务器平台实现可移植函数。&lt;/p&gt;
 &lt;p&gt;  &lt;em&gt;(Daniel Oh,    &lt;a href="https://creativecommons.org/licenses/by-sa/4.0/"&gt;CC BY-SA 4.0&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;p&gt;via:   &lt;a href="https://opensource.com/article/21/6/java-serverless-functions-kubernetes"&gt;https://opensource.com/article/21/6/java-serverless-functions-kubernetes&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;作者：  &lt;a href="https://opensource.com/users/daniel-oh"&gt;Daniel Oh&lt;/a&gt; 选题：  &lt;a href="https://github.com/lujun9972"&gt;lujun9972&lt;/a&gt; 译者：  &lt;a href="https://github.com/cool-summer-021"&gt;cool-summer-021&lt;/a&gt; 校对：  &lt;a href="https://github.com/wxy"&gt;wxy&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;本文由   &lt;a href="https://github.com/LCTT/TranslateProject"&gt;LCTT&lt;/a&gt; 原创编译，  &lt;a href="https://linux.cn/"&gt;Linux中国&lt;/a&gt; 荣誉推出&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62472-%E4%BC%98%E5%8C%96-kubernetes-java</guid>
      <pubDate>Wed, 26 Oct 2022 15:16:34 CST</pubDate>
    </item>
    <item>
      <title>前端首屏渲染时间的极致优化</title>
      <link>https://itindex.net/detail/62461-%E5%89%8D%E7%AB%AF-%E6%B8%B2%E6%9F%93-%E6%97%B6%E9%97%B4</link>
      <description>&lt;p&gt;  &lt;img alt="20220722174820" src="https://segmentfault.com/img/bVc23MP" title="20220722174820"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;我们知道，用户体验是 Web 产品最为重要的部分。尽可能减少首屏加载时间，更为流畅地展示用户所需求的内容，会是用户是否留存的关键因素。&lt;/p&gt; &lt;p&gt;而随着现代 Web 业务可供用户的交互行为越来越多，前端项目的复杂度越来越高，每个页面的渲染时间也必然越来越长，这就导致了用户的体验不佳，用户的操作变慢。&lt;/p&gt; &lt;p&gt;为此，前端工程师们在首屏请求的各个阶段中持续钻研，不断探究如何将首次页面渲染的时间减少到更小，力求提供更为优秀的产品体验。&lt;/p&gt; &lt;h2&gt;CSR（Client Side Render）&lt;/h2&gt; &lt;p&gt;  &lt;img alt="20220720162452" src="https://segmentfault.com/img/bVc23MS" title="20220720162452"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;浏览器渲染是最简单，最符合 Web 应用设计思路的渲染方式。&lt;/p&gt; &lt;p&gt;所谓浏览器渲染，就是将应用所需的页面展示、前端逻辑、接口请求全都在用户的浏览器中执行。它很好的实现了前后端的解耦，让前端开发更为独立，也让后台实现更为简单。&lt;/p&gt; &lt;p&gt;同时，为了缓解用户的等待焦虑，我们可以用 loading 态，或者骨架屏，进一步提升异步请求接口时的用户体验。&lt;/p&gt; &lt;p&gt;不过，随着业务复杂程度提高，浏览器渲染的开销也会变大，我们无法控制用户侧使用的机器性能，很多时候，用户使用的机器性能甚至不足以满足应用的需求，造成卡顿，甚至崩溃，这一点在移动端上尤甚。&lt;/p&gt; &lt;p&gt;而浏览器渲染由于前端的动态性过高，也会带来 SEO 不佳的问题。&lt;/p&gt; &lt;h2&gt;SSR（Server Side Render）&lt;/h2&gt; &lt;p&gt;  &lt;img alt="20220720162513" src="https://segmentfault.com/img/bVc23MT" title="20220720162513"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;服务端渲染的出现时间实际上是要比浏览器渲染要更早的。在 Web 应用发展的早期，所有的 ASP、JSP 等模板引擎构建的前端页面实际上就是服务端渲染的结果。而此时的服务端渲染无法进行前后端职责的解耦，因此逐步被浏览器渲染淘汰。&lt;/p&gt; &lt;p&gt;但在处理首屏体验的问题上，服务端渲染有着独到的优势。它能提前再服务端中完成页面模板的数据填充，从而一次性返回完整的首屏内容，从而面对 SEO 的爬取时能获取到更多有效的关键信息。&lt;/p&gt; &lt;p&gt;此外，由于其能快速直出首页的真实数据，体验往往比 loading 态更佳，在 TTI 的表现上更为出色。&lt;/p&gt; &lt;p&gt;但是，服务端渲染也有其自身的局限性。因为从本质上来说，SSR 服务无法完全与前端页面解耦开来。因此市面上较完备的 SSR 解决方案都只解决首屏的服务端渲染，并采用同构的方式，增加一层 node 中间层的方式来解决前端与 SSR 服务的更新同步问题，并与后端开发项目解耦。&lt;/p&gt; &lt;p&gt;但这无疑增加了项目的复杂度，并且随着业务的复杂程度变高，服务端渲染往往需要调起多个接口去请求数据并填充页面，这样可能会导致在 TTFB 上有一定劣势。&lt;/p&gt; &lt;p&gt;当然，最重要的是，服务端渲染对于服务器的负载要求是很高的。&lt;/p&gt; &lt;p&gt;  &lt;img alt="20220722153734" src="https://segmentfault.com/img/bVc23MU" title="20220722153734"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;上图是引用的字节的某项目的 SSR 服务的单机 QPS 承载表现。我们可以看出，对于一个高访问量的网页应用来说，提供一个较为复杂的 SSR 服务的成本是相当高的，需要花费大量的金钱来堆机器。&lt;/p&gt; &lt;p&gt;因此，从降本增效的角度考虑，我们需要评估 SSR 带来的 ROI 是否符合预期。&lt;/p&gt; &lt;h2&gt;NSR（Native Side Render）&lt;/h2&gt; &lt;p&gt;在移动互联网的浪潮下，移动端机能飞速提升，那么 Web 应用是否能搭上这一班车，将 Native 的性能利用起来，提升页面渲染性能呢？答案是肯定的，这就需要介绍到 NSR 了。&lt;/p&gt; &lt;p&gt;  &lt;img alt="20220720162547" src="https://segmentfault.com/img/bVc23MV" title="20220720162547"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;Native 渲染的本质其实还是 SSR，只不过提供服务的 Server 转变为了客户端。由于需要用到客户端机能，因此此种实现通常应用在移动端 APP，或者 PWA 下。&lt;/p&gt; &lt;p&gt;当链接被点击时，先借助浏览器启用一个 JS 运行时，并加载 APP 中存储的 Html 模板，发送 xhr 请求预加载页面数据，从而在客户端本地拼接并渲染生成一个有数据的 Html 首屏，形成首次 NSR。同时可以将该首屏 Html 缓存在客户端，供下次页面打开时，实现   &lt;code&gt;stale-while-revalidate&lt;/code&gt; 的缓存效果。&lt;/p&gt; &lt;p&gt;由于 NSR 将服务器的渲染工作放在了客户端的一个个独立设备中，既实现了页面的预加载，同时又不会增加额外的服务器压力。达到秒看的效果。&lt;/p&gt; &lt;p&gt;这种能力在拥有客户端或者支持 PWA 的应用中应用广泛，例如手 Q，腾讯文档 APP 中都拥有通过 APP 中的离线包来实现首屏渲染加速的能力。&lt;/p&gt; &lt;h2&gt;ESR（Edge Side Render）&lt;/h2&gt; &lt;p&gt;那么，对于纯 Web 应用，而又由于兼容性等原因暂时无法支持 PWA 的页面，有没有一个合适的首屏渲染加速方案呢？&lt;/p&gt; &lt;p&gt;随着云与边缘计算的快速发展，前端页面也需要考虑分布式的请求处理优化。&lt;/p&gt; &lt;p&gt;  &lt;img alt="20220720162606" src="https://segmentfault.com/img/bVc23MW" title="20220720162606"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;我们知道，CDN 节点相比真实服务节点更贴近用户，能更快将内容返回。因此我们可以将静态的 Html 内容模板缓存在 CDN 上。当接到请求时，先快速将静态模板页面返回给用户，同时在 CDN 服务器上对页面动态部分发起向后端发起请求，并将获取到的动态内容在以流式下发的方式继续返回给用户。&lt;/p&gt; &lt;p&gt;这里实际上利用到了 HTTP 的 SSE（Server Send Events）协议，通过服务器向客户端发送单向事件流，实现同一个 Html 文件的分块传输预渲染。&lt;/p&gt; &lt;h2&gt;最佳实践是？&lt;/h2&gt; &lt;p&gt;这也是我们最近在腾讯文档中探索实践并落地的，通过服务中间节点的流式下发能力，实现多级首屏渲染加速。&lt;/p&gt; &lt;p&gt;对于一个复杂前端页面来说，首屏需要加载和运算的资源类型可能有很多，有需要客户端解析并执行的 JS 动效，也有需要服务端获取数据直出的数据分片展示页面。&lt;/p&gt; &lt;p&gt;通常来说，客户端只能等待服务端获取分片数据，并生成经过 SSR 渲染后的 HTML，才能开始进行 script 解析与 js 资源拉取的行为，最终渲染出完整的页面数据以及动效。&lt;/p&gt; &lt;p&gt;而既然他们所需要的计算方式不同，那么为什么不能并行来做呢？&lt;/p&gt; &lt;p&gt;我们可以在版本发布前，将未经过服务端直出的模板 HTML 进行解析，将需要发起资源请求的所有的外链脚本 url 提取出来，生成一个 HTML Header 结构，并将该 Header 内容伪装为正常 HTML 缓存在 CDN 节点中。&lt;/p&gt; &lt;p&gt;结合之前我们介绍的 HTTP SSE 协议，当用户请求时，我们可以以最快的速度向用户返回 CDN 中的 HTML header，从而让用户的浏览器提前拉取并解析外链资源。于此同时，CDN 节点将用户的请求转发给真实的服务端，从而让服务端进行真实数据的获取拼接并返回给客户端。&lt;/p&gt; &lt;p&gt;由于客户端此时已经提前拉取了外链资源，因此收到服务端分片的 SSR 后，客户端可以直接将真实数据渲染到页面中，而不需要再次等待外链资源的解析。&lt;/p&gt; &lt;p&gt;由于并行的关系，这样的 SSR 与 NSR 混合方式能大大降低复杂页面首屏渲染的时间，提升用户体验。&lt;/p&gt; &lt;p&gt;以百度首页的请求为例，通过 Chorme Network 提供的瀑布图，通过我们可以直观的看到一条请求的执行过程。&lt;/p&gt; &lt;p&gt;  &lt;img alt="20220913172530" src="https://segmentfault.com/img/bVc23MX" title="20220913172530"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;我们可以看出，除了 DNS 寻址与 SSL 建连是我们无法控制的以外，占用请求时间的大头是   &lt;code&gt;Waiting for server response&lt;/code&gt;，请求服务器 (CDN) 的时间，以及   &lt;code&gt;Content Download&lt;/code&gt;，外链资源的拉取时间。&lt;/p&gt; &lt;p&gt;而使用本文的混合方案后，理论上可以使总请求时间降低到 Max(A, B), (A 为   &lt;code&gt;Waiting for server response&lt;/code&gt;，B 为   &lt;code&gt;Content Download&lt;/code&gt;) 的水平。（当然，实际操作过程中，由于 CDN 节点进行了一次请求转发，因此拥有 SSR 能力的页面请求返回时间会更长一些）。&lt;/p&gt; &lt;h2&gt;结语&lt;/h2&gt; &lt;p&gt;前端的页面首屏时间优化是永恒的话题，本文介绍了前端界对首屏时间优化的进程，并提供了一种 SSR 与 NSR 混合的新思路，通过并行处理耗时任务的方式，进一步提升首屏加载时间，希望能够给大家提供一点参考价值。&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>javascript</category>
      <guid isPermaLink="true">https://itindex.net/detail/62461-%E5%89%8D%E7%AB%AF-%E6%B8%B2%E6%9F%93-%E6%97%B6%E9%97%B4</guid>
      <pubDate>Tue, 18 Oct 2022 11:19:21 CST</pubDate>
    </item>
    <item>
      <title>字节跳动基于 ClickHouse 的复杂查询实现与优化</title>
      <link>https://itindex.net/detail/62448-%E5%AD%97%E8%8A%82-clickhouse-%E5%A4%8D%E6%9D%82</link>
      <description>&lt;p&gt;  &lt;strong&gt;1. ClickHouse执行模式&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;ClickHouse 的执行模式相对比较简单，和Druid、ES 类似，其基本查询模式分为两个阶段：&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;第一阶段&lt;/strong&gt;，Coordinator 收到查询后将请求发送给对应的 worker 节点；&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;第二阶段&lt;/strong&gt;，Coordinator 收到各个 worker 节点的结果后汇聚起来处理后返回。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;以下面的SQL为例：&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;Select name from student_distribute where id = 5&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;①当 Coordinator 收到请求后，由于student_distribute是一个分布式表，因此需要将SQL 改写为对local表查询，并转发请求给每一个shard的worker；&lt;/p&gt; &lt;p&gt;②Worker收到请求后查询本地的local表数据，返回结果给coordinator；&lt;/p&gt; &lt;p&gt;③Coordinator汇总每一个shard的数据并把结果返回给client。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;Select name from student_local where id = 5&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;第二阶段执行的模式&lt;/strong&gt;能够高效地支持很多常见场景，比如常见的针对大宽表的各类查询，但是随着业务场景的复杂化，  &lt;strong&gt;也存在以下三点问题：&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;其一，  &lt;strong&gt;第一阶段返回的&lt;/strong&gt;  &lt;strong&gt;数据&lt;/strong&gt;比较多且第二阶段的计算比较复杂时，对于Coordinator的压力会比较大，  &lt;strong&gt;容易成为query的瓶颈，且shard越多可能计算越慢，瓶颈越大&lt;/strong&gt;。例如一些重计算的agg算子count distinct。如果我们使用hash表去重时，第二阶段需要在coordinator单机上merge各个worker的hash表，计算量很重且不能并行；又比如说group by基数比较大或者window计算。&lt;/p&gt; &lt;p&gt;其二，join是SQL的重要场景。由于不支持Shuffle操作，  &lt;strong&gt;对于Join来说右表必须是全量数据&lt;/strong&gt;。无论是普通Join还是Global Join，当Join的右表比较大时都放到内存里容易OOM，而Spill到磁盘虽然解决内存问题，可能会因为有磁盘 io和序列化计算的开销影响性能。特别是当Join为最常见的Hash Join 时，右表如果是大表构建也比较慢。虽然社区最近也做了一些右表构建的优化，通过单机按照 join key split 来达到并行构建hash table。但是额外的代价是左右表都增加了一次 split 操作。&lt;/p&gt; &lt;p&gt;其三，  &lt;strong&gt;对于复杂查询&lt;/strong&gt;（如多表 Join、嵌套多个子查询、window function等）  &lt;strong&gt;的支持并不友好&lt;/strong&gt;，由于不能通过shuffle来分散数据，生成的pipeline在一些case下不能充分并行，难以充分发挥集群的全部资源。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;2. 其他MPP数据库&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;目前主流的MPP数据库基本都  &lt;strong&gt;支持Stage执行的方式&lt;/strong&gt;。以Presto为例，如下图所示，一个两表join的agg sql可拆分为5个 Stage。&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;其中 Stage3、Stage4分别对应左右表数据读取，Stage2完成两表Join和partial agg 计算，Stage1完成final agg计算，Stage0收集Stage1的数据后汇总和输出。在这个过程中，Stage 3、4、2、1可以在多个节点上并行执行，单个复杂的query被拆分成若干Stage，从而实现了Stage之间，不同worker的数据传输。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;3. 业务背景和目标&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;随着业务复杂程度提高，业务并不希望所有的数据都通过etl 产生大宽表；复杂查询（特别是多轮分布式 Join和比较多的agg）的需求越来越强烈，而整体的数据量又在不断增长。在集群资源有限的情况下，我们希望能够充分利用机器资源，基于ClickHouse 高效地支持复杂查询。&lt;/p&gt; &lt;p&gt;ByteHouse是字节跳动研发同学基于开源ClickHouse 进行了深度优化和改造的版本，提供海量数据上更强的查询服务和数据写入性能，支持多种应用场景。如图所示，ByteHouse在内部多个场景如行为分析、画像分析、智能营销分析、APP 日志分析上得到充分的验证和使用，并在多个方面进行了增强，具备特有的能力。&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;02&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;技术方案&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;1. 设计思想&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;基于 ClickHouse 的复杂查询的实现采用分Stage的方式，替换目前 ClickHouse的两阶段执行方式。类似其他分布式数据库引擎（如 Presto、Impala 等），将一个复杂的Query按照数据交换情况切分成多个Stage，Stage和Stage之间通过 exchange完成数据的交换，单个Stage内不存在数据交换。  &lt;strong&gt;Stage间的数据交换主要有以下三种形式：&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;①按照单（多）个 key 进行 Shuffle（shuffle）&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;②由1个或者多个节点汇聚到一个节点（我们称为 gather）&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;③同一份数据复制到多个节点（也称为 broadcast 或者说广播）&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;按照不同的功能切分不同的模块，  &lt;strong&gt;设计目标如下：&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;①各个模块约定好接口，尽量减少彼此的依赖和耦合。一旦某个模块有变动不会影响别的模块，例如Stage生成逻辑的调整不影响调度的逻辑。&lt;/p&gt; &lt;p&gt;②模块采用插件的架构，允许模块根据配置灵活支持不同的策略。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;2. 相关术语&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;ExchangeNode	在语法树中表示数据交换的节点&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;PlanSegment	单个 Stage 对应的执行的计划片段&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;ExchangeManager	管理数据的 exchange，负责不同 Stage 节点之间的数据交换&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;SegmentScheduler	计划片段调度器，负责下发计划片段给 worker，由 Coordinator 节点调用&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;InterpreterPlanSegment	计划片段执行器，执行一个具体的计划片段&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;3. 执行流程&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;①Coordinator 接受复杂查询后，在目前 ClickHouse 语法树的基础上，根据节点类型和数据分布情况插入 Exchange 节点并生成分布式 Plan。&lt;/p&gt; &lt;p&gt;②Coordinator 根据 Exchange Node 类型，切分分布式 Plan 生成每个 Stage 的执行片段 PlanSegment。&lt;/p&gt; &lt;p&gt;③Coordinator 调用 SegmentScheduler 将各阶段的 PlanSegment 发送到 Worker 节点。&lt;/p&gt; &lt;p&gt;④Worker 节点接受 PlanSegment 通过 InterpreterPlanSegment 完成数据的读取和执行，通过 ExchangeManager 完成数据的交互。&lt;/p&gt; &lt;p&gt;⑤Coordinator 从最后一轮 Stage 对应节点的 ExchangeManager 读取数据后处理后返回给 client。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;4. Plan切分&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;下面是一个Plan切分的例子，这是1个2表Join的查询场景，根据Exchange信息，将整个分布式 Plan切分成4个Stage。&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;5. 查询片段调度器（SegmentScheduler）&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;查询片段调度器SegmentScheduler 根据上下游依赖关系和数据分布，以及 Stage 并行度和worker 分布和状态信息，按照一定的调度策略，将 PlanSemgent 发给不同的 Worker 节点。&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;目前支持的2种策略是：&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;①依赖调度&lt;/strong&gt;：根据 Stage 依赖关系定义拓扑结构，产生 DAG 图，根据 DAG 图调度 stage，类似于拓扑排序，等到依赖的 Stage 启动后再启动新的 Stage。例如刚才的两表 join，会先调度左右表读取 stage，再调度 join stage。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;②AllAtOnce&lt;/strong&gt;：类似于Presto的AllAtOnce策略，会先计算每一个 Stage 的相关信息，一次性调度所有的Stage。&lt;/p&gt; &lt;p&gt;相比而言，这两种策略是在容错、资源使用和延时上做取舍。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;第一种调度策略可以实现更好的容错&lt;/strong&gt;，由于 ClickHouse 可以有多个副本，当前一个 Stage 部分节点连接失败时可以尝试切换到副本节点，对后续依赖 stage 无感知。这里指的是读数据的 Stage，我们称为 Source Stage，非 Source Stage 因为没有数据依赖，容错能力会更强，只要保证并行度的节点数即可，甚至极端情况下可以降低 stage 并行度来支持更好的容错。缺点是调度有依赖，不能完全并行，会增加调度时长，对于一些数据量和计算量小，但是 stage 多的节点调度延时可能会占 SQL 整体时间不小的比例。我们也做了一些针对性的优化，对于无依赖关系的尽可能支持并行。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;第二种调度策略通过并行可以极大降低调度延时&lt;/strong&gt;，为防止大量网络 io 线程，我们通过异步化并且控制线程数目；这种策略的缺点是容错性没有依赖调度好，因为每一个 stage 的 worker 在调度前就已经确定，如果有一个 worker 出现连接异常则整个查询会直接失败。并且可能有一些 Stage 上游数据还没有 Ready 就被调度执行了，需要长时间等数据。例如 final agg stage，需要等 partial agg 完成后才能拿到数据。虽然我们做了一些优化，并不会长时间空跑浪费 cpu 资源，但是毕竟也消耗了一部分资源，比如创建了执行的线程。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;6. 查询片段执行器（InterpreterPlanSegment）&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;下面介绍下计划片段是如何执行的，原本 ClickHouse的查询和节点执行主要是 SQL 形式，切分Stag后需要支持执行一个单独的PlanSemgent。因此 InterpreterPlanSegment 的主要功能就是接受一个序列化后的 PlanSemgent，能够在 Worker 节点上运行整个 PlanSemgent 的逻辑。  &lt;strong&gt;主要的步骤为：&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;①根据 input 信息读取数据，如果 input 是具体的 table，则从本地读取数据；如果 input 是一个 exchange input，则从对应的 ExchangeManager 读取数据；&lt;/p&gt; &lt;p&gt;②执行 PlanSemgent 的逻辑；&lt;/p&gt; &lt;p&gt;③输出处理后的结果数据，如果是 Coordinator 节点，就将数据发给 Client；如果是非Coordinator 节点，就按照数据的exchange方式写给本实例对应的 ExchangeManager。&lt;/p&gt; &lt;p&gt;Interpreter部分我们尽量复用当前ClickHouse的执行逻辑，例如processor 执行方式，process list管理等等。相比于InterpreterSelect逻辑要更简单一些，可以认为1 个Stage只有1个阶段。当然我们也做了很多功能和性能的增强，例如我们支持1个 stage处理多个join等，这样可以减少stage数目和不必要的数据传输，在一张大表（通常情况下是事实表） join 多个维度表的场景有比较好的帮助。&lt;/p&gt; &lt;p&gt;InterpreterPlan Segment执行完会向coordinator上报对应的状态信息。执行异常的时候会将异常信息报告给查询片段调度器，取消Query其他worker的执行。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;7. 数据交换（ExchangeManager）&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;ExchangeManager&lt;/strong&gt;是PlanSegment数据交换的媒介，更是平衡数据上下游处理能力的重要组件。整体上采用 push 的方式，当上游数据 ready 时主动推送给下游，并支持反压。  &lt;strong&gt;其架构如下图所示：&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;具体的流程如下：&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;①  &lt;strong&gt;下游&lt;/strong&gt;  &lt;strong&gt;PlanSegment&lt;/strong&gt;执行时，当input为exchange input时，根据一定的 token 规则 （通常由 query_id+segment_id+index_id 等组成）和数据 source 信息，向上游 ExchangeManager 注册对应的数据请求；&lt;/p&gt; &lt;p&gt;②  &lt;strong&gt;上游&lt;/strong&gt;  &lt;strong&gt;ExchangeManager&lt;/strong&gt;收到请求后，建立上下游数据通道，并将上游的数据推送到下游，如果通道一直建立不了会 block 上游的执行。&lt;/p&gt; &lt;p&gt;在这个过程中，上下游都会通过队列来优化发送和读取，当队列饱和的时候通过反压的机制控制上游的执行速度。由于采用了 push 和队列，这里我们要考虑一个特殊的场景，在某些 case 下下游的 Stage 并不需要读取全部的上游数据，一个典型的场景是 limit。例如 limit 100，下游 stage 是需要读取 100 条数据即可，而上游可能会输出更大规模的数据，因此在这种情况下，当下游 stage 读到足够的数据后，需要能主动取消上游数据的执行并清空队列。这是一个特定场景的优化，能够大大加速查询时间。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;ExchangeManager 需要考虑和优化的点还有：&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;①细粒度的内存控制，能够按照实例、query、segment 多层次进行内存控制，避免 OOM，更长期的考虑是支持 spill 到磁盘上，降低对内存的使用。为了提升传输效率，小数据需要进行 merge，大数据要 split。同时，网络处理在某些场景要保证有序性，比如 sort 时，partial sort 和 merge sort 的网络传输必须有序，否则数据可能是有问题的。&lt;/p&gt; &lt;p&gt;②连接复用和网络优化，包括针对上下游在同一个节的场景下选择走内存的交换不走网络，可以减少网络的开销和减少数据序列化、反序列化的代价。另外，由于 ClickHouse 在计算方面做了非常充足的优化，有些场景下甚至内存带宽成为瓶颈，我们在ExchangeManager的一些场景上也应用zero copy等技术来减少内存的拷贝。&lt;/p&gt; &lt;p&gt;③异常处理和监控，相比于单机执行，分布式情况下异常情况更复杂且不好感知。通过重试能避免一些节点的暂时高负载或者异常，以及出问题时能够快速感知、排查和做针对性解决和优化。这里的工程实践更多一些。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;03&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;优化与诊断&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;1. Join 多种实现&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;根据数据的规模和分布，我们支持了多种Join实现，目前已经支持的有：&lt;/p&gt; &lt;p&gt;①Shuffle Join，最通用的 Join；&lt;/p&gt; &lt;p&gt;②Broadcast Join，针对大表Join小表的场景，通过把右表广播到左表的所有 worker 节点来减少左表的传输；&lt;/p&gt; &lt;p&gt;③Colocate Join，针对左右表根据Join key保持相通分布的场景，减少左右表数据传输。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;2. 网络连接优化&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;网络连接的优化的核心本质就是减少连接的使用&lt;/strong&gt;。特别是数据需要Shuffle 的时候，下一轮 Stage的每一个节点需要从上一轮Stage的每一个节点拉取数据。当一个集群的节点比较多的时候，如果存在比较多的复杂 Query(Stage多，并行度（节点数）比较大)，集群的Worker节点会建立非常多的连接，如下图所示，单节点建立的连接数与集群节点数、并发stage数成正比。&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;字节内部的clickhouse集群规模非常大，最大的集群（单集群几千台规模）在目前 ClickHouse 的执行模式下单机最大可能会建立上几万个网络连接。因此如果支持复杂 Query 执行，由于stage变多了，需要优化网络连接，特别是支持连接复用。我们通过尽可能复用连接，在不同节点之间只会建立固定数目的连接，不同的查询会复用这些连接，不随 query 和 stage 的规模而增长。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;3. 网络传输优化&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;在数据中心领域，远程直接内存访问（RDMA）是一种绕过远程主机操作系统内核访问其内存中数据的技术，由于不经过操作系统，不仅节省了大量CPU资源，同样也提高了系统吞吐量、降低了系统的网络通信延迟，尤其适合在大规模并行计算机集群中有广泛应用。&lt;/p&gt; &lt;p&gt;由于ClickHouse在计算层面做了很多优化，而网络带宽相比于内存带宽要小不少，在一些数据量传输特别大的场景，网络传输会成为一定的瓶颈。为了提升网络传输的效率和提升数据exchange的吞吐，一方面我们引入压缩来降低传输数据量，另一方面我们引入 RDMA 来减少一定的开销。经过测试，在一些数据传输量大的场景，有不小的收益。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;4. Runtime Filter&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;Join算子通常是OLAP引擎中最耗时的算子。如果想优化 Join 算子，可以有两种思路，一方面可以提升Join算子的性能，例如更好的Hash Table实现和Hash算法，以及更好的并行。另一方面可以尽可能减少参与Join计算的数据。&lt;/p&gt; &lt;p&gt;Runtime Filter在一些场景，特别是事实表join维度表的星型模型场景下会有比较大的效果。因为这种情况下通常事实表的规模比较大，而大部分过滤条件都在维度表上，事实表可能要全量join维度表。Runtime Filter的作用是通过在 Join 的 probe 端（就是左表）提前过滤掉那些不会命中Join的输入数据来大幅减少 Join 中的数据传输和计算，从而减少整体的执行时间。以下图为例：&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;左表并没有直接过滤条件，右表带有过滤条件item.proce &amp;gt; 1000。当完成右表查询时，可以确定item.id 的范围和集合，根据join类型inner join和join条件sales.item_id=item.id可以推断出sales.item的范围和集合。我们可以把sales.item 的范围和集合作为一个过滤条件，在join前过滤sales的数据。&lt;/p&gt; &lt;p&gt;我们在复杂查询上支持了Runtime Filter，目前主要支持minmax和bloomfilter。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;总体执行流程如下：&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;①build plan segment worker（right table）会将生成的单节点 runtime filter 发送到coordinator节点；&lt;/p&gt; &lt;p&gt;②coordinator 在等待各个 worker的 runtime filter 都发送完成之后进行一次merge操作，将合并好的 runtime filter 分发到各个 execute plan segment worker（left table）节点中去；&lt;/p&gt; &lt;p&gt;③在 runtime filter 构造期间，execute plan segment（left table） 需要等待一定的时间，在超时之前如果runtime filter已经下发，则通过 runtime filter 执行过滤。&lt;/p&gt; &lt;p&gt;这里需要思考一个问题，  &lt;strong&gt;Runtime filter column 是否构建索引（主键、skip index等）和命中prewhere？&lt;/strong&gt;如果runtime filter的列（join column）构建了索引是需要重新生成 pipeline 的。因为命中索引后，可能会减少数据的读取，pipeline并行度和对应数据的处理range都可能发生变化。如果runtime filter的列跟索引无关，可以在计划生成的时候预先带上过滤条件，只不过一开始作为占位是空的，runtime filter下发的时候把占位信息改成真正的过滤条件即可。这样即使runtime filter 下发超时了，查询片段已经开始执行了，只要查询片段没有执行完，之后的数据仍然可以进行过滤。&lt;/p&gt; &lt;p&gt;需要注意的是，runtime filter 是一种特殊场景下的优化，其针对的场景是右表数据量不大，且构建的 runtime filter 对左表有比较强的过滤效果。如果右表数据量比较大，构建runtime filter比较慢，或者对左表的数据过滤效果很差甚至没有，那么 runtime filter 反而会增加查询的耗时。因此，要根据数据的特征和规模来决定是否开启。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;5. 诊断和分析&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;引入复杂查询的多Stage 执行模型后，SQL的执行模式变得复杂了。特别是当用户查询一些非常复杂的查询，几百行的sql生成的stage会非常多，把stage都看一遍并理解sql的含义要花比较长的时间。题外话：我们很早之前就完整的跑通了所有的tpcds query，这里面就有一些sql可能会产生几十个 stage。那么在这种情况下，  &lt;strong&gt;如何定位 SQL 的瓶颈并加以优化是一个难题。&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;我们做了如下两点优化：&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;首先，最常见的做法是  &lt;strong&gt;增加各类完善的metrics&lt;/strong&gt;，包括整个Query的执行时间和不同Stage的执行时间、IO数据量、算子处理数据和执行情况、算子 metrics 和profile event等。&lt;/p&gt; &lt;p&gt;其次，我们  &lt;strong&gt;记录了反压信息和上下游队列长度，以此来推断 stage 执行情况和瓶颈&lt;/strong&gt;。&lt;/p&gt; &lt;p&gt;坦率地说，SQL 场景包括万象，很多非常复杂的场景目前还是需要对引擎比较熟悉的同学才能诊断和分析SQL才能给出优化建议。在不断积累经验的过程中，我们希望通过能够不断完善 metrics 和分析路径，不断减轻oncall的负担，并且在某些场景下可以更智能的给出优化提示，这对于使用同学来说也是有好处的。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;04&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;效果及展望&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;1. 复杂查询效果&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;根据上面的执行模型的三个缺点，分别测试如下三个场景：&lt;/p&gt; &lt;p&gt;①第二阶段的计算比较复杂&lt;/p&gt; &lt;p&gt;②Hash Join 右表为大表&lt;/p&gt; &lt;p&gt;③多表 Join&lt;/p&gt; &lt;p&gt;以SSB 1T数据作为数据集，集群包含8个节点。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;2. 第二阶段的计算比较复杂&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;这个case SQL 如下图所示&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;p&gt;uniqExact是count distinct的默认算法，采用hash table进行数据去重。使用复杂查询后，query 执行时间从 8.514s=&amp;gt;2.198s，第二阶段 agg uniqExact 算子的合并原本由 coordinator单点合并，现在通过按照group by key shuffle 后可以由多个节点并行完成。因此通过shuffle减轻了coordinator的merge agg 压力。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;3. Hash Join 右表为大表&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;这个 case 演示了右表是一个大表的场景，由于 ClickHouse 对多表的优化做的还不是很到位。这里采用子查询来下推过滤的条件。&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;在这个case中，采用复杂查询模式后，query 执行时间从17.210=&amp;gt;1.749s。lineorder 是一张大表，通过shuffle可以将大表数据按照join key shuffle到每个worker节点，减少了右表构建的压力。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;4. 多表 Join&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;这个 case 是一个 5 表 join 的 case。&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;开启复杂查询模式后，query 执行时间从8.583s=&amp;gt;4.464s，所有的右表可同时开始数据读取和构建&lt;/strong&gt;。为了和现有模式对比，针对复杂查询没有开启 runtime filter，开启 runtime filter后效果会更快。&lt;/p&gt; &lt;p&gt;这里还要重点说一下，今天的分享主要是从执行模式上讲解如何支持复杂查询。实际上，  &lt;strong&gt;优化器对于复杂查询的性能提升也非常大&lt;/strong&gt;。通过一些rbo的规则，比如常见的谓词下推、相关子查询处理等。实际上这里的优化规则非常多，可以极大的提升 SQL 的执行效率。上面的 SQL 其实原本比较简单，5 表 join 和一些维表的过滤条件，这里写成子查询是为了在 ClickHouse 现有模式下右表过滤条件更好下推。其实对于我们来说，在复杂查询的模式下，由于有优化器的存在，用户不用写的这么复杂，优化器会自动完成下推和rbo优化。&lt;/p&gt; &lt;p&gt;上面是一些规则的优化，实际上在复杂查询中，  &lt;strong&gt;cbo 的优化也有很大作用&lt;/strong&gt;。举一个例子，在 ClickHouse 中，相同的两个表，大表 join 小表的性能比小表 join 大表要好很多。前一个效果 2 中如果把表顺序调整一下会快很多；另外，选用哪一种 join 的实现对 join 性能影响比较大，如果满足 join key 分布，colcate join 比 shuffle join 来说完全减少了数据的 shuffle。多表 join 中，join 的顺序和 join 的实现方式对执行的时长影响会比 2 表 join 影响更大。借助数据的统计信息，通过一些 cbo 优化，可以得到一个比较优的执行模式。&lt;/p&gt; &lt;p&gt;有了优化器，业务同学可以按照业务逻辑来写任何的 SQL，引擎自动计算出相对最优的 SQL 计划并执行，加速查询的执行。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;5. 展望&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;CLickHouse 目前的模式其实在很多单表查询的场景上表现优异。我们主要是针对复杂的查询场景做优化，主要是实现多stage的执行模式，并实现了stage之间数据传输。工程实践上来说，做了比较多的尝试和优化来提升执行和网络传输的性能，并且希望通过完善metrics和智能诊断来降低SQL分析和调优的门槛，并减少oncall 的压力。&lt;/p&gt; &lt;p&gt;目前的实现只是第一步，未来我们还有很多努力的方向。&lt;/p&gt; &lt;p&gt;首先，肯定是继续提升执行和 Exchange 的性能。这里不谈论引擎执行通用的优化，比如更好的索引或者算子的优化，主要是跟复杂查询模式有关。&lt;/p&gt; &lt;p&gt;其次是Metrics 和智能诊断加强，就如同刚才提到的，SQL 的灵活度太高了，对于一些复杂的查询没有 metrics 几乎难以诊断和调优，这个我们会长期持续的去做。&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;欢迎大家使用 ByteHouse 产品，可以扫码或者搜索ByteHouse进入官网点击立刻试用来免费试用，复杂查询的能力也已经输出到ByteHouse中。&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;欢迎关注二维码，进入字节跳动数据平台官方交流群，群内可以获取技术干货、精彩直播活动，同时针对本场直播问题，也可以在群里与我交流。除此之外，我们团队在大量招募研发工程师，欢迎关注字节跳动数据平台公众号，获取相关信息。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;今天的分享就到这里，谢谢大家。   &lt;br /&gt;&lt;/strong&gt;&lt;/p&gt; &lt;hr&gt;&lt;/hr&gt; &lt;p&gt;在文末分享、点赞、在看，给个3连击呗~&lt;/p&gt; &lt;hr&gt;&lt;/hr&gt; &lt;p&gt;  &lt;strong&gt;01&lt;/strong&gt;  &lt;strong&gt;/&lt;/strong&gt;  &lt;strong&gt;分享嘉宾&lt;/strong&gt;&lt;/p&gt; &lt;img&gt;&lt;/img&gt; &lt;p&gt;  &lt;strong&gt;董一峰&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;字节跳动&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;火山引擎ByteHouse资深研发工程师&lt;/p&gt; &lt;hr&gt;&lt;/hr&gt; &lt;p&gt;字节跳动数据平台资深研发工程师，2016 年加入字节跳动 OLAP 团队，一直从事大数据查询引擎的开发和推广工作，先后负责 Hive、Spark、Druid、ClickHouse 等大数据引擎，目前主要聚焦于 ClickHouse 执行层相关的研发。&lt;/p&gt; &lt;hr&gt;&lt;/hr&gt; &lt;p&gt;  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;02/免费下载资料&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;img title="&amp;#22823;&amp;#25968;&amp;#25454;&amp;#19987;&amp;#39064;&amp;#20070;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;03&lt;/strong&gt;  &lt;strong&gt;/&lt;/strong&gt;  &lt;strong&gt;报名看直播 免费领PPT&lt;/strong&gt;  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;  &lt;img title="&amp;#20844;&amp;#20247;&amp;#21495;&amp;#23614;banner.jpg"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;04&lt;/strong&gt;  &lt;strong&gt;/&lt;/strong&gt;  &lt;strong&gt;关于我们&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;DataFun：&lt;/strong&gt;专注于大数据、人工智能技术应用的分享与交流。发起于2017年，在北京、上海、深圳、杭州等城市举办超过100+线下和100+线上沙龙、论坛及峰会，已邀请超过2000位专家和学者参与分享。其公众号 DataFunTalk 累计生产原创文章800+，百万+阅读，15万+精准粉丝。&lt;/p&gt; &lt;p&gt;   &lt;strong&gt;分享、点赞、在看&lt;/strong&gt;，给个  &lt;strong&gt;3连击&lt;/strong&gt;呗！  &lt;strong&gt;&lt;/strong&gt;  &lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>dev</category>
      <guid isPermaLink="true">https://itindex.net/detail/62448-%E5%AD%97%E8%8A%82-clickhouse-%E5%A4%8D%E6%9D%82</guid>
      <pubDate>Wed, 12 Oct 2022 00:00:00 CST</pubDate>
    </item>
    <item>
      <title>项目上线一年，整理了一份SpringBoot性能优化方案！</title>
      <link>https://itindex.net/detail/62432-%E9%A1%B9%E7%9B%AE-%E4%B8%8A%E7%BA%BF-%E4%B8%80%E5%B9%B4</link>
      <description>&lt;div&gt;  &lt;br /&gt;  &lt;h1&gt;文章来源：https://juejin.cn/post/7062548565800779789&lt;/h1&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;img&gt;&lt;/img&gt;  &lt;br /&gt;  &lt;p&gt;前言&lt;/p&gt;  &lt;img&gt;&lt;/img&gt;  &lt;p&gt;SpringBoot已经成为Java届的No.1框架，每天都在蹂躏着数百万的程序员们。当服务的压力上升，对SpringBoot服务的优化就会被提上议程。&lt;/p&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;p&gt;本文将详细讲解SpringBoot服务优化的一般思路，并附上若干篇辅助文章作为开胃菜。&lt;/p&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;img&gt;&lt;/img&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;p&gt;1.有监控才有方向&lt;/p&gt;  &lt;br /&gt;  &lt;p&gt;在开始对SpringBoot服务进行性能优化之前，我们需要做一些准备，把SpringBoot服务的一些数据暴露出来。&lt;/p&gt;比如，你的服务用到了缓存，就需要把缓存命中率这些数据进行收集；用到了数据库连接池，就需要把连接池的参数给暴露出来。我们这里采用的监控工具是Prometheus，它是一个是时序数据库，能够存储我们的指标。SpringBoot可以非常方便的接入到Prometheus中。创建一个SpringBoot项目后，首先，加入maven依赖。  &lt;pre&gt;   &lt;pre&gt;    &lt;code&gt;&amp;lt;dependency&amp;gt;     &lt;br /&gt;     &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;     &lt;br /&gt;     &amp;lt;artifactId&amp;gt;spring-boot-starter-actuator&amp;lt;/artifactId&amp;gt;     &lt;br /&gt; &amp;lt;/dependency&amp;gt;     &lt;br /&gt; &amp;lt;dependency&amp;gt;     &lt;br /&gt;     &amp;lt;groupId&amp;gt;io.micrometer&amp;lt;/groupId&amp;gt;     &lt;br /&gt;     &amp;lt;artifactId&amp;gt;micrometer-registry-prometheus&amp;lt;/artifactId&amp;gt;     &lt;br /&gt; &amp;lt;/dependency&amp;gt;     &lt;br /&gt; &amp;lt;dependency&amp;gt;     &lt;br /&gt;     &amp;lt;groupId&amp;gt;io.micrometer&amp;lt;/groupId&amp;gt;     &lt;br /&gt;     &amp;lt;artifactId&amp;gt;micrometer-core&amp;lt;/artifactId&amp;gt;     &lt;br /&gt; &amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;   &lt;p&gt;    &lt;br /&gt;&lt;/p&gt;&lt;/pre&gt;然后，我们需要在  &lt;code&gt;application.properties &lt;/code&gt;配置文件中，开放相关的监控接口。  &lt;pre&gt;   &lt;pre&gt;    &lt;code&gt;management.endpoint.metrics.enabled=true     &lt;br /&gt;management.endpoints.web.exposure.include=*     &lt;br /&gt;management.endpoint.prometheus.enabled=true     &lt;br /&gt;management.metrics.export.prometheus.enabled=true&lt;/code&gt;&lt;/pre&gt;   &lt;p&gt;    &lt;br /&gt;&lt;/p&gt;&lt;/pre&gt;启动之后，我们就可以通过访问  &lt;code&gt;http://localhost:8080/actuator/prometheus&lt;/code&gt;来获取监控数据。  &lt;img&gt;&lt;/img&gt;想要监控业务数据也是比较简单的。你只需要注入一个MeterRegistry实例即可。下面是一段示例代码：  &lt;pre&gt;   &lt;pre&gt;    &lt;code&gt;@Autowired     &lt;br /&gt;MeterRegistry registry;     &lt;br /&gt;     &lt;br /&gt;@GetMapping(&amp;quot;/test&amp;quot;)     &lt;br /&gt;@ResponseBody     &lt;br /&gt;public String test() {     &lt;br /&gt;    registry.counter(&amp;quot;test&amp;quot;,     &lt;br /&gt;            &amp;quot;from&amp;quot;, &amp;quot;127.0.0.1&amp;quot;,     &lt;br /&gt;            &amp;quot;method&amp;quot;, &amp;quot;test&amp;quot;     &lt;br /&gt;    ).increment();     &lt;br /&gt;     &lt;br /&gt;    return &amp;quot;ok&amp;quot;;     &lt;br /&gt;}&lt;/code&gt;&lt;/pre&gt;   &lt;p&gt;    &lt;br /&gt;&lt;/p&gt;&lt;/pre&gt;从监控连接中，我们可以找到刚刚添加的监控信息。  &lt;pre&gt;   &lt;pre&gt;    &lt;code&gt;test_total{from=&amp;quot;127.0.0.1&amp;quot;,method=&amp;quot;test&amp;quot;,} 5.0&lt;/code&gt;&lt;/pre&gt;   &lt;p&gt;    &lt;br /&gt;&lt;/p&gt;&lt;/pre&gt;这里简单介绍一下流行的Prometheus监控体系，Prometheus使用拉的方式获取监控数据，这个暴露数据的过程可以交给功能更加齐全的telegraf组件。  &lt;img&gt;&lt;/img&gt;如图，我们通常使用Grafana进行监控数据的展示，使用AlertManager组件进行提前预警。这一部分的搭建工作不是我们的重点，感兴趣的同学可自行研究。下图便是一张典型的监控图，可以看到Redis的缓存命中率等情况。  &lt;img&gt;&lt;/img&gt;  &lt;h2&gt;   &lt;br /&gt;&lt;/h2&gt;  &lt;p&gt;2.Java生成火焰图&lt;/p&gt;  &lt;br /&gt;  &lt;p&gt;火焰图是用来分析程序运行瓶颈的工具。在纵向，表示的是调用栈的深度；横向表明的是消耗的时间。所以格子的宽度越大，越说明它可能是一个瓶颈。   &lt;br /&gt;&lt;/p&gt;火焰图也可以用来分析Java应用。可以从github上下载 async-profiler 的压缩包 进行相关操作。比如，我们把它解压到/root/目录。然后以javaagent的方式来启动Java应用。命令行如下：  &lt;pre&gt;   &lt;pre&gt;    &lt;code&gt;java -agentpath:/root/build/libasyncProfiler.so=start,svg,file=profile.svg -jar spring-petclinic-2.3.1.BUILD-SNAPSHOT.jar&lt;/code&gt;&lt;/pre&gt;   &lt;p&gt;    &lt;br /&gt;&lt;/p&gt;&lt;/pre&gt;运行一段时间后，停止进程，可以看到在当前目录下，生成了  &lt;code&gt;profile.svg&lt;/code&gt;文件，这个文件是可以用浏览器打开的，一层层向下浏览，即可找到需要优化的目标。  &lt;br /&gt;  &lt;p&gt;3.Skywalking&lt;/p&gt;  &lt;br /&gt;  &lt;p&gt;对于一个web服务来说，最缓慢的地方就在于数据库操作。所以，使用本地缓存和分布式缓存优化，能够获得最大的性能提升。   &lt;br /&gt;&lt;/p&gt;对于如何定位到复杂分布式环境中的问题，我这里想要分享另外一个工具：Skywalking。Skywalking是使用探针技术（JavaAgent）来实现的。通过在Java的启动参数中，加入javaagent的Jar包，即可将性能数据和调用链数据封装、发送到Skywalking的服务器。下载相应的安装包（如果使用ES存储，需要下载专用的安装包），配置好存储之后，即可一键启动。将agent的压缩包，解压到相应的目录。  &lt;pre&gt;   &lt;code&gt;tar xvf skywalking-agent.tar.gz  -C /opt/&lt;/code&gt;&lt;/pre&gt;在业务启动参数中加入agent的包。比如，原来的启动命令是：  &lt;pre&gt;   &lt;pre&gt;    &lt;code&gt;java  -jar /opt/test-service/spring-boot-demo.jar  --spring.profiles.active=dev&lt;/code&gt;&lt;/pre&gt;   &lt;p&gt;    &lt;br /&gt;&lt;/p&gt;&lt;/pre&gt;改造后的启动命令是：  &lt;pre&gt;   &lt;pre&gt;    &lt;code&gt;java -javaagent:/opt/skywalking-agent/skywalking-agent.jar -Dskywalking.agent.service_name=the-demo-name  -jar /opt/test-service/spring-boot-demo.ja  --spring.profiles.active=dev&lt;/code&gt;&lt;/pre&gt;   &lt;p&gt;    &lt;br /&gt;&lt;/p&gt;&lt;/pre&gt;访问一些服务的链接，打开Skywalking的UI，即可看到下图的界面。我们可以从图中找到响应比较慢QPS又比较高的的接口，进行专项优化。  &lt;img&gt;&lt;/img&gt;  &lt;h2&gt;   &lt;br /&gt;&lt;/h2&gt;  &lt;p&gt;4.优化思路&lt;/p&gt;  &lt;br /&gt;  &lt;p&gt;对一个普通的Web服务来说，我们来看一下，要访问到具体的数据，都要经历哪些主要的环节。   &lt;br /&gt;&lt;/p&gt;如下图，在浏览器中输入相应的域名，需要通过DNS解析到具体的IP地址上。为了保证高可用，我们的服务一般都会部署多份，然后使用Nginx做反向代理和负载均衡。Nginx根据资源的特性，会承担一部分动静分离的功能。其中，动态功能部分，会进入我们的SpringBoot服务。  &lt;img&gt;&lt;/img&gt;SpringBoot默认使用内嵌的tomcat作为Web容器，使用典型的MVC模式，最终访问到我们的数据。  &lt;h2&gt;   &lt;br /&gt;&lt;/h2&gt;  &lt;p&gt;5.HTTP优化&lt;/p&gt;  &lt;br /&gt;  &lt;p&gt;下面我们举例来看一下，哪些动作能够加快网页的获取。为了描述方便，我们仅讨论HTTP1.1协议的。   &lt;br /&gt;&lt;/p&gt;  &lt;br /&gt;  &lt;strong&gt;1.使用CDN加速文件获取&lt;/strong&gt;比较大的文件，尽量使用CDN（Content Delivery Network）分发。甚至是一些常用的前端脚本、样式、图片等，都可以放到CDN上。CDN通常能够加快这些文件的获取，网页加载也更加迅速。  &lt;strong&gt;2.合理设置Cache-Control值&lt;/strong&gt;浏览器会判断HTTP头Cache-Control的内容，用来决定是否使用浏览器缓存，这在管理一些静态文件的时候，非常有用。相同作用的头信息还有Expires。Cache-Control表示多久之后过期，Expires则表示什么时候过期。这个参数可以在Nginx的配置文件中进行设置。  &lt;pre&gt;   &lt;pre&gt;    &lt;code&gt;location ~* ^.+\.(ico|gif|jpg|jpeg|png)$ {     &lt;br /&gt;            # 缓存1年     &lt;br /&gt;            add_header Cache-Control:no-cache, max-age=31536000;     &lt;br /&gt;}&lt;/code&gt;&lt;/pre&gt;   &lt;p&gt;    &lt;br /&gt;&lt;/p&gt;&lt;/pre&gt;  &lt;strong&gt;3.减少单页面请求域名的数量&lt;/strong&gt;减少每个页面请求的域名数量，尽量保证在4个之内。这是因为，浏览器每次访问后端的资源，都需要先查询一次DNS，然后找到DNS对应的IP地址，再进行真正的调用。DNS有多层缓存，比如浏览器会缓存一份、本地主机会缓存、ISP服务商缓存等。从DNS到IP地址的转变，通常会花费  &lt;code&gt;20-120ms&lt;/code&gt;的时间。减少域名的数量，可加快资源的获取。  &lt;strong&gt;4.开启gzip&lt;/strong&gt;开启gzip，可以先把内容压缩后，浏览器再进行解压。由于减少了传输的大小，会减少带宽的使用，提高传输效率。在nginx中可以很容易的开启。配置如下：  &lt;pre&gt;   &lt;pre&gt;    &lt;code&gt;gzip on;     &lt;br /&gt;gzip_min_length 1k;     &lt;br /&gt;gzip_buffers 4 16k;     &lt;br /&gt;gzip_comp_level 6;     &lt;br /&gt;gzip_http_version 1.1;     &lt;br /&gt;gzip_types text/plain application/javascript text/css;&lt;/code&gt;&lt;/pre&gt;   &lt;p&gt;    &lt;br /&gt;&lt;/p&gt;&lt;/pre&gt;  &lt;strong&gt;5.对资源进行压缩&lt;/strong&gt;对JavaScript和CSS，甚至是HTML进行压缩。道理类似，现在流行的前后端分离模式，一般都是对这些资源进行压缩的。  &lt;strong&gt;6.使用keepalive&lt;/strong&gt;由于连接的创建和关闭，都需要耗费资源。用户访问我们的服务后，后续也会有更多的互动，所以保持长连接可以显著减少网络交互，提高性能。nginx默认开启了对客户端的keep avlide支持。你可以通过下面两个参数来调整它的行为。  &lt;pre&gt;   &lt;pre&gt;    &lt;code&gt;http {     &lt;br /&gt;    keepalive_timeout  120s 120s;     &lt;br /&gt;    keepalive_requests 10000;     &lt;br /&gt;}&lt;/code&gt;&lt;/pre&gt;   &lt;p&gt;    &lt;br /&gt;&lt;/p&gt;&lt;/pre&gt;nginx与后端upstream的长连接，需要手工开启，参考配置如下：  &lt;pre&gt;   &lt;pre&gt;    &lt;code&gt;location ~ /{     &lt;br /&gt;       proxy_pass http://backend;     &lt;br /&gt;       proxy_http_version 1.1;     &lt;br /&gt;       proxy_set_header Connection&amp;quot;&amp;quot;;     &lt;br /&gt;}&lt;/code&gt;&lt;/pre&gt;   &lt;p&gt;    &lt;br /&gt;&lt;/p&gt;&lt;/pre&gt;  &lt;h2&gt;   &lt;br /&gt;&lt;/h2&gt;  &lt;p&gt;6.Tomcat优化&lt;/p&gt;  &lt;br /&gt;  &lt;p&gt;Tomcat本身的优化，也是非常重要的一环。可以直接参考下面的文章。   &lt;br /&gt;&lt;/p&gt;  &lt;a href="https://mp.weixin.qq.com/s?__biz=MzA4MTc4NTUxNQ==&amp;mid=2650523823&amp;idx=1&amp;sn=aa5f41974950e759d373505e9b7c32f6&amp;chksm=8780cf6bb0f7467d0e1b4d2d957e3633726c11f30e185d9ed354f2f6ad2ff607357f555097ef&amp;scene=21#wechat_redirect" target="_blank"&gt;搞定tomcat重要参数调优！&lt;/a&gt;  &lt;h2&gt;   &lt;br /&gt;&lt;/h2&gt;  &lt;p&gt;7.自定义Web容器&lt;/p&gt;  &lt;br /&gt;  &lt;p&gt;如果你的项目并发量比较高，想要修改最大线程数、最大连接数等配置信息，可以通过自定义Web容器的方式，代码如下所示。&lt;/p&gt;  &lt;pre&gt;   &lt;pre&gt;    &lt;code&gt;@SpringBootApplication(proxyBeanMethods = false)     &lt;br /&gt;public class App implements WebServerFactoryCustomizer&amp;lt;ConfigurableServletWebServerFactory&amp;gt; {     &lt;br /&gt; public static void main(String[] args) {     &lt;br /&gt;  SpringApplication.run(PetClinicApplication.class, args);     &lt;br /&gt; }     &lt;br /&gt; @Override     &lt;br /&gt; public void customize(ConfigurableServletWebServerFactory factory) {     &lt;br /&gt;  TomcatServletWebServerFactory f = (TomcatServletWebServerFactory) factory;     &lt;br /&gt;        f.setProtocol(&amp;quot;org.apache.coyote.http11.Http11Nio2Protocol&amp;quot;);     &lt;br /&gt;     &lt;br /&gt;  f.addConnectorCustomizers(c -&amp;gt; {     &lt;br /&gt;   Http11NioProtocol protocol = (Http11NioProtocol) c.getProtocolHandler();     &lt;br /&gt;   protocol.setMaxConnections(200);     &lt;br /&gt;   protocol.setMaxThreads(200);     &lt;br /&gt;   protocol.setSelectorTimeout(3000);     &lt;br /&gt;   protocol.setSessionTimeout(3000);     &lt;br /&gt;   protocol.setConnectionTimeout(3000);     &lt;br /&gt;  });     &lt;br /&gt; }     &lt;br /&gt;}&lt;/code&gt;&lt;/pre&gt;   &lt;p&gt;    &lt;br /&gt;&lt;/p&gt;&lt;/pre&gt;注意上面的代码，我们设置了它的协议为org.apache.coyote.http11.Http11Nio2Protocol，意思就是开启了Nio2。这个参数在Tomcat8.0之后才有，开启之后会增加一部分性能。对比如下：  &lt;strong&gt;默认。&lt;/strong&gt;  &lt;pre&gt;   &lt;pre&gt;    &lt;code&gt;[root@localhost wrk2-master]# ./wrk -t2 -c100 -d30s -R2000 http://172.16.1.57:8080/owners?lastName=     &lt;br /&gt;Running 30s test @ http://172.16.1.57:8080/owners?lastName=     &lt;br /&gt;  2 threads and 100 connections     &lt;br /&gt;  Thread calibration: mean lat.: 4588.131ms, rate sampling interval: 16277ms     &lt;br /&gt;  Thread calibration: mean lat.: 4647.927ms, rate sampling interval: 16285ms     &lt;br /&gt;  Thread Stats   Avg      Stdev     Max   +/- Stdev     &lt;br /&gt;    Latency    16.49s     4.98s   27.34s    63.90%     &lt;br /&gt;    Req/Sec   106.50      1.50   108.00    100.00%     &lt;br /&gt;  6471 requests in 30.03s, 39.31MB read     &lt;br /&gt;  Socket errors: connect 0, read 0, write 0, timeout 60     &lt;br /&gt;Requests/sec:    215.51     &lt;br /&gt;Transfer/sec:      1.31MB&lt;/code&gt;&lt;/pre&gt;   &lt;p&gt;    &lt;br /&gt;&lt;/p&gt;&lt;/pre&gt;  &lt;strong&gt;Nio2。&lt;/strong&gt;  &lt;pre&gt;   &lt;pre&gt;    &lt;code&gt;[root@localhost wrk2-master]# ./wrk -t2 -c100 -d30s -R2000 http://172.16.1.57:8080/owners?lastName=     &lt;br /&gt;Running 30s test @ http://172.16.1.57:8080/owners?lastName=     &lt;br /&gt;  2 threads and 100 connections     &lt;br /&gt;  Thread calibration: mean lat.: 4358.805ms, rate sampling interval: 15835ms     &lt;br /&gt;  Thread calibration: mean lat.: 4622.087ms, rate sampling interval: 16293ms     &lt;br /&gt;  Thread Stats   Avg      Stdev     Max   +/- Stdev     &lt;br /&gt;    Latency    17.47s     4.98s   26.90s    57.69%     &lt;br /&gt;    Req/Sec   125.50      2.50   128.00    100.00%     &lt;br /&gt;  7469 requests in 30.04s, 45.38MB read     &lt;br /&gt;  Socket errors: connect 0, read 0, write 0, timeout 4     &lt;br /&gt;Requests/sec:    248.64     &lt;br /&gt;Transfer/sec:      1.51MB&lt;/code&gt;&lt;/pre&gt;   &lt;p&gt;    &lt;br /&gt;&lt;/p&gt;&lt;/pre&gt;你甚至可以将tomcat替换成undertow。undertow也是一个Web容器，更加轻量级一些，占用的内容更少，启动的守护进程也更少，更改方式如下：  &lt;pre&gt;   &lt;pre&gt;    &lt;code&gt;&amp;lt;dependency&amp;gt;     &lt;br /&gt;      &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;     &lt;br /&gt;      &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;     &lt;br /&gt;      &amp;lt;exclusions&amp;gt;     &lt;br /&gt;        &amp;lt;exclusion&amp;gt;     &lt;br /&gt;          &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;     &lt;br /&gt;          &amp;lt;artifactId&amp;gt;spring-boot-starter-tomcat&amp;lt;/artifactId&amp;gt;     &lt;br /&gt;        &amp;lt;/exclusion&amp;gt;     &lt;br /&gt;      &amp;lt;/exclusions&amp;gt;     &lt;br /&gt;    &amp;lt;/dependency&amp;gt;     &lt;br /&gt;    &amp;lt;dependency&amp;gt;     &lt;br /&gt;      &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;     &lt;br /&gt;      &amp;lt;artifactId&amp;gt;spring-boot-starter-undertow&amp;lt;/artifactId&amp;gt;     &lt;br /&gt;    &amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;   &lt;p&gt;    &lt;br /&gt;&lt;/p&gt;&lt;/pre&gt;  &lt;h2&gt;   &lt;br /&gt;&lt;/h2&gt;  &lt;p&gt;8.各个层次的优化方向&lt;/p&gt;  &lt;br /&gt;  &lt;h4&gt;   &lt;strong&gt;Controller层&lt;/strong&gt;&lt;/h4&gt;controller层用于接收前端的查询参数，然后构造查询结果。现在很多项目都采用前后端分离的架构，所以controller层的方法，一般会使用@ResponseBody注解，把查询的结果，解析成JSON数据返回（兼顾效率和可读性）。由于controller只是充当了一个类似功能组合和路由的角色，所以这部分对性能的影响就主要体现在数据集的大小上。如果结果集合非常大，JSON解析组件就要花费较多的时间进行解析。大结果集不仅会影响解析时间，还会造成内存浪费。假如结果集在解析成JSON之前，占用的内存是10MB，那么在解析过程中，有可能会使用20M或者更多的内存去做这个工作。我见过很多案例，由于返回对象的嵌套层次太深、引用了不该引用的对象（比如非常大的byte[]对象），造成了内存使用的飙升。所以，对于一般的服务，保持结果集的精简，是非常有必要的，这也是DTO(data transfer object)存在的必要。如果你的项目，返回的结果结构比较复杂，对结果集进行一次转换是非常有必要的。另外，可以使用异步Servlet对Controller层进行优化。它的原理如下：Servlet 接收到请求之后，将请求转交给一个异步线程来执行业务处理，线程本身返回至容器，异步线程处理完业务以后，可以直接生成响应数据，或者将请求继续转发给其它 Servlet。  &lt;h4&gt;   &lt;strong&gt;Service层&lt;/strong&gt;&lt;/h4&gt;  &lt;p&gt;service层用于处理具体的业务，大部分功能需求都是在这里完成的。service层一般是使用单例模式（prototype），很少会保存状态，而且可以被controller复用。&lt;/p&gt;service层的代码组织，对代码的可读性、性能影响都比较大。我们常说的设计模式，大多数都是针对于service层来说的。这里要着重提到的一点，就是分布式事务。  &lt;img&gt;&lt;/img&gt;如上图，四个操作分散在三个不同的资源中。要想达到一致性，需要三个不同的资源进行统一协调。它们底层的协议，以及实现方式，都是不一样的。那就无法通过Spring提供的Transaction注解来解决，需要借助外部的组件来完成。很多人都体验过，加入了一些保证一致性的代码，一压测，性能掉的惊掉下巴。分布式事务是性能杀手，因为它要使用额外的步骤去保证一致性，常用的方法有：两阶段提交方案、TCC、本地消息表、MQ事务消息、分布式事务中间件等。  &lt;img&gt;&lt;/img&gt;如上图，分布式事务要在改造成本、性能、实效等方面进行综合考虑。有一个介于分布式事务和非事务之间的名词，叫做  &lt;strong&gt;柔性事务&lt;/strong&gt;。柔性事务的理念是将业务逻辑和互斥操作，从资源层上移至业务层面。关于传统事务和柔性事务，我们来简单比较一下。  &lt;strong&gt;ACID&lt;/strong&gt;关系数据库, 最大的特点就是事务处理, 即满足ACID。  &lt;ul&gt;   &lt;li&gt;    &lt;p&gt;原子性（Atomicity）：事务中的操作要么都做，要么都不做。&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;一致性（Consistency）：系统必须始终处在强一致状态下。&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;隔离性（Isolation）：一个事务的执行不能被其他事务所干扰。&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;持续性（Durability）：一个已提交的事务对数据库中数据的改变是永久性的。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;strong&gt;BASE&lt;/strong&gt;BASE方法通过牺牲一致性和孤立性来提高可用性和系统性能。BASE为BasicallyAvailable, Soft-state, Eventually consistent三者的缩写，其中BASE分别代表：  &lt;ul&gt;   &lt;li&gt;    &lt;p&gt;基本可用（Basically Available）：系统能够基本运行、一直提供服务。&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;软状态（Soft-state）：系统不要求一直保持强一致状态。&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;最终一致性（Eventual consistency）：系统需要在某一时刻后达到一致性要求。&lt;/li&gt;&lt;/ul&gt;互联网业务，推荐使用补偿事务，完成最终一致性。比如，通过一系列的定时任务，完成对数据的修复。具体可以参照下面的文章。  &lt;a href="https://mp.weixin.qq.com/s?__biz=MzA4MTc4NTUxNQ==&amp;mid=2650525007&amp;idx=1&amp;sn=1dccfcb84f21fd9fe335a756a9c78c0f&amp;scene=21#wechat_redirect" target="_blank"&gt;常用的 分布式事务 都有哪些？我该用哪个？&lt;/a&gt;  &lt;h4&gt;   &lt;strong&gt;Dao层&lt;/strong&gt;&lt;/h4&gt;  &lt;p&gt;经过合理的数据缓存，我们都会尽量避免请求穿透到Dao层。除非你对ORM本身提供的缓存特性特别的熟悉，否则，都推荐你使用更加通用的方式去缓存数据。   &lt;br /&gt;&lt;/p&gt;  &lt;strong&gt;Dao层&lt;/strong&gt;，主要在于对ORM框架的使用上。比如，在JPA中，如果加了一对多或者多对多的映射关系，而又没有开启懒加载，级联查询的时候就容易造成深层次的检索，造成了内存开销大、执行缓慢的后果。在一些数据量比较大的业务中，多采用分库分表的方式。在这些分库分表组件中，很多简单的查询语句，都会被重新解析后分散到各个节点进行运算，最后进行结果合并。举个例子，select count(*) from a这句简单的count语句，就可能将请求路由到十几张表中去运算，最后在协调节点进行统计，执行效率是可想而知的。目前，分库分表中间件，比较有代表性的是驱动层的ShardingJdbc和代理层的MyCat，它们都有这样的问题。这些组件提供给使用者的视图是一致的，但我们在编码的时候，一定要注意这些区别。  &lt;h2&gt;   &lt;br /&gt;&lt;/h2&gt;  &lt;p&gt;End&lt;/p&gt;  &lt;br /&gt;  &lt;p&gt;下面我们来总结一下。   &lt;br /&gt;&lt;/p&gt;我们简单看了一下SpringBoot常见的优化思路。我们介绍了三个新的性能分析工具。一个是监控系统Prometheus，可以看到一些具体的指标大小；一个是火焰图，可以看到具体的代码热点；一个是Skywalking，可以分析分布式环境中的调用链。在对性能有疑惑的时候，我们都会采用类似于  &lt;strong&gt;神农氏尝百草&lt;/strong&gt;的方式，综合各种测评工具的结果进行分析。SpringBoot自身的Web容器是Tomcat，那我们就可以通过对Tomcat的调优来获取性能提升。当然，对于服务上层的负载均衡Nginx，我们也提供了一系列的优化思路。最后，我们看了在经典的MVC架构下，Controller、Service、Dao的一些优化方向，并着重看了Service层的分布式事务问题。这里有一个具体的优化示例。  &lt;a href="https://mp.weixin.qq.com/s?__biz=MzA4MTc4NTUxNQ==&amp;mid=2650524679&amp;idx=1&amp;sn=8881766b35e1d65a65e520f3514f0ec9&amp;chksm=8780cbc3b0f742d5721f10ec8eea9823ed8a7dbec4364846706e34df44b9e4518fed11b02906&amp;scene=21#wechat_redirect" target="_blank"&gt;5秒到1秒，记一次效果“非常”显著的性能优化&lt;/a&gt;SpringBoot作为一个广泛应用的服务框架，在性能优化方面已经做了很多工作，选用了很多高速组件。比如，数据库连接池默认使用hikaricp，Redis缓存框架默认使用lettuce，本地缓存提供caffeine等。对于一个普通的于数据库交互的Web服务来说，缓存是最主要的优化手。但细节决定成败，你要是想对系统做极致的优化，还需要参考下面的这篇文章。  &lt;a href="https://mp.weixin.qq.com/s?__biz=MzA4MTc4NTUxNQ==&amp;mid=2650522440&amp;idx=1&amp;sn=e06d3848bf84ec0da769a41b2ec59482&amp;chksm=8780c48cb0f74d9a9cfcc6b69f36d1f197f1d3942e9864225ed4212d2130b6f7862c427eba26&amp;token=1153507763&amp;lang=zh_CN&amp;scene=21#wechat_redirect" target="_blank"&gt;卓越性能 の 军火库（非广告）&lt;/a&gt;  &lt;img&gt;&lt;/img&gt;  &lt;img&gt;&lt;/img&gt;  &lt;p&gt;欢迎扫码加入儒猿技术交流群，每天晚上20:00都有Java面试、Redis、MySQL、RocketMQ、SpringCloudAlibaba、Java架构等技术答疑分享，更能跟小伙伴们一起交流技术&lt;/p&gt;  &lt;img&gt;&lt;/img&gt;  &lt;p&gt;   &lt;strong&gt;另外推荐儒猿课堂的1元系列课程给您，欢迎加入一起学习~&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;strong&gt;互联网Java工程师面试突击课&lt;/strong&gt;  &lt;strong&gt;（1元专享）&lt;/strong&gt;  &lt;p&gt;   &lt;img&gt;&lt;/img&gt;&lt;/p&gt;  &lt;strong&gt;SpringCloudAlibaba零基础入门到项目实战&lt;/strong&gt;  &lt;strong&gt;（1元专享）&lt;/strong&gt;  &lt;p&gt;   &lt;img&gt;&lt;/img&gt;&lt;/p&gt;  &lt;strong&gt;亿级流量下的电商详情页系统实战项目&lt;/strong&gt;  &lt;strong&gt;（1元专享）&lt;/strong&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;strong&gt;Kafka消息中间件内核源码精讲&lt;/strong&gt;  &lt;strong&gt;（1元专享）&lt;/strong&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;strong&gt;12个实战案例带你玩转Java并发编程&lt;/strong&gt;  &lt;strong&gt;（1元专享）&lt;/strong&gt;  &lt;p&gt;   &lt;img&gt;&lt;/img&gt;&lt;/p&gt;  &lt;strong&gt;Elasticsearch零基础入门到精通&lt;/strong&gt;  &lt;strong&gt;（1元专享）&lt;/strong&gt;  &lt;p&gt;   &lt;img&gt;&lt;/img&gt;&lt;/p&gt;  &lt;strong&gt;基于Java手写分布式中间件系统实战&lt;/strong&gt;  &lt;strong&gt;（1元专享）&lt;/strong&gt;  &lt;p&gt;   &lt;img&gt;&lt;/img&gt;&lt;/p&gt;  &lt;strong&gt;基于ShardingSphere的分库分表实战课&lt;/strong&gt;  &lt;strong&gt;（1元专享）&lt;/strong&gt;  &lt;p&gt;   &lt;img&gt;&lt;/img&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>dev</category>
      <guid isPermaLink="true">https://itindex.net/detail/62432-%E9%A1%B9%E7%9B%AE-%E4%B8%8A%E7%BA%BF-%E4%B8%80%E5%B9%B4</guid>
      <pubDate>Fri, 23 Sep 2022 00:00:00 CST</pubDate>
    </item>
    <item>
      <title>引入 Kubernetes 是过早优化的危险信号</title>
      <link>https://itindex.net/detail/62427-kubernetes-%E4%BC%98%E5%8C%96-%E4%BF%A1%E5%8F%B7</link>
      <description>&lt;br /&gt;【编者的话】引入 Kubernetes 是过早优化的危险信号？且听听本文作者怎么说吧。 &lt;br /&gt;
 &lt;br /&gt;如果你所在的企业引入了 Kubernetes，那么你们很有可能会把精力花在一些偏离主线的事情上。 &lt;br /&gt;
 &lt;br /&gt;乍一听这句话可能会感觉到很奇怪，毕竟我们花了这么长的时间来布道和兜售 Kubernetes 的发行版以及咨询服务，致力于帮助人们能够更加充分地利用它，但是事情就是这样！ &lt;strong&gt;你也许不应该针对你的产品使用 Kubernetes 以及其他一堆“酷”的东西&lt;/strong&gt;。 &lt;br /&gt;
 &lt;br /&gt;绝大多数的初创以及扩张阶段的企业在构建软件时都应该避免使用 Kubernetes 以及其他的一些过早优化。如果你所在的企业使用了 Kubernetes，那么你们很有可能会把精力花在一些偏离主线的事情上。你们可能已经跌入了过早优化的陷阱。 &lt;br /&gt;
 &lt;br /&gt;请不要觉得这篇文章只是针对 Kubernetes。不是的。这篇文章针对的是工程师们在构建软件的过程中可能做出的所有过早优化。 &lt;br /&gt;
 &lt;br /&gt;以下是我见过的一些例子： &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;将 Kubernetes 用于一个应用（还是个 Web 应用）的企业；&lt;/li&gt;  &lt;li&gt;应用程序用到了不只一种语言。比如，后端用的是 Golang、Ruby 或者是 PHP 等语言，然后前端 Web 用的是 React 或者 Vue 等框架；&lt;/li&gt;  &lt;li&gt;没有用云服务来托管应用。比如可以用 Heroku、Vercel、Netlify 或者    &lt;a href="http://fly.io/"&gt;Fly.io&lt;/a&gt; 等。对于绝大多数产品团队来说，如果他们必须组建一个运维或者基础架构团队的话，他们的解决方案也将会是过度设计的。&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt;试想一下，一个人在真正开始玩他的爱好之前花费了大量的时间和金钱为这个爱好挑选最好的装备。 &lt;br /&gt;
 &lt;br /&gt;当然，这里面有一些观点是比较主观的。也许你知道你会长期坚持你的新爱好，而且你有一个朋友刚好是这方面的行家，他可以帮你挑选合适的装备。不得不说，我自己就很擅长为自己辩解为什么要挑选精英装备，尽管我可能永远不会真的注意到这其中的区别。 &lt;br /&gt;
 &lt;br /&gt; &lt;h3&gt;好钢要用在刀刃上&lt;/h3&gt;如果你所在的企业认为它需要 Kubernetes，那么你也许正处在一个试图过早地为未来优化的地方。一个可能永远不会出现的未来。当你采用任何一项技术时，换个角度，也即是在为你所在的组织作出一个有效期长达数年的承诺，这将会增加产品的表面积，同时也会给开发人员带来心智负担。最终，你将不得不组建一个专门的团队来维护它。这一切都会从你的核心使命里夺走资源。 &lt;br /&gt;
 &lt;br /&gt;工程师们很容易跌入这个陷阱。我们很容易被新兴的炫酷技术分散注意力。我们想要学习和成长，而实现这一点的最佳途径就是把最新的技术融入到我们的产品里。然后，我们会想出各种理由来证明我们的决定是合理的。 &lt;br /&gt;
 &lt;br /&gt;我来给你们讲几个故事吧，关于我是如何跌入这个陷阱的。 &lt;br /&gt;
 &lt;br /&gt;我记得在我刚加入 OCUS 的时候我们有过一次讨论，我发现我们当时正在使用 Kubernetes。我说了一些话，类似于，“哦，那太好了。万一有一天我们如果想要弃用 AWS ，那么引入 Kubernetes 刚好可以帮助我们解决这个问题”。你能看出当时我有多疯狂吗？ &lt;br /&gt;
 &lt;br /&gt;还有一次，我们的数据科学团队告诉我们，他们的数据管道需要一个编排工具。我倾向于选用  &lt;a href="https://argoproj.github.io/workflows"&gt;Argo Workflow&lt;/a&gt;（它跑在 Kubernetes 里），而不是他们已经做了 PoC 的  &lt;a href="https://www.prefect.io/"&gt;Perfect&lt;/a&gt;（一款 SaaS 产品）。对于这个决定，我可以给出种种理由。不幸的是，它们都是基于过早优化的前提。故事的最后，我们的团队需要去构建一组新的 Terraform 和 Helm Chart 来自动化部署 Argo Workflow，然后把它集成到我们的 SSO 等等。对于这个决定我感到很遗憾。我认为正是由于做出了这个决定，这导致我们延误了数周甚至数月的时间才向最终用户交付功能。这就是过早优化！ &lt;br /&gt;
 &lt;br /&gt;如果我们能够避免过早优化，我们将可以比竞争对手更快地行动，取悦我们的用户，构建一套可持续和可行的产品的可能性也会提高。 &lt;br /&gt;
 &lt;br /&gt;那么我们怎样才能打破这种思维呢？ &lt;br /&gt;
 &lt;br /&gt; &lt;h3&gt;用户有没有提这个要求？&lt;/h3&gt;解决沿途遇到的问题而不必再提前考虑。我们所做的工作都应该切实地解决用户的问题。问问自己，我试图通过我的工作影响哪些人类行为？ &lt;br /&gt;
 &lt;br /&gt;如果你可以始终专注于用户行为，并且只解决那些它们自己冒出来的实际问题，那么你将会对自己产生的影响力感到惊讶。你也许还会对自己曾经向用户作出的诸多假设感到惊讶，因为你已经很久没谈论这些了。 &lt;br /&gt;
 &lt;br /&gt;我相信，严格坚持这种方法的企业将会为他们的用户和股东们创造更大的影响力并且产生更多的价值。 &lt;br /&gt;
 &lt;br /&gt;如果我把所有的精力都倾注在我的新爱好而不是研究设备上，那么我自然会知道我想要的到底是什么。在起步阶段，我并不需要最 “Gucci” 的装备，即便我有最好的装备，我甚至也会因为不知道如何使用它而显得格格不入。最好是用一套入门级设备来碾压别人，因为我所有的精力都用在了学习我的新爱好上。然后当我确实想要升级到 “Gucci” 装备时，它就会真的变得不同凡响。 &lt;br /&gt;
 &lt;br /&gt; &lt;h3&gt;事半功倍&lt;/h3&gt;值得庆幸的是，科技界正在经历一场大规模的路线修正。随着利率上升，廉价债务和风险资本开始枯竭。现如今的初创企业再也无法获得疯狂的资金，他们必须更加专注于他们的使命。能够生存下来的将是那些拥有坚实基础的企业。 &lt;br /&gt;
 &lt;br /&gt;产品必须交给那些能够以越来越快的速度交付业务成果的更小团队来构建。 &lt;br /&gt;
 &lt;br /&gt;在 Kubernetes 完全普及之前，我无法预见到 Kubernetes 在一个精益组织中的位置。即便如此，我认为 Kubernetes 还是可以当作一个扩展引入的。大多数组织可以考虑通过云厂商提供的一些更高层面的构建块来引入它。 &lt;br /&gt;
 &lt;br /&gt;别忘了，在 Facebook 以 190 亿美元收购 WhatsApp 时，它只有 35 名开发人员却服务了 4.5 亿用户！ &lt;br /&gt;
 &lt;br /&gt;如果要问本文给读者的忠告是什么的话，我想应当会是：高度关注实现组织使命所需的内容。不要被你想学习的东西（比如 Kubernetes 或 Golang）分心 —— 把它留给家庭实验室吧。 &lt;br /&gt;
 &lt;br /&gt;如果你可以继续高度专注于对自己的产品进行迭代，以影响业务结果的方式影响人类行为，那么我毫不怀疑你将最终能够脱颖而出。 &lt;br /&gt;
 &lt;br /&gt;这就是本周的全部内容，伙计们，祝你度过愉快的一周！ &lt;br /&gt;
 &lt;br /&gt; &lt;strong&gt;原文链接：  &lt;a href="https://www.jeremybrown.tech/8-kubernetes-is-a-red-flag-signalling-premature-optimisation"&gt;Kubernetes is a red flag signalling premature optimisation.&lt;/a&gt; （翻译：Colstuwjx）&lt;/strong&gt;
                                
                                                            &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62427-kubernetes-%E4%BC%98%E5%8C%96-%E4%BF%A1%E5%8F%B7</guid>
      <pubDate>Mon, 19 Sep 2022 00:27:42 CST</pubDate>
    </item>
    <item>
      <title>mysql高手进阶优化篇</title>
      <link>https://itindex.net/detail/62399-mysql-%E9%AB%98%E6%89%8B-%E4%BC%98%E5%8C%96</link>
      <description>&lt;h1&gt;MySql理论&lt;/h1&gt;
 &lt;h2&gt;逻辑架构&lt;/h2&gt;
 &lt;p&gt;连接层--&amp;gt;服务层--&amp;gt;引擎层--&amp;gt;存储层&lt;/p&gt;
 &lt;h2&gt;存储引擎&lt;/h2&gt;
 &lt;h3&gt;查看方式&lt;/h3&gt;
 &lt;p&gt;1.查看mysql现在提供的搜索引擎---&amp;gt;show engines&lt;/p&gt;
 &lt;p&gt;2.查看mysql当前默认存储引擎show variables like &amp;apos;$storage_engine$&amp;apos;&lt;/p&gt;
 &lt;h3&gt;存储引擎对比&lt;/h3&gt;
 &lt;table&gt;

  &lt;tr&gt;
   &lt;th&gt;&lt;/th&gt;
   &lt;th&gt;InnoDB&lt;/th&gt;
   &lt;th&gt;MyISAM&lt;/th&gt;
&lt;/tr&gt;


  &lt;tr&gt;
   &lt;td&gt;主外键&lt;/td&gt;
   &lt;td&gt;支持&lt;/td&gt;
   &lt;td&gt;不支持&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;事务&lt;/td&gt;
   &lt;td&gt;支持&lt;/td&gt;
   &lt;td&gt;不支持&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;行表锁&lt;/td&gt;
   &lt;td&gt;行锁 操作是只锁住某一行不对其他行有影响 适合高并发&lt;/td&gt;
   &lt;td&gt;表锁 即使操作一条数据也会锁住整个表 不适合高并发操作&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;缓存&lt;/td&gt;
   &lt;td&gt;不仅缓存索引 还缓存真实数据 对内存要求较高 内存大小对性能有绝对性因素&lt;/td&gt;
   &lt;td&gt;只缓存索引，不缓存数据&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;表空间&lt;/td&gt;
   &lt;td&gt;大&lt;/td&gt;
   &lt;td&gt;小&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;关注点&lt;/td&gt;
   &lt;td&gt;事务&lt;/td&gt;
   &lt;td&gt;性能&lt;/td&gt;
&lt;/tr&gt;

&lt;/table&gt;
 &lt;table&gt;

  &lt;tr&gt;
   &lt;th&gt;    &lt;strong&gt;存储引擎&lt;/strong&gt;&lt;/th&gt;
   &lt;th&gt;    &lt;strong&gt;InnoDB&lt;/strong&gt;&lt;/th&gt;
   &lt;th&gt;    &lt;strong&gt;MyISAM&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;


  &lt;tr&gt;
   &lt;td&gt;存储文件&lt;/td&gt;
   &lt;td&gt;.frm表定义文件 .ibd数据文件&lt;/td&gt;
   &lt;td&gt;.frm表定义文件 .myd数据文件 .myi 索引文件&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;锁&lt;/td&gt;
   &lt;td&gt;表锁，行锁&lt;/td&gt;
   &lt;td&gt;表锁&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;事务&lt;/td&gt;
   &lt;td&gt;ACID&lt;/td&gt;
   &lt;td&gt;不支持&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;CRUD&lt;/td&gt;
   &lt;td&gt;读写&lt;/td&gt;
   &lt;td&gt;读多&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;count&lt;/td&gt;
   &lt;td&gt;扫表&lt;/td&gt;
   &lt;td&gt;专门存储的地方&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;索引结构&lt;/td&gt;
   &lt;td&gt;B+Tree&lt;/td&gt;
   &lt;td&gt;B+Tree&lt;/td&gt;
&lt;/tr&gt;

&lt;/table&gt;
 &lt;p&gt;  &lt;strong&gt;MyISAM:&lt;/strong&gt;  B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候，首先按照B+Tree搜索算法搜索索引，如果指定的Key存在，则取出其 data 域的值，然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;InnoDB:&lt;/strong&gt;  其数据文件本身就是索引文件。相比MyISAM，索引文件和数据文件是分离的，其表数据文件本身就是按B+Tree组织的一个索引结构，树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键，因此InnoDB表数据文件本身就是主索引。这被称为“聚簇索引（或聚集索引）”。而其余的索引都作为辅助索引，辅助索引的data域存储相应记录主键的值而不是地址，这也是和MyISAM不同的地方。  &lt;strong&gt;在根据主索引搜索时，直接找到key所在的节点即可取出数据；在根据辅助索引查找时，则需要先取出主键的值，再走一遍主索引。&lt;/strong&gt;    &lt;strong&gt;因此，在设计表的时候，不建议使用过长的字段作为主键，也不建议使用非单调的字段作为主键，这样会造成主索引频繁分裂&lt;/strong&gt;&lt;/p&gt;
 &lt;h4&gt;锁机制与InnoDB算法&lt;/h4&gt;
 &lt;p&gt;  &lt;strong&gt;MyISAM和InnoDB存储引擎使用的锁：&lt;/strong&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;MyISAM采用表级锁(table-level locking)。&lt;/li&gt;
  &lt;li&gt;InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;表级锁和行级锁对比：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;表级锁： MySQL中锁定 粒度最大 的一种锁，对当前操作的整张表加锁，实现简单，资源消耗也比较少，加锁快，不会出现死锁。其锁定粒度最大，触发锁冲突的概率最高，并发度最低，MyISAM和 InnoDB引擎都支持表级锁。&lt;/li&gt;
  &lt;li&gt;行级锁： MySQL中锁定 粒度最小 的一种锁，只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小，并发度高，但加锁的开销也最大，加锁慢，会出现死锁。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;strong&gt;InnoDB存储引擎的锁的算法有三种：&lt;/strong&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;Record lock：单个行记录上的锁&lt;/li&gt;
  &lt;li&gt;Gap lock：间隙锁，锁定一个范围，不包括记录本身&lt;/li&gt;
  &lt;li&gt;Next-key lock：record+gap 锁定一个范围，包含记录本身&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;性能下降SQL慢的原因&lt;/h2&gt;
 &lt;p&gt;1.查询语句写的不好&lt;/p&gt;
 &lt;p&gt;2.索引失效&lt;/p&gt;
 &lt;p&gt;3.关联查询太多join&lt;/p&gt;
 &lt;p&gt;4.服务器调优及各个参数设置(缓冲，线程数等 )&lt;/p&gt;
 &lt;h2&gt;Mysql索引&lt;/h2&gt;
 &lt;p&gt;定义:是帮助MySql高效获取数据的数据结构(排好序的快速查找的数据结构)&lt;/p&gt;
 &lt;p&gt;如果没有特别指明，都是B树(多路搜索树 并不一定是二叉树)结构组织索引。&lt;/p&gt;
 &lt;h3&gt;优点&lt;/h3&gt;
 &lt;p&gt;通过索引对数据进行排序。降低数据排序的成本，降低了CPU的消耗&lt;/p&gt;
 &lt;h3&gt;缺点&lt;/h3&gt;
 &lt;p&gt;1.索引也是一张表，该表保存了主键与索引字段，并指向实体表记录，所以索引列也是占用空间的&lt;/p&gt;
 &lt;p&gt;2.虽然索引提高了查询速度，但是降低更新表的速度(insert update delete) 。因为更新表Mysql不仅要保存数据，还要保存一下索引文件每次更新添加了索引列的字段，都会因为调整更新所带来的减值变化的的索引信息&lt;/p&gt;
 &lt;p&gt;3.索引只是提高效率的一个因素，如果Mysql有大数据量的表，就需要花时间研究建立最优秀的    索引或优化查询&lt;/p&gt;
 &lt;h3&gt;索引的建立&lt;/h3&gt;
 &lt;p&gt;创建索引:create [unique] index 索引名字 on 表名(列名)&lt;/p&gt;
 &lt;p&gt;alter 表名 ADD [unique] index 索引名字 on(列名)&lt;/p&gt;
 &lt;p&gt;删除索引: drop index [索引名字] on 表名&lt;/p&gt;
 &lt;p&gt;查看索引: show index from 表名&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bf956cc25052459fbd092baddcd0e6d4~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;索引结构&lt;/h3&gt;
 &lt;p&gt;B-Tree&lt;/p&gt;
 &lt;p&gt;树中每个节点最多包含m个孩子 (最多有m个 分支)&lt;/p&gt;
 &lt;p&gt;出根节点与叶子节点以外，每个节点至少有[ceil(m/2)]个孩子(中间的节点)&lt;/p&gt;
 &lt;p&gt;若根节点不是叶子节点，则至少有两个孩子（根节点最少有两个分支）&lt;/p&gt;
 &lt;p&gt;所有叶子节点都在同一层 (没有子节点的都在一层)&lt;/p&gt;
 &lt;p&gt;每个非叶子节点有n个key和n+1个指针组成其中[ceil(m/2)-1]&amp;lt;=n&amp;lt;=m-1 (指针数最多和m相等)&lt;/p&gt;
 &lt;p&gt;以5叉B-Tree(m=5)为例[ceil(m/2)-1]&amp;lt;=n&amp;lt;=m-1 的 2&amp;lt;=n&amp;lt;=4.当n&amp;gt;4中间节点分裂到父节点，两边节点分裂。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c5ec3b4175a84db2ae4931d957c8fe5d~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;B+Tree索引&lt;/p&gt;
 &lt;p&gt;B树变种&lt;/p&gt;
 &lt;p&gt;n叉B+Tree最多含有n个key，而BTree最多含有n-1个key&lt;/p&gt;
 &lt;p&gt;B+Tree的叶子节点保留索引key信息，依key大小进行顺序排列&lt;/p&gt;
 &lt;p&gt;所有非叶子节点都可以看着是key的索引部分&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8f31ec3366c647a48077070c3006ce15~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;B+Tree查找数据必须到叶子节点，查询任何key都有从root走到叶子节点，查询稳定性高‘&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;mysql索引数据结构对经典的 B+Tree进行了优化。在原B+Tree的基础上，增加一个指向相邻叶子节点的链表指针，就形成了带有顺序指针的B+Tree,提高区间访问性能(范围查找)&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a885a0f8d4934d62b98f8b8766309f73~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;2.Hash索引&lt;/p&gt;
 &lt;p&gt;3.full-text全文索引&lt;/p&gt;
 &lt;p&gt;4.R-Tree索引&lt;/p&gt;
 &lt;h3&gt;创建索引的条件&lt;/h3&gt;
 &lt;p&gt;那些情况需要创建索引&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;主键自动建立唯一索引&lt;/li&gt;
  &lt;li&gt;频繁作为查询条件的字段应该创建索引&lt;/li&gt;
  &lt;li&gt;查询中与其他表关联的字段，外键关系应该创建索引&lt;/li&gt;
  &lt;li&gt;查询后排序的字段，排序字段若通过索引去访问将大大提高排序速度&lt;/li&gt;
  &lt;li&gt;查询中统计或者分组字段&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;那些情况不需要创建索引&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;表太少&lt;/li&gt;
  &lt;li&gt;经常增删改的表&lt;/li&gt;
  &lt;li&gt;数据重复且分布平均的表字段&lt;/li&gt;
&lt;/ol&gt;
 &lt;h1&gt;explain&lt;/h1&gt;
 &lt;p&gt;作用&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;表的读取顺序
数据读取操作的操作类型
哪些索引可以使用 
哪些索引实际被使用
表之间的引用
每张表有多少行被优化其查到
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;使用  &lt;br /&gt;
explain 查询语句    &lt;br /&gt;
eg：explain select * from orders&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1ab1fe4abc3d4487ad84bb173a3a02fd~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;id&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;select查询的序列号，包含一组数字，表示查询中执行select子句或操作表数据
三种情况:1.id相同：执行顺序由上到下
2.id不同:如果是子查询id号会递增，id值越大优先级越高，越先被执行
        3.id相同不同，同时存在:不同的-id越大优先级越高，相同的 执行顺序由上到下
&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;select_type&lt;/h3&gt;
 &lt;pre&gt;  &lt;code&gt;代表查询的类型，主要是用于区别普通查询、联合查询、子查询等的复杂查询。

simple 简单的 select 查询,查询中不包含子查询或者 UNION
primary 查询中若包含任何复杂的子部分，最外层查询则被标记为 Primary
DERIVED 在 FROM 列表中包含的子查询被标记为 DERIVED(衍生)
MySQL 会递归执行这些子查询, 把结果放在临时表里。
SUBQUERY 在SELECT或WHERE列表中包含了子查询
DEPEDENT SUBQUERY 在SELECT或WHERE列表中包含了子查询,子查询基于外层
UNCACHEABLE SUBQUERY 无法使用缓存的子查询
UNION 若第二个SELECT出现在UNION之后，则被标记为UNION；若UNION包含在FROM子句的子查询中,外层SELECT将被标记为：DERIVED
UNION RESULT 从UNION表获取结果的SELECT
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;table  &lt;br /&gt;
这个数据是基于哪张表的。  &lt;br /&gt;
partitions  &lt;br /&gt;
显示的为分区表命中的分区情况。非分区表该字段为空（null）。  &lt;br /&gt;
type&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;type 是查询的访问类型。是较为重要的一个指标，结果值从最好到最坏依次是：
system &amp;gt; const &amp;gt; eq_ref &amp;gt; ref &amp;gt; range &amp;gt; index &amp;gt;all(简化版 常见到的 优化到range就可以 最好ref)
===========================================================================================
system：表只有一行记录（等于系统表），这是 const 类型的特列，平时不会出现，这个也可以忽略不计 只在Myisam和memory
const:表示通过索引一次就找到了,const 常出现在使用 primary key 或者 unique 索引查询时。 如将主键置于 where 列表中
eq_ref:唯一性索引扫描，对于每个索引键，表中只有一条记录与之匹配。常见于主键或唯一索引扫描。
ref:非唯一性索引扫描，返回匹配某个单独值的所有行.本质上也是一种索引访问，它返回所有匹配某个单独值的行，然而，它可能会找到多个符合条件的行，所以他应该属于查找和扫描的混合体。
range:只检索给定范围的行,使用一个索引来选择行。key 列显示使用了哪个索引一般就是在你的 where 语句中出现了 between、&amp;lt;、&amp;gt;、in 等的查询这种范围扫描索引扫描比全表扫描要好
index:出现index是sql使用了索引但是没有通过索引进行过滤，一般是使用了覆盖索引或者是利用索引进行了排序分组。
all:将遍历全表以找到匹配的行
&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;possible_keys&lt;/h3&gt;
 &lt;pre&gt;  &lt;code&gt;显示可能应用在这张表中的索引，一个或多个。查询涉及到的字段上若存在索引，则该索引将被列出，但不一定被查询实际使用。
&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;key&lt;/h3&gt;
 &lt;p&gt;实际使用的索引。如果为NULL，则没有使用索引。&lt;/p&gt;
 &lt;h3&gt;key_len&lt;/h3&gt;
 &lt;p&gt;表示索引 使用的字节数，可通过该列计算查询中使用的索引的长度。 key_len 字段能够帮你检查是否充分的 利用上了索引。ken_len 越长，说明索引使用的越充分。&lt;/p&gt;
 &lt;h3&gt;ref&lt;/h3&gt;
 &lt;p&gt;显示索引的哪一列被使用了，如果可能的话，是一个常数。哪些列或常量被用于查找索引列上的值&lt;/p&gt;
 &lt;h3&gt;rows&lt;/h3&gt;
 &lt;p&gt;rows 列显示 MySQL 认为它执行查询时必须检查的行数。越少越好&lt;/p&gt;
 &lt;h3&gt;filtered&lt;/h3&gt;
 &lt;p&gt;表示存储引擎返回的数据在server层过滤后，剩下多少满足查询的记录数量的比例，注意是百分比，不是具体记录数。&lt;/p&gt;
 &lt;h3&gt;extra&lt;/h3&gt;
 &lt;p&gt;其他的额外重要的信息。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;Using filesort mysql会对数据使用一个外部的索引排序，而不是按照表内的索引顺序进行读取。MySQL中无法利用索引完成的排序操作称为“文件排序”--也就是说排序的时候索引有没使用到的或者没有按顺序使用
Using temporary 使了用临时表保存中间结果,MySQL 在对查询结果排序时使用临时表。常见于排序 order by 和分组查询 group by。
Using index  代表表示相应的 select 操作中使用了覆盖索引(Covering Index)，避免访问了表的数据行，效率不错！
Using where 查询列未被索引覆盖
Using join buffer  使用了连接缓存区
impossible where 语句不合理
select tables optimized away 在没有 GROUPBY 子句的情况下，基于索引优化 MIN/MAX 操作或者对于 MyISAM 存储引擎优化 COUNT(*)操作，不必等到执行阶段再进行计算，查询执行计划生成的阶段即完成优化。
&lt;/code&gt;&lt;/pre&gt;
 &lt;h1&gt;优化实战&lt;/h1&gt;
 &lt;h2&gt;单表索引优化法则&lt;/h2&gt;
 &lt;p&gt;【优化总结口诀】&lt;/p&gt;
 &lt;p&gt;全值匹配我最爱，最左前缀要遵守 ；&lt;/p&gt;
 &lt;p&gt;带头大哥不能死，中间兄弟不能断；&lt;/p&gt;
 &lt;p&gt;索引列上少计算，范围之后全失效；&lt;/p&gt;
 &lt;p&gt;Like百分写最右，覆盖索引不写星；&lt;/p&gt;
 &lt;p&gt;不等空值还有or，索引失效要少用；&lt;/p&gt;
 &lt;p&gt;VAR引号不可丢，SQL高级也不难！&lt;/p&gt;
 &lt;p&gt;1.全值匹配我最爱&lt;/p&gt;
 &lt;p&gt;查询的字段按照顺序在索引中都可以匹配到&lt;/p&gt;
 &lt;p&gt;CREATE INDEX idx_age_deptid_name ON emp(age,deptid,NAME);&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f54c2287d8d841f1b6ac1f461df4287e~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;如果查询时索引给的是一个确定值(a=2 ,a=&amp;quot;a&amp;quot;)情况多个索引是可以颠倒顺序的索引&lt;/p&gt;
 &lt;p&gt;2.最佳左前缀法则&lt;/p&gt;
 &lt;p&gt;过滤条件要使用索引必须按照索引建立时的顺序，依次满足，一旦跳过某个字段，索引后面的字段都无法被使用&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6874e776094845178e87ff46e67dfa2c~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;3.不要在索引列上做任何计算、类型转换&lt;/p&gt;
 &lt;p&gt;不在索引列上做任何操作（计算、函数、(自动 or 手动)类型转换），会导致索引失效而转向全表扫描&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/05af2e6e5b024f319210b2cc7167e77b~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;4.索引列上不能有范围查询&lt;/p&gt;
 &lt;p&gt;将可能做范围查询的字段的索引顺序放在最后 范围之后的索引会失效&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1aec115360744c59202af8faf0310a4~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;5.尽量使用覆盖索引&lt;/p&gt;
 &lt;p&gt;即查询列和索引列一致，不要写 select *&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/389eb4df7f704227acdc3ce0bde65aa9~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;6.不要使用不等于(!= 或者&amp;lt;&amp;gt; 不等于)的时候&lt;/p&gt;
 &lt;p&gt;mysql 在使用不等于(!= 或者&amp;lt;&amp;gt;)时，有时会无法使用索引会导致全表扫描&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4411510a7acc4a8db6a84a55257d81a9~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;7.字段的 is not null 和 is null&lt;/p&gt;
 &lt;p&gt;is not null 用不到索引，is null 可以用到索引 实际上是没有使用到&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/60c6984ac6ac4cd395f44aafc9af85f5~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;8.like 的前后模糊匹配&lt;/p&gt;
 &lt;p&gt;前缀不能出现模糊匹配&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b962dfec251f49109d69930aed5e31a9~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;如果必须用% 在左右 要覆盖索引 且查询字段必须是索引中的不能含有没创建索引的字段&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dc15975646af40ef82c515a4b6d25e0a~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;9.减少使用 or&lt;/p&gt;
 &lt;p&gt;使用 union all 或者 union 来替&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/094f46fe72ad481b87d1a5b1b6fb21d3~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ffcd2c8ce1364bb8aa663301d095329e~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;10.字符串不加双引号会导致索引失效&lt;/p&gt;
 &lt;p&gt;查询语句中有字符串是必须加上 &amp;apos; &amp;apos;双引号&lt;/p&gt;
 &lt;p&gt;优化原则&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;小表驱动大表&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/75e22be772554b0c9188814c01107f19~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b72c8247103747588625c64d181fb83e~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1f3081aca1c947a1bede7fb4cc669ddf~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;关联查询优化&lt;/h2&gt;
 &lt;ol&gt;
  &lt;li&gt;在优化关联查询时，只有在被驱动表上建立索引才有效！&lt;/li&gt;
  &lt;li&gt;left join 时，左侧的为驱动表，右侧为被驱动表！ 也就是右侧建索引    &lt;strong&gt;（join操作表小于百万级别）&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;right join时，就是左侧建立索引    &lt;strong&gt;（join操作表小于百万级别） 多用单表查询&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;inner join 时，mysql 会自己帮你把小结果集的表选为驱动表 索引结果是一样的&lt;/li&gt;
  &lt;li&gt;子查询尽量不要放在被驱动表，有可能使用不到索引,&lt;/li&gt;
  &lt;li&gt;在范围判断时，尽量不要使用 not in 和 not exists，使用 left join on xxx is null 代替&lt;/li&gt;
&lt;/ol&gt;
 &lt;h3&gt;Join的实现原理&lt;/h3&gt;
 &lt;p&gt;mysql只支持一种join算法&lt;/p&gt;
 &lt;p&gt;嵌套循环连接--多重for循环&lt;/p&gt;
 &lt;p&gt;但嵌套循环有三种变种&lt;/p&gt;
 &lt;p&gt;简单嵌套循环 在被驱动表建立索引&lt;/p&gt;
 &lt;p&gt;索引嵌套循环 通过驱动表中的索引吧需要的数据获取出来 在跟被驱动表进行匹配&lt;/p&gt;
 &lt;p&gt;块嵌套循环 利用join buffer 从驱动表中读取一批数据进行跟被驱动表中的数据进行匹配&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;优化思路：&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;尽量减少join语句中嵌套循环的总次数&lt;/p&gt;
 &lt;p&gt;优先优化嵌套循环的内层循环&lt;/p&gt;
 &lt;p&gt;保证join语句中被驱动表上join条件字段已被索引&lt;/p&gt;
 &lt;p&gt;无法保证被驱动表join条件字段被索引条件允许情况下加大join buffer&lt;/p&gt;
 &lt;p&gt;show variables like &amp;apos;%join_buffer%&amp;apos;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;注意点：&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;并发量太高的时候，系统整体性能 可能会急剧下降&lt;/p&gt;
 &lt;p&gt;复杂的join语句，所需要锁定的资源也就越多，所以阻塞的其他线程也就越多&lt;/p&gt;
 &lt;h2&gt;Order By 和 Group By优化&lt;/h2&gt;
 &lt;p&gt;1.无过滤不索引&lt;/p&gt;
 &lt;p&gt;无过滤，不索引。where，limit 都相当于一种过滤条件，所以才能使用上索引！&lt;/p&gt;
 &lt;ol start="2"&gt;
  &lt;li&gt;顺序错，必排序&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;算上where过滤使用上的索引的到order by使用的索引顺序必须和建立索引的顺序相同 且中间不能出现无索引字段&lt;/p&gt;
 &lt;ol start="3"&gt;
  &lt;li&gt;方向反，必排序&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;在使用order by时出现多个字段要排序 如果都是升序或者降序 是可以的 不能出现一个升序一个降序&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/782db86e8def492fb5a204bdc4461bd4~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;4.Group By&lt;/p&gt;
 &lt;p&gt;group by 使用索引的原则几乎跟 order by 一致 ，唯一区别是 groupby 即使没有过滤条件用到索引，也可以直 接使用索引。&lt;/p&gt;
 &lt;h2&gt;慢查询日志&lt;/h2&gt;
 &lt;pre&gt;  &lt;code&gt;（1）MySQL的慢查询日志是MySQL提供的一种日志记录，它用来记录在MySQL中响应时间超过阀值的语句，具 体指运行时间超过long_query_time值的SQL，则会被记录到慢查询日志中。 
（2）具体指运行时间超过long_query_time值的SQL，则会被记录到慢查询日志中。long_query_time的默认值为 10，意思是运行10秒以上的语句。 
（3）由他来查看哪些SQL超出了我们的最大忍耐时间值，比如一条sql执行超过5秒钟，我们就算慢SQL，希望能 收集超过5秒的sql，结合之前explain进行全面分析。
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;  &lt;code&gt;show variables like &amp;apos;%query%&amp;apos; 全部配置信息&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;show variables like &amp;apos;%slow_query%&amp;apos; 查看慢查询日志位置和是否开启&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;set global slow_query_log=0 设置慢查询日中是否开启 1开启 0关闭&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;show variables like &amp;apos;%long_query%&amp;apos; 查看慢查询时间&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;set global long_query_time=1 修改慢查询时间默认秒&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;show processlist 查看正在执行的sql&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;日志分析工具 mysqldumpslow&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;得到返回记录集最多的 10 个 SQL
mysqldumpslow -s r -t 10 /var/lib/mysql/atguigu-slow.log
得到访问次数最多的 10 个 SQL
mysqldumpslow -s c -t 10 /var/lib/mysql/atguigu-slow.log
得到按照时间排序的前 10 条里面含有左连接的查询语句
mysqldumpslow -s t -t 10 -g &amp;quot;left join&amp;quot; /var/lib/mysql/atguigu-slow.log
另外建议在使用这些命令时结合 | 和 more 使用 ，否则有可能出现爆屏情况
mysqldumpslow -s r -t 10 /var/lib/mysql/atguigu-slow.log | more
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;锁  &lt;br /&gt;
InnoDB是行锁    &lt;br /&gt;
mysql中锁的是索引或者主键 如果是查询等操作时，如果没有用到索引或主键就锁的是整个表 如果使用到了就是锁的行 锁定的行对一个行的操作不影响对其他行的操作   &lt;br /&gt;
共享锁  &lt;br /&gt;
lock in share mode 共享锁  &lt;br /&gt;
读锁&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;begin ---开启事务

select * from customers where Id = 1 lock in share mode ---lock in share mode 对这个查询语句使用共享锁

commit ---提交
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;mysql 共享锁 (lock in share mode)&lt;/p&gt;
 &lt;p&gt;结论&lt;/p&gt;
 &lt;p&gt;1.允许其它事务也增加共享锁读取&lt;/p&gt;
 &lt;p&gt;2.不允许其它事物增加排他锁 (for update)&lt;/p&gt;
 &lt;p&gt;3.当事务同时增加共享锁时候，事务的更新必须等待先执行的事务 commit 后才行，如果同时并发太大可能很容易造成死锁&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;共享锁，事务都加，都能读。修改是惟一的，必须等待前一个事务 commit，才可以&lt;/strong&gt;&lt;/p&gt;
 &lt;h2&gt;排它锁&lt;/h2&gt;
 &lt;p&gt;for update&lt;/p&gt;
 &lt;p&gt;写锁&lt;/p&gt;
 &lt;p&gt;当一个事物加入排他锁后，不允许其他事务加共享锁或者排它锁读取，更加不允许其他事务修改加锁的行。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;begin

select * from customers where Id = 1 for update ---for date 对这个查询语句使用排他锁

commit
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;总结  &lt;br /&gt;
1.事务之间不允许其它排他锁或共享锁读取，修改更不可能  &lt;br /&gt;
2.一次只能有一个排他锁执行 commit 之后，其它事务才可执行  &lt;br /&gt;
不允许其它事务增加共享或排他锁读取。修改是惟一的，必须等待前一个事务 commit，才可  &lt;br /&gt;
意向锁  &lt;br /&gt;
表锁 意向共享锁和意向排它锁都是互相兼容的  &lt;br /&gt;
意向共享锁  &lt;br /&gt;
  &lt;br /&gt;
意向排它锁  &lt;br /&gt;
临键锁  &lt;br /&gt;
是一个范围锁 左开右闭 范围查询并且有数据命中&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;begin
--查询的字段必须是索引 且必须是范围查询 (2,4(不包含4 可以理解为到3)]这个范围1,2是没有锁住的 3+都是被锁住了 临检索锁住的是一个范围 这个范围的操作会被阻塞
select * from customers where id &amp;gt; 2 and id &amp;lt;4 for update  --for update 查询范围的时候就是临键锁

commit
&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;间隙锁&lt;/h2&gt;
 &lt;p&gt;是一个范围锁 查询没有记录命中，就退化成间隙锁了&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;begin
--查询的字段必须是索引 且必须是范围查询 (5,8(不包含8 可以理解为到7)]没有查到数据 这时候1,2，3,4,5是没有锁住的 8+都是被锁住了 这个范围的操作会被阻塞
--间隙锁锁住的就是没有查到这个范围的右边 左边不会被锁住 和临键锁类似
select * from customers where id &amp;gt; 5 and id &amp;lt;8 for update  --for update 查询范围的时候就是临键锁

commit
&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62399-mysql-%E9%AB%98%E6%89%8B-%E4%BC%98%E5%8C%96</guid>
      <pubDate>Fri, 02 Sep 2022 17:26:42 CST</pubDate>
    </item>
    <item>
      <title>电商RN项目秒开优化实践</title>
      <link>https://itindex.net/detail/62315-%E7%94%B5%E5%95%86-rn-%E9%A1%B9%E7%9B%AE</link>
      <description>&lt;hr&gt;&lt;/hr&gt;
 &lt;h2&gt;highlight: zenburn
theme: cyanosis&lt;/h2&gt;
 &lt;p&gt;持续创作，加速成长！这是我参与「掘金日新计划 · 6 月更文挑战」的第34天，  &lt;a href="https://juejin.cn/post/7099702781094674468" title="https://juejin.cn/post/7099702781094674468"&gt;点击查看活动详情&lt;/a&gt;&lt;/p&gt;
 &lt;h3&gt;1. 包预置、预下载 、预渲染&lt;/h3&gt;
 &lt;p&gt;可以申请 App 启动时预下载首包，建议拆包后申请，可以大幅度降低包下载时间。&lt;/p&gt;
 &lt;p&gt;预渲染提前渲染页面相当于从第一个阶段创建容器便开始优化。&lt;/p&gt;
 &lt;h3&gt;2. 模块拆包，Tree-shaking，懒加载&lt;/h3&gt;
 &lt;p&gt;模块拆分：可以拆分首包，可大幅提升包下载更新和加载性能。&lt;/p&gt;
 &lt;p&gt;懒加载：首屏不相关模块实现懒加载，减少加载时间。&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;Inline Requires
采用   &lt;code&gt;inline require&lt;/code&gt;方式打包，可以把实际代码中没有使用的模块去除，减少 size。
加载模块的时机在实际真正使用到这个模块的时候，实现懒加载效果。
模块赋值时采用   &lt;code&gt;get&lt;/code&gt;属性的方式进行赋值，可以在   &lt;code&gt;inline require&lt;/code&gt;打包方式下实现懒加载。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;开启 inline require 配置&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;// metro.config.js
module.exports = {
  transformer: {
    getTransformOptions: async () =&amp;gt; ({
      transform: {
        experimentalImportSupport: true,
        inlineRequires: true,
      },
    }),
  },
};
&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;3. 渲染引擎优化&lt;/h3&gt;
 &lt;p&gt;V8 or Hermes 引擎性能对比&lt;/p&gt;
 &lt;p&gt;CodeCache：JS 预编译后的字节码产物，会缓存至磁盘，下次页面加载时可直接复用。目前 Hermes 已开启。&lt;/p&gt;
 &lt;h3&gt;4. 减少 Native 通信&lt;/h3&gt;
 &lt;p&gt;可减少一些 Call Native 的操作，同时将一些 ab 和静态数据增量提供批量获取的接口减少通信次数，以及减少 log 埋点的次数。&lt;/p&gt;
 &lt;h3&gt;5. 预加载、引擎复用 + 深度预加载&lt;/h3&gt;
 &lt;p&gt;预加载 Bundle Load：Native 提供 loadBusinessScript。 只加载 JS，不触发生命周期。&lt;/p&gt;
 &lt;p&gt;引擎复用 + 深度预加载：提前渲染备用，可自动生成 CodeCache，实际进入页面时可提升性能。&lt;/p&gt;
 &lt;h3&gt;6. 前页信息复用、服务预取和缓存&lt;/h3&gt;
 &lt;p&gt;前页信息复用：前页信息复用+后页骨架屏，前页跳转至后页时，可以采用前页信息提前渲染后页的部分内容。适合列表页-&amp;gt;详情页等场景。&lt;/p&gt;
 &lt;p&gt;服务预取：框架已实现服务预取  &lt;code&gt;Prefetch&lt;/code&gt;，可以在前页提前发服务预取后页信息。适用查询页 → 列表页、列表页 → 详情页等场景。&lt;/p&gt;
 &lt;p&gt;还有一类『在途服务预取』，是在进入下个页面时提前发下个页面的主服务，可以节省 200ms 左右的页面切换时间，在途服务仅在此次进入页面时有效。&lt;/p&gt;
 &lt;p&gt;Prefetch 不要滥用，因为会引起后端服务请求量上升，针对场景适当选用。&lt;/p&gt;
 &lt;p&gt;服务预取的缓存模式也支持长期过期时间：目前唐图已采用。场景是将之前的网络请求相应报文存储到本地 cache（磁盘文件缓存和内存缓存均支持）。页面加载时直接读取缓存，如果有缓存直接渲染 UI，同时发服务获取最新数据后再 diff 刷新 UI，同时更新缓存。&lt;/p&gt;
 &lt;h4&gt;由于涉及公司内部 APP 隐私就不截图了&lt;/h4&gt;
 &lt;h3&gt;7. BFF 接口聚合、接口设计和性能优化&lt;/h3&gt;
 &lt;p&gt;BFF 接口聚合：尽可能在 BFF 层聚合首屏所需服务，需要 case by case 制定方案。&lt;/p&gt;
 &lt;p&gt;接口设计和性能优化：梳理 BFF 接口的契约合理性，以及提出具体的性能要求。&lt;/p&gt;
 &lt;p&gt;即使有接口聚合，和复用前页信息并不冲突。&lt;/p&gt;
 &lt;p&gt;对于第三方或者外部直接跳转至详情页场景， 接口聚合可以直接发挥作用；&lt;/p&gt;
 &lt;p&gt;在列表页 → 详情页场景先使用信息复用，再一次性加载好完整页面，只要控制好 CLS（至少保证页面上半部分组件位置保持不变），对用户体验仍有大量提升。&lt;/p&gt;
 &lt;p&gt;竞品 - PDD：头图复用前页图片，页面下半部分一次性服务完成渲染。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="1327cc2603aa7104fd4b412328e65d4.jpg" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c696b0fccbc3447aab7903bb457ec4e1~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;8. 分屏渲染、页面设计约束&lt;/h3&gt;
 &lt;p&gt;分屏渲染：CRN 与 Native 通信性能不佳，导致多次渲染引发“动画片”效果，Dom 量大的情况下效果更差。建议改用 Promise.all 来同步渲染性能接近的异步请求。&lt;/p&gt;
 &lt;p&gt;页面设计约束：Layout 变化会导致页面抖动，直接影响 CLS 指数（见附录参考 Web 性能核心指标之一），对用户体验影响很大。&lt;/p&gt;
 &lt;p&gt;避免导致页面 Layout 变化的设计，至少页面的上半部分 Layout 不要有大幅变化的可能性；也可以利用前页信息，自动调整当前页面的骨架屏，避免页面 Layout 大幅变化，尤其尽可能避免页面上半部分的 Layout 变化。&lt;/p&gt;
 &lt;p&gt;竞品 - 美团：复用前页信息（通常是列表页到详情页），页面上半部分不会大幅变化，即骨架屏相对固定，不会存在额外加载新元素导致页面抖动的情况。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="23f131ea885c707bd6fdfaf8ebba58f.jpg" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2389556c9aae4ed7834c804034c7deb0~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="cb4a98c5c5beb36c119929ba6a19d18.jpg" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eee9b0e8077f40d38d27bc5d83073e33~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;9. DomTree 预渲染&lt;/h3&gt;
 &lt;p&gt;DomTree 预渲染，相当于客户端实现预加载直出&lt;/p&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;p&gt;以上是本人在开发公司出海App使用rn方面所总结的一些经验，项目规模不大，如有错误，欢迎各路大佬批评指教，对   &lt;code&gt;DomTree 预渲染&lt;/code&gt;感兴趣的话，下一篇我会单独介绍一下&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62315-%E7%94%B5%E5%95%86-rn-%E9%A1%B9%E7%9B%AE</guid>
      <pubDate>Wed, 29 Jun 2022 23:58:50 CST</pubDate>
    </item>
    <item>
      <title>vue项目你一定会用到的性能优化！</title>
      <link>https://itindex.net/detail/62214-vue-%E9%A1%B9%E7%9B%AE-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96</link>
      <description>&lt;hr&gt;&lt;/hr&gt;
 &lt;h2&gt;theme: juejin&lt;/h2&gt;
 &lt;p&gt;提起  &lt;code&gt;性能优化&lt;/code&gt; 很多人眼前浮现的面试经验是不是历历在目呢？反正，性能优化在我看来他永远是前端领域的  &lt;code&gt;热度之王&lt;/code&gt;。&lt;/p&gt;
 &lt;p&gt;而本渣最近维护的项目恰巧在这个方向下了很大功夫，一些经验之谈奉上，希望对大家有些许帮助！&lt;/p&gt;
 &lt;h1&gt;性能优化标准&lt;/h1&gt;
 &lt;p&gt;既然说性能优化，那他总得有一个公认的标准，这就是我们很多次听到的  &lt;code&gt;Lighthouse&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/30c76b87ecd54ce280d8720d46e02e5d~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;在很多单位，都有着自己的性能监控平台，我们只需要引入相应的sdk，那么在平台上就能分析出你页面的存在的性能问题，大家是不是学的很神奇！&lt;/p&gt;
 &lt;p&gt;其实除了苛刻的业务，需要  &lt;code&gt;特殊的定制&lt;/code&gt;，大多数的情况下我们单位的性能优化平台本质上其实就是利用无头浏览器（Puppeteer）跑  &lt;code&gt;Lighthouse&lt;/code&gt;。&lt;/p&gt;
 &lt;p&gt;理解了我们单位的性能监控平台的原理之后，我们就能针对性的做性能优化，也就是面向  &lt;code&gt;Lighthouse&lt;/code&gt;编程&lt;/p&gt;
 &lt;h2&gt;Lighthouse&lt;/h2&gt;
 &lt;p&gt;  &lt;a href="https://link.zhihu.com/?target=https%3A//github.com/GoogleChrome/lighthouse"&gt;lighthouse&lt;/a&gt; 是 Google Chrome 推出的一款开源自动化工具，它可以搜集多个现代网页性能指标，分析 Web 应用的性能并生成报告，为开发人员进行性能优化的提供了参考方向。&lt;/p&gt;
 &lt;p&gt;说起  &lt;code&gt;Lighthouse&lt;/code&gt;在现代的谷歌浏览器中业已经集成&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e85a649b59934257b85c83fea9719026~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;他可以分析出我们的页面性能，通过几个指标&lt;/p&gt;
 &lt;p&gt;Lighthouse 会衡量以下性能指标项：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;a href="https://web.dev/i18n/zh/fcp/"&gt;首次内容绘制&lt;/a&gt;（First Contentful Paint）。即浏览器首次将任意内容（如文字、图像、canvas 等）绘制到屏幕上的时间点。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;a href="https://web.dev/i18n/zh/tti/"&gt;可交互时间&lt;/a&gt;（Time to Interactive）。指的是所有的页面内容都已经成功加载，且能够快速地对用户的操作做出反应的时间点。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;a href="https://web.dev/speed-index/"&gt;速度指标&lt;/a&gt;（Speed Index）。衡量了首屏可见内容绘制在屏幕上的速度。在首次加载页面的过程中尽量展现更多的内容，往往能给用户带来更好的体验，所以速度指标的值约小越好。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;a href="https://web.dev/i18n/zh/tbt/"&gt;总阻塞时间&lt;/a&gt;（Total Blocking Time）。指First Contentful Paint 首次内容绘制 (FCP)与Time to Interactive 可交互时间 (TTI)之间的总时间&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;a href="https://web.dev/i18n/zh/lcp/"&gt;最大内容绘制&lt;/a&gt;（Largest Contentful Paint）。度量标准报告视口内可见的最大图像或文本块的呈现时间&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;a href="https://web.dev/i18n/zh/cls/"&gt;累积布局偏移&lt;/a&gt;（# Cumulative Layout Shift）。衡量的是页面整个生命周期中每次元素发生的非预期布局偏移得分的总和。每次可视元素在两次渲染帧中的起始位置不同时，就说是发生了LS（Layout Shift）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;在一般情况下，据我的经验，由于性能监控平台的和本地平台的  &lt;code&gt;差异&lt;/code&gt;，本地可能要达到  &lt;strong&gt;70分&lt;/strong&gt;，线上才有可能达到及格的状态,如果有性能优化的需求时，大家酌情处理即可（不过本人觉得，及格即可， 毕竟  &lt;code&gt;大学考试有曰：60分万岁，61分浪费&lt;/code&gt;，传承不能丢，咱们要把更多的时间，放到更重要的事情上来!）&lt;/p&gt;
 &lt;h1&gt;通用常规优化手段&lt;/h1&gt;
 &lt;p&gt;  &lt;code&gt;lighthouse&lt;/code&gt;的的牛x之处就是它能找出你页面中的一些常规的性能瓶颈，并提出优化建议，比如：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1bcb278a36354d22b442754ba47ecdec~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/021fe0c87d3446d78060f8799890ce19~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;于是针对这些优化建议，我们需要做一些常规的优化：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;减少未使用的javascript&lt;/li&gt;
  &lt;li&gt;移出阻塞渲染的资源&lt;/li&gt;
  &lt;li&gt;图片质量压缩&lt;/li&gt;
  &lt;li&gt;限制使用字体数量，尽可能少使用变体&lt;/li&gt;
  &lt;li&gt;优化关键渲染路径：只加载当前页面渲染所需的必要资源，将次要资源放在页面渲染完成后加载&lt;/li&gt;
&lt;/ol&gt;
 &lt;h1&gt;通用性能优化分析&lt;/h1&gt;
 &lt;p&gt;我们知道lighthouse 中有六个性能指标，而在这六个指标中，  &lt;code&gt;LCP、 FCP、speed index&lt;/code&gt;、 这三个指数尤为重要，因为在一般情况下 这个三个指标会影响   &lt;code&gt;TTI、TBT、CLS&lt;/code&gt; 的分数&lt;/p&gt;
 &lt;p&gt;所以在我们在优化时， 需要提高LCP、 FCP和speedIndex 的分数，经过测试， 即使是空页面也会有时间上的损耗， 初始分数基本都是  &lt;code&gt;0.8&lt;/code&gt;秒&lt;/p&gt;
 &lt;p&gt;注意： 需要值得大家注意的是，我们当前所有测试全部建立在，  &lt;code&gt;移动端&lt;/code&gt;（之所以用移动端，是由于pc 的强大算力，很少有性能瓶颈）的基础上,并且页面上必须有一下内容，才能得出分数，内容必须包括一下的一种或者多种&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;内嵌在svg元素内的image元素&lt;/li&gt;
  &lt;li&gt;video元素（使用封面图像）&lt;/li&gt;
  &lt;li&gt;通过   &lt;a href="https://developer.mozilla.org/docs/Web/CSS/url()"&gt;url()&lt;/a&gt;函数（而非使用   &lt;a href="https://developer.mozilla.org/docs/Web/CSS/CSS_Images/Using_CSS_gradients"&gt;CSS 渐变&lt;/a&gt;）加载的带有背景图像的元素&lt;/li&gt;
  &lt;li&gt;包含文本节点或其他行内级文本元素子元素的   &lt;a href="https://developer.mozilla.org/docs/Web/HTML/Block-level_elements"&gt;块级元素&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;否则就会有如下错误&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/acba73b3bcea4e3fb8f4da2854a7780f~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;接下来我们就从LCP、 FCP和speedIndex 这三个指标入手&lt;/p&gt;
 &lt;h2&gt;FCP（First Contentful Paint）&lt;/h2&gt;
 &lt;p&gt;顾名思义就是  &lt;code&gt;首次内容绘制&lt;/code&gt;，也就是页面最开始绘制内容的时间，但是由于我们现在开发的页面都是spa应用，所以，框架层面的初始化是一定会有一定的  &lt;code&gt;性能损耗&lt;/code&gt;的，以vue-cli 搭建的脚手架为例，当我初始化空的脚手架，打包后上传cdn部署，FCP 就会从0.8s提上到1.5秒，由此可见vue 的diff 也不是  &lt;code&gt;免费&lt;/code&gt;的他也会有性能上的损耗&lt;/p&gt;
 &lt;p&gt;在优化页面的内容之前我们声明三个前提&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;提高FCP的时间其实就是在优化   &lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/Performance/Critical_rendering_path"&gt;关键渲染路径&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;如果它是一个样式文件（CSS文件），浏览器就必须在渲染页面之前完全解析它（这就是为什么说CSS具有渲染阻碍性）&lt;/li&gt;
  &lt;li&gt;如果它是一个脚本文件（JavaScript文件），浏览器必须： 停止解析，下载脚本，并运行它。只有在这之后，它才能继续解析，因为 JavaScript 脚本可以改变页面内容（特别是HTML）。（这就是为什么说JavaScript阻塞解析）&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;针对以上的用例测试，我们发现，无论我们怎么优化，框架本身的性能损耗是无法抹除的，我们唯一能做的就是让框架更早的去执行初始化，并且初始化更少的内容，可做的优化手段如下：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;所有初始化用不到的js 文件全部走异步加载，也就是加上   &lt;code&gt;defer&lt;/code&gt;或者   &lt;code&gt;asnyc&lt;/code&gt; ，并且一些需要走cdn的第三方插件需要放在页面底部（因为放在顶部，他的解析会阻止html 的解析，从而影响css 等文件的下载，这也是   &lt;code&gt;雅虎军规&lt;/code&gt;的一条）&lt;/li&gt;
  &lt;li&gt;js 文件拆包，以vue-cli 为例，一般情况下我们可以通过cli的配置 splitChunks 做代码分割，将一些第三方的包走cdn，或者拆包。如果有路由的情况下将路由做拆包处理，保证每个路由只   &lt;code&gt;加载当前路由对应的js代码&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;优化文件大小 减少字体包、css文件、以及js文件的大小（当然这些 脚手架默认都已经做了）&lt;/li&gt;
  &lt;li&gt;优化项目结构，每个组件的初始化都是有   &lt;code&gt;性能损耗&lt;/code&gt;的，在在保证   &lt;code&gt;可维护性&lt;/code&gt;的基础上，尽量减少初始化组件的加载数量
5、网络协议层面的优化，这个优化手段需要服务端配合纯前端已经无法达到，在现在   &lt;code&gt;云服务器&lt;/code&gt;盛行的时代,自家单位一般都会默认在云服务器中开启这些优化手段，比如开启   &lt;code&gt;gzip&lt;/code&gt;，使用   &lt;code&gt;cdn&lt;/code&gt; 等等&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;其实说来说去，提高FCP 的核心只有理念之后两个   &lt;code&gt;减少初始化视图内容&lt;/code&gt;和   &lt;code&gt; 减少初始化下载资源大小&lt;/code&gt;&lt;/p&gt;
 &lt;h2&gt;LCP(Largest Contentful Paint)&lt;/h2&gt;
 &lt;p&gt;顾名思义就是  &lt;code&gt;最大内容绘制&lt;/code&gt;， 何时报告LCP,官方是这样说的&lt;/p&gt;
 &lt;p&gt;为了应对这种潜在的变化，浏览器会在绘制第一帧后立即分发一个  &lt;code&gt;largest-contentful-paint&lt;/code&gt;类型的  &lt;a href="https://developer.mozilla.org/docs/Web/API/PerformanceEntry"&gt;   &lt;code&gt;PerformanceEntry&lt;/code&gt;&lt;/a&gt;，用于识别最大内容元素。但是，在渲染后续帧之后，浏览器会在最大内容元素发生变化时分发另一个  &lt;a href="https://developer.mozilla.org/docs/Web/API/PerformanceEntry"&gt;   &lt;code&gt;PerformanceEntry&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;
 &lt;p&gt;例如，在一个带有文本和首图的网页上，浏览器最初可能只渲染文本部分，并在此期间分发一个  &lt;code&gt;largest-contentful-paint&lt;/code&gt;条目，其  &lt;code&gt;element&lt;/code&gt;属性通常会引用一个  &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;或  &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; 。随后，一旦首图完成加载，浏览器就会分发第二个  &lt;code&gt;largest-contentful-paint&lt;/code&gt;条目，其  &lt;code&gt;element&lt;/code&gt;属性将引用  &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; 。&lt;/p&gt;
 &lt;p&gt;需要注意的是，一个元素只有在渲染完成并且对用户可见后才能被视为最大内容元素。尚未加载的图像不会被视为&amp;quot;渲染完成&amp;quot;。 在  &lt;a href="https://developer.mozilla.org/docs/Web/CSS/@font-face/font-disply#The_font_display_timeline"&gt;字体阻塞期&lt;/a&gt;使用网页字体的文本节点亦是如此。在这种情况下，较小的元素可能会被报告为最大内容元素，但一旦更大的元素完成渲染，就会通过另一个  &lt;code&gt;PerformanceEntry&lt;/code&gt;对象进行报告。&lt;/p&gt;
 &lt;p&gt;其实用大白话解释就是，通常情况下，  &lt;code&gt;图片、视频以及大量文本绘制完成后&lt;/code&gt;就会报告LCP&lt;/p&gt;
 &lt;p&gt;理解了这一点，的优化手段就明确了,尽量减少这些资源的大小就可以了，经过测试，减少首屏渲染的图片以及视频内容大小后，整体分数显著提高，提供一些优化方法：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;本地图片可以使用在线压缩工具自己压缩 推荐   &lt;a href="https://tinypng.com/"&gt;tinypng.com&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;接口中附带图片，一般情况下单位中都有对应的oss或者cdn传参配置通过地址栏传参方式控制图片质量&lt;/li&gt;
  &lt;li&gt;图片懒加载&lt;/li&gt;
&lt;/ol&gt;
 &lt;h2&gt;SpeedIndex（速度指数）&lt;/h2&gt;
 &lt;p&gt;  &lt;code&gt;Speed Index&lt;/code&gt;采用可视页面加载的视觉进度，计算内容绘制速度的总分。为此，首先需要能够计算在页面加载期间，各个时间点“完成”了多少部分。在WebPagetest中，通过捕获在浏览器中加载页面的视频并检查每个视频帧（在启用视频捕获的测试中，每秒10帧）来完成的，这个算法在下面有描述，但现在假设我们可以为每个视频帧分配一个完整的百分比（在每个帧下显示的数字）&lt;/p&gt;
 &lt;p&gt;以上是官方解释的计算方式，其实通俗的将，所谓速度指数就是衡量页面内容填充的速度&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5db99555d1f54f93b651a91b238ae4fe~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;一图胜千言&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;经过测试，跟LCP相同，图片以及视频内容对于SpeedIndex的影响巨大，所有优化方向，通之前一致，总的来说，只要提高LCP 以及FCP 的时间SpeedIndex 的时间就会有显著提高&lt;/p&gt;
 &lt;p&gt;不过需要注意的是，接口的速度也会影响SpeedIndex的时间，由于AJAX流行的今天，我们大多数的数据都是使用接口拉取。如果接口速度过慢，他就会影响你页面的初始渲染， 导致性能问题，所以，在做性能优化的同时，  &lt;code&gt;请求后端伙伴协助，也是性能优化的一个方案&lt;/code&gt;&lt;/p&gt;
 &lt;h2&gt;排查性能瓶颈&lt;/h2&gt;
 &lt;p&gt;上述分析，根据三个指标提供了一些常规的优化手段，那么在这些优化手段中，有的你可以立马排查到，并且优化例如：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;优化图像，优化字体大小&lt;/li&gt;
  &lt;li&gt;跟服务端配合利用浏览器缓存机制.启用cdn、启用gzip等&lt;/li&gt;
  &lt;li&gt;减少网络协议过程中的消耗，减少http 请求、减少dns查询、避免重定向&lt;/li&gt;
  &lt;li&gt;优化关键渲染路径，异步加载js等&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;但是有的优化手段我们不容易排查，因为他是打在包里面的，这个js 文件包含了很多逻辑怎么办，这里我有两个手段或许能够帮助排查出性能瓶颈发生在哪里：&lt;/p&gt;
 &lt;h3&gt;分析包内容&lt;/h3&gt;
 &lt;p&gt;在通常情况下，我们无法判断的优化点，都是在打包后，我们无法分析出，那些东西不是我们在首屏必须需要的，从而不能做出针对新的优化，为了解决当前问题，各大bundle厂商也都有各自的分析包的方案&lt;/p&gt;
 &lt;p&gt;以vue-cli 为例&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;quot;report&amp;quot;: &amp;quot;vue-cli-service build --report&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;我们只需要在脚手架中提供以上命令，就能在打包时生成，整个包的分析文件&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/95f39aefca9a4b3eac3e05ac11345987~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;如上图所示 在打包后就能分析出打包后的js 文件他包含什么组件，如此以来，我们就能知道那些文件是没必要同步加载的，或者走cdn的，通过配置将他单独的隔离开来，从而找出性能的问题&lt;/p&gt;
 &lt;h3&gt;利用chorme devtool 的代码覆盖率&lt;/h3&gt;
 &lt;p&gt;如下图所示，&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9bd24ecd18ca41daa51497dfbdd496cc~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;利用 devtool的代码覆盖率检查就能知道那些js 或者css 文件的代码没有被使用过，结合包内容的分析，我们就能大概的猜出性能的瓶颈在哪里从而做相应的特殊处理&lt;/p&gt;
 &lt;h1&gt;针对vue 的特殊优化&lt;/h1&gt;
 &lt;p&gt;以上内容都是通用的一些优化手段，您在哪都能查到，只是我表达了一下做这些常规优化的深层原因。能让您更清楚的了解这些原因之后，  &lt;code&gt;在性能瓶颈的时候能游刃有余，而不是为了面试死记硬背，一到用的时候就不灵&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;然后我司是vue啊，咱得上得vue 的手段&lt;/p&gt;
 &lt;h2&gt;图片懒加载&lt;/h2&gt;
 &lt;p&gt;所谓图片懒加载，就是页面只渲染当前可视区域内的图片，如此一来，减少了其他图片渲染数量，能大大提高  &lt;code&gt;SpeedIndex&lt;/code&gt;和  &lt;code&gt;LCP&lt;/code&gt;的时间，从而提高分数&lt;/p&gt;
 &lt;p&gt;在vue中提起图片懒加载插件，首推  &lt;a href="https://github.com/hilongjw/vue-lazyload"&gt;vue-lazyload&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;使用方式简单，功能丰富&lt;/p&gt;
 &lt;h2&gt;虚拟滚动&lt;/h2&gt;
 &lt;p&gt;在一含有长列表页面中,你有没有发现你是往下越滑越卡，此时虚拟滚动就排上用场了，
他的基本原理就是只渲染可视区域内的几条数据，但是模拟出正常滑动的效果，因为每次只渲染可是剧域内的数据,在滑动的时候他的性能就会有飞速提升&lt;/p&gt;
 &lt;p&gt;在vue中比较好用的插件有两个  &lt;a href="https://github.com/Akryum/vue-virtual-scroller"&gt;vue-virtual-scroller&lt;/a&gt;和  &lt;a href="https://github.com/tangbc/vue-virtual-scroll-list"&gt;vue-virtual-scroll-list&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;目前我司统一用的vue-virtual-scroll-list 他下拉的时候到了分页的地方能加些loading提示&lt;/p&gt;
 &lt;h2&gt;vue 中的函数式组件&lt;/h2&gt;
 &lt;p&gt;在vue中我们知道组件的初始化是比较损耗性能的，大家可以去试一下，使用vue 直接渲染一个文字内容，和直接渲染一个app.vue 组件他的分数是略有不同的。&lt;/p&gt;
 &lt;p&gt;但是当有了函数式组件，这个问题就迎刃而解了&lt;/p&gt;
 &lt;p&gt;因为函数是组件顾名思义他就是个函数，说白了就是个  &lt;code&gt;render函数&lt;/code&gt;，他少了组件初始化的过程，省去了很多初始化过程的  &lt;code&gt;开销&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;什么时候用函数式组件呢?&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;当你的组件中没有业务逻辑只展示内容时，这时候函数式组件就派上用场了&lt;/p&gt;
 &lt;h2&gt;利用v-show 、KeepAlive 复用dom&lt;/h2&gt;
 &lt;p&gt;我们知道v-show是通过display 控制dom的展示隐藏，他并不会删除dom 而我们在切换v-show的时候其实是减少了diff的对比，而KeepAlive 则是直接复用dom，连diff 的过程都没了，并且他们俩的合理使用还不会影响到初始化渲染。如此一来减少了js 的执行开销，但是值得注意的是，  &lt;code&gt;他并不能优化你初始化的性能，而是操作中的性能&lt;/code&gt;&lt;/p&gt;
 &lt;h2&gt;分批渲染组件&lt;/h2&gt;
 &lt;p&gt;在前面我们提到过SpeedIndex 的渐进渲染是提高SpeedIndex的关键，有了这个前提，我们就可以分批异步渲染组件。先看到内容，然后在渲染其他内容&lt;/p&gt;
 &lt;p&gt;举个例子：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;template&amp;gt;
    &amp;lt;div&amp;gt;
        {{ data1 }}
    &amp;lt;/div&amp;gt;
    &amp;lt;div v-if=&amp;quot;data1&amp;quot;&amp;gt;
        {{ data2 }}
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script&amp;gt;
import { ref } from &amp;apos;vue&amp;apos;
export default {
    setup() {
        let data1 = ref(&amp;apos;&amp;apos;)
        let data2 = ref(&amp;apos;&amp;apos;)
        // 假设 这是从后端取到的数据
        const data = {
            data1: &amp;apos;这是渲染内容1&amp;apos;,
            data2: &amp;apos;这是渲染内容2&amp;apos;
        }
        data1.value = data.data1
        //利用requestAnimationFrame 在空闲的时候当前渲染之后在渲染剩余内容
        requestIdleCallback(() =&amp;gt; {
            data2.value = data.data2
        })
        return {
            data1,
            data2
        }
    },
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;上述例子比较简单可能描述的不太贴切，在这里特此说明一下，当前方法适用于组件内容较多，每次render 时间过长，导致白屏时间过长，比如，  &lt;code&gt;一次拉取用户列表&lt;/code&gt;，那么分批渲染就非常合适，先展示一部分用户信息，最后直到慢慢将所有内容渲染完毕。如此对浏览器的SpeedIndex 也非常友好&lt;/p&gt;
 &lt;h1&gt;最后&lt;/h1&gt;
 &lt;p&gt;性能优化一直是一个很火的话题， 不管从面试以及工作中都非常重要，有了这些优化的点，你在写代码或者优化老项目时都能游刃有余，能提前考虑到其中的一些坑，并且规避。&lt;/p&gt;
 &lt;p&gt;但是大家需要明白的是，不要为了性能优化而性能优化，我们在要  &lt;code&gt;因地制宜&lt;/code&gt;，在不破坏项目可维护性的基础上去优化，千万不要你优化个项目性能是好了，但是大家都看不懂了，这就有点得不偿失了，还是那句话，  &lt;code&gt;60分万岁61份浪费，差不多得了&lt;/code&gt;，把经历留着去干更重要的事情！&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62214-vue-%E9%A1%B9%E7%9B%AE-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96</guid>
      <pubDate>Fri, 22 Apr 2022 01:56:11 CST</pubDate>
    </item>
    <item>
      <title>iOS - 冷启动优化</title>
      <link>https://itindex.net/detail/62167-ios-%E5%86%B7%E5%90%AF%E5%8A%A8-%E4%BC%98%E5%8C%96</link>
      <description>&lt;p&gt;随着App不断迭代，使的业务模块增加，逻辑变得复杂，集成了更多的第三方库，App 启动也会越来越慢，因此我们希望能在业务扩张的同时，保持较好的启动速度，给用户带来良好的体验。&lt;/p&gt;
 &lt;h1&gt;一、名词概念理论&lt;/h1&gt;
 &lt;p&gt;为了更准确地了解 App 冷启动的流程，我们需要掌握一些基本的概念&lt;/p&gt;
 &lt;h2&gt;1.1.Mach-O&lt;/h2&gt;
 &lt;p&gt;  &lt;a href="https://links.jianshu.com/go?to=https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/MachOOverview.html"&gt;Mach-O&lt;/a&gt;（Mach Object File Format)是一种用于记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。App 编译生成的二进制  &lt;strong&gt;可执行文件&lt;/strong&gt;就是 Mach-O 格式的，iOS 工程所有的类编译后会生成对应的目标文件   &lt;code&gt;.o&lt;/code&gt; 文件，而这个可执行文件就是这些   &lt;code&gt;.o&lt;/code&gt; 文件的集合。&lt;/p&gt;
 &lt;p&gt;在 Xcode 的控制台输入以下命令，可以打印出运行时所有加载进应用程序的 Mach-O 文件。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;image list -o -f
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;Mach-O 文件主要由三部分组成：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;Mach header：描述 Mach-O 的 CPU 架构、文件类型以及加载命令等；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;Load commands：描述了文件中数据的具体组织结构，不同的数据类型使用不同的加载命令；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;Data：Data 中的每个段（segment）的数据都保存在这里，每个段都有一个或多个 Section，它们存放了具体的数据与代码，主要包含这三种类型：&lt;/p&gt;
   &lt;ul&gt;
    &lt;li&gt;     &lt;code&gt;__TEXT&lt;/code&gt; 包含 Mach header，被执行的代码和只读常量（如C 字符串）。只读可执行（r-x）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;   &lt;code&gt;__DATA&lt;/code&gt; 包含全局变量，静态变量等。可读写（rw....）。&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;   &lt;code&gt;__LINKEDIT&lt;/code&gt; 包含了加载程序的   &lt;strong&gt;元数据&lt;/strong&gt;，比如函数的名称和地址。只读（r...）。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;1.2.dylib&lt;/h2&gt;
 &lt;p&gt;dylib 也是一种 Mach-O 格式的文件，后缀名为   &lt;code&gt;.dylib&lt;/code&gt; 的文件就是动态库（也叫动态链接库）。动态库是运行时加载的，可以被多个 App 的进程共用。&lt;/p&gt;
 &lt;p&gt;如果想知道 TestDemo 中依赖的所有动态库，可以通过下面的指令实现：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;otool -L /TestDemo.app/TestDemo
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;动态链接库分为  &lt;strong&gt;系统 dylib&lt;/strong&gt; 和  &lt;strong&gt;内嵌 dylib&lt;/strong&gt;（embed dylib，即开发者手动引入的动态库）。系统 dylib 有：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;iOS 中用到的所有系统 framework，比如 UIKit、Foundation；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;系统级别的 libSystem（如 libdispatch(GCD) 和 libsystem_blocks(Block)）；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;加载 OC runtime 方法的 libobjc；&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;1.2.1.dyld&lt;/h3&gt;
 &lt;p&gt;  &lt;a href="https://links.jianshu.com/go?to=https://opensource.apple.com/tarballs/dyld/"&gt;dyld&lt;/a&gt;（Dynamic Link Editor）：动态链接器，其本质也是 Mach-O 文件，一个专门用来加载 dylib 文件的库。 dyld 位于   &lt;code&gt;/usr/lib/dyld&lt;/code&gt;，可以在 mac 和越狱机中找到。dyld 会将 App 依赖的动态库和 App 文件加载到内存后执行。&lt;/p&gt;
 &lt;h3&gt;1.2.2.dyld shared cache&lt;/h3&gt;
 &lt;p&gt;dyld shared cache 就是动态库共享缓存。当需要加载的动态库非常多时，相互依赖的符号也更多了，为了节省解析处理符号的时间，OS X 和 iOS 上的动态链接器使用了共享缓存。OS X 的共享缓存位于   &lt;code&gt;/private/var/db/dyld/&lt;/code&gt;，iOS 的则在   &lt;code&gt;/System/Library/Caches/com.apple.dyld/&lt;/code&gt;。&lt;/p&gt;
 &lt;p&gt;当加载一个 Mach-O 文件时，dyld 首先会检查是否存在于共享缓存，存在就直接取出使用。每一个进程都会把这个共享缓存映射到了自己的地址空间中。这种方法大大优化了 OS X 和 iOS 上程序的启动时间。&lt;/p&gt;
 &lt;h3&gt;1.2.3.dyld shared cache&lt;/h3&gt;
 &lt;p&gt;dyld shared cache 就是动态库共享缓存。当需要加载的动态库非常多时，相互依赖的符号也更多了，为了节省解析处理符号的时间，OS X 和 iOS 上的动态链接器使用了共享缓存。OS X 的共享缓存位于   &lt;code&gt;/private/var/db/dyld/&lt;/code&gt;，iOS 的则在   &lt;code&gt;/System/Library/Caches/com.apple.dyld/&lt;/code&gt;。&lt;/p&gt;
 &lt;p&gt;当加载一个 Mach-O 文件时，dyld 首先会检查是否存在于共享缓存，存在就直接取出使用。每一个进程都会把这个共享缓存映射到了自己的地址空间中。这种方法大大优化了 OS X 和 iOS 上程序的启动时间。&lt;/p&gt;
 &lt;h3&gt;1.2.4.images&lt;/h3&gt;
 &lt;p&gt;images 在这里不是指图片，而是  &lt;strong&gt;镜像&lt;/strong&gt;。每个 App 都是以 images 为单位进行加载的。images 类型包括：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;executable：应用的二进制可执行文件；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;dylib：动态链接库；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;bundle：资源文件，属于不能被链接的 dylib，只能在运行时通过    &lt;code&gt;dlopen()&lt;/code&gt; 加载。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;1.2.5.framework&lt;/h3&gt;
 &lt;p&gt;framework 可以是动态库，也是静态库，是一个包含 dylib、bundle 和头文件的文件夹。&lt;/p&gt;
 &lt;h1&gt;二、冷启动相关（首页为原生）&lt;/h1&gt;
 &lt;p&gt;当用户按下 home 键，iOS App 不会立刻被 kill，而是存活一段时间，这段时间里用户再打开 App，App 基本上不需要做什么，就能还原到退到后台前的状态。我们把 App 进程还在系统中，无需开启新进程的启动过程称为  &lt;strong&gt;热启动&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;而  &lt;strong&gt;冷启动&lt;/strong&gt;则是指 App 不在系统进程中，比如设备重启后，或是手动杀死 App 进程，又或是 App 长时间未打开过，用户再点击启动 App 的过程，这时需要创建一个新进程分配给 App。我们可以将冷启动看作一次完整的 App 启动过程，本文讨论的就是冷启动的优化。&lt;/p&gt;
 &lt;h2&gt;1.冷启动：&lt;/h2&gt;
 &lt;h3&gt;1.1冷启动的出处&lt;/h3&gt;
 &lt;p&gt;WWDC 2016 中首次出现了 App 启动优化的话题，其中提到：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;App 启动最佳速度是400ms以内，因为从点击 App 图标启动，然后 Launch Screen 出现再消失的时间就是400ms；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;App 启动最慢不得大于20s，否则进程会被系统杀死；（启动时间最好以 App 所支持的最低配置设备为准。）&lt;/li&gt;
&lt;/ul&gt;
 &lt;h4&gt;1.1.1关于冷启动的两种说法：&lt;/h4&gt;
 &lt;p&gt;说法一：&lt;/p&gt;
 &lt;p&gt;冷启动的整个过程是指从用户唤起 App 开始到 AppDelegate 中的   &lt;code&gt;didFinishLaunchingWithOptions&lt;/code&gt; 方法执行完毕为止，并以执行   &lt;code&gt;main()&lt;/code&gt; 函数的时机为分界点，分为   &lt;code&gt;pre-main&lt;/code&gt; 和   &lt;code&gt;main()&lt;/code&gt; 两个阶段。&lt;/p&gt;
 &lt;p&gt;说法二：&lt;/p&gt;
 &lt;p&gt;也有一种说法是将整个冷启动阶段以主 UI 框架的   &lt;code&gt;viewDidAppear&lt;/code&gt; 函数执行完毕才算结束。这两种说法都可以，前者的界定范围是 App 启动和初始化完毕，后者的界定范围是用户视角的启动完毕，也就是首屏已经被加载出来。&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;   &lt;strong&gt;注意&lt;/strong&gt;：这里很多文章都会把第二个阶段描述为    &lt;strong&gt;main 函数之后&lt;/strong&gt;，个人认为这种说法不是很好，容易让人误解。要知道 main 函数在 App 运行过程中是不会退出的，无论是 AppDelegate 中的    &lt;code&gt;didFinishLaunchingWithOptions&lt;/code&gt; 方法还是 ViewController 中的   &lt;code&gt;viewDidAppear&lt;/code&gt; 方法，都还是在 main 函数内部执行的。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;h3&gt;1.2.pre-main 阶段&lt;/h3&gt;
 &lt;p&gt;  &lt;code&gt;pre-main&lt;/code&gt; 阶段指的是从用户唤起 App 到   &lt;code&gt;main()&lt;/code&gt; 函数执行之前的过程。&lt;/p&gt;
 &lt;h4&gt;1.2.1查看阶段耗时（以xcode13为‘分水岭’）&lt;/h4&gt;
 &lt;h5&gt;1.2.1.1. Xcode13之前&lt;/h5&gt;
 &lt;p&gt;1.我们可以在 Xcode 中配置环境变量&lt;/p&gt;
 &lt;p&gt;Product -&amp;gt; Edit Scheme -&amp;gt; Run -&amp;gt; Arguments -&amp;gt;Environment Variables -&amp;gt; +&lt;/p&gt;
 &lt;p&gt;DYLD_PRINT_STATISTICS 设置为 1&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4f17001cf7524420a5f44ed682942e44~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b18220852a104f088ffe65a8b7ccf5e1~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;
这时在 iOS 10 以上系统中运行这个 Demo，  &lt;code&gt;pre-main&lt;/code&gt; 阶段的启动时间会在控制台中打印出来（备注：本人x-code 已经升级到13.3，无法打印出日志）&lt;/p&gt;
 &lt;p&gt;如果要更详细的信息，就设置   &lt;code&gt;DYLD_PRINT_STATISTICS_DETAILS&lt;/code&gt; 为 1。&lt;/p&gt;
 &lt;h5&gt;1.2.1.2在Xcode13之后上面的方法就失效了&lt;/h5&gt;
 &lt;p&gt;可以采用下面的方法&lt;/p&gt;
 &lt;p&gt;代码贴出来如下&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;



NS_ASSUME_NONNULL_BEGIN



 @interface AppLaunchTime : NSObject



+ (void)mark;



 @end
&lt;/code&gt;&lt;/pre&gt;
 &lt;pre&gt;  &lt;code&gt;#import &amp;quot;AppLaunchTime.h&amp;quot;

#import &amp;lt;sys/sysctl.h&amp;gt;

#import &amp;lt;mach/mach.h&amp;gt;

 @implementation AppLaunchTime



double __t1; // 创建进程时间
double __t2; // before main
double __t3; // didfinsh

/// 获取进程创建时间

+ (CFAbsoluteTime)processStartTime 
{
  if (__t1 == 0) 
  {
    struct kinfo_proc procInfo;
    int pid = [[NSProcessInfo processInfo] processIdentifier];
    int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
    size_t size = sizeof(procInfo);
    if (sysctl(cmd, sizeof(cmd)/sizeof(*cmd), &amp;amp;procInfo, &amp;amp;size, NULL, 0) == 0) {
      __t1 = procInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000.0 + procInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0;
    }
  }
  return __t1;
}

/// 开始记录：在DidFinish中调用
+ (void)mark 
{
  double __t1 = [AppLaunchTime processStartTime];
  dispatch_async(dispatch_get_main_queue(), ^{ // 确保didFihish代码执行后调用
    if (__t3 == 0) 
    {
      __t3 = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970;
    }

    double pret = __t2 - __t1 / 1000;
    double didfinish = __t3 - __t2;
    double total = __t3 - __t1 / 1000;

    NSLog(@&amp;quot;----------App启动---------耗时:pre-main:%f&amp;quot;,pret);
    NSLog(@&amp;quot;----------App启动---------耗时:didfinish:%f&amp;quot;,didfinish);
    NSLog(@&amp;quot;----------App启动---------耗时:total:%f&amp;quot;,total);
  });
}
// 构造方法在main调用前调用

// 获取pre-main()阶段的结束时间点相对容易，可以直接取main()主函数的开始执行时间点.推荐使用__attribute__((constructor)) 构建器函数的被调用时间点作为pre-main()阶段结束时间点：__t2能最大程度实现解耦：

void static __attribute__ ((constructor)) before_main() 
{
  if (__t2 == 0) 
  {
    __t2 = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970;
  }
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;代用运行打印&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

  [AppLaunchTime mark];

  return YES;

}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;日志打印&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;冷启动优化[3454:116391] ----------App启动---------耗时:pre-main:0.718716
冷启动优化[3454:116391] ----------App启动---------耗时:didfinish:0.028895
冷启动优化[3454:116391] ----------App启动---------耗时:total:0.747611
&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;1.2.2.启动过程分析与优化&lt;/h4&gt;
 &lt;h5&gt;1.2.2.1.总体启动过程分析&lt;/h5&gt;
 &lt;p&gt;启动一个应用时，系统会通过   &lt;code&gt;fork()&lt;/code&gt; 方法来新创建一个进程，然后执行镜像通过   &lt;code&gt;exec()&lt;/code&gt; 来替换为另一个可执行程序，然后执行如下操作：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;把可执行文件加载到内存空间，从可执行文件中能够分析出 dyld 的路径；&lt;/li&gt;
  &lt;li&gt;把 dyld 加载到内存；&lt;/li&gt;
  &lt;li&gt;dyld 从可执行文件的依赖开始，递归加载所有的依赖动态链接库 dylib 并进行相应的初始化操作。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;结合上面   &lt;code&gt;pre-main&lt;/code&gt; 打印的结果，我们可以大致了解整个启动过程如下图所示：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/37a7d3b1846a43d3947a8ce2c3cd9f41~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h5&gt;1.2.2.2.Load Dylibs&lt;/h5&gt;
 &lt;p&gt;这一步，指的是  &lt;strong&gt;动态库加载&lt;/strong&gt;。在此阶段，dyld 会：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;分析 App 依赖的所有 dylib；&lt;/li&gt;
  &lt;li&gt;找到 dylib 对应的 Mach-O 文件；&lt;/li&gt;
  &lt;li&gt;打开、读取这些 Mach-O 文件，并验证其有效性；&lt;/li&gt;
  &lt;li&gt;在系统内核中注册代码签名；&lt;/li&gt;
  &lt;li&gt;对 dylib 的每一个 segment 调用    &lt;code&gt;mmap()&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;一般情况下，iOS App 需要加载 100-400 个 dylibs。这些动态库包括系统的，也包括开发者手动引入的。其中大部分 dylib 都是系统库，系统已经做了优化，因此开发者更应关心自己手动集成的内嵌 dylib，加载它们时性能开销较大。&lt;/p&gt;
 &lt;p&gt;App 中依赖的 dylib 越少越好，Apple 官方建议尽量将内嵌 dylib 的个数维持在6个以内。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;优化方案&lt;/strong&gt;：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;尽量不使用内嵌 dylib；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;合并已有内嵌 dylib；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;检查 framework 的    &lt;code&gt;optional&lt;/code&gt; 和    &lt;code&gt;required&lt;/code&gt; 设置，如果 framework 在当前的 App 支持的 iOS 系统版本中都存在，就设为    &lt;code&gt;required&lt;/code&gt;，因为设为    &lt;code&gt;optional&lt;/code&gt; 会有额外的检查；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;使用静态库作为代替；（不过静态库会在编译期被打进可执行文件，造成可执行文件体积增大，两者各有利弊，开发者自行权衡。）&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;懒加载 dylib。（但使用    &lt;code&gt;dlopen()&lt;/code&gt; 对性能会产生影响，因为 App 启动时是原本是单线程运行，系统会取消加锁，但    &lt;code&gt;dlopen()&lt;/code&gt; 开启了多线程，系统不得不加锁，这样不仅会使性能降低，可能还会造成死锁及未知的后果，不是很推荐这种做法。）&lt;/li&gt;
&lt;/ul&gt;
 &lt;h5&gt;1.2.2.3.Rebase/Binding&lt;/h5&gt;
 &lt;p&gt;这一步，做的是  &lt;strong&gt;指针重定位&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;在 dylib 的加载过程中，系统为了安全考虑，引入了 ASLR（Address Space Layout Randomization）技术和代码签名。由于 ASLR 的存在，镜像会在新的随机地址（actual_address）上加载，和之前指针指向的地址（preferred_address）会有一个偏差（slide，slide=actual_address-preferred_address），因此 dyld 需要修正这个偏差，指向正确的地址。具体通过这两步实现：&lt;/p&gt;
 &lt;p&gt;第一步：  &lt;strong&gt;Rebase&lt;/strong&gt;，在 image 内部调整指针的指向。将 image 读入内存，并以 page 为单位进行加密验证，保证不会被篡改，性能消耗主要在 IO。&lt;/p&gt;
 &lt;p&gt;第二步：  &lt;strong&gt;Binding&lt;/strong&gt;，符号绑定。将指针指向 image 外部的内容。查询符号表，设置指向镜像外部的指针，性能消耗主要在 CPU 计算。&lt;/p&gt;
 &lt;p&gt;通过以下命令可以查看 rebase 和 bind 等信息：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;xcrun dyldinfo -rebase -bind -lazy_bind TestDemo.app/TestDemo
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;通过 LC_DYLD_INFO_ONLY 可以查看各种信息的偏移量和大小。如果想要更方便直观地查看，推荐使用   &lt;a href="https://links.jianshu.com/go?to=https://github.com/fiteen/fiteen.github.io/releases/tag/v0.1.2"&gt;MachOView&lt;/a&gt; 工具。&lt;/p&gt;
 &lt;p&gt;指针数量越少，指针修复的耗时也就越少。所以，优化该阶段的关键就是减少   &lt;code&gt;__DATA&lt;/code&gt; 段中的指针数量。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;优化方案&lt;/strong&gt;：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;减少 ObjC 类（class）、方法（selector）、分类（category）的数量，比如合并一些功能，删除无效的类、方法和分类等（可以借助 AppCode 的 Inspect Code 功能进行代码瘦身）；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;减少 C++ 虚函数；（虚函数会创建 vtable，这也会在    &lt;code&gt;__DATA&lt;/code&gt; 段中创建结构。）&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;多用 Swift Structs。（因为 Swift Structs 是静态分发的，它的结构内部做了优化，符号数量更少。）&lt;/li&gt;
&lt;/ul&gt;
 &lt;h5&gt;1.2.2.4.ObjC Setup&lt;/h5&gt;
 &lt;p&gt;完成 Rebase 和 Bind 之后，通知 runtime 去做一些代码运行时需要做的事情：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;dyld 会注册所有声明过的 ObjC 类；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;将分类插入到类的方法列表中；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;检查每个 selector 的唯一性。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;strong&gt;优化方案&lt;/strong&gt;：&lt;/p&gt;
 &lt;p&gt;Rebase/Binding 阶段优化好了，这一步的耗时也会相应减少。&lt;/p&gt;
 &lt;h5&gt;1.2.2.5.Initializers&lt;/h5&gt;
 &lt;p&gt;Rebase 和 Binding 属于静态调整（fix-up），修改的是   &lt;code&gt;__DATA&lt;/code&gt; 段中的内容，而这里则开始动态调整，往堆和栈中写入内容。具体工作有：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;调用每个 Objc 类和分类中的    &lt;code&gt;+load&lt;/code&gt; 方法；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;调用 C/C++ 中的构造器函数（用    &lt;code&gt;attribute((constructor))&lt;/code&gt; 修饰的函数）；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;创建非基本类型的 C++ 静态全局变量。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;strong&gt;优化方案&lt;/strong&gt;：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;尽量避免在类的    &lt;code&gt;+load&lt;/code&gt; 方法中初始化，可以推迟到    &lt;code&gt;+initiailize&lt;/code&gt; 中进行；（因为在一个    &lt;code&gt;+load&lt;/code&gt; 方法中进行运行时方法替换操作会带来 4ms 的消耗）&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;避免使用    &lt;code&gt;__atribute__((constructor))&lt;/code&gt; 将方法显式标记为初始化器，而是让初始化方法调用时再执行。比如用    &lt;code&gt;dispatch_once()&lt;/code&gt;、   &lt;code&gt;pthread_once()&lt;/code&gt; 或    &lt;code&gt;std::once()&lt;/code&gt;，相当于在第一次使用时才初始化，推迟了一部分工作耗时。：&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;减少非基本类型的 C++ 静态全局变量的个数。（因为这类全局变量通常是类或者结构体，如果在构造函数中有繁重的工作，就会拖慢启动速度）&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;总结一下   &lt;code&gt;pre-main&lt;/code&gt; 阶段可行的优化方案：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;重新梳理架构，减少不必要的内置动态库数量&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;进行代码瘦身，合并或删除无效的ObjC类、Category、方法、C++ 静态全局变量等&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;将不必须在    &lt;code&gt;+load&lt;/code&gt; 方法中执行的任务延迟到    &lt;code&gt;+initialize&lt;/code&gt; 中&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;减少 C++ 虚函数&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;1.3.main() 阶段&lt;/h3&gt;
 &lt;p&gt;对于   &lt;code&gt;main()&lt;/code&gt; 阶段，主要测量的就是从   &lt;code&gt;main()&lt;/code&gt; 函数开始执行到   &lt;code&gt;didFinishLaunchingWithOptions&lt;/code&gt; 方法执行结束的耗时。&lt;/p&gt;
 &lt;h4&gt;1.3.1.查看阶段耗时&lt;/h4&gt;
 &lt;p&gt;这里介绍两种查看   &lt;code&gt;main()&lt;/code&gt; 阶段耗时的方法。&lt;/p&gt;
 &lt;h5&gt;  &lt;strong&gt;方法一：手动插入代码，进行耗时计算。&lt;/strong&gt;&lt;/h5&gt;
 &lt;p&gt;第一步：在 main() 函数里用变量 MainStartTime 记录当前时间&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;#import &amp;lt;UIKit/UIKit.h&amp;gt;

#import &amp;quot;AppDelegate.h&amp;quot;



CFAbsoluteTime MainStartTime;



int main(int argc, char * argv[]) 

{

  NSString * appDelegateClassName;

  MainStartTime = CFAbsoluteTimeGetCurrent();

  

  @autoreleasepool {

    appDelegateClassName = NSStringFromClass([AppDelegate class]);

  }

  return UIApplicationMain(argc, argv, nil, appDelegateClassName);

}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;第二步：在 AppDelegate.m 文件中用 extern 声明全局变量&lt;/p&gt;
 &lt;p&gt;第三步：在 didFinishLaunchingWithOptions 方法结束前，再获取一下当前时间，与 MainStartTime 的差值就是 main() 函数阶段的耗时&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;#import &amp;quot;AppDelegate.h&amp;quot;

#import &amp;quot;AppLaunchTime.h&amp;quot;

extern CFAbsoluteTime MainStartTime;

 @interface AppDelegate ()

 @end



 @implementation AppDelegate



- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

  

  [AppLaunchTime mark];

   

  double mainLaunchTime = (CFAbsoluteTimeGetCurrent() - MainStartTime);

  NSLog(@&amp;quot;main() 阶段耗时：%.2fms&amp;quot;, mainLaunchTime * 1000);

   

  return YES;

}



//日志：

 //冷启动优化[3616:126842] main() 阶段耗时：21.92ms
&lt;/code&gt;&lt;/pre&gt;
 &lt;h5&gt;  &lt;strong&gt;方法二：借助 Instruments 的 Time Profiler 工具查看耗时。&lt;/strong&gt;&lt;/h5&gt;
 &lt;p&gt;打开方式为：  &lt;code&gt;Xcode → Open Developer Tool → Instruments → Time Profiler&lt;/code&gt;。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/26c3c6ab4cc94eb6bce682ea47ccc9c5~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;
操作步骤：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;配置 Scheme。点击    &lt;code&gt;Edit Scheme&lt;/code&gt; 找到    &lt;code&gt;Profile&lt;/code&gt; 下的    &lt;code&gt;Build Configuration&lt;/code&gt;，设置为    &lt;code&gt;Debug&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9cdd8a1c64b343dfb6a9737cf4c13ff5~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;
2.  配置 PROJECT。点击 PROJECT，在   &lt;code&gt;Build Settings&lt;/code&gt; 中找到   &lt;code&gt;Build Options&lt;/code&gt; 选项里的   &lt;code&gt;Debug Information Format&lt;/code&gt;，把   &lt;code&gt;Debug&lt;/code&gt; 对应的值改为   &lt;code&gt;DWARF with dSYM File&lt;/code&gt;。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ae57b986b7704c92a7c80e2e1c5f5f7a~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;
3.  启动 Time Profiler，点击左上角红色圆形按钮开始检测，然后就可以看到执行代码的完整路径和对应的耗时。&lt;/p&gt;
 &lt;p&gt;为了方面查看应用程序中实际代码的执行耗时和代码路径实际所在的位置，可以勾选上   &lt;code&gt;Call Tree&lt;/code&gt; 中的   &lt;code&gt;Separate Thread&lt;/code&gt; 和   &lt;code&gt;Hide System Libraries&lt;/code&gt;。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a00d684ccf564f57868c0a88a4bf6493~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h4&gt;1.3.2.启动优化&lt;/h4&gt;
 &lt;p&gt;  &lt;code&gt;main()&lt;/code&gt; 被调用之后，  &lt;code&gt;didFinishLaunchingWithOptions&lt;/code&gt; 阶段，App 会进行必要的初始化操作，而   &lt;code&gt;viewDidAppear&lt;/code&gt;执行结束之前则是做了首页内容的加载和显示。&lt;/p&gt;
 &lt;p&gt;关于   &lt;strong&gt;App 的初始化&lt;/strong&gt;，除了统计、日志这种须要在 App 一启动就配置的事件，有一些配置也可以考虑延迟加载。如果你在   &lt;code&gt;didFinishLaunchingWithOptions&lt;/code&gt; 中同时也涉及到了  &lt;strong&gt;首屏的加载&lt;/strong&gt;，那么可以考虑从这些角度优化：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;用纯代码的方式，而不是 xib/Storyboard，来加载首页视图&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;延迟暂时不需要的二方/三方库加载；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;延迟执行部分业务逻辑和 UI 配置；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;延迟加载/懒加载部分视图；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;避免首屏加载时大量的本地/网络数据读取；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;在 release 包中移除 NSLog 打印；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;在视觉可接受的范围内，压缩页面中的图片大小；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;
 &lt;h1&gt;三.首页为H5 页面优化&lt;/h1&gt;
 &lt;p&gt;可参考 VasSonic 的原理
Sonic是腾讯团队研发的一个轻量级的高性能的Hybrid框架，专注于提升页面首屏加载速度&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;终端耗时&lt;/p&gt;
   &lt;ul&gt;
    &lt;li&gt;webView 预加载：在 App 启动时期预先加载了一次 webView，通过创建空的 webView，预先启动 Web 线程，完成一些全局性的初始化工作，对二次创建 webView 能有数百毫秒的提升。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;页面耗时（静态页面）&lt;/p&gt;
   &lt;ul&gt;
    &lt;li&gt;静态直出：服务端拉取数据后通过 Node.js 进行渲染，生成包含首屏数据的 HTML 文件，发布到 CDN 上，webView 直接从 CDN 上获取；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;离线预推：使用离线包。&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;页面耗时（经常需要动态更新的页面）&lt;/p&gt;
   &lt;ul&gt;
    &lt;li&gt;并行加载：WebView 的打开和资源的请求并行；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;动态缓存：动态页面缓存在客户端，用户下次打开的时候先打开缓存页面，然后再刷新；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;动静分离：将页面分为静态模板和动态数据，根据不同的启动场景进行不同的刷新方案；&lt;/li&gt;
&lt;/ul&gt;

 &lt;ul&gt;
  &lt;li&gt;预加载：提前拉取需要的增量更新数据。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h1&gt;四.总结&lt;/h1&gt;
 &lt;p&gt;冷启动本就是一个比较复杂的流程，它的优化没有固定的方式，我们需要结合业务，配合一些性能分析工具和线上监控日志,灵活应用&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62167-ios-%E5%86%B7%E5%90%AF%E5%8A%A8-%E4%BC%98%E5%8C%96</guid>
      <pubDate>Fri, 25 Mar 2022 06:30:56 CST</pubDate>
    </item>
    <item>
      <title>[原]一文读懂直播卡顿优化那些事儿</title>
      <link>https://itindex.net/detail/62100-%E6%96%87%E8%AF%BB-%E7%9B%B4%E6%92%AD-%E4%BC%98%E5%8C%96</link>
      <description>&lt;div&gt;
                    

                    
                    
                    
                      &lt;p&gt;   &lt;strong&gt;动手点关注 干货不迷路&lt;/strong&gt; &lt;/p&gt;  &lt;blockquote&gt;   &lt;p&gt;希望本文可以带给大家一个相对全局的视角看待卡顿问题，认识到卡顿是什么、卡顿的成因、卡顿的分类、卡顿的优化和一些经验积累，有的放矢地解决 App 流畅性问题。接下来会从以下五个方面进行讲述：&lt;/p&gt;   &lt;p&gt;✦什么是卡顿&lt;/p&gt;   &lt;p&gt;✦为什么会发生卡顿&lt;/p&gt;   &lt;p&gt;✦如何评价卡顿&lt;/p&gt;   &lt;p&gt;✦如何优化卡顿&lt;/p&gt;   &lt;p&gt;✦加入我们&lt;/p&gt;&lt;/blockquote&gt;  &lt;h2&gt;1. 什么是卡顿&lt;/h2&gt;  &lt;p&gt;卡顿，顾名思义就是   &lt;strong&gt;用户体感界面不流畅&lt;/strong&gt;。我们知道手机的屏幕画面是按照一定频率来刷新的，理论上讲，24 帧的画面更新就能让人眼感觉是连贯的。但是实际上，这个只是针对普通的视频而言。对于一些   &lt;strong&gt;强交互或者较为敏感的场景&lt;/strong&gt;来说，比如游戏，起码需要 60 帧，30 帧的游戏会让人感觉不适；位移或者大幅度动画 30 帧会有明显顿挫感；跟手动画如果能到 90 帧甚至 120 帧，会让人感觉十分细腻，这也是近来厂商主打高刷牌的原因。&lt;/p&gt;  &lt;p&gt;对于用户来说，从体感角度大致可以将卡顿分为以下几类：&lt;/p&gt;  &lt;img alt="fcc9266d9614c026bf9301f1619fcf28.png" src="https://img-blog.csdnimg.cn/img_convert/fcc9266d9614c026bf9301f1619fcf28.png"&gt;&lt;/img&gt;  &lt;p&gt;这些体验对于用户可以说是非常糟糕的，甚至会引起感官的烦躁，进而导致用户不愿意继续停留在我们的 App。可以说，流畅的体验对于用户来说至关重要。&lt;/p&gt;  &lt;h2&gt;2. 为什么会发生卡顿&lt;/h2&gt;  &lt;p&gt;用户体感的卡顿问题原因很多，且常常是一个复合型的问题，为了聚焦，这里暂只考虑真正意义上的掉帧卡顿。&lt;/p&gt;  &lt;h3&gt;2.1 绕不开的 VSYNC&lt;/h3&gt;  &lt;p&gt;我们通常会说，屏幕的刷新率是 60 帧，需要在 16ms 内做完所有的操作才不会造成卡顿。但是这里需要明确几个基本问题：&lt;/p&gt;  &lt;ol&gt;   &lt;li&gt;    &lt;p&gt;为什么是 16ms？&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;16ms 内都需要完成什么？&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;系统如何尽力保证任务在 16ms 内完成？&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;16ms 内没有完成，一定会造成卡顿吗？&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;  &lt;p&gt;   &lt;strong&gt;这里先回答第一个问题：为什么是 16ms&lt;/strong&gt;。早期的 Android 是没有 vsync 机制的，CPU 和 GPU 的配合也比较混乱，这也造成著名的 tearing 问题，即 CPU/GPU 直接更新正在显示的屏幕 buffer 造成画面撕裂。后续 Android 引入了双缓冲机制，但是 buffer 的切换也需要一个比较合适的时机，也就是屏幕扫描完上一帧后的时机，这也就是引入 vsync 的原因。&lt;/p&gt;  &lt;p&gt;早先一般的屏幕刷新率是 60fps，所以每个 vsync 信号的间隔也是 16ms，不过随着技术的更迭以及厂商对于流畅性的追求，越来越多 90fps 和 120fps 的手机面世，相对应的间隔也就变成了 11ms 和 8ms。&lt;/p&gt;  &lt;p&gt;那既然有了 VSYNC，   &lt;strong&gt;谁在消费 VSYNC&lt;/strong&gt;？其实 Android 的 VSYNC 消费者有两个，也就对应两类 VSYNC 信号，分别是 VSYNC-app 和 VSYNC-sf，所对应的也是上层 view 绘制和 surfaceFlinger 的合成，具体的我们接下来详细说。&lt;/p&gt;  &lt;img alt="3f29426dbd13a63ce29090e57363428a.png" src="https://img-blog.csdnimg.cn/img_convert/3f29426dbd13a63ce29090e57363428a.png"&gt;&lt;/img&gt;  &lt;p&gt;这里还有一些比较有意思的点，有些厂商会有 vsync offset 的设计，App 和 sf 的 vsync 信号之间是有偏移量的，这也在一定程度上使得 App 和 sf 的协同效应更好。&lt;/p&gt;  &lt;h3&gt;2.2 View 颠沛流离的一生&lt;/h3&gt;  &lt;p&gt;在讲下一 part 之前先引入一个话题：&lt;/p&gt;  &lt;blockquote&gt;   &lt;p&gt;一个 view 究竟是如何显示在屏幕上的？&lt;/p&gt;&lt;/blockquote&gt;  &lt;p&gt;我们一般都比较了解 view 渲染的三大流程，但是 view 的渲染远不止于此：&lt;/p&gt;  &lt;blockquote&gt;   &lt;p&gt;此处以一个通用的硬件加速流程来表征&lt;/p&gt;&lt;/blockquote&gt;  &lt;img alt="e2a5df46a6a766d57e870f239fc20212.png" src="https://img-blog.csdnimg.cn/img_convert/e2a5df46a6a766d57e870f239fc20212.png"&gt;&lt;/img&gt;  &lt;ol&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;Vsync 调度&lt;/strong&gt;：很多同学的一个认知误区在于认为 vsync 是每 16ms 都会有的，但是其实 vsync 是需要调度的，没有调度就不会有回调；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;消息调度&lt;/strong&gt;：主要是 doframe 的消息调度，如果消息被阻塞，会直接造成卡顿；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;input 处理&lt;/strong&gt;：触摸事件的处理；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;动画处理&lt;/strong&gt;：animator 动画执行和渲染；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;view 处理&lt;/strong&gt;：主要是 view 相关的遍历和三大流程；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;measure、layout、draw&lt;/strong&gt;：view 三大流程的执行；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;DisplayList 更新&lt;/strong&gt;：view 硬件加速后的 draw op；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;OpenGL 指令转换&lt;/strong&gt;：绘制指令转换为 OpenGL 指令；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;指令 buffer 交换&lt;/strong&gt;：OpenGL 的指令交换到 GPU 内部执行；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;GPU 处理&lt;/strong&gt;：GPU 对数据的处理过程；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;layer 合成&lt;/strong&gt;：surface buffer 合成屏幕显示 buffer 的流程；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;光栅化&lt;/strong&gt;：将矢量图转换为位图；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;Display&lt;/strong&gt;：显示控制；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;buffer 切换&lt;/strong&gt;：切换屏幕显示的帧 buffer；&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;  &lt;p&gt;Google 将这个过程划分为：其他时间/VSync 延迟、输入处理、动画、测量/布局、绘制、同步和上传、命令问题、交换缓冲区。也就是我们常用的 GPU 严格模式，其实道理是一样的。到这里，我们也就回答出来了第二个问题：   &lt;strong&gt;16ms 内都需要完成什么&lt;/strong&gt;？&lt;/p&gt;  &lt;blockquote&gt;   &lt;p&gt;准确地说，这里仍可以进一步细化：16ms 内完成 APP 侧数据的生产；16ms 内完成 sf layer 的合成&lt;/p&gt;&lt;/blockquote&gt;  &lt;img alt="7ff50d2ff0da157a92a3fa92ba135ca6.png" src="https://img-blog.csdnimg.cn/img_convert/7ff50d2ff0da157a92a3fa92ba135ca6.png"&gt;&lt;/img&gt;  &lt;p&gt;View 的视觉效果正是通过这一整条复杂的链路一步步展示出来的，有了这个前提，那就可以得出一个结论：   &lt;strong&gt;上述任意链路发生卡顿，均会造成卡顿&lt;/strong&gt;。&lt;/p&gt;  &lt;h3&gt;2.3 生产者和消费者&lt;/h3&gt;  &lt;img alt="fcbfc03e923dfec651492ed001081046.png" src="https://img-blog.csdnimg.cn/img_convert/fcbfc03e923dfec651492ed001081046.png"&gt;&lt;/img&gt;  &lt;p&gt;我们再回到 Vsync 的话题，消费 Vsync 的双方分别是 App 和 sf，   &lt;strong&gt;其中 App 代表的是生产者，sf 代表的是消费者，两者交付的中间产物则是 surface buffer&lt;/strong&gt;。&lt;/p&gt;  &lt;p&gt;再具体一点，生产者大致可以分为两类，一类是以 window 为代表的页面，也就是我们平时所看到的 view 树这一套；另一类是以视频流为代表的可以直接和 surface 完成数据交换的来源，比如相机预览等。&lt;/p&gt;  &lt;p&gt;对于一般的生产者和消费者模式，我们知道会存在   &lt;strong&gt;相互阻塞&lt;/strong&gt;的问题。比如生产者速度快但是消费者速度慢，亦或是生产者速度慢消费者速度快，都会导致整体速度慢且造成资源浪费。所以 Vsync 的协同以及双缓冲甚至三缓冲的作用就体现出来了。&lt;/p&gt;  &lt;blockquote&gt;   &lt;p&gt;思考一个问题：是否缓冲的个数越多越好？过多的缓冲会造成什么问题？    &lt;br /&gt;    &lt;br /&gt;答案是会造成另一个严重的问题：lag，响应延迟&lt;/p&gt;&lt;/blockquote&gt;  &lt;p&gt;这里结合 view 的一生，我们可以把两个流程合在一起，让我们的视角再高一层：&lt;/p&gt;  &lt;img alt="d25a2b80def720db5d0bfd83616a25c4.png" src="https://img-blog.csdnimg.cn/img_convert/d25a2b80def720db5d0bfd83616a25c4.png"&gt;&lt;/img&gt;  &lt;h3&gt;2.4 机制上的保护&lt;/h3&gt;  &lt;p&gt;这里我们来回答   &lt;strong&gt;第三个问题&lt;/strong&gt;，从系统的渲染架构上来说，机制上的保护主要有几方面：&lt;/p&gt;  &lt;ol&gt;   &lt;li&gt;    &lt;p&gt;Vsync 机制的协同；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;多缓冲设计；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;surface 的提供；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;同步屏障的保护；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;硬件绘制的支持；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;渲染线程的支持；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;GPU 合成加速；&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;  &lt;p&gt;这些机制上的保护在系统层面最大程度地保障了 App 体验的流畅性，但是并不能帮我们彻底解决卡顿。为了提供更加流畅的体验，   &lt;strong&gt;一方面&lt;/strong&gt;，我们可以加强系统的机制保护，比如 FWatchDog；   &lt;strong&gt;另一方面&lt;/strong&gt;，需要我们从 App 的角度入手，治理应用内的卡顿问题。&lt;/p&gt;  &lt;h3&gt;2.5 再看卡顿的成因&lt;/h3&gt;  &lt;p&gt;经过上面的讨论，我们得出一个卡顿分析的核心理论支撑：   &lt;strong&gt;渲染机制中的任何流转过程发生异常，均会造成卡顿&lt;/strong&gt;。&lt;/p&gt;  &lt;p&gt;那么接下来，我们逐个分析，看看都会有哪些原因可能造成卡顿。&lt;/p&gt;  &lt;h4&gt;2.5.1 渲染流程&lt;/h4&gt;  &lt;ol&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;Vsync 调度&lt;/strong&gt;：这个是起始点，但是调度的过程会经过线程切换以及一些委派的逻辑，有可能造成卡顿，但是一般可能性比较小，我们也基本无法介入；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;消息调度&lt;/strong&gt;：主要是 doframe Message 的调度，这就是一个普通的 Handler 调度，如果这个调度被其他的 Message 阻塞产生了时延，会直接导致后续的所有流程不会被触发。这里直播建立了一个 FWtachDog 机制，可以通过优化消息调度达到插帧的效果，使得界面更加流畅；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;input 处理&lt;/strong&gt;：input 是一次 Vsync 调度最先执行的逻辑，主要处理 input 事件。如果有大量的事件堆积或者在事件分发逻辑中加入大量耗时业务逻辑，会造成当前帧的时长被拉大，造成卡顿。抖音基础技术同学也有尝试过事件采样的方案，减少 event 的处理，取得了不错的效果；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;动画处理&lt;/strong&gt;：主要是 animator 动画的更新，同理，动画数量过多，或者动画的更新中有比较耗时的逻辑，也会造成当前帧的渲染卡顿。对动画的降帧和降复杂度其实解决的就是这个问题；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;view 处理&lt;/strong&gt;：主要是接下来的三大流程，过度绘制、频繁刷新、复杂的视图效果都是此处造成卡顿的主要原因。比如我们平时所说的降低页面层级，主要解决的就是这个问题；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;measure/layout/draw&lt;/strong&gt;：view 渲染的三大流程，因为涉及到遍历和高频执行，所以这里涉及到的耗时问题均会被放大，比如我们会降不能在 draw 里面调用耗时函数，不能 new 对象等等；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;DisplayList 的更新&lt;/strong&gt;：这里主要是 canvas 和 displaylist 的映射，一般不会存在卡顿问题，反而可能存在映射失败导致的显示问题；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;OpenGL 指令转换&lt;/strong&gt;：这里主要是将 canvas 的命令转换为 OpenGL 的指令，一般不存在问题。不过这里倒是有一个可以探索的点，会不会存在一类特殊的 canvas 指令，转换后的 OpenGL 指令消耗比较大，进而导致 GPU 的损耗？有了解的同学可以探讨一下；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;buffer 交换&lt;/strong&gt;：这里主要指 OpenGL 指令集交换给 GPU，这个一般和指令的复杂度有关。一个有意思的事儿是这里一度被我们作为线上采集 GPU 指标的数据源，但是由于多缓冲的因素数据准确度不够被放弃了；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;GPU 处理&lt;/strong&gt;：顾名思义，这里是 GPU 对数据的处理，耗时主要和任务量和纹理复杂度有关。这也就是我们降低 GPU 负载有助于降低卡顿的原因；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;layer 合成&lt;/strong&gt;：这里主要是 layer 的 compose 的工作，一般接触不到。偶尔发现 sf 的 vsync 信号被 delay 的情况，造成 buffer 供应不及时，暂时还不清楚原因；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;光栅化/Display&lt;/strong&gt;：这里暂时忽略，底层系统行为；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;Buffer 切换&lt;/strong&gt;：主要是屏幕的显示，这里 buffer 的数量也会影响帧的整体延迟，不过是系统行为，不能干预。&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;  &lt;h4&gt;2.5.2 视频流&lt;/h4&gt;  &lt;p&gt;除了上述的渲染流程引起的卡顿，还有一些其他的因素，典型的就是视频流。&lt;/p&gt;  &lt;ol&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;渲染卡顿&lt;/strong&gt;：主要是 TextureView 渲染，textureview 跟随 window 共用一个 surface，每一帧均需要一起协同渲染并相互影响，UI 卡顿会造成视频流卡顿，视频流的卡顿有时候也会造成 UI 的卡顿；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;解码&lt;/strong&gt;：解码主要是将数据流解码为 surface 可消费的 buffer 数据，是除了网络外最重要的耗时点。现在我们一般都会采用硬解，比软解的性能高很多。但是帧的复杂度、编码算法的复杂度、分辨率等也会直接导致解码耗时被拉长；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;OpenGL 处理&lt;/strong&gt;：有时会对解码完成的数据做二次处理，这个如果比较耗时会直接导致渲染卡顿；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;网络&lt;/strong&gt;：这个就不再赘述了，包括 DNS 节点优选、cdn 服务、GOP 配置等；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;推流异常&lt;/strong&gt;：这个属于数据源出了问题，这里暂时以用户侧的视角为主，暂不讨论。&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;  &lt;p&gt;2.5.3 系统负载&lt;/p&gt;  &lt;ol&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;内存&lt;/strong&gt;：内存的吃紧会直接导致 GC 的增加甚至 ANR，是造成卡顿的一个不可忽视的因素；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;CPU&lt;/strong&gt;：CPU 对卡顿的影响主要在于线程调度慢、任务执行的慢和资源竞争，比如降频会直接导致应用卡顿；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;GPU&lt;/strong&gt;：GPU 的影响见渲染流程，但是其实还会间接影响到功耗和发热；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;功耗/发热&lt;/strong&gt;：功耗和发热一般是不分家的，高功耗会引起高发热，进而会引起系统保护，比如降频、热缓解等，间接的导致卡顿。&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;  &lt;h3&gt;2.6 卡顿的分类&lt;/h3&gt;  &lt;p&gt;我们此处再整体整理并归类，为了更完备一些，这里将推流也放了上来。在一定程度上，   &lt;strong&gt;我们遇到的所有卡顿问题，均能在这里找到理论依据&lt;/strong&gt;，这也是指导我们优化卡顿问题的理论支撑。&lt;/p&gt;  &lt;img alt="019f94392591b1ffeef93ed19de0f3b8.png" src="https://img-blog.csdnimg.cn/img_convert/019f94392591b1ffeef93ed19de0f3b8.png"&gt;&lt;/img&gt;  &lt;h2&gt;3. 如何评价卡顿&lt;/h2&gt;  &lt;h3&gt;3.1 线上指标&lt;/h3&gt;  &lt;table&gt;   &lt;tr&gt;    &lt;th&gt;指标&lt;/th&gt;    &lt;th width="73"&gt;释义&lt;/th&gt;    &lt;th width="218"&gt;计算方式&lt;/th&gt;    &lt;th width="85"&gt;数据来源&lt;/th&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;FPS&lt;/td&gt;    &lt;td width="73"&gt;帧率&lt;/td&gt;    &lt;td width="238"&gt;     &lt;p&gt;取 vsync 到来的时间为起点，doFrame 执行完成的事件为终点，作为每帧的渲染耗时，同时利用渲染耗时/刷新率可以得出每次渲染的丢帧数。平均 FPS = 一段时间内渲染帧的个数 * 60 / (渲染帧个数 + 丢帧个数)&lt;/p&gt;&lt;/td&gt;    &lt;td width="85"&gt;vsync&lt;/td&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;     &lt;p&gt;stall_video_ui_rate&lt;/p&gt;&lt;/td&gt;    &lt;td width="73"&gt;     &lt;p&gt;总卡顿率&lt;/p&gt;&lt;/td&gt;    &lt;td width="27"&gt;（UI 卡顿时长 + 流卡顿时长） / 采集时长&lt;/td&gt;    &lt;td width="85"&gt;     &lt;p&gt;vsync&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;stall_ui_rate&lt;/td&gt;    &lt;td width="73"&gt;UI 卡顿率&lt;/td&gt;    &lt;td width="27"&gt;【&amp;gt; 3 帧】UI 卡顿时长 / 采集时长&lt;/td&gt;    &lt;td width="85"&gt;vsync&lt;/td&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;stall_video_rate&lt;/td&gt;    &lt;td width="73"&gt;流卡顿率&lt;/td&gt;    &lt;td width="238"&gt;流卡顿时长 / 采集时长&lt;/td&gt;    &lt;td width="85"&gt;vsync&lt;/td&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;stall_ui_slight_rate&lt;/td&gt;    &lt;td width="73"&gt;轻微卡顿率&lt;/td&gt;    &lt;td width="27"&gt;【3 - 6】帧丢帧时长 / 采集时长&lt;/td&gt;    &lt;td width="85"&gt;vsync&lt;/td&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;stall_ui_moderate_rate&lt;/td&gt;    &lt;td width="73"&gt;中等卡顿率&lt;/td&gt;    &lt;td width="27"&gt;【7 - 13】帧丢帧时长 / 采集时长&lt;/td&gt;    &lt;td width="85"&gt;vsync&lt;/td&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;stall_ui_serious_rate&lt;/td&gt;    &lt;td width="73"&gt;严重卡顿率&lt;/td&gt;    &lt;td width="27"&gt;【&amp;gt; 14】帧丢帧时长 / 采集时长&lt;/td&gt;    &lt;td width="85"&gt;vsync&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;  &lt;h3&gt;3.2 线下指标&lt;/h3&gt;  &lt;blockquote&gt;   &lt;p&gt;Diggo 是字节自研的一个开放的开发调试工具平台，是一个集「评价、分析、调试」为一体的，一站式工具平台。内置性能测评、界面分析、卡顿分析、内存分析、崩溃分析、即时调试等基础分析能力，可为产品开发阶段提供强大助力。&lt;/p&gt;&lt;/blockquote&gt;  &lt;table&gt;   &lt;tr&gt;    &lt;th&gt;指标&lt;/th&gt;    &lt;th&gt;释义&lt;/th&gt;    &lt;th&gt;计算方式&lt;/th&gt;    &lt;th&gt;数据来源&lt;/th&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;     &lt;p&gt;FPS&lt;/p&gt;&lt;/td&gt;    &lt;td&gt;     &lt;p&gt;时机渲染帧率&lt;/p&gt;&lt;/td&gt;    &lt;td&gt;     &lt;p&gt;数据获取时间周期内，实际渲染帧数/ 数据获取间隔时间&lt;/p&gt;&lt;/td&gt;    &lt;td&gt;     &lt;p&gt;SF &amp;amp; GFXInfo&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;RFPS&lt;/td&gt;    &lt;td&gt;相对帧率&lt;/td&gt;    &lt;td&gt;数据获取时间周期内，（理论满帧-实际掉帧数）/ 数据获取间隔时间&lt;/td&gt;    &lt;td&gt;GFXInfo&lt;/td&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;Stutter&lt;/td&gt;    &lt;td&gt;卡顿率&lt;/td&gt;    &lt;td&gt;卡顿比。当发生 jank 的帧的累计时长与区间时长的比值。&lt;/td&gt;    &lt;td&gt;SF&lt;/td&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;Janky Count&lt;/td&gt;    &lt;td&gt;普通卡顿次数&lt;/td&gt;    &lt;td&gt;单帧绘制耗时大于 MOVIE_FRAME_TIME 时，计一次 janky。&lt;/td&gt;    &lt;td&gt;SF&lt;/td&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;Big Janky Count&lt;/td&gt;    &lt;td&gt;严重卡顿次数&lt;/td&gt;    &lt;td&gt;单帧绘制耗时大于 3*MOVIE_FRAME_TIME 时，计一次 big janky。&lt;/td&gt;    &lt;td&gt;SF&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;  &lt;h2&gt;4. 如何优化卡顿&lt;/h2&gt;  &lt;h3&gt;4.1 常用的工具&lt;/h3&gt;  &lt;h4&gt;4.1.1 线上工具&lt;/h4&gt;  &lt;table&gt;   &lt;tr&gt;    &lt;th&gt;名称&lt;/th&gt;    &lt;th&gt;释义&lt;/th&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;正式包慢函数&lt;/td&gt;    &lt;td&gt;相对于灰度包，过滤了比较多监控，对性能损耗比较小，但是需要手动打开，单点反馈中不能保留反馈现场&lt;/td&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;灰度包慢函数&lt;/td&gt;    &lt;td&gt;灰度上全量打开，针对版本间的数据对比和新增卡顿问题解决比较有效&lt;/td&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;ANR&lt;/td&gt;    &lt;td&gt;ANR 的及时响应和处理&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;  &lt;h4&gt;4.1.2 线下工具&lt;/h4&gt;  &lt;table&gt;   &lt;tr&gt;    &lt;th&gt;工具名&lt;/th&gt;    &lt;th&gt;备注&lt;/th&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;Systrace&lt;/td&gt;    &lt;td&gt;暂不赘述&lt;/td&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;perfetto&lt;/td&gt;    &lt;td&gt;加强版 systrace，可定制，可以参考官方文档&lt;/td&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;Rhea&lt;/td&gt;    &lt;td&gt;最常用也是最好用的工具，方便发现下下问题和归因，和 perfetto 一起使用绝配，感兴趣的同学可以移步 github 搜索 btrace&lt;/td&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;profiler&lt;/td&gt;    &lt;td&gt;Androidstudio 自带工具，比较方便，但是数据准确度不高&lt;/td&gt;&lt;/tr&gt;   &lt;tr&gt;    &lt;td&gt;sf / gfxinfo&lt;/td&gt;    &lt;td&gt;主要用于脚本和工具&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;  &lt;h3&gt;4.2 常用的思路&lt;/h3&gt;  &lt;blockquote&gt;   &lt;p&gt;这里主要针对 UI 卡顿和 UI/流相互影响打来的卡顿。&lt;/p&gt;&lt;/blockquote&gt;  &lt;p&gt;对于 UI 卡顿来说，我们手握卡顿优化的 8 板大斧子，所向披靡：&lt;/p&gt;  &lt;ol&gt;   &lt;li&gt;    &lt;p&gt;下线代码；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;减少执行次数；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;异步；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;打散；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;预热；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;复用；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;方案优化；&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;硬件加速；&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;  &lt;p&gt;   &lt;strong&gt;总体思路&lt;/strong&gt;就是「能不干就不干、能少干就少干、能早点干就早点儿干、能晚点儿干就晚点儿干、能让别人干就让别人干、能干完一次当 10 次就只干一次，实在不行，再考虑自己大干一场」。&lt;/p&gt;  &lt;p&gt;这里例举出一些常见的优化思路，注意这一定也不可能是全部，如果有其他好的优化思路，我们可以一起交流。&lt;/p&gt;  &lt;img alt="9b55af7857de61c2f21f9ae318f7b06f.png" src="https://img-blog.csdnimg.cn/img_convert/9b55af7857de61c2f21f9ae318f7b06f.png"&gt;&lt;/img&gt;  &lt;h3&gt;4.3 一些做过的事儿&lt;/h3&gt;  &lt;h4&gt;4.3.1 解决 UI 卡顿引起的流卡顿&lt;/h4&gt;  &lt;p&gt;直播对于    &lt;strong&gt;SurfaceView&lt;/strong&gt; 的切换是一个长期的专项，分为多期逐步将 SurfaceView 在直播全量落地，场景覆盖秀场直播、聊天室、游戏直播、电商直播、媒体直播等，业务上对于渗透率和停留时长有比较显著的收益，同时功耗的收益也很可观。&lt;/p&gt;  &lt;p&gt;这里是一个权衡的问题，SurfaceView 的   &lt;strong&gt;兼容性问题 pk 带来的收益&lt;/strong&gt;是否能打平，一般来说，越是复杂的业务场景，收益约大。&lt;/p&gt;  &lt;h4&gt;4.3.2 解决 message 调度&lt;/h4&gt;  &lt;p&gt;FWatchDog 是基于对 MessageQueue 的调度策略和同步屏障原理，以均帧耗时为阈值判定丢帧后主动在 MessageQueue 中插入同步屏障，保证渲染异步 message 和 doframe 的优先执行，达到一种渲染插帧的效果，同时具备 ANR 自动恢复同步屏障的能力，保障打散的有效。&lt;/p&gt;  &lt;p&gt;所以    &lt;strong&gt;FWatchDog 和打散是好的搭档&lt;/strong&gt;，能产生 1+1 大于 2 的效果。&lt;/p&gt;  &lt;h4&gt;4.3.3 减少执行次数&lt;/h4&gt;  &lt;p&gt;一个典型的应用场景就是   &lt;strong&gt;滑动场景的 GC 抑制&lt;/strong&gt;，能够显著提高用户上下滑的使用体验。这个场景相信每个业务都会存在，特别是存在大量遍历的逻辑，优化效果明显。&lt;/p&gt;  &lt;h4&gt;4.3.4 代码下线&lt;/h4&gt;  &lt;p&gt;一些   &lt;strong&gt;老的框架、无用的逻辑&lt;/strong&gt;以及   &lt;strong&gt;存在性不高的代码&lt;/strong&gt;都可以下线，这里基本业务强相关，就不举具体的例子了。&lt;/p&gt;  &lt;h4&gt;4.3.5 解决耗时函数（打散/异步）&lt;/h4&gt;  &lt;p&gt;首先是   &lt;strong&gt;打散&lt;/strong&gt;，直播做了很多 task 的拆分以及打散，第一可以减轻当前渲染帧的耗时压力，第二可以和 FWatchDog 结合达到插帧的效果。这里其实还可以控制 task 的执行优先级，包括队列的插队等，总之 MessageQueue 的合理调度是很有必要的。&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;异步&lt;/strong&gt;的使用也相对比较多，一个埋点日志的框架，以及一些 inflate 的加载等，都可以使用异步来解决卡顿问题。&lt;/p&gt;  &lt;h4&gt;4.3.6 预热&lt;/h4&gt;  &lt;p&gt;直播提供了一个   &lt;strong&gt;预热框架&lt;/strong&gt;，可以让直播内部的一次性成本逻辑得到在宿主侧执行的机会，同时提供完备的队列优先级管理、同步异步管理和 task 生命周期管理，降低直播内部首次加载的卡顿问题。&lt;/p&gt;  &lt;h4&gt;4.3.7 硬件加速&lt;/h4&gt;  &lt;p&gt;拉高   &lt;strong&gt;硬件&lt;/strong&gt;的运行性能，比如 CPU 频率、GPU 频率、线程绑大核以及网络相关的调优，从底层提高 App 的运行体验。&lt;/p&gt;  &lt;h2&gt;5. 加入我们&lt;/h2&gt;  &lt;p&gt;直播客户端技术团队是一个集   &lt;strong&gt;体验优化、平台建设、跨端、端智能、稳定性&lt;/strong&gt;为一体的综合性团队，团队氛围 nice，技术成长快，有充足的自由度发挥自己的特长，为亿级 DAU 产品保驾护航，也面临更加丰富多样的挑战，每一行代码都会让数亿的用户体验变得更好！现诚邀各位英才加入，对这些方向感兴趣的同学都可以来聊一聊，   &lt;strong&gt;内推链接&lt;/strong&gt;点击   &lt;strong&gt;阅读原文&lt;/strong&gt;。&lt;/p&gt;
                &lt;/div&gt;                     &lt;div&gt;
                        作者：ByteDanceTech 发表于 2022/02/09 12:00:00   &lt;a href="https://blog.csdn.net/ByteDanceTech/article/details/122852655"&gt;原文链接&lt;/a&gt; https://blog.csdn.net/ByteDanceTech/article/details/122852655                    &lt;/div&gt;
                     &lt;div&gt;
                        阅读：1206 评论：2   &lt;a href="https://blog.csdn.net/ByteDanceTech/article/details/122852655#comments" target="_blank"&gt;查看评论&lt;/a&gt;                    &lt;/div&gt;
                    
                &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62100-%E6%96%87%E8%AF%BB-%E7%9B%B4%E6%92%AD-%E4%BC%98%E5%8C%96</guid>
      <pubDate>Wed, 09 Feb 2022 12:00:00 CST</pubDate>
    </item>
    <item>
      <title>非常哇塞的 ES读场景、写场景 性能优化指南！你值得拥有！</title>
      <link>https://itindex.net/detail/61994-es-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96-%E5%80%BC%E5%BE%97</link>
      <description>&lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;原创：小姐姐味道（微信公众号ID：xjjdog），欢迎分享，转载请保留出处。&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;ES作为NoSQL数据库里非常重要的一员，使用越来越广泛。虽然它因为索引延迟的原因，数据在时效性上有一些缺陷，但其大容量、分布式的优秀设计，使得它在时效性要求并不是特别高的类实时搜索领域，能够大展身手。&lt;/p&gt; &lt;p&gt;根据使用场景和用途，ES可以分为写入和读取两种典型的应用方式。比如ELKB，我们就需要额外关注它的  &lt;code&gt;写优化&lt;/code&gt;；再比如从MySQL中同步数据到ES的宽表，我们就需要额外关注它的  &lt;code&gt;读优化&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;废话不多说，我们直接show一下优化方法。如果你对ES的一些概念还不是很清楚，建议收藏本文慢慢看。&lt;/p&gt; &lt;h2&gt;1.写入优化&lt;/h2&gt; &lt;p&gt;日志属于写多读少的业务场景，对写入速度要求很高。拿我们其中一个集群来说，单集群日志量达到百TB，每秒钟日志写入量达到10W条。&lt;/p&gt; &lt;p&gt;数据写入，主要有三个动作：flush、refresh和merge。通过调整它们的行为，即可在性能和数据可靠性之间进行权衡。&lt;/p&gt; &lt;h3&gt;1.1 translog异步化&lt;/h3&gt; &lt;p&gt;首先，ES需要写一份translog，它类似于MySQL中的redolog，为的是避免在断电的时候数据丢失。ES默认每次请求都进行一次flush，但对于日志来说，这没有必要，可以将这个过程改为异步的，刷盘间隔为60秒。参数如下：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;curl-H&amp;quot;Content-Type: application/json&amp;quot;-XPUT&amp;apos;http://localhost:9200/_all/_settings?preserve_existing=true&amp;apos;-d&amp;apos;{   &lt;br /&gt;  &amp;quot;index.translog.durability&amp;quot; : &amp;quot;async&amp;quot;,   &lt;br /&gt;  &amp;quot;index.translog.flush_threshold_size&amp;quot; : &amp;quot;512mb&amp;quot;,   &lt;br /&gt;  &amp;quot;index.translog.sync_interval&amp;quot; : &amp;quot;60s&amp;quot;   &lt;br /&gt;}&amp;apos;   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这可以说是最重要的一步优化了，对性能的影响最大，但在极端情况下会有丢失部分数据的可能。对于日志系统来说，是可以忍受的。&lt;/p&gt; &lt;h3&gt;1.2 增加refresh间隔&lt;/h3&gt; &lt;p&gt;除了写translog，ES还会将数据写入到一个缓冲区中。但是注意了！此时，缓冲区的内容是无法被搜索到的，它还需要写入到segment里面才可以，也就是刷新到lucence索引里面。这就是refresh动作，默认1秒。也就是你写入的数据，大概率1秒之后才会被搜索到。&lt;/p&gt; &lt;p&gt;这也是为什么ES不是实时搜索系统的原因，它从数据写入到数据读出，一般是有一个合并过程的，有一定的时间差。&lt;/p&gt; &lt;p&gt;通过index.refresh_interval可以修改这个刷新间隔。&lt;/p&gt; &lt;p&gt;对于日志系统来说，当然要把它调大一点啦。xjjdog这里调整到了120s，减少了这些落到segment的频率，I/O的压力自然会小，写入速度自然会快。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;curl-H&amp;quot;Content-Type: application/json&amp;quot;-XPUT&amp;apos;http://localhost:9200/_all/_settings?preserve_existing=true&amp;apos;-d&amp;apos;{   &lt;br /&gt;  &amp;quot;index.refresh_interval&amp;quot; : &amp;quot;120s&amp;quot;   &lt;br /&gt;}&amp;apos;   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;1.3 merge&lt;/h3&gt; &lt;p&gt;merge其实是lucene的机制，它主要是合并小的segment块，生成更大的segment，来提高检索的速度。&lt;/p&gt; &lt;p&gt;原因就是refresh过程会生成一大堆小segment文件，数据删除也会产生空间碎片。所以merge，通俗来讲就像是碎片整理进程。像postgresql等，也有vaccum进程在干同样的事。&lt;/p&gt; &lt;p&gt;显而易见，这种整理操作，既浪费I/O，又浪费CPU。&lt;/p&gt; &lt;p&gt;如果你的系统merge非常频繁，那么调整merge的块大小和频率，是一个比较好的方法。&lt;/p&gt; &lt;h2&gt;2.读取优化&lt;/h2&gt; &lt;h3&gt;2.1 指定路由&lt;/h3&gt; &lt;p&gt;如果你向ES里写数据，那么它会为你设置一个离散的隐藏ID，落到哪个分片，是不一定的。如果你根据一个查询条件查询数据，你设置了6个shards的话，它要查询6次才行。如果能够在路由的时候就知道数据在哪个分片上，查询速度自然会上升，这就要求我们在构造数据的时候，人工指定路由规则。它的实际运行规则如下：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;shard = hash(routing) % number_of_primary_shards   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;比如，一个查询会变成这样。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;GET my-index-000001/_search   &lt;br /&gt;{   &lt;br /&gt;  &amp;quot;query&amp;quot;: {   &lt;br /&gt;    &amp;quot;terms&amp;quot;: {   &lt;br /&gt;      &amp;quot;_routing&amp;quot;: [ &amp;quot;user1&amp;quot; ]    &lt;br /&gt;    }   &lt;br /&gt;  }   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;当然，如果你的查询维度较多，又对数据的查询速度有非常高的有求，根据routing存放多份数据是一个比较好的选择。&lt;/p&gt; &lt;h3&gt;2.2 rollover冷热分离&lt;/h3&gt; &lt;p&gt;rollover根据索引大小，文档数或使用期限自动过渡到新索引。当rollover触发后，将创建新索引，写别名将更新为指向新索引，所有后续更新都将写入新索引，比如  &lt;code&gt;indexname-000001.&lt;/code&gt;这种模式。&lt;/p&gt; &lt;p&gt;从rollover这个名字可以看出来，它和Java的log日志有一定的相似之处，比如Log4j的RollingFileAppender。&lt;/p&gt; &lt;p&gt;当索引变的非常大，通常是几十GB，那它的查询效率将变的非常的低，索引重建的成本也较大。实际上，很多索引的数据在时间维度上有较为明显的规律，一些冷数据将很少被用到。这个时候，建立滚动索引将是一个比较好的办法。&lt;/p&gt; &lt;p&gt;滚动索引一般可以与索引模板结合使用，实现按一定条件自动创建索引，ES的官方文档有具体的_rollover建立方法。&lt;/p&gt; &lt;h3&gt;2.3 使用BoolQuery替代TermQuery&lt;/h3&gt; &lt;p&gt;Bool查询现在包括四种子句，must、filter、should和must_not。Bool查询是true、false对比，而TermQuery是精确的字符串比对，所以如果需求相似，BoolQuery自然会快于TermQuery。&lt;/p&gt; &lt;h3&gt;2.4 将大查询拆成分段查询&lt;/h3&gt; &lt;p&gt;有些业务的查询比较复杂，我们不得不拼接一张非常大的宽表放在ES中，这有两个比较明显的问题。&lt;/p&gt; &lt;ol&gt;  &lt;li&gt;宽表的数据往往需要从其他数据源中回查拼接而成，数据更新时对源库或者ES本身都有较大的压力&lt;/li&gt;  &lt;li&gt;业务的查询JSON需要书写的非常复杂，查询效率未知，一次查询锁定的内存过高，无法进行深入优化&lt;/li&gt;&lt;/ol&gt; &lt;p&gt;其实，宽表不论在RDBMS中还是ES中，都会与复杂的查询语句有关，其锁定时间都较长，业务也不够灵活。&lt;/p&gt; &lt;p&gt;应对这种场景的策略，通常将复杂的数据查询，转移到业务代码的拼接上来。比如，将一段非常冗长的单条查询，拆分成循环遍历的100条小查询。所有的数据库都对较小的查询请求有较好的响应，其整体性能整体上将优于复杂的单条查询。&lt;/p&gt; &lt;p&gt;这对我们的ES索引建模能力和编码能力提出了挑战。毕竟，在ES层面，互不相关的几个索引，将作为整体为其他服务提供所谓的数据中台接口。&lt;/p&gt; &lt;h3&gt;2.5 增加第一次索引的速度&lt;/h3&gt; &lt;p&gt;很多业务的索引数据往往来自于MySQL等传统数据库，第一次索引往往是全量索引，后面才是增量索引。必要的时候，也会进行索引的重建，大量的数据灌入造成了ES的索引速度建立缓慢。&lt;/p&gt; &lt;p&gt;为了缓解这种情况，建议在创建索引的时候，把副本数量设置成1，即没有从副本。等所有数据索引完毕，再将副本数量增加到正常水平。&lt;/p&gt; &lt;p&gt;这样，数据能够快速索引，副本会在后台慢慢复制。&lt;/p&gt; &lt;h2&gt;3.通用优化&lt;/h2&gt; &lt;p&gt;当然，我们还可以针对ES做一些通用的优化。比如，使用监控接口或者trace工具，发现线程池有明显的瓶颈，则需要调整线程池的大小。&lt;/p&gt; &lt;p&gt;具体的优化项如下。&lt;/p&gt; &lt;h3&gt;3.1 线程池优化&lt;/h3&gt; &lt;p&gt;新版本对线程池的配置进行了优化，不需要配置复杂的search、bulk、index线程池。有需要配置下面几个就行了：thread_pool.get.size, thread_pool.write.size, thread_pool.listener.size, thread_pool.analyze.size。具体可观测_cat/thread_pool接口暴露的数据进行调整。&lt;/p&gt; &lt;h3&gt;3.2 物理冷热分离&lt;/h3&gt; &lt;p&gt;上面的rollover接口，我们可以实现索引滚动。但是如何将冷数据存放在比较慢但是便宜的节点上？如何将某些索引移动过去？&lt;/p&gt; &lt;p&gt;ES支持给节点打标签，具体方式是在elasticsearch.yml文件中增加一些属性。比如：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;//热节点   &lt;br /&gt;node.attr.temperature: hot    &lt;br /&gt;//冷节点   &lt;br /&gt;node.attr.temperature: cold    &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;节点有了冷热属性后，接下来就是指定数据的冷热属性，来设置和调整数据分布。ES提供了index shard filtering功能来实现索引的迁移。&lt;/p&gt; &lt;p&gt;首先，可以对索引也设置冷热属性。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;PUT hot_data_index/_settings   &lt;br /&gt;{   &lt;br /&gt;    &amp;quot;index.routing.allocation.require.temperature&amp;quot;: &amp;quot;hot&amp;quot;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这些索引将自动转移到冷设备上。我们可以写一些定时任务，通过_cat接口的数据，自动的完成这个转移过程。&lt;/p&gt; &lt;h3&gt;3.2 多磁盘分散I/O&lt;/h3&gt; &lt;p&gt;其实，可以通过配置多块磁盘的方式，来分散I/O的压力，但容易会造成数据热点集中在单块磁盘上。&lt;/p&gt; &lt;p&gt;ES支持在一台机器上配置多块磁盘，所以在存储规模上有更大的伸缩性。在配置文件中，配置path.data属性，即可挂载多块磁盘。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;path.data : /data1, /data2, /data3   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;值得注意的是，如果你是在扩容，那么就需要配合reroute接口进行索引的重新分配。&lt;/p&gt; &lt;h3&gt;3.3 减少单条记录的大小&lt;/h3&gt; &lt;p&gt;Lucene的索引建立过程，非常耗费CPU，可以减少倒排索引的数量来减少CPU的损耗。第一个优化就是减少字段的数量；第二个优化就是减少索引字段的数量。具体的操作，是将不需要搜索的字段，index属性设置为not_analyzed或者no。至于_source和_all，在实际调试中效果不大，不再赘述。&lt;/p&gt; &lt;h2&gt;End&lt;/h2&gt; &lt;p&gt;ES的使用越来越广泛，从ELKB到APM，从NoSQL到搜索引擎，ES在企业中的地位也越来越重要。本文通过分析ES写入和读取场景的优化，力求从原理到实践层面，助你为ES加速。希望你在使用ES时能够更加得心应手。&lt;/p&gt; &lt;p&gt;通常，一个ES集群对配置的要求是较高的，尤其是APM等场景，甚至会占到PaaS平台的1/3资源甚至更多。ES提供了较多的配置选项，我们可以根据应用场景，调整ES的表现，使其更好的为我们服务。&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;作者简介：   &lt;strong&gt;小姐姐味道&lt;/strong&gt; (xjjdog)，一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构，日百亿流量，与你探讨高并发世界，给你不一样的味道。我的个人微信xjjdog0，欢迎添加好友，进一步交流。&lt;/p&gt;&lt;/blockquote&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>dev</category>
      <guid isPermaLink="true">https://itindex.net/detail/61994-es-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96-%E5%80%BC%E5%BE%97</guid>
      <pubDate>Mon, 03 Jan 2022 00:00:00 CST</pubDate>
    </item>
    <item>
      <title>系统架构性能优化思路</title>
      <link>https://itindex.net/detail/61941-%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96</link>
      <description>&lt;br /&gt;今天谈下业务系统性能问题分析诊断和性能优化方面的内容。这篇文章重点还是谈已经上线的业务系统后续出现性能问题后的问题诊断和优化重点。 &lt;br /&gt;
 &lt;h3&gt;系统性能问题分析流程&lt;/h3&gt;我们首先来分析下如果一个业务系统上线前没有性能问题，而在上线后出现了比较严重的性能问题，那么实际上潜在的场景主要来自于以下几个方面。 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;业务出现大并发的访问，导致出现性能瓶颈&lt;/li&gt;  &lt;li&gt;上线后的系统数据库数据日积月累，数据量增加后出现性能瓶颈&lt;/li&gt;  &lt;li&gt;其它关键环境改变，比如我们常说的网络带宽影响&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt;正是由于这个原因，当我们发现性能问题的时候，首先就需要判断是单用户非并发状态下本身就有性能问题，还是说在并发状态才存在性能问题。对于单用户性能问题往往比较容易测试和验证，对于并发性能问题我们可以在测试环境进行加压测试和验证，以判断并发下的性能。 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211207/98f17fd90baa20c53360454a12c7db8c.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="1.jpeg" src="http://dockone.io/uploads/article/20211207/98f17fd90baa20c53360454a12c7db8c.jpeg" title="1.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
如果是单用户本身就存性性能问题，那么大部分问题都出在程序代码和SQL需要进一步优化上面。如果是并发性能问题，我们就需要进一步分析数据库和中间件本身的状态，看是否需要对中间件进行性能调优。 &lt;br /&gt;
 &lt;br /&gt;在加压测试过程中，我们还需要对CPU，内存和JVM进行监控，观察是否存在类似内存泄漏无法释放等情况，即并发下性能问题本身也可能是代码本身原因导致性能异常。 &lt;br /&gt;
 &lt;h3&gt;性能问题影响因素分析&lt;/h3&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211207/62bf2ce04edb86470d85071691ed7e85.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="2.jpeg" src="http://dockone.io/uploads/article/20211207/62bf2ce04edb86470d85071691ed7e85.jpeg" title="2.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
对于性能问题影响因素，简单来说包括了硬件环境，软件运行环境和软件程序三个方面的主要内容。下面分别再展开说明下。 &lt;br /&gt;
 &lt;h4&gt;硬件环境&lt;/h4&gt;硬件环境就是我们常说的计算，存储和网络资源。 &lt;br /&gt;
 &lt;br /&gt;对于服务器的计算能力，一般来说厂家都会提供TPMC参数作为一个参考数据，但是我们实际看到相同TPMC能力下的X86服务器能力仍然低于小型机的能力。 &lt;br /&gt;
 &lt;br /&gt;除了服务器的计算能力参数，另外一个重点就是我们说的存储设备，影响到存储的重点又是IO读写性能问题。有时候我们监控发现CPU和内存居高不下，而真正的瓶颈通过分析反而发现是由于IO瓶颈导致，由于读写性能跟不上，导致大量数据无法快速持久化并释放内存资源。 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211207/614f354cb8bfa7092f489b3012dfcdde.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="3.jpeg" src="http://dockone.io/uploads/article/20211207/614f354cb8bfa7092f489b3012dfcdde.jpeg" title="3.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
比如在Linux环境下，本身也提供了性能监控工具方便进行性能分析。比如常用的iostat，ps，sar，top，vmstat等，这些工具可以对CPU，内存，JVM，磁盘IO等进行性能监控和分析，以发现真正的性能问题在哪里。 &lt;br /&gt;
 &lt;br /&gt;比如我们常说的内存使用率持续告警，你就必须发现是高并发调用导致，还是JVM内存泄漏导致，还是本身由于磁盘IO瓶颈导致。 &lt;br /&gt;
 &lt;br /&gt;对于CPU，内存，磁盘IO性能监控和分析的一个思路可以参考： &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211207/2021983bf3e3ccde6ad2a0082d2015e5.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="4.jpeg" src="http://dockone.io/uploads/article/20211207/2021983bf3e3ccde6ad2a0082d2015e5.jpeg" title="4.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;h3&gt;运行环境-数据库和应用中间件&lt;/h3&gt;数据库和应用中间件性能调优是另外一个经常出现性能问题的地方。 &lt;br /&gt;
 &lt;h4&gt;数据库性能调优&lt;/h4&gt;拿Oracle数据库来说，影响数据库性能的因素包括：系统、数据库、网络。数据库的优化包括：优化数据库磁盘I/O、优化回滚段、优化Rrdo日志、优化系统全局区、优化数据库对象。 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211207/07982a23ca63f846423d7ba0515ca4d7.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="5.jpeg" src="http://dockone.io/uploads/article/20211207/07982a23ca63f846423d7ba0515ca4d7.jpeg" title="5.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
要调整首先就需要对数据库性能进行监控： &lt;br /&gt;
 &lt;br /&gt;我们可以在init.ora参数文件中设置TIMED_STATISTICS=TRUE和在你的会话层设置ALTER SESSION SET STATISTICS=TRUE 。运行svrmgrl用connect internal注册，在你的应用系统正常活动期间，运行utlbstat.sql开始统计系统活动，达到一定的时间后，执行utlestat.sql停止统计。统计结果将产生在report.txt文件中。 &lt;br /&gt;
 &lt;br /&gt;数据库性能优化应该是一个持续性的工作，一个方面是本身的性能和参数巡检，另外一个方面就是DBA也会经常提取最占用内存的低效SQL语句给开发人员进一步分析，同时也会从数据库本身的以下告警KPI指标中发现问题。 &lt;br /&gt;
 &lt;br /&gt;比如我们可能会发现Oracle数据库出现内存使用率高的告警，而通过检查会发现是产生了大量的Redo日志导致，那么我们就需要从程序上进一步分析为何会产生如此多的回滚。 &lt;br /&gt;
 &lt;h4&gt;应用中间件性能分析和调优&lt;/h4&gt;应用中间件容器即我们常说的Weblogic, Tomcat等应用中间件容器或Web容器。应用中间件调优一个方面是本身的配置参数优化设置，一个方面就是JVM内存启动参数调优。 &lt;br /&gt;
 &lt;br /&gt;对于应用中间件本身的参数设置，主要包括了JVM启动参数设置，线程池设置，连接数的最小最大值设置等。如果是集群环境，还涉及到集群相关的配置调优。 &lt;br /&gt;
 &lt;br /&gt;对于JVM启动参数调优，往往也是应用中间件调优的一个关键点，但是一般JVM参数调优会结合应用程序一起进行分析。 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211207/491e5d070528874c78b368d3d076dbe8.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="6.jpeg" src="http://dockone.io/uploads/article/20211207/491e5d070528874c78b368d3d076dbe8.jpeg" title="6.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
比如我们常见的JVM堆内存溢出，如果程序代码没有内存泄漏问题的话，我就需要考虑调整JVM启动时候堆内存设置。在32位操作系统下只能够设置到4G，但是在64位操作系统下已经可以设置到8G甚至更大的值。 &lt;br /&gt;
 &lt;br /&gt;其中JVM启动的主要控制参数说明如下： &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;-Xmx设置最大堆空间&lt;/li&gt;  &lt;li&gt;-Xms设置最小堆空间&lt;/li&gt;  &lt;li&gt;-XX:MaxNewSize设置最大新生代空间&lt;/li&gt;  &lt;li&gt;-XX:NewSize设置最小新生代空间&lt;/li&gt;  &lt;li&gt;-XX:MaxPermSize设置最大永久代空间（注：新内存模型已经替换为Metaspace）&lt;/li&gt;  &lt;li&gt;-XX:PermSize设置最小永久代空间（注：新内存模型已经替换为Metaspace）&lt;/li&gt;  &lt;li&gt;-Xss设置每个线程的堆栈大小&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt;那么这些值究竟设置多大合适，具体来讲： &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211207/641e4322c397e0a7f82119e5ec9174cb.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="7.jpeg" src="http://dockone.io/uploads/article/20211207/641e4322c397e0a7f82119e5ec9174cb.jpeg" title="7.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
Java整个堆大小设置，Xmx和Xms设置为老年代存活对象的3-4倍，即FullGC之后的老年代内存占用的3-4倍。永久代PermSize和MaxPermSize设置为老年代存活对象的1.2-1.5倍。 &lt;br /&gt;
 &lt;br /&gt;年轻代Xmn的设置为老年代存活对象的1-1.5倍。 &lt;br /&gt;
 &lt;br /&gt;老年代的内存大小设置为老年代存活对象的2-3倍。 &lt;br /&gt;
 &lt;br /&gt; &lt;blockquote&gt;  &lt;br /&gt;注意在新的JVM内存模型下已经没有PermSize而是变化为Metaspace，因此需要考虑Heap内存和Metaspace大小的配比，同时还需要考虑相关的垃圾回收机制是采用哪种类型等。&lt;/blockquote&gt;对于JVM内存溢出问题，我前面写过一篇专门的分析文章可以参考： &lt;a href="https://www.toutiao.com/i6844670025068970500" rel="nofollow" target="_blank"&gt;https://www.toutiao.com/i6844670025068970500&lt;/a&gt; &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211207/87e4b05c1204e200b5e5fcb9e712e7bf.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="8.jpeg" src="http://dockone.io/uploads/article/20211207/87e4b05c1204e200b5e5fcb9e712e7bf.jpeg" title="8.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;h4&gt;软件程序性能问题分析&lt;/h4&gt;在这里首先要强调的一点就是，当我们发现性能问题后首先想到的就是扩展资源，但是大部分的性能问题本身并不是资源能力不够导致，而是我们程序实现上出现明显缺陷。 &lt;br /&gt;
 &lt;br /&gt;比如我们经常看到的大量循环创建连接，资源使用了不释放，SQL语句低效执行等。 &lt;br /&gt;
 &lt;br /&gt;为了解决这些性能问题，最好的方法仍然是在事前控制。其中包括了事前的代码静态检查工具的使用，也包括了开发团队对代码进行的Code Review来发现性能问题。 &lt;br /&gt;
 &lt;br /&gt;所有已知的问题都必须形成开发团队的开发规范要求，避免重复再犯。 &lt;br /&gt;
 &lt;h3&gt;业务系统性能问题扩展思考&lt;/h3&gt;对于业务系统的性能优化，除了上面谈到的标准分析流程和分析要素外，再谈下其它一些性能问题引发的关键思考。 &lt;br /&gt;
 &lt;h4&gt;上线前的性能测试是否有用？&lt;/h4&gt;有时候大家可能觉得奇怪，为何我们系统上线前都做了性能测试，为何上线后还是会出现系统性能问题。那么我们可以考虑下实际上我们上线前性能测试可能存在的一些无法真实模拟生产环境的地方，具体为： &lt;br /&gt;
 &lt;ol&gt;  &lt;li&gt;硬件能否完全模拟真实环境？最好的性能测试往往是直接在搭建完成的生产环境进行。&lt;/li&gt;  &lt;li&gt;数据量能否模拟实际场景？真实场景往往是多个业务表都已经存在大数据量的积累而非空表。&lt;/li&gt;  &lt;li&gt;并发能否模拟真实场景？一个是需要录制复合业务场景，一个是需要多台压测机。&lt;/li&gt;&lt;/ol&gt; &lt;br /&gt;
 &lt;br /&gt;而实际上我们在做性能测试的时候以上几个点都很难真正做到，因此要想完全模拟出生产真实环境是相当困难的，这也导致了很多性能问题是在真正上线后才发现。 &lt;br /&gt;
 &lt;h4&gt;系统本身水平弹性扩展是否完全解决性能问题？&lt;/h4&gt;第二个点也是我们经常谈的比较多的点，就是我们的业务系统在进行架构设计的时候，特别是面对非功能性需求，我们都会谈到系统本身的数据库，中间件都采用了集群技术，能够做到弹性水平扩展。那么这种弹性水平扩展能力是否又真正解决了性能问题？ &lt;br /&gt;
 &lt;br /&gt;实际上我们看到对于数据库往往很难真正做到无限的弹性水平扩展，即使对于Oracle RAC集群往往也是最多扩展到单点的2到3倍性能。对于应用集群往往可以做到弹性水平扩展，当前技术也比较成熟。 &lt;br /&gt;
 &lt;br /&gt;当中间件能够做到完全弹性扩展的时候，实际上仍然可能存在性能问题，即随着我们系统的运行和业务数据量的不断积累增值。实际上你可以看到往往非并发状态下的单用户访问本身就很慢，而不是说并发上来后满。因此也是我们常说的要给点，即： &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;单点访问性能正常的时候可以扩展集群来应对大并发状态下的同时访问&lt;/li&gt;  &lt;li&gt;单点访问本身性能就有问题的时候，要优先优化单节点访问性能&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;h4&gt;业务系统性能诊断的分类&lt;/h4&gt;对于业务系统性能诊断，如果从静态角度我们可以考虑从以下三个方面进行分类： &lt;br /&gt;
 &lt;ol&gt;  &lt;li&gt;操作系统和存储层面&lt;/li&gt;  &lt;li&gt;中间件层面（包括了数据库，应用服务器中间件）&lt;/li&gt;  &lt;li&gt;软件层面（包括了数据库SQL和存储过程，逻辑层，前端展现层等）&lt;/li&gt;&lt;/ol&gt; &lt;br /&gt;
 &lt;br /&gt;那么一个业务系统应用功能出现问题了，我们当然也可以从动态层面来看实际一个应用请求从调用开始究竟经过了哪些代码和硬件基础设施，通过分段方法来定位和查询问题。 &lt;br /&gt;
 &lt;br /&gt;比如我们常见的就是一个查询功能如果出现问题了，首先就是找到这个查询功能对应的SQL语句在后台查询是否很慢，如果这个SQL本身就慢，那么就要优化优化SQL语句。如果SQL本身快但是查询慢，那就要看下是否是前端性能问题或者集群问题等。 &lt;br /&gt;
 &lt;h4&gt;软件代码的问题往往是最不能忽视的一个性能问题点&lt;/h4&gt;对于业务系统性能问题，我们经常想到的就是要扩展数据库的硬件性能，比如扩展CPU和内存，扩展集群，但是实际上可以看到很多应用的性能问题并不是硬件性能导致的，而是由于软件代码性能引起的。对于软件代码常见的性能问题我在以往的博客文章里面也谈过到，比较典型的包括了。 &lt;br /&gt;
 &lt;ol&gt;  &lt;li&gt;循环中初始化大的结构对象，数据库连接等&lt;/li&gt;  &lt;li&gt;资源不释放导致的内存泄露等&lt;/li&gt;  &lt;li&gt;没有基于场景需求来适度通过缓存等方式提升性能&lt;/li&gt;  &lt;li&gt;长周期事务处理耗费资源&lt;/li&gt;  &lt;li&gt;处理某一个业务场景或问题的时候，没有选择最优的数据结构或算法&lt;/li&gt;&lt;/ol&gt; &lt;br /&gt;
 &lt;br /&gt;以上都是常见的一些软件代码性能问题点，而这些往往需要通过我们进行Code Review或代码评审的方式才能够发现出来。因此如果要做全面的性能优化，对于软件代码的性能问题排查是必须的。 &lt;br /&gt;
 &lt;h4&gt;通过IT资源监控或APM应用工具来发现性能问题&lt;/h4&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211207/99ea3c9f1d57d6775b51bdb1e75efc81.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="9.jpeg" src="http://dockone.io/uploads/article/20211207/99ea3c9f1d57d6775b51bdb1e75efc81.jpeg" title="9.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;em&gt;图片来源OneAPM&lt;/em&gt; &lt;br /&gt;
 &lt;br /&gt;对于性能问题的发现一般有两条路径，一个就是通过我们IT资源的监控，APM的性能监控和预警来提前发现性能问题，一个是通过业务用户在使用过程中的反馈来发现性能问题。 &lt;br /&gt;
 &lt;br /&gt;APM应用性能管理主要指对企业的关键业务应用进行监测、优化，提高企业应用的可靠性和质量，保证用户得到良好的服务，降低IT总拥有成本（TCO）。 &lt;br /&gt;
 &lt;h4&gt;资源池-》应用层-》业务层&lt;/h4&gt;这个可以理解为APM的一个关键点，原有的网管类监控软件更多的是资源和操作系统层面，包括计算和存储资源的使用和利用率情况，网络本身的性能情况等。但是当要分析所有的资源层问题如何对应到具体的应用，对应到具体的业务功能的时候很难。 &lt;br /&gt;
 &lt;br /&gt;传统模式下，当出现CPU或内存满负荷的时候，如果要查找到具体是哪个应用，哪个进程或者具体哪个业务功能，哪个sql语句导致的往往并不是容易的事情。在实际的性能问题优化中往往也需要做大量的日志分析和问题定位，最终才可能找到问题点。 &lt;br /&gt;
 &lt;br /&gt; &lt;blockquote&gt;  &lt;br /&gt;比如在我们最近的项目实施中，结合APM和服务链监控，我们可以快速的发现究竟是哪个服务调用出现了性能问题，或者快速的定位出哪个SQL语句有验证的性能问题。这个都可以帮助我们快速的进行性能问题分析和诊断。&lt;/blockquote&gt;资源上承载的是应用，应用本身又包括了数据库和应用中间件容器，同时也包括了前端；在应用之上则是对应到具体的业务功能。因此APM一个核心就是要将资源-》应用-》功能之间进行整合分析和衔接。 &lt;br /&gt;
 &lt;br /&gt;而随着DevOps和自动化运维的思路推进，我们更加希望是通过APM等工具主动监控来发现性能问题，对于APM工具最大的好处就是可以进行服务全链路的性能分析，方便我们发现性能问题究竟发生在哪里。比如我们提交一个表单很慢，通过APM分析我们很容易发现究竟是调用哪个业务服务慢，或者是处理哪个SQL语句慢。这样可以极大的提升我们性能问题分析诊断的效率。 &lt;br /&gt;
 &lt;br /&gt;原文链接： &lt;a href="http://blog.sina.com.cn/s/blog_493a84550102z8t2.html" rel="nofollow" target="_blank"&gt;http://blog.sina.com.cn/s/blog ... .html&lt;/a&gt;
                                                                 &lt;div&gt;
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                &lt;/div&gt;
                                
                                                                 &lt;ul&gt;
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            &lt;/ul&gt;
                                                            &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/61941-%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96</guid>
      <pubDate>Tue, 07 Dec 2021 22:50:52 CST</pubDate>
    </item>
    <item>
      <title>性能优化：如何更快地接收数据 (zhuanlan.zhihu.com)</title>
      <link>https://itindex.net/detail/61854-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96-%E6%8E%A5%E6%94%B6-%E6%95%B0%E6%8D%AE</link>
      <description>&lt;div&gt;  &lt;div&gt;   &lt;p&gt;从网卡到应用程序，数据包会经过一系列组件，其中驱动做了什么？内核做了什么？为了优化，我们又能做些什么？整个过程中涉及到诸多细微可调的软硬件参数，并且相互影响，不存在一劳永逸的“银弹”。本文中又拍云系统开发高级工程师杨鹏将结合自己的的实践经验，介绍在深入理解底层机制的基础上如何做出“场景化”的最优配置。&lt;/p&gt;   &lt;blockquote&gt;文章根据杨鹏在又拍云 Open Talk 技术沙龙北京站主题演讲《性能优化：更快地接收数据》整理而成，现场视频及 PPT 可下拉文末点击阅读原文查看。&lt;/blockquote&gt;   &lt;p&gt;大家好，我是又拍云开发工程师杨鹏，在又拍云工作已有四年时间，期间一直从事 CDN 底层系统开发的工作，负责调度、缓存、负载均衡等 CDN的核心组件，很高兴来跟大家分享在网络数据处理方面的经验和感受。今天分享的主题是《如何更快地接收数据》，主要介绍加速网络数据处理的方法和实践。希望能帮助大家更好的了解如何在系统的层面，尽量在应用程序无感的情况下做到极致的优化。言归正传，进入主题。&lt;/p&gt;   &lt;p&gt;首先需要清楚在尝试做任何优化的时候，想到的第一件事情应该是什么？个人觉得是衡量指标。做任何改动或优化之前，都要明确地知道，是怎样的指标反映出了当前的问题。那么在做了相应的调整或改动之后，也才能通过指标去验证实际效果与作用。&lt;/p&gt;   &lt;p&gt;针对要分享的主题，有一个围绕上面指标核心的基本原则。在网络层面做优化，归根结底只需要看一点，假如可以做到网络栈的每个层次，加入能监控到对应层次的丢包率，这样核心的指标，就可以明确地知道问题出在哪一层。有了明确可监控的指标，之后做相应的调整与实际效果的验证也就很简单了。当然上述两点相对有点虚，接下来就是比较干的部分了。&lt;/p&gt;   &lt;img src="data:image/svg+xml;utf8,&lt;svg xmlns='http://www.w3.org/2000/svg' width='1026' height='652'&gt;&lt;/svg&gt;" width="1026"&gt;&lt;/img&gt;   &lt;p&gt;如上图所示，当收到一个数据包，从进入网卡，一直到达应用层，总的数据流程有很多。在当前阶段，无需关注每个流程，留意其中几个核心的关键路径即可：&lt;/p&gt;   &lt;ul&gt;    &lt;li&gt;第一个，数据包到达网卡；&lt;/li&gt;    &lt;li&gt;第二个，网卡在收到数据包时，它需要产生一个中断，告诉 CPU 数据已经到了；&lt;/li&gt;    &lt;li&gt;第三步，内核从这个时候开始进行接管，把数据从网卡中拿出来，交到后面内核的协议栈去处理。&lt;/li&gt;&lt;/ul&gt;   &lt;p&gt;以上是三个关键的路径。上图中右边的手绘图指的就是这三个步骤，并有意区分了两个颜色。之所以这么区分是因为    &lt;strong&gt;接下来会按这两部分进行分享，一是上层驱动部分，二是下层涉及到内核的部分。&lt;/strong&gt;当然内核比较多，通篇只涉及到内核网络子系统，更具体来说是内核跟驱动交互部分的内容。&lt;/p&gt;   &lt;h2&gt;    &lt;br /&gt;网卡驱动&lt;/h2&gt;   &lt;p&gt;网卡驱动的部分，网卡是硬件，驱动（driver）是软件，包括了网卡驱动部分的大部分。这部分可简单分四个点，依次是初始化、启动、监控与调优驱动它的初始化流程&lt;/p&gt;   &lt;p&gt;    &lt;strong&gt;网卡驱动-初始化&lt;/strong&gt;&lt;/p&gt;   &lt;p&gt;驱动初始化的过程和硬件相关，无需过分关注。但需注意一点就是注册 ethool 的一系列操作，这个工具可以对网卡做各种各样的操作，不止可以读取网卡的配置，还可以更改网卡的配置参数，是一个非常强大的工具。&lt;/p&gt;   &lt;p&gt;那它是如何控制网卡的呢？每个网卡的驱动在初始化时，通过接口，去注册支持 ethool 工具的一系列操作。ethool 是一套很通用的接口，比如说它支持 100 个功能，但每个型号的网卡，只能支持一个子集。所以具体支持哪些功能，会在这一步进行声明。&lt;/p&gt;   &lt;img src="data:image/svg+xml;utf8,&lt;svg xmlns='http://www.w3.org/2000/svg' width='1057' height='589'&gt;&lt;/svg&gt;" width="1057"&gt;&lt;/img&gt;   &lt;p&gt;上图截取的部分，是在初始化时结构体的赋值。前面两个可以简单看一下，驱动在初始化的时候会告诉内核，如果想要操作这块网卡对应的回调函数，其中最主要的是启动和关闭，有用 ifconfig 工具操作网卡的应该都很熟悉，当用 ifconfigup/down一张网卡的时候，调用的都是它初始化时指定的这几个函数。&lt;/p&gt;   &lt;p&gt;    &lt;strong&gt;网卡驱动-启动&lt;/strong&gt;&lt;/p&gt;   &lt;p&gt;驱动初始化过程之后就是启动（open）中的流程了，一共分为四步：分配 rx/tx 队列内存、&lt;/p&gt;   &lt;p&gt;开启 NAPI、注册中断处理函数、开启中断。其中注册中断处理函数和开启中断是理所当然的，任何一个硬件接入到机器上都需要做这个操作。当后面收到一些事件时，它需要通过中断去通知系统，然后开启中断。&lt;/p&gt;   &lt;p&gt;第二步的 NAPI 后面会详细说明，这里先重点关注启动过程中对内存的分配。网卡在收到数据时，都必须把数据从链路层拷贝到机器的内存里，而这块内存就是网卡在启动时，通过接口向内核、向操作系统申请而来的。内存一旦申请下来，地址确定之后，后续网卡在收到数据的时候，就可以直接通过 DMA 的机制，直接把数据包传送到内存固定的地址中去，甚至不需要 CPU 的参与。&lt;/p&gt;   &lt;img src="data:image/svg+xml;utf8,&lt;svg xmlns='http://www.w3.org/2000/svg' width='601' height='423'&gt;&lt;/svg&gt;" width="601"&gt;&lt;/img&gt;   &lt;p&gt;到队列内存的分配可以看下上图，很早之前的网卡都是单队列的机制，但现代的网卡大多都是多队列的。好处就是机器网卡的数据接收可以被负载均衡到多个 CPU 上，因此会提供多个队列，这里先有个概念后面会详细说明。&lt;/p&gt;   &lt;img src="data:image/svg+xml;utf8,&lt;svg xmlns='http://www.w3.org/2000/svg' width='752' height='536'&gt;&lt;/svg&gt;" width="752"&gt;&lt;/img&gt;   &lt;p&gt;下面来详细介绍启动过程中的第二步 NAPI，这是现代网络数据包处理框架中非常重要的一个扩展。之所以现在能支持10G、20G、25G等非常高速的网卡，NAPI 机制起到了非常大的作用。当然 NAPI 并不复杂，其核心就两点：中断、轮循。一般来说，网卡在接收数据时肯定是收一个包，产生一个中断，然后在中断处理函数的时候将包处理掉。处在收包、处理中断，下一个收包，再处理中断，这样的循环中。而 NAPI 机制优势在于只需要一次中断，收到之后就可以通过轮循的方式，把队列内存中所有的数据都拿走，达到非常高效的状态。&lt;/p&gt;   &lt;p&gt;    &lt;strong&gt;网卡驱动-监控&lt;/strong&gt;&lt;/p&gt;   &lt;p&gt;接下来就是在驱动这层可以做的监控了，需要去关注其中一些数据的来源。&lt;/p&gt;   &lt;div&gt;    &lt;pre&gt;     &lt;code&gt;$ sudo ethtool -S eth0
NIC statistics:
     rx_packets: 597028087
     tx_packets: 5924278060
     rx_bytes: 112643393747
     tx_bytes: 990080156714
     rx_broadcast: 96
     tx_broadcast: 116
     rx_multicast:20294528
     ....&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;   &lt;p&gt;首先非常重要的是ethool 工具，它可以拿到网卡中统计的数据、接收的包数量、处理的流量等等常规的信息，而我们更多的是需要关注到异常信息。&lt;/p&gt;   &lt;div&gt;    &lt;pre&gt;     &lt;code&gt;$ cat /sys/class/net/eth0/statistics/rx_dropped
2&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;   &lt;p&gt;通过 sysfs 的接口，可以看到网卡的丢包数，这就是系统出现异常的一个标志。&lt;/p&gt;   &lt;img src="data:image/svg+xml;utf8,&lt;svg xmlns='http://www.w3.org/2000/svg' width='1097' height='163'&gt;&lt;/svg&gt;" width="1097"&gt;&lt;/img&gt;   &lt;p&gt;三个途径拿到的信息与前面差不多，只是格式有些乱，仅做了解即可。&lt;/p&gt;   &lt;img src="data:image/svg+xml;utf8,&lt;svg xmlns='http://www.w3.org/2000/svg' width='1213' height='624'&gt;&lt;/svg&gt;" width="1213"&gt;&lt;/img&gt;   &lt;p&gt;上图是要分享的一个线上案例。当时业务上出现异常，经过排查最后是怀疑到网卡这层，为此需要做进一步的分析。通过 ifconfig 工具可以很直观的查看到网卡的一些统计数据，图中可以看到网卡的 errors 数据指标非常高，明显出现了问题。但更有意思的一点是， errors 右边最后的 frame 指标数值跟它完全相同。因为 errors 指标是网卡中很多错误累加之后的指标，与它相邻的 dropped、overruns 这俩个指标都是零，也就是说在当时的状态下，网卡的错误大部分来自 frame。&lt;/p&gt;   &lt;p&gt;当然这只是瞬时的状态，上图中下面部分是监控数据，可以明显看到波动的变化，确实是某一台机器异常了。frame 错误一般是在网卡收到数据包，进行 RCR 校验时失败导致的。当收到数据包，会对该包中的内容做校验，当发现跟已经存下来的校验不匹配，说明包是损坏的，因此会直接将其丢掉。&lt;/p&gt;   &lt;p&gt;这个原因是比较好分析的，两点一线，机器的网卡通过网线接到上联交换机。当这里出现问题，不是网线就是机器本身的网卡问题，或者是对端交换机的端口，也就是上联交换机端口出现问题。当然按第一优先级去分析，协调运维去更换了机器对应的网线，后面的指标情况也反映出了效果，指标直接突降直到完全消失，错误也就不复存在了，对应上层的业务也很快恢复了正常。&lt;/p&gt;   &lt;p&gt;    &lt;strong&gt;网卡驱动-调优&lt;/strong&gt;&lt;/p&gt;   &lt;p&gt;说完监控之后来看下最后的调优。在这个层面能调整的东西不多，主要是针对网卡多队列的调整，比较直观。调整队列数目、大小，各队列间的权重，甚至是调整哈希的字段，都是可以的。&lt;/p&gt;   &lt;div&gt;    &lt;pre&gt;     &lt;code&gt;$ sudo ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX:   0
TX:   0
Other:    0
Combined: 8
Current hardware settings:
RX:   0
TX:   0
Other:    0
Combined: 4&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;   &lt;p&gt;上图是针对多队列的调整。为了说明刚才的概念，举个例子，比如有个 web server 绑定到了 CPU2，而机器有多个 CPU，这个机器的网卡也是多队列的，其中某个队列会被 CPU2 处理。这个时候就会有一个问题，因为网卡有多个队列，所以 80 端口的流量只会被分配到其中一个队列上去。假如这个队列不是由 CPU2 处理的，就会涉及到一些数据的腾挪。底层把数据接收上来后再交给应用层的时候，需要把这个数据移动一下。如果本来在 CPU1 处理的，需要挪到 CPU2 去，这时会涉及到 CPU cache 的失效，这对高速运转的 CPU 来说是代价很高的操作。&lt;/p&gt;   &lt;p&gt;那么该怎么做呢？我们可以通过前面提到的工具，特意把 80 端口 tcp 数据流量导向到对应 CPU2 处理的网卡队列。这么做的效果是数据包从到达网卡开始，到内核处理完再到送达应用层，都是同一个 CPU。这样最大的好处就是缓存，CPU 的 cache 始终是热的，如此整体下来，它的延迟、效果也会非常好。当然这个例子并不实际，主要是为了说明能做到的一个效果。&lt;/p&gt;   &lt;h2&gt;    &lt;strong&gt;内核网络子系统&lt;/strong&gt;&lt;/h2&gt;   &lt;p&gt;说完了整个网卡驱动部分，接下来是讲解内核子系统部分，这块会分为软中断与网络子系统初始化两部分来分享。&lt;/p&gt;   &lt;p&gt;    &lt;strong&gt;软中断&lt;/strong&gt;&lt;/p&gt;   &lt;img src="data:image/svg+xml;utf8,&lt;svg xmlns='http://www.w3.org/2000/svg' width='912' height='362'&gt;&lt;/svg&gt;" width="912"&gt;&lt;/img&gt;   &lt;p&gt;上图的 NETDEV 是 linux 网络子系统每年都会开的一个分会，其中比较有意思的点是每年大会举办的届数会以一个特殊字符来表示。图中是办到了 0X15 届，想必也都发现这是 16 进制的数字，0X15 刚好就是 21 年，也是比较极客范。对网络子系统感兴趣的可以去关注一下。&lt;/p&gt;   &lt;img src="data:image/svg+xml;utf8,&lt;svg xmlns='http://www.w3.org/2000/svg' width='823' height='638'&gt;&lt;/svg&gt;" width="823"&gt;&lt;/img&gt;   &lt;p&gt;言归正传，内核延时任务有多种机制，而软中断只是其中一种。上图是 linux 的基本结构，上层是用户态，中间是内核，下层是硬件，很抽象的一个分层。用户态和内核态之间会有两种交互的方式：通过系统调用，或者通过异常可以陷入到内核态里面。那底层的硬件跟内核又是怎么交互的呢？答案是中断，硬件跟内核交互的时候必须通过中断，处理任何事件都需要产生一个中断信号来告知 CPU 与内核。&lt;/p&gt;   &lt;p&gt;不过这样的机制一般情况下也许没有问题，但是对网络数据来说，一个数据报一个中断，这样会有很明显的两个问题。&lt;/p&gt;   &lt;p&gt;    &lt;strong&gt;问题一：中断在处理期间，会屏蔽之前的中断信号。当一个中断处理的时间很长，在处理期间收到的中断信号都会丢掉。&lt;/strong&gt;如果处理一个包用了十秒，在这十秒期间又收到了五个数据包，但因为中断信号丢了，即便前面的处理完了，后面的数据包也不会再处理了。对应到 tcp 这边，假如客户端给服务端发了一个数据包，几秒后处理完了，但在处理期间客户端又发了后续的三个包，但是服务端后面并不知道，以为只收到了一个包，这时客户端又在等待服务端的回包，如此会导致两边都卡住了，也说明了信号丢失是一个极其严重的问题。&lt;/p&gt;   &lt;p&gt;    &lt;strong&gt;问题二：一个数据包触发一次中断处理的话，当有大量的数据包到来后，就会产生非常大量的中断。&lt;/strong&gt;如果达到了 10 万、50 万、甚至百万的 pps，那 CPU 就需要处理大量的网络中断，也就不用干其他事情了。&lt;/p&gt;   &lt;p&gt;    &lt;strong&gt;而针对以上两点问题的解决方法就是让中断处理尽可能的短。&lt;/strong&gt;具体来说，不能在中断处理函数，只能把它揪出来，交到软中断机制里。这样之后的实际结果是硬件的中断处理做的事情就很少了，将接收数据等一些必须的事情交到软中断去完成，这也是软中断存在的意义。&lt;/p&gt;   &lt;div&gt;    &lt;pre&gt;     &lt;code&gt;static struct smp_hotplug_thread softirq_threads = {
  .store              = &amp;amp;ksoftirqd,
  .thread_should_run  = ksoftirqd_should_run,
  .thread_fn          = run_ksoftirqd,
  .thread-comm        = “ksoftirqd/%u”,
};

static _init int spawn_ksoftirqd(void)
{
  regiter_cpu_notifier(&amp;amp;cpu_nfb);
  
  BUG_ON(smpboot_register_percpu_thread(&amp;amp;softirq_threads));

  return 0;
}
early_initcall(spawn_ksoftirqd);&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;   &lt;p&gt;软中断机制是通过内核的线程来实现的。图中是对应的一个内核线程。服务器 CPU 都会有一个 ksoftirqd 这样的内核线程，多 CPU 的机器会相对应的有多个线程。图中结构体最后一个成员 ksoftirqd/，如果有三个 CPU 对应就会有 /0/1/2 三个内核线程。&lt;/p&gt;   &lt;img src="data:image/svg+xml;utf8,&lt;svg xmlns='http://www.w3.org/2000/svg' width='1035' height='490'&gt;&lt;/svg&gt;" width="1035"&gt;&lt;/img&gt;   &lt;p&gt;软中断机制的信息在 softirqs 下面可以看到。软中断并不多只有几种，其中需要关注的，跟网络相关的就是 NET-TX 和 NET-RX，网络数据收发的两种场景。&lt;/p&gt;   &lt;p&gt;    &lt;strong&gt;内核初始化&lt;/strong&gt;&lt;/p&gt;   &lt;p&gt;铺垫完软中断之后，下面来看内核初始化的流程。主要为两步：&lt;/p&gt;   &lt;ul&gt;    &lt;li&gt;针对每个 CPU，创建一个数据结构，这上面挂了非常多的成员，与后面的处理密切相关；&lt;/li&gt;    &lt;li&gt;注册一个软中断处理函数，对应上面看到的 NET-TX 和 NET-RX 这两个软中断的处理函数。&lt;/li&gt;&lt;/ul&gt;   &lt;img src="data:image/svg+xml;utf8,&lt;svg xmlns='http://www.w3.org/2000/svg' width='885' height='626'&gt;&lt;/svg&gt;" width="885"&gt;&lt;/img&gt;   &lt;p&gt;上图是手绘的一个数据包的处理流程：&lt;/p&gt;   &lt;ul&gt;    &lt;li&gt;第一步网卡收到了数据包；&lt;/li&gt;    &lt;li&gt;第二步把数据包通过 DMA 拷到了内存里面；&lt;/li&gt;    &lt;li&gt;第三步产生了一个中断告诉 CPU 并开始处理中断。重点的中断处理可分为两步：一是将中断信号屏蔽了，二是唤醒 NAPI 机制。&lt;/li&gt;&lt;/ul&gt;   &lt;div&gt;    &lt;pre&gt;     &lt;code&gt;static irqreturn_t igb_msix_ring(int irq, void *data)
{
  struct igb_q_vector *q_vector = data;
  
  /* Write the ITR value calculated from the previous interrupt. */
  igb_write_itr(q_vector);
  
  napi_schedule(&amp;amp;q_vector-&amp;gt;napi);
  
  return IRO_HANDLED;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;   &lt;p&gt;上面的代码是 igb 网卡驱动中断处理函数做的事情。如果省略掉开始的变量声明和后面的返回，这个中断处理函数只有两行代码，非常短。需要关注的是第二个，在硬件中断处理函数中，只用激活外部 NIPA 软中断处理机制，无需做其他任何事情。因此这个中断处理函数会返回的非常快。&lt;/p&gt;   &lt;p&gt;    &lt;strong&gt;NIPI 激活&lt;/strong&gt;&lt;/p&gt;   &lt;div&gt;    &lt;pre&gt;     &lt;code&gt;/* Called with irq disabled */
static inline void ____napi_schedule(struct softnet_data *sd, struct napi_struct *napi)
{
  list_add_tail(&amp;amp;napi-&amp;gt;poll_list, &amp;amp;sd-&amp;gt;poll_list);
  _raise_softirq_irqoff(NET_RX_SOFTIRQ);
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;   &lt;p&gt;NIPI 的激活也很简单，主要为两步。内核网络系统在初始化的时每个 CPU 都会有一个结构体，它会把队列对应的信息插入到结构体的链表里。换句话说，每个网卡队列在收到数据的时候，需要把自己的队列信息告诉对应的 CPU，将这两个信息绑定起来，保证某个 CPU 处理某个队列。&lt;/p&gt;   &lt;p&gt;除此之外，还要与触发硬中断一样，需要触发软中断。下图将很多步骤放到了一块，前面讲过的就不再赘述了。图中要关注的是软中断是怎么触发的。与硬中断差不多，软中断也有中断的向量表。每个中断号，都会对应一个处理函数，当需要处理某个中断，只需要在对应的中断向量表里找就好了，跟硬中断的处理是一模一样的。&lt;/p&gt;   &lt;img src="data:image/svg+xml;utf8,&lt;svg xmlns='http://www.w3.org/2000/svg' width='1200' height='806'&gt;&lt;/svg&gt;" width="1200"&gt;&lt;/img&gt;   &lt;p&gt;    &lt;strong&gt;数据接收-监控&lt;/strong&gt;&lt;/p&gt;   &lt;p&gt;说完了运作机制，再来看看有哪些地方可以做监控。在 proc 下面有很多东西，可以看到中断的处理情况。第一列就是中断号，每个设备都有独立的中断号，这是写死的。对网络来说只需要关注网卡对应的中断号，图中是 65、66、67、68 等。当然看实际的数字并没有意义，而是需要看它的分布情况，中断是不是被不同 CPU 在处理，如果所有的中断都是被一个 CPU 处理，那么就需要做些调整，把它分散开。&lt;/p&gt;   &lt;img src="data:image/svg+xml;utf8,&lt;svg xmlns='http://www.w3.org/2000/svg' width='1092' height='398'&gt;&lt;/svg&gt;" width="1092"&gt;&lt;/img&gt;   &lt;p&gt;    &lt;strong&gt;数据接收-调优&lt;/strong&gt;&lt;/p&gt;   &lt;p&gt;中断可以做的调整有两个：一是中断合并，二是中断亲和性。&lt;/p&gt;   &lt;p&gt;    &lt;strong&gt;自适应中断合并&lt;/strong&gt;&lt;/p&gt;   &lt;ul&gt;    &lt;li&gt;     &lt;strong&gt;rx-usecs：&lt;/strong&gt;数据帧到达后，延迟多长时间产生中断信号，单位微秒&lt;/li&gt;    &lt;li&gt;     &lt;strong&gt;rx-frames：&lt;/strong&gt;触发中断前积累数据帧的最大个数&lt;/li&gt;    &lt;li&gt;     &lt;strong&gt;rx-usecs-irq：&lt;/strong&gt;如果有中断处理正在执行，当前中断延迟多久送达     &lt;strong&gt;CPU&lt;/strong&gt;&lt;/li&gt;    &lt;li&gt;     &lt;strong&gt;rx-frames-irq：&lt;/strong&gt;如果有中断处理正在执行，最多积累多少个数据帧&lt;/li&gt;&lt;/ul&gt;   &lt;p&gt;上面列的都是硬件网卡支持的功能。NAPI 本质上也是中断合并的机制，假如有很多包的到来，NAPI 就可以做到只产生一个中断，因此不需要硬件来帮助做中断合并，实际效果是跟 NAPI 是相同的，都是减少了总的中断数量。&lt;/p&gt;   &lt;p&gt;    &lt;strong&gt;中断亲和性&lt;/strong&gt;&lt;/p&gt;   &lt;div&gt;    &lt;pre&gt;     &lt;code&gt;$ sudo bash -c ‘echo 1 &amp;gt; /proc/irq/8/smp_affinity’&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;   &lt;p&gt;这个与网卡多队列是密切相关的。如果网卡有多个队列，就能手动来明确指定由哪个 CPU 来处理，均衡的把数据处理的负载分散到机器的可用 CPU 上。配置也比较简单，只需把数字写入到 /proc 对应的这个文件中就可以了。这是个位数组，转成二进制后就会有对应的 CPU 去处理。如果写个 1，可能就是 CPU0 来处理；如果写个 4，转化成二进制是 100，那么就会交给 CPU2 去处理。&lt;/p&gt;   &lt;p&gt;另外有个小问题需要注意，很多发行版可能会自带一个 irqbalance 的守护进程（    &lt;a href="https://link.zhihu.com/?target=http%3A//irqbalance.github.io/irqbalance" rel="nofollow noreferrer" target="_blank"&gt;http://irqbalance.github.io/irqbalance&lt;/a&gt;），会将手动中断均衡的设置给覆盖掉。这个程序做的核心事情就是把上面手动设置文件的操作放到程序里，有兴趣可以去看下它的代码（    &lt;a href="https://link.zhihu.com/?target=https%3A//github.com/Irqbalance/irqbalance/blob/master/activate.c" rel="nofollow noreferrer" target="_blank"&gt;https://github.com/Irqbalance/irqbalance/blob/master/activate.c&lt;/a&gt;），也是把这个文件打开，写对应的数字进去就可以了。&lt;/p&gt;   &lt;p&gt;    &lt;strong&gt;内核-数据处理&lt;/strong&gt;&lt;/p&gt;   &lt;p&gt;最后是数据处理部分了。当数据到达网卡，进入队列内存后，就需要内核从队列内存中将数据拉出来。如果机器的 PPS 达到了十万甚至百万，而 CPU 只处理网络数据的话，那其他基本的业务逻辑也就不用干了，因此不能让数据包的处理独占整个 CPU，而核心点是怎么去做限制。&lt;/p&gt;   &lt;p&gt;针对上述问题主要有两方面的限制：整体的限制和单次的限制&lt;/p&gt;   &lt;div&gt;    &lt;pre&gt;     &lt;code&gt;while (!list_empty(&amp;amp;sd-&amp;gt;poll_list)){
  struct napi_struct *n;
  int work,weight;
  
  /* If softirq window is exhausted then punt.
   * Allow this to run for 2 jiffies since which will allow
   * an average latency of 1.5/HZ.
   */
   if (unlikely(budget &amp;lt;= 0 || time_after_eq(jiffies, time_limit)))
   goto softnet_break;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;   &lt;p&gt;整体限制很好理解，就是一个 CPU 对应一个队列。如果 CPU 的数量比队列数量少，那么一个 CPU 可能需要处理多个队列。&lt;/p&gt;   &lt;div&gt;    &lt;pre&gt;     &lt;code&gt;weight = n-&amp;gt;weight;

work = 0;
if (test_bit(NAPI_STATE_SCHED, &amp;amp;n-&amp;gt;state)) {
        work = n-&amp;gt;poll(n,weight);
        trace_napi_poll(n);
}

WARN_ON_ONCE(work &amp;gt; weight);

budget -= work;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;   &lt;p&gt;单次限制则是限制一个队列在一轮里处理包的数量。达到限制之后就停下来，等待下一轮的处理。&lt;/p&gt;   &lt;div&gt;    &lt;pre&gt;     &lt;code&gt;softnet_break:
  sd-&amp;gt;time_squeeze++;
  _raise_softirq_irqoff(NET_RX_SOFTIRQ);
  goto out;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;   &lt;p&gt;而停下来就是很关键的节点，幸运的是有对应的指标记录，有 time-squeeze 这样中断的计数，拿到这个信息就可以判断出机器的网络处理是否有瓶颈，被迫中断的频率高低。&lt;/p&gt;   &lt;img src="data:image/svg+xml;utf8,&lt;svg xmlns='http://www.w3.org/2000/svg' width='1280' height='274'&gt;&lt;/svg&gt;" width="1280"&gt;&lt;/img&gt;   &lt;p&gt;上图是监控 CPU 指标的数据，格式很简单，每行对应一个 CPU，数值之间用空格分割，输出格式为 16 进制。那么每一列数值又代表什么呢？很不幸，这个没有文档，只能通过检查使用的内核版本，然后去看对应的代码。&lt;/p&gt;   &lt;div&gt;    &lt;pre&gt;     &lt;code&gt;seq_printf（seq,
     &amp;quot;%08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x\n&amp;quot;,
     sd-&amp;gt;processed, sd-&amp;gt;dropped, sd-&amp;gt;time_squeeze, 0,
     0, 0, 0, 0, /* was fastroute */
     sd-&amp;gt;cpu_collision, sd-&amp;gt;received_rps, flow_limit_count）;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;   &lt;p&gt;下面说明了文件中每个字段都是怎么来的，实际情况可能会有所不同，因为随着内核版本的迭代，字段的数量以及字段的顺序都有可能发生变化，其中与网络数据处理被中断次数相关的就是 squeeze 字段：&lt;/p&gt;   &lt;ul&gt;    &lt;li&gt;* sd-&amp;gt;processed          处理的包数量（多网卡 bond 模式可能多于实际的收包数量）&lt;/li&gt;    &lt;li&gt;* sd-&amp;gt;dropped             丢包数量，因为队列满了&lt;/li&gt;    &lt;li&gt;* sd-&amp;gt;time_spueeze      软中断处理 net_rx_action 被迫打断的次数&lt;/li&gt;    &lt;li&gt;* sd-&amp;gt;cpu_collision       发送数据时获取设备锁冲突，比如多个 CPU 同时发送数据&lt;/li&gt;    &lt;li&gt;* sd-&amp;gt;received_rps       当前 CPU 被唤醒的次数（通过处理器间中断）&lt;/li&gt;    &lt;li&gt;* sd-&amp;gt;flow_limit_count    触发 flow limit 的次数&lt;/li&gt;&lt;/ul&gt;   &lt;p&gt;下图是业务中遇到相关问题的案例，最后排查到 CPU 层面。图一是 TOP 命令的输出，显示了每个 CPU 的使用量，其中红框标出的 CPU4 的使用率存在着异常，尤其是倒数第二列的 SI 占用达到了 89%。SI 是 softirq 的缩写，表示 CPU 花在软中断处理上的时间占比，而图中 CPU4 在时间占比上明显过高。图二则是对应图一的输出结果，CPU4 对应的是第五行，其中第三列数值明显高于其他 CPU，表明它在处理网络数据的时被频繁的打断。&lt;/p&gt;   &lt;img src="data:image/svg+xml;utf8,&lt;svg xmlns='http://www.w3.org/2000/svg' width='668' height='657'&gt;&lt;/svg&gt;" width="668"&gt;&lt;/img&gt;   &lt;img src="data:image/svg+xml;utf8,&lt;svg xmlns='http://www.w3.org/2000/svg' width='804' height='638'&gt;&lt;/svg&gt;" width="804"&gt;&lt;/img&gt;   &lt;p&gt;针对上面的问题推断 CPU4 存在一定的性能衰退，也许是质量不过关或其他的原因。为了验证是否是性能衰退，写了一个简单的 python 脚本，一个一直去累加的死循环。每次运行时，把这段脚本绑定到某个 CPU 上，然后观察不同 CPU 耗时的对比。最后对比结果也显示 CPU4 的耗时比其他的 CPU 高了几倍，也验证了之前的推断。之后协调运维更换了 CPU，意向指标也就恢复正常了。&lt;/p&gt;   &lt;h2&gt;    &lt;strong&gt;总结&lt;/strong&gt;&lt;/h2&gt;   &lt;p&gt;以上所有操作都只是在数据包从网卡到了内核层，还没到常见的协议，只是完成了万里长征第一步，后面还有一系列的步骤，例如数据包的压缩（GRO）、网卡多队列软件（RPS）还有 RFS 在负载均衡的基础上考虑流的特征，就是 IP 端口四元组的特征，最后才是把数据递交到 IP 层，以及到熟悉的 TCP 层。&lt;/p&gt;   &lt;p&gt;总的来说，今天的分享都是围绕驱动来做的，我想强调的性能优化的核心点在于指标，不能测量也就很难去改善，要有指标的存在，这样一切的优化才有意义。&lt;/p&gt;   &lt;p&gt;    &lt;strong&gt;现场视频、PPT请戳：&lt;/strong&gt;&lt;/p&gt;   &lt;a href="https://link.zhihu.com/?target=https%3A//www.upyun.com/opentalk/461.html" target="_blank"&gt;&lt;/a&gt;   &lt;p&gt;&lt;/p&gt;   &lt;p&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>jianshu</category>
      <guid isPermaLink="true">https://itindex.net/detail/61854-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96-%E6%8E%A5%E6%94%B6-%E6%95%B0%E6%8D%AE</guid>
      <pubDate>Thu, 28 Oct 2021 00:00:00 CST</pubDate>
    </item>
    <item>
      <title>新一代CTR预测服务的GPU优化实践</title>
      <link>https://itindex.net/detail/61772-ctr-%E9%A2%84%E6%B5%8B-%E6%9C%8D%E5%8A%A1</link>
      <description>&lt;h2&gt;1 背景&lt;/h2&gt; &lt;p&gt;CTR（Click-Through-Rate）即点击通过率，是指网络广告的点击到达率，即该广告的实际点击次数除以广告的展现量。为CTR指标服务的打分模型，一般称为CTR模型。我们可以将此概念进一步扩展到互联网应用中各种预估转化率的模型。CTR模型在推荐、搜索、广告等场景被广泛应用。相对于CV（计算机视觉）、NLP（自然语音处理）场景的模型，CTR模型的历史结构比较简单，计算量较小。美团的CTR模型一直沿用CPU推理的方式。随着近几年深度神经网络的引入，CTR模型结构逐渐趋于复杂，计算量也越来越大，CPU开始不能满足模型对于算力的需求。&lt;/p&gt; &lt;p&gt;而GPU拥有几千个计算核心，可以在单机内提供密集的并行计算能力，在CV、NLP等领域展示了强大的能力。通过CUDA[1]及相关API，英伟达建立了完整的GPU生态。基于此，美团基础研发平台通过一套方案将CTR模型部署到GPU上。单从模型预测阶段看，我们提供的基于英伟达T4的GPU深度优化方案，在相同成本约束下，对比CPU，提升了10倍的吞吐能力。同时，在典型的搜索精排场景中，从端到端的维度来看，整体吞吐能力提升了一倍以上。&lt;/p&gt; &lt;p&gt;除了提高吞吐、降低成本外，GPU方案还为CTR模型的应用带来了额外的可能。例如，在某搜索框自动补全的场景，由于天然的交互属性，时延要求非常苛刻，一般来说无法使用复杂的模型。而在GPU能力的加持下，某复杂模型的平均响应时间从15毫秒降低至6~7毫秒，已经达到了上线要求。&lt;/p&gt; &lt;p&gt;接下来，本文将与大家探讨美团机器学习平台提供的新一代CTR预测服务的GPU优化思路、效果、优势与不足，希望对从事相关工作的同学有所帮助或者启发。&lt;/p&gt; &lt;h2&gt;2 CTR模型GPU推理的挑战&lt;/h2&gt; &lt;h3&gt;2.1 应用层的挑战&lt;/h3&gt; &lt;ol&gt;  &lt;li&gt;CTR模型结构多变，包含大量业务相关的结构，同时新的SOTA模型也层出不穷，硬件供应商由于人力受限，会重点优化常用的经典结构，如ResNet。对于没有收敛的结构，官方没有端到端的优化工具可以支持。&lt;/li&gt;  &lt;li&gt;CTR模型中通常包含较大的Embedding表结构，要考虑到Embedding表存在显存放不下的情况。&lt;/li&gt;  &lt;li&gt;在典型的推荐场景中，为了达到更快的POI曝光的目的，模型的时效性要求很高，在线模型服务需要提供增量更新模型的能力。&lt;/li&gt;&lt;/ol&gt; &lt;h3&gt;2.2 框架层的挑战&lt;/h3&gt; &lt;ol&gt;  &lt;li&gt;   &lt;strong&gt;算子层面&lt;/strong&gt;：目前主流的深度学习框架，如TensorFlow和PyTorch，可以说是深度学习第二代框架，它们首先要解决第一代框架Caffe的问题，Caffe有一个明显问题就是Layer的粒度过粗，导致那个时代的算法开发者都必须有“自己写自定义层”的能力。TensorFlow和PyTorch都把模型表达能力放在较高的优先级，导致算子粒度比较小，无论是对CPU还是GPU架构，都会带来很大的额外开销。&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;框架层面&lt;/strong&gt;：TensorFlow和PyTorch本质都是训练框架，对算法开发者比较友好，但非部署友好。其中隐含了很多为了方便分布式训练做的设计，比如TensorFlow为了方便将Variable拆到不同的PS上，内置了Partitioned_Variable的设计。在基于GPU单机预测的场景下，这些结构也会带来额外的开销。&lt;/li&gt;&lt;/ol&gt; &lt;h3&gt;2.3 硬件层的挑战&lt;/h3&gt; &lt;p&gt;第一，TensorFlow的算子粒度划分较细，导致一个模型通常由几千个算子构成，这些算子在GPU上的执行转变为对应的GPU kernel的执行。kernel是GPU上并行执行的函数。&lt;/p&gt; &lt;p&gt;GPU kernel大体上可以划分为传输数据、kernel启动、kernel计算等几个阶段，其中每个kernel的启动需要约10左右。大量的小算子导致每个kernel的执行时间很短，kernel启动的耗时占了大部分。相邻的kernel之间需要通过读写显存进行数据的传输，产生大量的访存开销。而GPU的访存吞吐远远低于计算吞吐，导致性能低下，GPU利用率并不高。&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://p0.meituan.net/travelcube/ce6fb7428cddec157fa6c783b51d086d58285.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;第二，GPU卡上包含多个计算单元，理论上，不同计算单元是可以跑不同kernel的，但实际上为了编程简单，CUDA默认假设在同一时刻一个Stream里跑同一个kernel。虽然可以通过多Stream的方式跑，但是多Steam之间又缺少细粒度的协同机制。&lt;/p&gt; &lt;p&gt;在经过充分调研与讨论后，我们决定第一期重点关注TensorFlow框架下如何解决常见CTR模型结构在英伟达GPU上执行效率不高的问题，我们先将问题收敛为以下两个子问题：
1. 算子粒度过细，GPU执行效率低下。
2. 模型结构多变，手工优化投入大，通用性差。&lt;/p&gt; &lt;h2&gt;3 优化手段&lt;/h2&gt; &lt;p&gt;为了解决上面的问题，我们对业界深度学习加速器进行了一些调研。业界比较成熟的推理优化方案主要是TensorRT/XLA/TVM。TensorRT采用手工优化，对一些定制的模型结构进行算子融合，并对计算密集型算子（如卷积）进行了高效调优。XLA是TensorFlow内置的编译优化工具，主要针对访存密集型结构，通过编译手段，实现算子的融合。TVM[2]具备较全面的优化能力，使用编译手段进行算子的融合，同时可以通过机器学习的方式实现计算密集型算子的自动调优。&lt;/p&gt; &lt;p&gt;经过广泛的调研和对比，我们最终选择了TVM作为优化工具。TVM通过编译手段，可以较好地应对多变的模型结构，解决了手工优化通用性差的问题。但TVM应用在业务模型也存在一系列问题：支持的算子数较少，而且目前对动态Shape的支持还不够好。针对这两个问题，我们将TVM和TensorFlow结合起来，结合CTR模型的结构特点与GPU的硬件特性，开发一系列流程，实现了对CTR模型的优化。&lt;/p&gt; &lt;h3&gt;3.1 算子融合&lt;/h3&gt; &lt;p&gt;通过将多个小算子融合为一个语义等价的大算子，可以有效减少GPU上的kernel数量。一方面，kernel数量减少直接降低了kernel发射的开销；另一方面，融合后的大kernel执行的计算量增加，避免了多个kernel间数据传输导致的频繁访存，提高了计算的访存比。&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://p0.meituan.net/travelcube/f55ee89e5c525c9f9ed6d172880daa54422481.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;可以看到，上图中的左右等价结构，左侧的21个算子执行的运算，可以在1个等价算子中完成。反映到GPU的活动上，左侧至少有21个GPU kernel以及21次显存的读写，而右侧只需要执行1个kernel以及1次显存读写。对于每个融合后的算子，需要有对应的kernel实现。然而，模型的算子组合是无穷的，对每种融合后算子手工实现kernel是不现实的。TVM通过编译手段，可以自动进行算子的融合以及设备代码生成，避免了逐一手写kernel的负担。&lt;/p&gt; &lt;h4&gt;3.1.1 TF-TVM自动切图优化&lt;/h4&gt; &lt;p&gt;TensorFlow模型中，如果包含TVM不支持的算子，会导致无法执行TVM转换。我们的思路是将可以用TVM优化的部分切出来，转为TVM的engine，其他部分依然使用TensorFlow的算子。在XLA和TRT转换的时候也有类似问题，我们分析了TF-XLA和TF-TRT二者的实现：&lt;/p&gt; &lt;ol&gt;  &lt;li&gt;TF-XLA的实现方案，在Grappler[4]优化图之后，有一个POST_REWRITE_FOR_EXEC（通过这个关键字可以在源码中搜索到）阶段，在这个阶段，会执行三个针对Graph的Pass，分别是用来标记算子，封装子图，改写子图并构建LaunchOp。&lt;/li&gt;  &lt;li&gt;TF-TRT的实现方案，TF-TRT在Grappler中注册了一个优化器，在这个优化器中，找到连通子图，并将其替换为TRT Engine。&lt;/li&gt;&lt;/ol&gt; &lt;p&gt;在最终方案实现上，我们参考了TF-TRT的设计。这个设计对比XLA的优势在于XLA切图方案与TensorFlow源码紧耦合，直接将XLA的三个Pass嵌入到了启动Session的主流程中。而切图策略，优化策略后续会有非常频繁的迭代，我们不希望与TensorFlow的源码太过耦合。我们扩展了TF-TVM的方案，在实际使用中我们把这个切图过程为一个独立流程。在模型部署或更新时，自动触发。&lt;/p&gt; &lt;p&gt;在推理阶段，优化过的子图使用TVM执行，其余的计算图使用TensorFlow原生实现执行，将两者结合共同完成模型的推理。由于TVM和TensorFlow的Runtime各自使用独立的内存管理，数据在不同框架间传输会导致额外的性能开销。为了降低这部分开销，我们打通了两个框架的底层数据结构，尽可能避免额外的数据拷贝。&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://p0.meituan.net/travelcube/6ea06d8887a5ab41cc58b2abd2ba5526309320.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h4&gt;3.1.2 计算图等价替换&lt;/h4&gt; &lt;p&gt;TensorFlow模型中过多的不被TVM支持的算子会导致TF-TVM切图零碎，影响最终的优化效果。为了让TF-TVM切图尽量大且完整，以及让TVM优化过程中的融合力度更大，我们对模型中的一些复杂结构进行检测，替换为执行更高效或更易于融合的等价结构。&lt;/p&gt; &lt;p&gt;例如，TensorFlow原生EmbeddingLookup结构，为了支持分布式训练，会对Embedding表进行切分，产生DynamicPartition和ParallelDynamicStitch等动态算子。这些动态算子不被TVM支持，导致TF-TVM图切分过于细碎。为了让TF-TVM切图更完整，我们通过图替换，对这种结构进行修改，通过将Embedding分表提前合并，得到简化的EmbeddingLookup结构。&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://p0.meituan.net/travelcube/dad48dc8e4844418b5fc72c7b9b93053434367.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h3&gt;3.2 CPU-GPU数据传输优化&lt;/h3&gt; &lt;p&gt;TVM优化后的子图被替换为一个节点，该节点在GPU上执行，通常有几十甚至几百个输入，该节点的前置输入（如Placeholder）通常是在CPU上执行，会涉及多次的CPU-GPU传输。频繁的小数据量传输，无法充分利用带宽。为了解决这个问题，我们对模型结构进行修改，在计算图中添加合并与拆分节点，控制切图的位置，减少数据传输的次数。&lt;/p&gt; &lt;p&gt;一种可能的合并方式是，对这些输入按相同的Shape和Dtype进行合并，后续进行拆分，将拆分节点切入TVM的子图一起优化。这种方式会导致一些问题，如部分子图的算子融合效果不佳；另一方面，GPU kernel函数的参数传递内存限制在4KB，对于TVM节点输入非常多的情况（如超过512个），会遇到生成代码不合法的情况。&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://p0.meituan.net/travelcube/1c2d4274556945073ba67fad2e33598f1662425.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h3&gt;3.3 高频子图手工优化&lt;/h3&gt; &lt;p&gt;对于TVM无法支持的子图，我们对业务中高频使用的结构进行抽象，采用手写自定义算子的方式，进行了高效GPU实现。&lt;/p&gt; &lt;p&gt;例如，模型中有部分时序特征使用String类型输入，将输入的字符串转为补齐的数字Tensor，将int类型的Tensor作为下标进行Embedding操作。这部分子图的语义如图，以下简称SE结构（StringEmbedding）：&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://p0.meituan.net/travelcube/0ca3d8db66f21eb313bbcb5324d6aafd735806.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;这一部分结构，TensorFlow的原生实现只有基于CPU的版本，在数据量较大且并行度较高的情景下，性能下降严重，成为整个模型的瓶颈。为了优化这部分结构的性能，我们在GPU上实现了高效的等价操作。&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://p0.meituan.net/travelcube/3af287d61ddcd73481ad9177e9b9425c789114.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;如图所示，PadString算子在CPU端将多个字符串按最大长度进行补齐，拼接成一个内存连续的uint8类型Tensor，以便一次性传输到GPU。StringEmbedding接收到补齐后的字符串后，利用GPU并行计算的特性，协同大量线程完成字符串的切分与查表操作。在涉及规约求和、求前缀和等关键过程中，使用了GPU上的Reduce/Scan算法，编码过程使用  &lt;code&gt;warp_shuffle&lt;/code&gt;指令，不同线程通过寄存器交换数据，避免了频繁访存的开销，获得了很好的性能。&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://p0.meituan.net/travelcube/7cc8229898c23d8dfa5f90c6c0344c8864382.jpg"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;GPU Scan算法示意，一个8个元素的前缀和操作，只需要3个迭代周期。在一个有几十路类似操作的模型中，手工优化前后的GPU timeline对比如下图，可以看到H2D + StringEmbedding这部分结构的耗时有很大的缩减，从42毫秒缩减到1.83毫秒。&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://p0.meituan.net/travelcube/481dec53e64530ce021f2a62b8738cd3200755.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;除了StringEmbedding结构，我们对StringSplit + ToNumber + SparseSegmentSqrt、多路并行StringEmbedding等结构都进行了高效融合实现，在优化流程中通过结构匹配进行相应的替换。&lt;/p&gt; &lt;h3&gt;3.4 CPU-GPU分流&lt;/h3&gt; &lt;p&gt;  &lt;img alt="" src="https://p0.meituan.net/travelcube/9abca80b01c6d157a5101c03f6d2efee1131364.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;实际线上的RPC请求，每个请求内的样本数（下文称Batch）是在[1,MaxValue]范围内变化的，MaxValue受上游业务系统，其他基础系统能力等多方面因素制约，相对固定。如上图所示，以某个搜索服务为例，我们统计了线上的Batch数值分布，Batch=MaxValue的请求占比约45%，Batch=45占比7.4%，Batch=1占比2.3%。其余的Batch占比从0.5%到1%不等。对于GPU来说，提高单个请求的Batch能更好地利用硬件资源，发挥GPU的并行计算能力，表现出相对CPU更优的延迟和吞吐；当Batch较小时，GPU相对CPU的优势就不明显了（下图是我们测试同样的模型在固定压力下，CPU/GPU上延迟的变化）。&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://files.mdnice.com/user/19015/93cf574b-1d8a-4e09-bb6a-7819da2e2766.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;大部分请求都由GPU在做了，CPU资源有较多空余，我们将一些小Batch的碎请求放在CPU运行，这样可以让整个Worker的资源利用更加均衡，提高系统整体的性能。我们根据测试设定了一个Batch阈值，以及计算图在异构硬件上区别执行的判断逻辑：对于小Batch的情况，直接在CPU上执行计算图，只有Batch超过阈值的请求才会在GPU上推理。从线上的统计数据来看，整体流量的77%跑在GPU上，23%跑在CPU上。&lt;/p&gt; &lt;p&gt;在GPU的一系列优化策略和动作中，Batch大小是很重要的信息，不同Batch下优化出的kernel实现可能是不同的，以达到对应workload下最优的计算性能；由于线上的流量特点，发送到GPU的请求Batch分布比较细碎，如果我们针对每个Batch都优化一个模型的kernel实现显然是不够经济和通用的。因此，我们设计了一个Batch分桶策略，生成N个固定Batch的优化模型，在实际请求到来时找到Batch距离最近的一个Bucket，将请求向上Padding到对应的Batch计算，从而提高了GPU的利用效率。&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://p0.meituan.net/travelcube/0f03609e2129b0a6919485609e9d9f5761709.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h2&gt;4 压测性能分析&lt;/h2&gt; &lt;p&gt;我们选取一个模型进行线上性能压测分析。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;CPU模型测试环境为16核Intel® Xeon® Gold 5218 CPU @ 2.30GHz，16G内存。&lt;/li&gt;  &lt;li&gt;GPU模型测试环境为8核Intel® Xeon® Gold 5218 CPU @ 2.30GHz，Tesla T4 GPU，16G内存。   &lt;br /&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;  &lt;img alt="" src="https://p0.meituan.net/travelcube/ea277d1eef9490097183ac49df1a0bb872202.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;下图对比了在不同的QPS下（x轴），GPU模型在各BatchSize下的推理时延（y轴）。GPU模型在BatchSize=128以下，推理耗时差异不明显，较大的BatchSize更有利于吞吐；对比BatchSize=256的GPU模型与BatchSize为25的CPU模型，在QPS低于64的情况下，二者推理耗时基本持平；QPS超过64的情况下，GPU的推理时延低于CPU。GPU的吞吐相比CPU提升了10倍。&lt;/p&gt; &lt;p&gt;同时，我们可以看到不同曲线的陡峭程度，CPU在QPS高出64后，时延会迅速上升，GPU则依然保持平稳，直到QPS超过128才会有明显上升，但仍旧比CPU更平稳。&lt;/p&gt; &lt;h2&gt;5 整体架构&lt;/h2&gt; &lt;p&gt;针对CTR模型的结构特点，我们抽象出了一套平台化的通用优化流程。通过对模型结构的分析，自动应用合适的优化策略，通过性能评估和一致性校验，保证模型的优化效果。&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://p0.meituan.net/travelcube/35da176717cc8dd7955e669212c02405312302.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h2&gt;6 不足之处与未来规划&lt;/h2&gt; &lt;p&gt;在易用性层面，目前的方案形式是提供了一套在线优化脚本，用户提交模型后，自动优化部署。由于涉及对计算图结构的分析、编辑以及TVM的编译等过程，目前的模型优化耗时较长，大部分模型优化耗时在20分钟左右。后续需要考虑加速TVM编译的效率。&lt;/p&gt; &lt;p&gt;在通用性层面，从我们的实际应用情况来看，TVM编译优化和高性能手写算子是最主要的收益来源。手工优化很考验开发同学对业务模型的理解和GPU编程的能力。编写一个高性能的融合算子已经不太容易，要做到有一定的迁移能力和扩展性则更有难度。&lt;/p&gt; &lt;p&gt;总的来说，CTR模型推理在GPU上未来需要考虑的问题还有很多。除了要基于业务理解提供更好的性能外，还要考虑模型规模巨大后无法完整放入显存的问题以及支持在线模型更新的问题。&lt;/p&gt; &lt;h2&gt;作者简介&lt;/h2&gt; &lt;p&gt;伟龙、小卓、文魁、駃飞、小新等，均来自美团基础研发平台-机器学习预测引擎组。&lt;/p&gt; &lt;h2&gt;参考资料&lt;/h2&gt; &lt;p&gt;[1]   &lt;a href="https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html"&gt;CUDA C++ Programming Guide&lt;/a&gt;
[2]   &lt;a href="https://tvm.apache.org/docs/"&gt;TVM Documentation&lt;/a&gt;
[3]   &lt;a href="https://docs.nvidia.com/deeplearning/frameworks/tf-trt-user-guide/index.html"&gt;Accelerating Inference In TF-TRT User Guide&lt;/a&gt;
[4]   &lt;a href="https://www.tensorflow.org/guide/graph_optimization"&gt;TensorFlow graph optimization with Grappler&lt;/a&gt;&lt;/p&gt; &lt;h2&gt;招聘信息&lt;/h2&gt; &lt;p&gt;美团机器学习平台大量岗位持续招聘中，实习、社招均可，坐标北京/上海，欢迎感兴趣的同学加入我们，构建多领域公司级机器学习平台，帮大家吃得更好，生活更好。简历可投递至：wangxin66@meituan.com。&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/61772-ctr-%E9%A2%84%E6%B5%8B-%E6%9C%8D%E5%8A%A1</guid>
      <pubDate>Thu, 09 Sep 2021 08:00:00 CST</pubDate>
    </item>
    <item>
      <title>Django 优化数据库查询的一些经验</title>
      <link>https://itindex.net/detail/61746-django-%E4%BC%98%E5%8C%96-%E6%95%B0%E6%8D%AE%E5%BA%93</link>
      <description>&lt;p&gt;ORM 帮我们节省了很多工作，基本上不用写 SQL，就可以完成很多 CRUD 操作，而且外键的关联也会自动被 ORM 处理好，使得开发的效率非常高。我觉得 Django 的 ORM 在 ORM 里面算是非常好用的了，尤其是自带的 Django-admin，可以节省很多工作，甚至比很多公司内部开发的后台界面都要优秀。&lt;/p&gt;
 &lt;p&gt;但是 ORM 也带来了一些问题，最严重的就是，这些外键关联会去自动 Fetch，导致非常容易写出来   &lt;a href="https://www.kawabangga.com/posts/3915"&gt;N + 1 查询&lt;/a&gt;，加上如果使用   &lt;a href="https://www.django-rest-framework.org/"&gt;django-rest-framework&lt;/a&gt;, Serializer 可以帮助你很方便地去渲染关联的外键，就更容易写出 N + 1 查询了。也是因为这个原因，之前在蚂蚁工作的时候，公司基本上是禁止（有一些小范围的内部项目还是可以使用）使用类似于 Hibernate 这种自动关联外键 ORM 的，都需要手写 SQL map，自己去 Join。但其实，我觉得这是一种非常粗暴的做法，用大量的人力换取降低错误出现的几率。这个问题是非常好解决的，我们只要对接口 Profile，看一下完成一次请求到底进行了哪些 SQL 查询即可，如果发现了 N + 1 就去解决。&lt;/p&gt;
 &lt;p&gt;这篇文章介绍 Django 中去 debug 和优化数据库查询的一些方法，其实对于其他的语言和框架，也差不多类似，也有同类的工具。&lt;/p&gt;
 &lt;p&gt;让我们从头开始思考，如何提升网站的性能。首先，最接近于用户的就是前端的代码，我们可以从前端开始 perf，看哪一个页面渲染用的时间最长，是请求 Block 住了，还是前端的组件写的性能差。如果是非常重要的面向用户的页面，可以系统性地使用一些监控工具发现性能问题。如果是对内的，其实只要自己去每一个页面点几下试试就可以发现性能问题了。如果自己没觉得卡顿，基本上就足够了。&lt;/p&gt;
 &lt;p&gt;对于内部的系统来说，如果前端不是写的出奇的差的话，性能问题一般都是由 API 慢引起的。所以本文就不过多介绍前端性能的优化了。&lt;/p&gt;
 &lt;h2&gt;Slow API&lt;/h2&gt;
 &lt;h3&gt;发现耗时长的 API&lt;/h3&gt;
 &lt;p&gt;Django 有一个 Prometheus 的 exporter 库，  &lt;a href="https://github.com/korfuri/django-prometheus"&gt;django-prometheus&lt;/a&gt;，可以使用这个库将 metrics 暴露出来，然后在 Grafana 上绘制每一个 view 的 P99/P95 等，发现耗时长的 API，然后针对性地进行优化，去 Debug 到底是慢在了哪里。&lt;/p&gt;
 &lt;h3&gt;Debug&lt;/h3&gt;
 &lt;p&gt;Django 的   &lt;a href="https://github.com/jazzband/django-debug-toolbar"&gt;django-debug-toolbar&lt;/a&gt; 是一个非常好用的工具。安装之后，设置好 debug 用的 IP，使用这个 IP 去访问的时候，它会自动对整个请求做 Profile，包括使用的 Cache，Template，Signal 等等。最有用的就是 SQL Profile 了。&lt;/p&gt;
 &lt;p&gt;它会把一个请求涉及的所有 SQL 都列出来，包括：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;这个 SQL 花了多少时间&lt;/li&gt;
  &lt;li&gt;这个 SQL 是由哪一行代码触发的，类似于一个 traceback&lt;/li&gt;
  &lt;li&gt;有多少个类似的 SQL，有多少个重复的 SQL （如果有的话，一般就是有问题了，意味着同样的 SQL 查询了多次）&lt;/li&gt;
  &lt;li&gt;点击 Expl 可以很方便地看到这个 SQL 的 Explain&lt;/li&gt;
  &lt;li&gt;点击 Sel 可以看到 SQL 的详情&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;如下所示：&lt;/p&gt;
 &lt;p&gt;  &lt;a href="https://www.kawabangga.com/wp-content/uploads/2021/08/django-debug-toolbar.png"&gt;   &lt;img alt="" height="1820" src="https://www.kawabangga.com/wp-content/uploads/2021/08/django-debug-toolbar.png" width="2520"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;然后就可以针对慢的 API 进行优化了。&lt;/p&gt;
 &lt;h3&gt;解决&lt;/h3&gt;
 &lt;h4&gt;N + 1 Query&lt;/h4&gt;
 &lt;p&gt;N + 1 查询是造成 API 慢的最常见的原因。比如这样一个需求：我们有一个列表页，对于列表中的每一个 Item，都要展示它相关的 Tag。如果使用 djagno-rest-framework 的   &lt;a href="https://www.django-rest-framework.org/api-guide/relations/#nested-relationships"&gt;Nested relationships&lt;/a&gt; 的话，实际的查询会：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;先查询出来当前列表页要展示的 item list&lt;/li&gt;
  &lt;li&gt;对于每一个 item，都去查询它的 tag&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;这就是 N + 1 查询。&lt;/p&gt;
 &lt;p&gt;解决的方法很简单，就是使用 Django 的   &lt;a href="https://docs.djangoproject.com/en/3.2/ref/models/querysets/#prefetch-related"&gt;   &lt;code&gt;prefetch_related()&lt;/code&gt;&lt;/a&gt;，它的原理是使用 in 一次性将所有的外键关联的数据查询出来，然后在内存中使用 Python 做 “join”，这样就只会产生两个查询：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;先查询出来列表页要展示的 item list&lt;/li&gt;
  &lt;li&gt;一次查询出来所有的 tag，使用 tag_id in (item_id, item2_id…)&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;参考一个官方文档中的例子：每一个 Pizze 都有不同的 Topping（浇头？）。&lt;/p&gt; &lt;pre&gt;from django.db import models

class Topping(models.Model):
    name = models.CharField(max_length=30)

class Pizza(models.Model):
    name = models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping)

    def __str__(self):
        return &amp;quot;%s (%s)&amp;quot; % (
            self.name,
            &amp;quot;, &amp;quot;.join(topping.name for topping in self.toppings.all()),
        )&lt;/pre&gt; &lt;p&gt;下面这个查询就是一个 N + 1，要先查出来所有的 Pizze，然后对于每一个 Pizza 去查询它的 Toppings：&lt;/p&gt; &lt;pre&gt;&amp;gt;&amp;gt;&amp;gt; Pizza.objects.all()
[&amp;quot;Hawaiian (ham, pineapple)&amp;quot;, &amp;quot;Seafood (prawns, smoked salmon)&amp;quot;...&lt;/pre&gt; &lt;p&gt;如果使用 prefetch 的话，就会只有 3 次查询（因为是一个 many-to-many 关系，所以要有一次是查询中间表的）：&lt;/p&gt; &lt;pre&gt;&amp;gt;&amp;gt;&amp;gt; Pizza.objects.all().prefetch_related(&amp;apos;toppings&amp;apos;)&lt;/pre&gt; &lt;p&gt; &lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;注意只能使用    &lt;code&gt;.all()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;prefetch 其实是缓存了一个 queryset(), 如果查询条件改变了，Django 就必须重新发起查询。以下这个用法，就不会用到 prefetch 的 cache：&lt;/p&gt; &lt;pre&gt;&amp;gt;&amp;gt;&amp;gt; pizzas = Pizza.objects.prefetch_related(&amp;apos;toppings&amp;apos;)
&amp;gt;&amp;gt;&amp;gt; [list(pizza.toppings.filter(spicy=True)) for pizza in pizzas]&lt;/pre&gt; &lt;p&gt;因为 prefetch 已经把所有的数据查询到内存里面了，所以我们应该这么用，就不会触发新的查询了：&lt;/p&gt; &lt;pre&gt;&amp;gt;&amp;gt;&amp;gt; pizzas = Pizza.objects.prefetch_related(&amp;apos;toppings&amp;apos;) 
&amp;gt;&amp;gt;&amp;gt; [pizza for pizza in pizzas 
         if any(topping.spicy==True for topping in pizza.toppings)
    ]&lt;/pre&gt; &lt;p&gt; &lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;   &lt;code&gt;Prefetch()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;prefetch_related()&lt;/code&gt; 所接收的参数，除了可以是一个   &lt;code&gt;string&lt;/code&gt; 外，也可以是一个   &lt;code&gt;Prefetch()&lt;/code&gt; 对象，可以用来更精确地控制 cache 的 queryset. 比如排序：&lt;/p&gt; &lt;pre&gt;&amp;gt;&amp;gt;&amp;gt; Restaurant.objects.prefetch_related(
...     Prefetch(&amp;apos;pizzas__toppings&amp;apos;, queryset=Toppings.objects.order_by(&amp;apos;name&amp;apos;)))&lt;/pre&gt; &lt;p&gt;也可以一次性 prefetch 多个外键（顺序很重要，  &lt;a href="https://docs.djangoproject.com/en/3.2/ref/models/querysets/#prefetch-related"&gt;参考文档&lt;/a&gt;），  &lt;code&gt;Prefetch()&lt;/code&gt; 和   &lt;code&gt;string&lt;/code&gt; 可以混用：&lt;/p&gt; &lt;pre&gt;&amp;gt;&amp;gt;&amp;gt; Restaurant.objects.prefetch_related( 
... Prefetch(&amp;apos;pizzas__toppings&amp;apos;, queryset=Toppings.objects.order_by(&amp;apos;name&amp;apos;)),
... &amp;quot;address&amp;quot;)&lt;/pre&gt; &lt;p&gt; &lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;many-to-many 和嵌套外键&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;对于嵌套的外键，可以用   &lt;code&gt;__&lt;/code&gt; 将 Model 的属性名字联合起来，比如这样：&lt;/p&gt; &lt;pre&gt;&amp;gt;&amp;gt;&amp;gt; Restaurant.objects.prefetch_related(&amp;apos;pizzas__toppings&amp;apos;)&lt;/pre&gt; &lt;p&gt;这样   &lt;code&gt;pizzas&lt;/code&gt; 和   &lt;code&gt;toppings&lt;/code&gt; 都会被 prefetch.&lt;/p&gt;
 &lt;p&gt; &lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;   &lt;code&gt;select_related()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;   &lt;a href="https://docs.djangoproject.com/en/3.2/ref/models/querysets/#select-related"&gt;select_related()&lt;/a&gt;&lt;/code&gt; 也是有类似作用的一个功能，只不过他和 prefetch 的区别是：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;code&gt;prefetch_related()&lt;/code&gt; 是用 in 然后用代码 join&lt;/li&gt;
  &lt;li&gt;   &lt;code&gt;select_related()&lt;/code&gt; 是用 SQL 直接 join&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;显然，  &lt;code&gt;select_related()&lt;/code&gt; 触发的查询更少，一次查询就可以解决问题。但是它的功能也有限，不能支持嵌套的外键查询。&lt;/p&gt;
 &lt;p&gt; &lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;   &lt;code&gt;prefetch_related_objects()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;以上的两个方法是用于 queryset 的，如果是对 object 的话，可以使用这个函数。&lt;/p&gt;
 &lt;p&gt;比如，我们要查询最近的一个订单关联的数据的话，可以这么使用：&lt;/p&gt; &lt;pre&gt;latest_order = self.orders.order_by(&amp;quot;-id&amp;quot;).first()
models.prefetch_related_objects(
    [last_group],
    &amp;quot;item&amp;quot;,
    &amp;quot;user_address&amp;quot;,
)&lt;/pre&gt; &lt;p&gt;&lt;/p&gt;
 &lt;h4&gt;&lt;/h4&gt;
 &lt;h4&gt;Cached Property&lt;/h4&gt;
 &lt;p&gt;Django 提供了一很实用的装饰器   &lt;a href="https://docs.djangoproject.com/en/3.2/ref/utils/"&gt;   &lt;code&gt;@cached_property&lt;/code&gt;&lt;/a&gt; ，用这个替换   &lt;code&gt;@property&lt;/code&gt; 的话，一个对象在读取这个 property 的时候只会计算一次，同一个对象在第一次之后来读取这个 property 都会使用缓存。&lt;/p&gt;
 &lt;p&gt;有点类似于 Python 中的   &lt;a href="https://docs.python.org/3/library/functools.html"&gt;   &lt;code&gt;@lru_cache&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;
 &lt;p&gt; &lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;减少不必要的展示字段&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;无论是 Cache 还是 prefetch 的方法，都是有一些复杂的。如果前端用户到一些字段，就没有必要一次性返回。&lt;/p&gt;
 &lt;p&gt;刚开始写 DRF 中的 Serializer 的时候，倾向于每一个 Model 都有一个 Serializer，然后这些 Serializer 都互相关联。最终，导致查询一个列表页的时候，每一个 item 相关的数据，以及这些数据相关的数据，都被一次性展示出来了。即使优化过后也难以维护。&lt;/p&gt;
 &lt;p&gt;后来总结出来一个比较好的实践，是每一个 Model 都有两个 Serializer：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;ListSerialzer：对于所有的外键只展开一层，不展开外键的外键
   &lt;ul&gt;
    &lt;li&gt;用于列表页 API 的显示&lt;/li&gt;
    &lt;li&gt;这样查询的时候，只需要对于每一个外键查询一次      &lt;code&gt;in&lt;/code&gt; 就可以了&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
  &lt;li&gt;DetailSerializer：按需求展示所有的外键
   &lt;ul&gt;
    &lt;li&gt;用于详情页的渲染&lt;/li&gt;
    &lt;li&gt;对于每一个外键关联的 row，可能都要再进行一次查询，把所有关联的外键都展开，方便展示。但是因为只有一个对象，所以也不会特别慢。但是依然要注意 N + 1，如果嵌套的太深，考虑不一次展示那么多，新提供一个 API 进行查询&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;这样的好处是我们可以按需进行 prefetch，List 页面的 API 只需要 prefetch 直接关联的外键就可以了，Detail 的 API 可以按需进行级联 prefetch. 总体的原则就是尽量避免多重外键的 prefetch.&lt;/p&gt;
 &lt;p&gt;值得一提的是在 django-rest-framework 中，是可以在同一个 ModelViewSet 里面，针对不同的 API，使用不同的 Serializer 的：&lt;/p&gt; &lt;pre&gt;def get_serializer_class(self):
    if self.action == &amp;quot;list&amp;quot;:
        return ExperimentListSerializer
    return super().get_serializer_class()&lt;/pre&gt; &lt;p&gt;&lt;/p&gt;
 &lt;h4&gt;&lt;/h4&gt;
 &lt;h4&gt;使用冗余字段&lt;/h4&gt;
 &lt;p&gt;现在存储已经很便宜了，在合适的场景下，可以考虑直接将 fields 多存几份，节省查询。&lt;/p&gt;
 &lt;p&gt;比如我的一个场景是：一个 group 里面有个并行执行的 Execution，如果所有的 Execution 都执行完了，这个 group 就可以被认为是执行完了。&lt;/p&gt;
 &lt;p&gt;之前的实现是在 group 上定义一个   &lt;code&gt;is_running&lt;/code&gt; 的字段，返回   &lt;code&gt;group.execution_set.filter(is_running=True).exists()&lt;/code&gt;。这样每次都需要查询外键。&lt;/p&gt;
 &lt;p&gt;其实可以在   &lt;code&gt;group&lt;/code&gt; 上保存一个   &lt;code&gt;is_running&lt;/code&gt; 的字段，然后当   &lt;code&gt;Execution&lt;/code&gt; 结束的时候顺便更新   &lt;code&gt;group.is_running&lt;/code&gt;. （  &lt;a href="https://docs.djangoproject.com/en/3.2/ref/signals/"&gt;Signal&lt;/a&gt; 其实不太好维护，我比较喜欢显式调用）。&lt;/p&gt;
 &lt;p&gt;这样的好处是：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;方便查询，业务逻辑变得简单&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;缺点是：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;另外肯定有某个地方的逻辑变得复杂了，因为要同步更新&lt;/li&gt;
  &lt;li&gt;可能又潜在的数据不一致&lt;/li&gt;
&lt;/ol&gt;
 &lt;h2&gt;Slow SQL&lt;/h2&gt;
 &lt;p&gt;随着数据越来越多，即使开发的环境中发现 API 造成的请求都很少，也很快，但是线上环境跑着跑着可能就有问题了。&lt;/p&gt;
 &lt;p&gt;所以最好对线上的 SQL 也进行观测。方法很简单，只要将查询时间 &amp;gt;1秒（或者其他时间）的 SQL log 出来就可以了。可以通过针对 Django ORM 设置 logging 配置来完成这件事：&lt;/p&gt;
 &lt;p&gt;添加一个新的 logger，然后 filter 类似于一下设置：&lt;/p&gt; &lt;pre&gt;&amp;quot;filters&amp;quot;: {
    &amp;quot;slow_sql_above_50ms&amp;quot;: {
        &amp;quot;()&amp;quot;: &amp;quot;django.utils.log.CallbackFilter&amp;quot;,
        &amp;quot;callback&amp;quot;: lambda record: not hasattr(record, &amp;quot;duration&amp;quot;)
        or record.duration &amp;gt; 0.05,  # output slow queries only
    },
},&lt;/pre&gt; &lt;p&gt;就可以将 SQL 日志过滤出来，然后只 log 请求时间 &amp;gt;50ms 的。&lt;/p&gt;
 &lt;p&gt; &lt;/p&gt;
 &lt;p&gt;最后，以 Django 中的一句话作为结尾：&lt;/p&gt;
 &lt;blockquote&gt;  &lt;p&gt;Always profile for your use case!&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;The post   &lt;a href="https://www.kawabangga.com/posts/4484"&gt;Django 优化数据库查询的一些经验&lt;/a&gt; first appeared on   &lt;a href="https://www.kawabangga.com"&gt;卡瓦邦噶！&lt;/a&gt;.&lt;/p&gt; &lt;div&gt;  &lt;h3&gt;相关文章:&lt;/h3&gt;  &lt;ul&gt;   &lt;li&gt;    &lt;a href="https://www.kawabangga.com/posts/3718"&gt;PyCon China 2019演讲Slide: Django Migration Under the Hood&lt;/a&gt;&lt;/li&gt;   &lt;li&gt;    &lt;a href="https://www.kawabangga.com/posts/2936"&gt;辅助Django开发的一些隐藏资源（文档）&lt;/a&gt;&lt;/li&gt;   &lt;li&gt;    &lt;a href="https://www.kawabangga.com/posts/1997"&gt;Django的信号机制&lt;/a&gt;&lt;/li&gt;   &lt;li&gt;    &lt;a href="https://www.kawabangga.com/posts/3660"&gt;上海PyCon见！&lt;/a&gt;&lt;/li&gt;   &lt;li&gt;    &lt;a href="https://www.kawabangga.com/posts/3382"&gt;记一个 Python logging 多进程 rotate 问题&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>Python cache Database django django-rest-framework</category>
      <guid isPermaLink="true">https://itindex.net/detail/61746-django-%E4%BC%98%E5%8C%96-%E6%95%B0%E6%8D%AE%E5%BA%93</guid>
      <pubDate>Fri, 20 Aug 2021 20:06:12 CST</pubDate>
    </item>
    <item>
      <title>中国大学 MOOC Android 性能优化：冷启动优化总结</title>
      <link>https://itindex.net/detail/61661-%E4%B8%AD%E5%9B%BD%E5%A4%A7%E5%AD%A6-mooc-android</link>
      <description>&lt;p&gt;  &lt;a href="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#39318;&amp;#22270;.gif"&gt;   &lt;img alt="&amp;#39318;&amp;#22270;" height="118" src="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#39318;&amp;#22270;.gif" width="700"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://techblog.youdao.com/wp-content/uploads/2021/08/&amp;#23553;&amp;#38754;4.png"&gt;   &lt;img alt="&amp;#23553;&amp;#38754;4" height="298" src="http://techblog.youdao.com/wp-content/uploads/2021/08/&amp;#23553;&amp;#38754;4.png" width="700"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;联系我们:&lt;/strong&gt;   &lt;strong&gt;有道技术团队助手&lt;/strong&gt;：ydtech01 /   &lt;strong&gt;邮箱&lt;/strong&gt;：ydtech@rd.netease.com&lt;/p&gt;
 &lt;p&gt;本文的重点在于如何定量的排查冷启动过程中的耗时操作，并提供对应的优化思路和实践方法总结。同时本文涉及到的冷启动优化主要涵盖两个方面：Application 的性能优化和 Launcher Activity 的性能优化。&lt;/p&gt;
 &lt;h2&gt;  &lt;strong&gt;一、背景&lt;/strong&gt;&lt;/h2&gt;
 &lt;p&gt;中国大学 MOOC 是网易与高教社携手推出的在线教育平台，目前，经过长期的产品打磨和专研，在课程数量、质量以及影响力，中国大学 MOOC 已成为全球领先的中文慕课平台。同时经过此次优化，冷启动速度  &lt;strong&gt;整体提升27%。&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;在我们日常开发中，随着 app 整体迭代次数增多，由于长久以来的迭代需求，android app 本身也集成了较多的第三方组件和 SDK，同时在日常迭代中，也是以业务迭代需求实现为主要目的，导致现在 app 本身，或多或少存在一些性能可优化空间。所以有必要进行性能优化，提升用户体验&lt;/p&gt;
 &lt;p&gt;此次优化，主要侧重于两个方面：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;Application 的性能优化&lt;/li&gt;
  &lt;li&gt;app 启动页性能优化&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;strong&gt;该文档重点不在于代码规范和业务代码逻辑导致的性能问题，而是在假设代码无明显、严重性能漏洞，并且不改变原有业务逻辑，量化性能监测数据和问题，并针对其进行优化修改。&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt;
 &lt;h2&gt;  &lt;strong&gt;二、冷启动速度优化&lt;/strong&gt;&lt;/h2&gt;
 &lt;h3&gt;  &lt;strong&gt;2.1 相关知识点&lt;/strong&gt;&lt;/h3&gt;
 &lt;h4&gt;  &lt;strong&gt;2.1.1 冷启动耗时统计&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;  &lt;a href="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#20195;&amp;#30721;18.png"&gt;   &lt;img alt="&amp;#20195;&amp;#30721;1" height="94" src="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#20195;&amp;#30721;18.png" width="784"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;上述 adb 命令中，几个关键参数说明：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;strong&gt;-S&lt;/strong&gt;：表示启动该 app 前先彻底关闭当前 app 进程&lt;/li&gt;
  &lt;li&gt;   &lt;strong&gt;-W&lt;/strong&gt;：启动并输出相关耗时数据&lt;/li&gt;
  &lt;li&gt;   &lt;strong&gt;packageName&lt;/strong&gt;：app 的 applicationID&lt;/li&gt;
  &lt;li&gt;   &lt;strong&gt;activityName&lt;/strong&gt;：app 启动需要拉起的 Activity，如果用于统计冷启动耗时，那么该参数即为应用的第一个启动的 Activity（intent-filter 为 LAUNCHER 的 Activity）&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;再执行上诉 adb 后，会成功唤起 APP，并在控制台输出三个比较关键的参数：&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#22270;12.png"&gt;   &lt;img alt="&amp;#22270;1" height="112" src="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#22270;12.png" width="600"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;strong&gt;LaunchState&lt;/strong&gt;：启动模式，上诉启动模式为冷启动&lt;/li&gt;
  &lt;li&gt;   &lt;strong&gt;WaitTime&lt;/strong&gt;：系统启动应用耗时= TotalTime +系统资源启动时间（单位 ms ）&lt;/li&gt;
  &lt;li&gt;   &lt;strong&gt;TotalTime&lt;/strong&gt;：应用自身启动耗时=该 Activity 启动时间+应用 application 等资源启动时间（单位 ms ）&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;strong&gt;对于应用层面得冷启动性能优化，我们关注的时间 TotalTime，该时间大致可以概括为：Application 构造方法→该 Activity 的 onWindowFocusChange 方法时间总和。而这个过程也可以粗略认知为，用户点击桌面图标到 app 第一个 Activity 获取焦点，业务代码执行的总时间（针对业务代码的优化，我们暂时不关心 Zygote 进程、Launcher 进程、AMS 进程的交互）。&lt;/strong&gt;&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;2.1.2 冷启动耗时堆栈观察方法：&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;在 Android API&amp;gt;=26 的系统版本中，建议使用 CPU Profile 或者 Debug.startMethodTracing 进行监控并导出 trace 文件进行分析。不管哪种方式，采集堆栈信息都有两种模式：采样模式和追踪模式。追踪模式会一直抓取数据，对设备性能要求较高。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;（1）CPU Profile&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#22270;22.png"&gt;   &lt;img alt="&amp;#22270;2" height="494" src="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#22270;22.png" width="550"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;（2）Debug.startMethodTracing&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;由于冷启动涉及到业务应用层面的时间是：该 Activity 启动时间+应用 application 等资源启动时间，所以我们在 Application 构造方法中开始采集，在第一个 Activity 的 onWindowFocusChange 中停止采集，并输出 trace文件。&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#20195;&amp;#30721;24.png"&gt;   &lt;img alt="&amp;#20195;&amp;#30721;2" height="584.5" src="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#20195;&amp;#30721;24.png" width="600"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#20195;&amp;#30721;33.png"&gt;   &lt;img alt="&amp;#20195;&amp;#30721;3" height="331" src="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#20195;&amp;#30721;33.png" width="785"&gt;&lt;/img&gt;&lt;/a&gt;   &lt;strong&gt;同时，由于该操作涉及到文件读写权限，需要手动授予 APP 该权限&lt;/strong&gt;&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;2.1.3 .trace 日志文件阅读：&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;在导出获取到 .trace 文件后，把 .trace 拖动至 androidStudio 编 辑区；或者直接浏览 CPU Profile 视图，便可对程序运行的堆栈进行分析：&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#22270;31.png"&gt;   &lt;img alt="&amp;#22270;3" height="327" src="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#22270;31.png" width="800"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;上图就是 trace 文件打开后的效果，展示的是基于 CPU 使用和线程运行状况，针对启动速度的优化，需要关注的上图标注的几个点：&lt;/p&gt;
 &lt;p&gt;（1）CPU 运行时间轴：横向拖动可以选择查看的时间范围&lt;/p&gt;
 &lt;p&gt;（4）当前设备 CPU 轮转的线程：点击可以选择需要查看的线程，我们重点关注主线程&lt;/p&gt;
 &lt;p&gt;（2）当前选择线程，跟随时间轴，各个方法栈的调用情况和其耗时状况。其不同颜色分别代表&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;ul&gt;
   &lt;li&gt;    &lt;strong&gt;黄色&lt;/strong&gt;：android 系统方法（FrameWork 层代码，如果需要最终更底层的方法，需要最终 C/C++ 方法调用栈）&lt;/li&gt;
   &lt;li&gt;    &lt;strong&gt;蓝色&lt;/strong&gt;：Java JDK 方法&lt;/li&gt;
   &lt;li&gt;    &lt;strong&gt;绿色&lt;/strong&gt;：属于当前 app 进程执行的方法，包括一些类加载器和我们的业务代码（启动速度优化主要针对这一部分）&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
 &lt;p&gt;（3）各个方法栈的调用顺序和耗时情况，可以选择不用的排序方式和视图。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;所以一般排查耗时方法时，建议先通过（2）视图直观检测到耗时较为严重的方法，锁定后，在（3）视图中查看具体的方法调用顺序。&lt;/strong&gt;&lt;/p&gt;
 &lt;h3&gt;  &lt;strong&gt;2.2 优化步骤&lt;/strong&gt;&lt;/h3&gt;
 &lt;p&gt;由于在冷启动过程中，业务代码耗时主要集中在 Application 和 launcher Activity 中，所以优化过程也是分别针对这两块进行优化。&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;2.2.1 优化成果&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;使用2.1.1的方式，在优化前后，分别做了10次冷启动耗时统计，结果如下：&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#34920;&amp;#26684;2.png"&gt;   &lt;img alt="&amp;#34920;&amp;#26684;2" height="590" src="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#34920;&amp;#26684;2.png" width="700"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;启动速度  &lt;strong&gt;整体提升 27%。&lt;/strong&gt;&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;2.2.2 Application 优化&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;  &lt;a href="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#22270;4.png"&gt;   &lt;img alt="&amp;#22270;4" height="185" src="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#22270;4.png" width="700"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;通过 trace 文件，可以直观的发现，在 application 中，耗时最长的方法是其生命周期中的 onCreate 方法，其中在 onCreate 方法中，耗时比较长的方法有：  &lt;strong&gt;initMudleFactory、initURS、Unicorn.init、initUmeng。&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#22270;5.png"&gt;   &lt;img alt="&amp;#22270;5" height="184" src="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#22270;5.png" width="600"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;在 Top Down 视图中，可以更加直观的看出，此次采样，也正是这四个方法耗时最多。&lt;/p&gt;
 &lt;p&gt;通过源码排查，这是个方法，均是第三方 SDK 的初始化，同时在这几个 SDK 内部，都含有较多的 IO 操作，并且内部实现了线程管理以保证线程安全，所以可以将这几个 SDK 的初始化，放在子线程中完成。这里以友盟 SDK 为例：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;/**
* 友盟SDK中有涉及到线程不安全的地方，都自己维护了线程，保证线程安全
**/
try {
    var6 = getClass(&amp;quot;com.umeng.umzid.ZIDManager&amp;quot;);
    if (var6 == null) {
        Log.e(&amp;quot;UMConfigure&amp;quot;, &amp;quot;---&amp;gt;&amp;gt;&amp;gt; SDK 初始化失败，请检查是否集成umeng-asms-1.2.x.aar库。&amp;lt;&amp;lt;&amp;lt;--- &amp;quot;);
        (new Thread() {
            public void run() {
                try {
                    Looper.prepare();
                    Toast.makeText(var5, &amp;quot;SDK 初始化失败，请检查是否集成umeng-asms-1.2.X.aar库。&amp;quot;, 1).show();
                    Looper.loop();
                } catch (Throwable var2) {
                }

            }
        }).start();
        return;
    }
} catch (Throwable var27) {
}

/**
* 在友盟SDK内部中有很多IO操作的地方，和加锁操作，所以可以将SDK初始化操作，放在子线程中
**/
if (!TextUtils.isEmpty(var1)) {
    sAppkey = var1;
    sChannel = var2;
    UMGlobalContext.getInstance(var3);
    k.a(var3);
    if (!needSendZcfgEnv(var3)) {
        FieldManager var4 = FieldManager.a();
        var4.a(var3);
    }

    synchronized(PreInitLock) {
        preInitComplete = true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;最终，我们可以把上面提到的几个 SDK 初始化工作放入在子线程中：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;private void initSDKAsyn(){
    new Thread(() -&amp;gt; {
        if (Util.inMainProcess()){
            // 登录
            initURS();

            if (BuildConfig.ENTERPRISE) {
                Unicorn.init(BaseApplication.this, &amp;quot;&amp;quot;, QiyuOptionConfig.options(), new QiyuImageLoader());
                initModuleRegister();
            } else {
                Unicorn.init(BaseApplication.this, &amp;quot;&amp;quot;, QiyuOptionConfig.options(), new QiyuImageLoader());
            }

            // 初始化下载服务
            try {
                initDownload();
            } catch (Exception e) {
                NTLog.f(TAG, e.toString());
            }
        }
        initModuleFactory();
        initUmeng();
    }).start();
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;  &lt;strong&gt;对于一些必须在主线程中初始化完成的 SDK，可以考虑使用 IdleHandler，在主线程空闲时，完成初始化（关于 IdleHandler 会在下面讲到）。&lt;/strong&gt;&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;2.2.3 Launcher Activity 优化&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;auncher Activity 是 WelcomeActivity，在对 Application 优化结束后，再对 WelcomeActivity 进行优化，还是和上路的思路一样，先通过 trace 文件追踪：&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#22270;6.png"&gt;   &lt;img alt="&amp;#22270;6" height="337" src="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#22270;6.png" width="700"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;可以看到，在 WelcomeActivity 的 onCreate 方法中，耗时较多的三个地方，分别是：initActionBar、EventBus.register、setContentView，下面针对这三块内容，分别进行对应的优化操作：&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#22270;72.png"&gt;   &lt;img alt="&amp;#22270;7" height="132" src="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#22270;72.png" width="700"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;   &lt;strong&gt;（1）initActionBar&lt;/strong&gt;&lt;/p&gt;
  &lt;p&gt;在上图中，可以看到，initActionBar 中最耗时的操作是 getSupportActionBar，通过研究代码发现，在WelcomeActivity中，并不需要操作 actionBar，所以直接复写父类方法，去掉 super 调用即可。&lt;/p&gt;
  &lt;p&gt;   &lt;strong&gt;（2）EventBus.register&lt;/strong&gt;&lt;/p&gt;
  &lt;p&gt;EventBus 注册时，性能较差，是因为在改过程中涉及到大量的反射操作，所以对性能损耗较大。通过查看官方文档，该问题在 EventBus3.0 中得到了很好的处理，主要是通过 apt 技术增加索引，提升效率。（当前项目未升级版本，待后期优化）&lt;/p&gt;
  &lt;p&gt;   &lt;strong&gt;（3）setContentView&lt;/strong&gt;&lt;/p&gt;
  &lt;p&gt;setContentView 是 Activity 渲染布局时的必要方法，其耗时的点在于，解析 xml 布局文件时，使用了反射，所以如果 xml 布局文件非常复查的时候，可以使用androidx.asynclayoutinflater:asynclayoutinflater进行异步加载 xml 文件，使用方式如下：&lt;/p&gt;
  &lt;p&gt;   &lt;a href="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#20195;&amp;#30721;6.png"&gt;    &lt;img alt="&amp;#20195;&amp;#30721;6" height="171" src="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#20195;&amp;#30721;6.png" width="749"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;h2&gt;  &lt;strong&gt;三、优化总体方法汇总&lt;/strong&gt;&lt;/h2&gt;
 &lt;p&gt;上面针对冷启动优化是基于当前项目本身做的步骤，这里汇总一些冷启动通用的  &lt;strong&gt;优化思路&lt;/strong&gt;：&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;（1）合理的使用异步初始化、延迟初始化和懒加载机制&lt;/strong&gt;：主要针对 Application 中各种 SDK 的初始化&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;（2）在主线程中应当避免很耗时的操作&lt;/strong&gt;，比如 IO 操作、数据库读写操作&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;（3）简化 launcher Activity 的布局结构&lt;/strong&gt;，如果非常复杂的布局，可以有以下两种方式进行优化：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;建议使用约束布局（ConstraintLayout）来减少布局嵌套避免过度渲染。&lt;/li&gt;
  &lt;li&gt;使用 androidx.asynclayoutinflater:asynclayoutinflater 进行异步加载 xml 文件。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;strong&gt;（4）合理使用 IdleHandler 进行延迟初始化&lt;/strong&gt;，使用方式如下：&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#20195;&amp;#30721;71.png"&gt;   &lt;img alt="&amp;#20195;&amp;#30721;7" height="439" src="http://techblog.youdao.com/wp-content/uploads/2021/07/&amp;#20195;&amp;#30721;71.png" width="786"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;（5）开始严苛模式（StrictMode）&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;该模式并不能帮我们自动优化性能，而是可以帮助我们检测出我们可能无意中或者一些第三方 SDK 中做的会阻塞 Main 线程的事情（比如磁盘操作、网络操作），并将它们提醒出来，以便在开发阶段进行修复。其检测策略有线程检测策略和虚拟机检测策略，我们可以设置需要检测的操作，当代码操作违规时，可以通过 Logcat 或者直接崩溃的形式提醒我们，  &lt;strong&gt;具体使用方式如下&lt;/strong&gt;：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;/**
 * 开启严苛模式，当代码有违规操作时，可以通过Logcat或崩溃的方式提醒我们
 */
private void startStrictMode() {
    if (BuildConfig.DEBUG) { //一定要在Debug模式下使用，避免在生产环境中发生不必要的奔溃和日志输出

        //线程检测策略
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectDiskReads()  //检测主线程磁盘读取操作
                .detectDiskWrites() //检测主线程磁盘写入操作
                .detectNetwork() //检测主线程网络请求操作
                .penaltyLog() //违规操作以log形式输出
                .build());

        //虚拟机检测策略
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                .detectLeakedSqlLiteObjects() //检测SqlLite泄漏
                .detectLeakedClosableObjects() //检测未关闭的closable对象泄漏
                .penaltyDeath() //发生违规操作时，直接奔溃
                .build());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>未分类</category>
      <guid isPermaLink="true">https://itindex.net/detail/61661-%E4%B8%AD%E5%9B%BD%E5%A4%A7%E5%AD%A6-mooc-android</guid>
      <pubDate>Thu, 05 Aug 2021 10:09:04 CST</pubDate>
    </item>
    <item>
      <title>写给中高级前端关于性能优化的9大策略和6大指标 | 网易四年实践</title>
      <link>https://itindex.net/detail/61610-%E5%89%8D%E7%AB%AF-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96-%E7%AD%96%E7%95%A5</link>
      <description>&lt;h3&gt;前言&lt;/h3&gt; &lt;p&gt;笔者近半年一直在参与项目重构，在重构过程中大量应用  &lt;strong&gt;性能优化&lt;/strong&gt;和  &lt;strong&gt;设计模式&lt;/strong&gt;两方面的知识。  &lt;strong&gt;性能优化&lt;/strong&gt;和  &lt;strong&gt;设计模式&lt;/strong&gt;两方面的知识不管在工作还是面试时都是高频应用场景，趁着这次参与大规模项目重构的机会，笔者认真梳理出一些常规且必用的  &lt;code&gt;性能优化建议&lt;/code&gt;，同时结合日常开发经验整理出笔者在网易四年来实践到的认为有用的所有  &lt;code&gt;性能优化建议&lt;/code&gt;，与大家一起分享分享！(由于篇幅有限，那  &lt;code&gt;设计模式&lt;/code&gt;在后面再专门出一篇文章呗)&lt;/p&gt; &lt;p&gt;可能有些  &lt;code&gt;性能优化建议&lt;/code&gt;已被大家熟知，不过也不影响这次分享，当然笔者也将一些平时可能不会注意的细节罗列出来。&lt;/p&gt; &lt;p&gt;平时大家认为  &lt;code&gt;性能优化&lt;/code&gt;是一种无序的应用场景，但在笔者看来它是一种有序的应用场景且很多  &lt;code&gt;性能优化&lt;/code&gt;都是互相铺垫甚至一带一路。从过程趋势来看，  &lt;code&gt;性能优化&lt;/code&gt;可分为  &lt;strong&gt;网络层面&lt;/strong&gt;和  &lt;strong&gt;渲染层面&lt;/strong&gt;；从结果趋势来看，  &lt;code&gt;性能优化&lt;/code&gt;可分为  &lt;strong&gt;时间层面&lt;/strong&gt;和  &lt;strong&gt;体积层面&lt;/strong&gt;。简单来说就是  &lt;code&gt;要在访问网站时使其快准狠地立马呈现在用户眼前&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#24615;&amp;#33021;&amp;#20248;&amp;#21270;.png" src="https://segmentfault.com/img/remote/1460000040343059" title="&amp;#24615;&amp;#33021;&amp;#20248;&amp;#21270;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;所有的  &lt;code&gt;性能优化&lt;/code&gt;都围绕着  &lt;code&gt;两大层面两小层面&lt;/code&gt;实现，核心层面是  &lt;code&gt;网络层面&lt;/code&gt;和  &lt;code&gt;渲染层面&lt;/code&gt;，辅助层面是  &lt;code&gt;时间层面&lt;/code&gt;和  &lt;code&gt;体积层面&lt;/code&gt;，而辅助层面则充满在核心层面里。于是笔者通过本文整理出关于前端  &lt;code&gt;性能优化&lt;/code&gt;的  &lt;strong&gt;九大策略&lt;/strong&gt;和  &lt;strong&gt;六大指标&lt;/strong&gt;。当然这些  &lt;code&gt;策略&lt;/code&gt;和  &lt;code&gt;指标&lt;/code&gt;都是笔者自己定义，方便通过某种方式为性能优化做一些规范。&lt;/p&gt; &lt;p&gt;因此在工作或面试时结合这些特征就能完美地诠释  &lt;code&gt;性能优化&lt;/code&gt;所延伸出来的知识了。  &lt;strong&gt;前方高能，不看也得收藏，走起！！！&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;所有代码示例为了凸显主题，只展示核心配置代码，其他配置并未补上，请自行脑补&lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;九大策略&lt;/h3&gt; &lt;h4&gt;网络层面&lt;/h4&gt; &lt;p&gt;  &lt;strong&gt;网络层面&lt;/strong&gt;的性能优化，无疑是如何让资源  &lt;code&gt;体积更小加载更快&lt;/code&gt;，因此笔者从以下四方面做出建议。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;构建策略&lt;/strong&gt;：基于构建工具(   &lt;code&gt;Webpack/Rollup/Parcel/Esbuild/Vite/Gulp&lt;/code&gt;)&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;图像策略&lt;/strong&gt;：基于图像类型(   &lt;code&gt;JPG/PNG/SVG/WebP/Base64&lt;/code&gt;)&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;分发策略&lt;/strong&gt;：基于内容分发网络(   &lt;code&gt;CDN&lt;/code&gt;)&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;缓存策略&lt;/strong&gt;：基于浏览器缓存(   &lt;code&gt;强缓存/协商缓存&lt;/code&gt;)&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;上述四方面都是一步接着一步完成，充满在整个项目流程里。  &lt;strong&gt;构建策略&lt;/strong&gt;和  &lt;strong&gt;图像策略&lt;/strong&gt;处于开发阶段，  &lt;strong&gt;分发策略&lt;/strong&gt;和  &lt;strong&gt;缓存策略&lt;/strong&gt;处于生产阶段，因此在每个阶段都可检查是否按顺序接入上述策略。通过这种方式就能最大限度增加  &lt;code&gt;性能优化&lt;/code&gt;应用场景。&lt;/p&gt; &lt;h5&gt;构建策略&lt;/h5&gt; &lt;p&gt;该策略主要围绕  &lt;code&gt;webpack&lt;/code&gt;做相关处理，同时也是接入最普遍的  &lt;code&gt;性能优化策略&lt;/code&gt;。其他构建工具的处理也是大同小异，可能只是配置上不一致。说到  &lt;code&gt;webpack&lt;/code&gt;的  &lt;code&gt;性能优化&lt;/code&gt;，无疑是从  &lt;code&gt;时间层面&lt;/code&gt;和  &lt;code&gt;体积层面&lt;/code&gt;入手。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;笔者发现目前webpack v5整体兼容性还不是特别好，某些功能配合第三方工具可能出现问题，故暂未升级到v5，继续使用v4作为生产工具，故以下配置均基于v4，但总体与v5的配置出入不大&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;笔者对两层面分别做出6个  &lt;code&gt;性能优化建议&lt;/code&gt;总共12个  &lt;code&gt;性能优化建议&lt;/code&gt;，为了方便记忆均使用四字词语概括，方便大家消化。⏱表示  &lt;code&gt;减少打包时间&lt;/code&gt;，表示  &lt;code&gt;减少打包体积&lt;/code&gt;。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;减少打包时间&lt;/strong&gt;：   &lt;code&gt;缩减范围&lt;/code&gt;、   &lt;code&gt;缓存副本&lt;/code&gt;、   &lt;code&gt;定向搜索&lt;/code&gt;、   &lt;code&gt;提前构建&lt;/code&gt;、   &lt;code&gt;并行构建&lt;/code&gt;、   &lt;code&gt;可视结构&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;减少打包体积&lt;/strong&gt;：   &lt;code&gt;分割代码&lt;/code&gt;、   &lt;code&gt;摇树优化&lt;/code&gt;、   &lt;code&gt;动态垫片&lt;/code&gt;、   &lt;code&gt;按需加载&lt;/code&gt;、   &lt;code&gt;作用提升&lt;/code&gt;、   &lt;code&gt;压缩资源&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;blockquote&gt;⏱缩减范围&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;配置include/exclude缩小Loader对文件的搜索范围&lt;/strong&gt;，好处是  &lt;code&gt;避免不必要的转译&lt;/code&gt;。  &lt;code&gt;node_modules目录&lt;/code&gt;的体积这么大，那得增加多少时间成本去检索所有文件啊？&lt;/p&gt; &lt;p&gt;  &lt;code&gt;include/exclude&lt;/code&gt;通常在各大  &lt;code&gt;Loader&lt;/code&gt;里配置，  &lt;code&gt;src目录&lt;/code&gt;通常作为源码目录，可做如下处理。当然  &lt;code&gt;include/exclude&lt;/code&gt;可根据实际情况修改。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;export default {
    // ...
    module: {
        rules: [{
            exclude: /node_modules/,
            include: /src/,
            test: /\.js$/,
            use: &amp;quot;babel-loader&amp;quot;
        }]
    }
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;⏱缓存副本&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;配置cache缓存Loader对文件的编译副本&lt;/strong&gt;，好处是  &lt;code&gt;再次编译时只编译修改过的文件&lt;/code&gt;。未修改过的文件干嘛要随着修改过的文件重新编译呢？&lt;/p&gt; &lt;p&gt;大部分  &lt;code&gt;Loader/Plugin&lt;/code&gt;都会提供一个可使用编译缓存的选项，通常包含  &lt;code&gt;cache&lt;/code&gt;字眼。以  &lt;code&gt;babel-loader&lt;/code&gt;和  &lt;code&gt;eslint-webpack-plugin&lt;/code&gt;为例。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import EslintPlugin from &amp;quot;eslint-webpack-plugin&amp;quot;;

export default {
    // ...
    module: {
        rules: [{
            // ...
            test: /\.js$/,
            use: [{
                loader: &amp;quot;babel-loader&amp;quot;,
                options: { cacheDirectory: true }
            }]
        }]
    },
    plugins: [
        new EslintPlugin({ cache: true })
    ]
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;⏱定向搜索&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;配置resolve提高文件的搜索速度&lt;/strong&gt;，好处是  &lt;code&gt;定向指定必须文件路径&lt;/code&gt;。若某些第三方库以常规形式引入可能报错或希望程序自动索引特定类型文件都可通过该方式解决。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;alias&lt;/code&gt;映射模块路径，  &lt;code&gt;extensions&lt;/code&gt;表明文件后缀，  &lt;code&gt;noParse&lt;/code&gt;过滤无依赖文件。通常配置  &lt;code&gt;alias&lt;/code&gt;和  &lt;code&gt;extensions&lt;/code&gt;就足够。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;export default {
    // ...
    resolve: {
        alias: {
            &amp;quot;#&amp;quot;: AbsPath(&amp;quot;&amp;quot;), // 根目录快捷方式
            &amp;quot;@&amp;quot;: AbsPath(&amp;quot;src&amp;quot;), // src目录快捷方式
            swiper: &amp;quot;swiper/js/swiper.min.js&amp;quot;
        }, // 模块导入快捷方式
        extensions: [&amp;quot;.js&amp;quot;, &amp;quot;.ts&amp;quot;, &amp;quot;.jsx&amp;quot;, &amp;quot;.tsx&amp;quot;, &amp;quot;.json&amp;quot;, &amp;quot;.vue&amp;quot;] // import路径时文件可省略后缀名
    }
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;⏱提前构建&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;配置DllPlugin将第三方依赖提前打包&lt;/strong&gt;，好处是  &lt;code&gt;将DLL与业务代码完全分离且每次只构建业务代码&lt;/code&gt;。这是一个古老配置，在  &lt;code&gt;webpack v2&lt;/code&gt;时已存在，不过现在  &lt;code&gt;webpack v4+&lt;/code&gt;已不推荐使用该配置，因为其版本迭代带来的性能提升足以忽略  &lt;code&gt;DllPlugin&lt;/code&gt;所带来的效益。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;DLL&lt;/strong&gt;意为  &lt;code&gt;动态链接库&lt;/code&gt;，指一个包含可由多个程序同时使用的代码库。在前端领域里可认为是另类缓存的存在，它把公共代码打包为DLL文件并存到硬盘里，再次打包时动态链接  &lt;code&gt;DLL文件&lt;/code&gt;就无需再次打包那些公共代码，从而提升构建速度，减少打包时间。&lt;/p&gt; &lt;p&gt;配置  &lt;code&gt;DLL&lt;/code&gt;总体来说相比其他配置复杂，配置流程可大致分为三步。&lt;/p&gt; &lt;p&gt;首先告知构建脚本哪些依赖做成  &lt;code&gt;DLL&lt;/code&gt;并生成  &lt;code&gt;DLL文件&lt;/code&gt;和  &lt;code&gt;DLL映射表文件&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import { DefinePlugin, DllPlugin } from &amp;quot;webpack&amp;quot;;

export default {
    // ...
    entry: {
        vendor: [&amp;quot;react&amp;quot;, &amp;quot;react-dom&amp;quot;, &amp;quot;react-router-dom&amp;quot;]
    },
    mode: &amp;quot;production&amp;quot;,
    optimization: {
        splitChunks: {
            cacheGroups: {
                vendor: {
                    chunks: &amp;quot;all&amp;quot;,
                    name: &amp;quot;vendor&amp;quot;,
                    test: /node_modules/
                }
            }
        }
    },
    output: {
        filename: &amp;quot;[name].dll.js&amp;quot;, // 输出路径和文件名称
        library: &amp;quot;[name]&amp;quot;, // 全局变量名称：其他模块会从此变量上获取里面模块
        path: AbsPath(&amp;quot;dist/static&amp;quot;) // 输出目录路径
    },
    plugins: [
        new DefinePlugin({
            &amp;quot;process.env.NODE_ENV&amp;quot;: JSON.stringify(&amp;quot;development&amp;quot;) // DLL模式下覆盖生产环境成开发环境(启动第三方依赖调试模式)
        }),
        new DllPlugin({
            name: &amp;quot;[name]&amp;quot;, // 全局变量名称：减小搜索范围，与output.library结合使用
            path: AbsPath(&amp;quot;dist/static/[name]-manifest.json&amp;quot;) // 输出目录路径
        })
    ]
};&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;然后在  &lt;code&gt;package.json&lt;/code&gt;里配置执行脚本且每次构建前首先执行该脚本打包出  &lt;code&gt;DLL文件&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;{
    &amp;quot;scripts&amp;quot;: {
        &amp;quot;dll&amp;quot;: &amp;quot;webpack --config webpack.dll.js&amp;quot;
    }
}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;最后链接  &lt;code&gt;DLL文件&lt;/code&gt;并告知  &lt;code&gt;webpack&lt;/code&gt;可命中的  &lt;code&gt;DLL文件&lt;/code&gt;让其自行读取。使用  &lt;a href="https://github.com/jharris4/html-webpack-tags-plugin" rel="nofollow noreferrer"&gt;html-webpack-tags-plugin&lt;/a&gt;在打包时自动插入  &lt;code&gt;DLL文件&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import { DllReferencePlugin } from &amp;quot;webpack&amp;quot;;
import HtmlTagsPlugin from &amp;quot;html-webpack-tags-plugin&amp;quot;;

export default {
    // ...
    plugins: [
        // ...
        new DllReferencePlugin({
            manifest: AbsPath(&amp;quot;dist/static/vendor-manifest.json&amp;quot;) // manifest文件路径
        }),
        new HtmlTagsPlugin({
            append: false, // 在生成资源后插入
            publicPath: &amp;quot;/&amp;quot;, // 使用公共路径
            tags: [&amp;quot;static/vendor.dll.js&amp;quot;] // 资源路径
        })
    ]
};&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;为了那几秒钟的时间成本，笔者建议配置上较好。当然也可使用  &lt;a href="https://github.com/asfktz/autodll-webpack-plugin" rel="nofollow noreferrer"&gt;autodll-webpack-plugin&lt;/a&gt;代替手动配置。&lt;/p&gt; &lt;blockquote&gt;⏱并行构建&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;配置Thread将Loader单进程转换为多进程&lt;/strong&gt;，好处是  &lt;code&gt;释放CPU多核并发的优势&lt;/code&gt;。在使用  &lt;code&gt;webpack&lt;/code&gt;构建项目时会有大量文件需解析和处理，构建过程是计算密集型的操作，随着文件增多会使构建过程变得越慢。&lt;/p&gt; &lt;p&gt;运行在  &lt;code&gt;Node&lt;/code&gt;里的  &lt;code&gt;webpack&lt;/code&gt;是单线程模型，简单来说就是  &lt;code&gt;webpack&lt;/code&gt;待处理的任务需一件件处理，不能同一时刻处理多件任务。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;文件读写&lt;/code&gt;与  &lt;code&gt;计算操作&lt;/code&gt;无法避免，能不能让  &lt;code&gt;webpack&lt;/code&gt;同一时刻处理多个任务，发挥多核  &lt;code&gt;CPU&lt;/code&gt;电脑的威力以提升构建速度呢？  &lt;a href="https://github.com/webpack-contrib/thread-loader" rel="nofollow noreferrer"&gt;thread-loader&lt;/a&gt;来帮你，根据  &lt;code&gt;CPU&lt;/code&gt;个数开启线程。&lt;/p&gt; &lt;p&gt;在此需注意一个问题，若项目文件不算多就不要使用该  &lt;code&gt;性能优化建议&lt;/code&gt;，毕竟开启多个线程也会存在性能开销。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import Os from &amp;quot;os&amp;quot;;

export default {
    // ...
    module: {
        rules: [{
            // ...
            test: /\.js$/,
            use: [{
                loader: &amp;quot;thread-loader&amp;quot;,
                options: { workers: Os.cpus().length }
            }, {
                loader: &amp;quot;babel-loader&amp;quot;,
                options: { cacheDirectory: true }
            }]
        }]
    }
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;⏱可视结构&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;配置BundleAnalyzer分析打包文件结构&lt;/strong&gt;，好处是  &lt;code&gt;找出导致体积过大的原因&lt;/code&gt;。从而通过分析原因得出优化方案减少构建时间。  &lt;code&gt;BundleAnalyzer&lt;/code&gt;是  &lt;code&gt;webpack&lt;/code&gt;官方插件，可直观分析  &lt;code&gt;打包文件&lt;/code&gt;的模块组成部分、模块体积占比、模块包含关系、模块依赖关系、文件是否重复、压缩体积对比等可视化数据。&lt;/p&gt; &lt;p&gt;可使用  &lt;a href="https://github.com/webpack-contrib/webpack-bundle-analyzer" rel="nofollow noreferrer"&gt;webpack-bundle-analyzer&lt;/a&gt;配置，有了它，我们就能快速找到相关问题。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import { BundleAnalyzerPlugin } from &amp;quot;webpack-bundle-analyzer&amp;quot;;

export default {
    // ...
    plugins: [
        // ...
        BundleAnalyzerPlugin()
    ]
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;分割代码&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;分割各个模块代码，提取相同部分代码&lt;/strong&gt;，好处是  &lt;code&gt;减少重复代码的出现频率&lt;/code&gt;。  &lt;code&gt;webpack v4&lt;/code&gt;使用  &lt;code&gt;splitChunks&lt;/code&gt;替代  &lt;code&gt;CommonsChunksPlugin&lt;/code&gt;实现代码分割。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;splitChunks&lt;/code&gt;配置较多，详情可参考  &lt;a href="https://webpack.docschina.org/configuration/optimization/#optimizationsplitchunks" rel="nofollow noreferrer"&gt;官网&lt;/a&gt;，在此笔者贴上常用配置。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;export default {
    // ...
    optimization: {
        runtimeChunk: { name: &amp;quot;manifest&amp;quot; }, // 抽离WebpackRuntime函数
        splitChunks: {
            cacheGroups: {
                common: {
                    minChunks: 2,
                    name: &amp;quot;common&amp;quot;,
                    priority: 5,
                    reuseExistingChunk: true, // 重用已存在代码块
                    test: AbsPath(&amp;quot;src&amp;quot;)
                },
                vendor: {
                    chunks: &amp;quot;initial&amp;quot;, // 代码分割类型
                    name: &amp;quot;vendor&amp;quot;, // 代码块名称
                    priority: 10, // 优先级
                    test: /node_modules/ // 校验文件正则表达式
                }
            }, // 缓存组
            chunks: &amp;quot;all&amp;quot; // 代码分割类型：all全部模块，async异步模块，initial入口模块
        } // 代码块分割
    }
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;摇树优化&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;删除项目中未被引用代码&lt;/strong&gt;，好处是  &lt;code&gt;移除重复代码和未使用代码&lt;/code&gt;。  &lt;code&gt;摇树优化&lt;/code&gt;首次出现于  &lt;code&gt;rollup&lt;/code&gt;，是  &lt;code&gt;rollup&lt;/code&gt;的核心概念，后来在  &lt;code&gt;webpack v2&lt;/code&gt;里借鉴过来使用。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;摇树优化&lt;/code&gt;只对  &lt;code&gt;ESM规范&lt;/code&gt;生效，对其他模块规范失效。  &lt;code&gt;摇树优化&lt;/code&gt;针对静态结构分析，只有  &lt;code&gt;import/export&lt;/code&gt;才能提供静态的  &lt;code&gt;导入/导出&lt;/code&gt;功能。因此在编写业务代码时必须使用  &lt;code&gt;ESM规范&lt;/code&gt;才能让  &lt;code&gt;摇树优化&lt;/code&gt;移除重复代码和未使用代码。&lt;/p&gt; &lt;p&gt;在  &lt;code&gt;webpack&lt;/code&gt;里只需将打包环境设置成  &lt;code&gt;生产环境&lt;/code&gt;就能让  &lt;code&gt;摇树优化&lt;/code&gt;生效，同时业务代码使用  &lt;code&gt;ESM规范&lt;/code&gt;编写，使用  &lt;code&gt;import&lt;/code&gt;导入模块，使用  &lt;code&gt;export&lt;/code&gt;导出模块。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;export default {
    // ...
    mode: &amp;quot;production&amp;quot;
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;动态垫片&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;通过垫片服务根据UA返回当前浏览器代码垫片&lt;/strong&gt;，好处是  &lt;code&gt;无需将繁重的代码垫片打包进去&lt;/code&gt;。每次构建都配置  &lt;code&gt;@babel/preset-env&lt;/code&gt;和  &lt;code&gt;core-js&lt;/code&gt;根据某些需求将  &lt;code&gt;Polyfill&lt;/code&gt;打包进来，这无疑又为代码体积增加了贡献。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;@babel/preset-env&lt;/code&gt;提供的  &lt;code&gt;useBuiltIns&lt;/code&gt;可按需导入  &lt;code&gt;Polyfill&lt;/code&gt;。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;false&lt;/strong&gt;：无视   &lt;code&gt;target.browsers&lt;/code&gt;将所有   &lt;code&gt;Polyfill&lt;/code&gt;加载进来&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;entry&lt;/strong&gt;：根据   &lt;code&gt;target.browsers&lt;/code&gt;将部分   &lt;code&gt;Polyfill&lt;/code&gt;加载进来(仅引入有浏览器不支持的   &lt;code&gt;Polyfill&lt;/code&gt;，需在入口文件   &lt;code&gt;import &amp;quot;core-js/stable&amp;quot;&lt;/code&gt;)&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;usage&lt;/strong&gt;：根据   &lt;code&gt;target.browsers&lt;/code&gt;和检测代码里ES6的使用情况将部分   &lt;code&gt;Polyfill&lt;/code&gt;加载进来(无需在入口文件   &lt;code&gt;import &amp;quot;core-js/stable&amp;quot;&lt;/code&gt;)&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;在此推荐大家使用  &lt;code&gt;动态垫片&lt;/code&gt;。  &lt;code&gt;动态垫片&lt;/code&gt;可根据浏览器  &lt;code&gt;UserAgent&lt;/code&gt;返回当前浏览器  &lt;code&gt;Polyfill&lt;/code&gt;，其思路是根据浏览器的  &lt;code&gt;UserAgent&lt;/code&gt;从  &lt;code&gt;browserlist&lt;/code&gt;查找出当前浏览器哪些特性缺乏支持从而返回这些特性的  &lt;code&gt;Polyfill&lt;/code&gt;。对这方面感兴趣的同学可参考  &lt;a href="https://github.com/Financial-Times/polyfill-library" rel="nofollow noreferrer"&gt;polyfill-library&lt;/a&gt;和  &lt;a href="https://github.com/Financial-Times/polyfill-service" rel="nofollow noreferrer"&gt;polyfill-service&lt;/a&gt;的源码。&lt;/p&gt; &lt;p&gt;在此提供两个  &lt;code&gt;动态垫片&lt;/code&gt;服务，可在不同浏览器里点击以下链接看看输出不同的  &lt;code&gt;Polyfill&lt;/code&gt;。相信  &lt;code&gt;IExplore&lt;/code&gt;还是最多  &lt;code&gt;Polyfill&lt;/code&gt;的，它自豪地说：  &lt;code&gt;我就是我，不一样的烟火&lt;/code&gt;。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;官方CDN服务&lt;/strong&gt;：   &lt;a href="https://polyfill.io/v3/polyfill.min.js" rel="nofollow noreferrer"&gt;https://polyfill.io/v3/polyfill.min.js&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;阿里CDN服务&lt;/strong&gt;：   &lt;a href="https://polyfill.alicdn.com/polyfill.min.js" rel="nofollow noreferrer"&gt;https://polyfill.alicdn.com/polyfill.min.js&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;使用  &lt;a href="https://github.com/jharris4/html-webpack-tags-plugin" rel="nofollow noreferrer"&gt;html-webpack-tags-plugin&lt;/a&gt;在打包时自动插入  &lt;code&gt;动态垫片&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import HtmlTagsPlugin from &amp;quot;html-webpack-tags-plugin&amp;quot;;

export default {
    plugins: [
        new HtmlTagsPlugin({
            append: false, // 在生成资源后插入
            publicPath: false, // 使用公共路径
            tags: [&amp;quot;https://polyfill.alicdn.com/polyfill.min.js&amp;quot;] // 资源路径
        })
    ]
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;按需加载&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;将路由页面/触发性功能单独打包为一个文件，使用时才加载&lt;/strong&gt;，好处是  &lt;code&gt;减轻首屏渲染的负担&lt;/code&gt;。因为项目功能越多其打包体积越大，导致首屏渲染速度越慢。&lt;/p&gt; &lt;p&gt;首屏渲染时只需对应  &lt;code&gt;JS代码&lt;/code&gt;而无需其他  &lt;code&gt;JS代码&lt;/code&gt;，所以可使用  &lt;code&gt;按需加载&lt;/code&gt;。  &lt;code&gt;webpack v4&lt;/code&gt;提供模块按需切割加载功能，配合  &lt;code&gt;import()&lt;/code&gt;可做到首屏渲染减包的效果，从而加快首屏渲染速度。只有当触发某些功能时才会加载当前功能的  &lt;code&gt;JS代码&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;webpack v4&lt;/code&gt;提供魔术注解命名  &lt;code&gt;切割模块&lt;/code&gt;，若无注解则切割出来的模块无法分辨出属于哪个业务模块，所以一般都是一个业务模块共用一个  &lt;code&gt;切割模块&lt;/code&gt;的注解名称。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;const Login = () =&amp;gt; import( /* webpackChunkName: &amp;quot;login&amp;quot; */ &amp;quot;../../views/login&amp;quot;);
const Logon = () =&amp;gt; import( /* webpackChunkName: &amp;quot;logon&amp;quot; */ &amp;quot;../../views/logon&amp;quot;);&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;运行起来控制台可能会报错，在  &lt;code&gt;package.json&lt;/code&gt;的  &lt;code&gt;babel&lt;/code&gt;相关配置里接入  &lt;a href="https://babeljs.io/docs/en/babel-plugin-syntax-dynamic-import.html" rel="nofollow noreferrer"&gt;@babel/plugin-syntax-dynamic-import&lt;/a&gt;即可。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;{
    // ...
    &amp;quot;babel&amp;quot;: {
        // ...
        &amp;quot;plugins&amp;quot;: [
            // ...
            &amp;quot;@babel/plugin-syntax-dynamic-import&amp;quot;
        ]
    }
}&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;作用提升&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;分析模块间依赖关系，把打包好的模块合并到一个函数中&lt;/strong&gt;，好处是  &lt;code&gt;减少函数声明和内存花销&lt;/code&gt;。  &lt;code&gt;作用提升&lt;/code&gt;首次出现于  &lt;code&gt;rollup&lt;/code&gt;，是  &lt;code&gt;rollup&lt;/code&gt;的核心概念，后来在  &lt;code&gt;webpack v3&lt;/code&gt;里借鉴过来使用。&lt;/p&gt; &lt;p&gt;在未开启  &lt;code&gt;作用提升&lt;/code&gt;前，构建后的代码会存在大量函数闭包。由于模块依赖，通过  &lt;code&gt;webpack&lt;/code&gt;打包后会转换成  &lt;code&gt;IIFE&lt;/code&gt;，大量函数闭包包裹代码会导致打包体积增大(  &lt;code&gt;模块越多越明显&lt;/code&gt;)。在运行代码时创建的函数作用域变多，从而导致更大的内存开销。&lt;/p&gt; &lt;p&gt;在开启  &lt;code&gt;作用提升&lt;/code&gt;后，构建后的代码会按照引入顺序放到一个函数作用域里，通过适当重命名某些变量以防止变量名冲突，从而减少函数声明和内存花销。&lt;/p&gt; &lt;p&gt;在  &lt;code&gt;webpack&lt;/code&gt;里只需将打包环境设置成  &lt;code&gt;生产环境&lt;/code&gt;就能让  &lt;code&gt;作用提升&lt;/code&gt;生效，或显式设置  &lt;code&gt;concatenateModules&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;export default {
    // ...
    mode: &amp;quot;production&amp;quot;
};
// 显式设置
export default {
    // ...
    optimization: {
        // ...
        concatenateModules: true
    }
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;压缩资源&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;压缩HTML/CSS/JS代码，压缩字体/图像/音频/视频&lt;/strong&gt;，好处是  &lt;code&gt;更有效减少打包体积&lt;/code&gt;。极致地优化代码都有可能不及优化一个资源文件的体积更有效。&lt;/p&gt; &lt;p&gt;针对  &lt;code&gt;HTML&lt;/code&gt;代码，使用  &lt;a href="https://github.com/jantimon/html-webpack-plugin" rel="nofollow noreferrer"&gt;html-webpack-plugin&lt;/a&gt;开启压缩功能。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import HtmlPlugin from &amp;quot;html-webpack-plugin&amp;quot;;

export default {
    // ...
    plugins: [
        // ...
        HtmlPlugin({
            // ...
            minify: {
                collapseWhitespace: true,
                removeComments: true
            } // 压缩HTML
        })
    ]
};&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;针对  &lt;code&gt;CSS/JS&lt;/code&gt;代码，分别使用以下插件开启压缩功能。其中  &lt;code&gt;OptimizeCss&lt;/code&gt;基于  &lt;code&gt;cssnano&lt;/code&gt;封装，  &lt;code&gt;Uglifyjs&lt;/code&gt;和  &lt;code&gt;Terser&lt;/code&gt;都是  &lt;code&gt;webpack&lt;/code&gt;官方插件，同时需注意压缩  &lt;code&gt;JS代码&lt;/code&gt;需区分  &lt;code&gt;ES5&lt;/code&gt;和  &lt;code&gt;ES6&lt;/code&gt;。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;a href="https://github.com/NMFR/optimize-css-assets-webpack-plugin" rel="nofollow noreferrer"&gt;optimize-css-assets-webpack-plugin&lt;/a&gt;：压缩   &lt;code&gt;CSS代码&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://github.com/webpack-contrib/uglifyjs-webpack-plugin" rel="nofollow noreferrer"&gt;uglifyjs-webpack-plugin&lt;/a&gt;：压缩   &lt;code&gt;ES5&lt;/code&gt;版本的   &lt;code&gt;JS代码&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://github.com/webpack-contrib/terser-webpack-plugin" rel="nofollow noreferrer"&gt;terser-webpack-plugin&lt;/a&gt;：压缩   &lt;code&gt;ES6&lt;/code&gt;版本的   &lt;code&gt;JS代码&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;pre&gt;  &lt;code&gt;import OptimizeCssAssetsPlugin from &amp;quot;optimize-css-assets-webpack-plugin&amp;quot;;
import TerserPlugin from &amp;quot;terser-webpack-plugin&amp;quot;;
import UglifyjsPlugin from &amp;quot;uglifyjs-webpack-plugin&amp;quot;;

const compressOpts = type =&amp;gt; ({
    cache: true, // 缓存文件
    parallel: true, // 并行处理
    [`${type}Options`]: {
        beautify: false,
        compress: { drop_console: true }
    } // 压缩配置
});
const compressCss = new OptimizeCssAssetsPlugin({
    cssProcessorOptions: {
        autoprefixer: { remove: false }, // 设置autoprefixer保留过时样式
        safe: true // 避免cssnano重新计算z-index
    }
});
const compressJs = USE_ES6
    ? new TerserPlugin(compressOpts(&amp;quot;terser&amp;quot;))
    : new UglifyjsPlugin(compressOpts(&amp;quot;uglify&amp;quot;));

export default {
    // ...
    optimization: {
        // ...
        minimizer: [compressCss, compressJs] // 代码压缩
    }
};&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;针对  &lt;code&gt;字体/音频/视频&lt;/code&gt;文件，还真没相关  &lt;code&gt;Plugin&lt;/code&gt;供我们使用，就只能拜托大家在发布项目到生产服前使用对应的压缩工具处理了。针对  &lt;code&gt;图像&lt;/code&gt;文件，大部分  &lt;code&gt;Loader/Plugin&lt;/code&gt;封装时均使用了某些图像处理工具，而这些工具的某些功能又托管在国外服务器里，所以导致经常安装失败。具体解决方式可回看笔者曾经发布的  &lt;a href="https://juejin.cn/post/6844904192247595022" rel="nofollow noreferrer"&gt;《聊聊NPM镜像那些险象环生的坑》&lt;/a&gt;一文寻求答案。&lt;/p&gt; &lt;p&gt;鉴于此，笔者花了一点小技巧开发了一个  &lt;code&gt;Plugin&lt;/code&gt;用于配合  &lt;code&gt;webpack&lt;/code&gt;压缩图像，详情请参考  &lt;a href="https://github.com/JowayYoung/tinyimg-webpack-plugin" rel="nofollow noreferrer"&gt;tinyimg-webpack-plugin&lt;/a&gt;。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import TinyimgPlugin from &amp;quot;tinyimg-webpack-plugin&amp;quot;;

export default {
    // ...
    plugins: [
        // ...
        TinyimgPlugin()
    ]
};&lt;/code&gt;&lt;/pre&gt; &lt;hr&gt;&lt;/hr&gt; &lt;p&gt;上述  &lt;code&gt;构建策略&lt;/code&gt;都集成到笔者开源的  &lt;a href="https://github.com/JowayYoung/bruce-cli" rel="nofollow noreferrer"&gt;bruce-cli&lt;/a&gt;里，它是一个  &lt;strong&gt;React/Vue&lt;/strong&gt;应用自动化构建脚手架，其零配置开箱即用的优点非常适合入门级、初中级、快速开发项目的前端同学使用，还可通过创建  &lt;code&gt;brucerc.js&lt;/code&gt;文件覆盖其默认配置，只需专注业务代码的编写无需关注构建代码的编写，让项目结构更简洁。详情请戳  &lt;a href="https://github.com/JowayYoung/bruce-cli" rel="nofollow noreferrer"&gt;这里&lt;/a&gt;，使用时记得查看文档，支持一个  &lt;a href="https://github.com/JowayYoung/bruce-cli" rel="nofollow noreferrer"&gt;Star&lt;/a&gt;哈！&lt;/p&gt; &lt;h5&gt;图像策略&lt;/h5&gt; &lt;p&gt;该策略主要围绕  &lt;code&gt;图像类型&lt;/code&gt;做相关处理，同时也是接入成本较低的  &lt;code&gt;性能优化策略&lt;/code&gt;。只需做到以下两点即可。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;图像选型&lt;/strong&gt;：了解所有图像类型的特点及其何种应用场景最合适&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;图像压缩&lt;/strong&gt;：在部署到生产环境前使用工具或脚本对其压缩处理&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;  &lt;code&gt;图像选型&lt;/code&gt;一定要知道每种图像类型的  &lt;code&gt;体积/质量/兼容/请求/压缩/透明/场景&lt;/code&gt;等参数相对值，这样才能迅速做出判断在何种场景使用何种类型的图像。&lt;/p&gt; &lt;table&gt;  &lt;tr&gt;   &lt;th align="center"&gt;类型&lt;/th&gt;   &lt;th align="center"&gt;体积&lt;/th&gt;   &lt;th align="center"&gt;质量&lt;/th&gt;   &lt;th align="center"&gt;兼容&lt;/th&gt;   &lt;th align="center"&gt;请求&lt;/th&gt;   &lt;th align="center"&gt;压缩&lt;/th&gt;   &lt;th align="center"&gt;透明&lt;/th&gt;   &lt;th&gt;场景&lt;/th&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;JPG&lt;/td&gt;   &lt;td align="center"&gt;小&lt;/td&gt;   &lt;td align="center"&gt;中&lt;/td&gt;   &lt;td align="center"&gt;高&lt;/td&gt;   &lt;td align="center"&gt;是&lt;/td&gt;   &lt;td align="center"&gt;有损&lt;/td&gt;   &lt;td align="center"&gt;不支持&lt;/td&gt;   &lt;td&gt;背景图、轮播图、色彩丰富图&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;PNG&lt;/td&gt;   &lt;td align="center"&gt;大&lt;/td&gt;   &lt;td align="center"&gt;高&lt;/td&gt;   &lt;td align="center"&gt;高&lt;/td&gt;   &lt;td align="center"&gt;是&lt;/td&gt;   &lt;td align="center"&gt;无损&lt;/td&gt;   &lt;td align="center"&gt;支持&lt;/td&gt;   &lt;td&gt;图标、透明图&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;SVG&lt;/td&gt;   &lt;td align="center"&gt;小&lt;/td&gt;   &lt;td align="center"&gt;高&lt;/td&gt;   &lt;td align="center"&gt;高&lt;/td&gt;   &lt;td align="center"&gt;是&lt;/td&gt;   &lt;td align="center"&gt;无损&lt;/td&gt;   &lt;td align="center"&gt;支持&lt;/td&gt;   &lt;td&gt;图标、矢量图&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;WebP&lt;/td&gt;   &lt;td align="center"&gt;小&lt;/td&gt;   &lt;td align="center"&gt;中&lt;/td&gt;   &lt;td align="center"&gt;低&lt;/td&gt;   &lt;td align="center"&gt;是&lt;/td&gt;   &lt;td align="center"&gt;兼备&lt;/td&gt;   &lt;td align="center"&gt;支持&lt;/td&gt;   &lt;td&gt;看兼容情况&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;Base64&lt;/td&gt;   &lt;td align="center"&gt;看情况&lt;/td&gt;   &lt;td align="center"&gt;中&lt;/td&gt;   &lt;td align="center"&gt;高&lt;/td&gt;   &lt;td align="center"&gt;否&lt;/td&gt;   &lt;td align="center"&gt;无损&lt;/td&gt;   &lt;td align="center"&gt;支持&lt;/td&gt;   &lt;td&gt;图标&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt; &lt;p&gt;  &lt;code&gt;图像压缩&lt;/code&gt;可在上述  &lt;code&gt;构建策略-压缩资源&lt;/code&gt;里完成，也可自行使用工具完成。由于现在大部分  &lt;code&gt;webpack&lt;/code&gt;图像压缩工具不是安装失败就是各种环境问题(  &lt;code&gt;你懂的&lt;/code&gt;)，所以笔者还是推荐在发布项目到生产服前使用图像压缩工具处理，这样运行稳定也不会增加打包时间。&lt;/p&gt; &lt;p&gt;好用的图像压缩工具无非就是以下几个，若有更好用的工具麻烦在评论里补充喔！&lt;/p&gt; &lt;table&gt;  &lt;tr&gt;   &lt;th align="center"&gt;工具&lt;/th&gt;   &lt;th align="center"&gt;开源&lt;/th&gt;   &lt;th align="center"&gt;收费&lt;/th&gt;   &lt;th align="center"&gt;API&lt;/th&gt;   &lt;th&gt;免费体验&lt;/th&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;    &lt;a href="https://www.tuhaokuai.com" rel="nofollow noreferrer"&gt;QuickPicture&lt;/a&gt;&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td align="center"&gt;✔️&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td&gt;可压缩类型较多，压缩质感较好，有体积限制，有数量限制&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;    &lt;a href="https://shrinkme.app" rel="nofollow noreferrer"&gt;ShrinkMe&lt;/a&gt;&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td&gt;可压缩类型较多，压缩质感一般，无数量限制，有体积限制&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;    &lt;a href="https://squoosh.app" rel="nofollow noreferrer"&gt;Squoosh&lt;/a&gt;&lt;/td&gt;   &lt;td align="center"&gt;✔️&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td align="center"&gt;✔️&lt;/td&gt;   &lt;td&gt;可压缩类型较少，压缩质感一般，无数量限制，有体积限制&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;    &lt;a href="https://tinyjpg.com" rel="nofollow noreferrer"&gt;TinyJpg&lt;/a&gt;&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td align="center"&gt;✔️&lt;/td&gt;   &lt;td align="center"&gt;✔️&lt;/td&gt;   &lt;td&gt;可压缩类型较少，压缩质感很好，有数量限制，有体积限制&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;    &lt;a href="https://tinypng.com" rel="nofollow noreferrer"&gt;TinyPng&lt;/a&gt;&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td align="center"&gt;✔️&lt;/td&gt;   &lt;td align="center"&gt;✔️&lt;/td&gt;   &lt;td&gt;可压缩类型较少，压缩质感很好，有数量限制，有体积限制&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;    &lt;a href="https://zhitu.isux.us" rel="nofollow noreferrer"&gt;Zhitu&lt;/a&gt;&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td&gt;可压缩类型一般，压缩质感一般，有数量限制，有体积限制&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt; &lt;p&gt;若不想在网站里来回拖动图像文件，可使用笔者开源的图像批处理工具  &lt;a href="https://github.com/JowayYoung/img-master" rel="nofollow noreferrer"&gt;img-master&lt;/a&gt;代替，不仅有压缩功能，还有分组功能、标记功能和变换功能。目前笔者负责的全部项目都使用该工具处理，一直用一直爽！&lt;/p&gt; &lt;p&gt;  &lt;code&gt;图像策略&lt;/code&gt;也许处理一张图像就能完爆所有  &lt;code&gt;构建策略&lt;/code&gt;，因此是一种很廉价但极有效的  &lt;code&gt;性能优化策略&lt;/code&gt;。&lt;/p&gt; &lt;h5&gt;分发策略&lt;/h5&gt; &lt;p&gt;该策略主要围绕  &lt;code&gt;内容分发网络&lt;/code&gt;做相关处理，同时也是接入成本较高的  &lt;code&gt;性能优化策略&lt;/code&gt;，需足够资金支持。&lt;/p&gt; &lt;p&gt;虽然接入成本较高，但大部分企业都会购买一些  &lt;code&gt;CDN服务器&lt;/code&gt;，所以在部署的事情上就不用过分担忧，尽管使用就好。该策略尽量遵循以下两点就能发挥  &lt;code&gt;CDN&lt;/code&gt;最大作用。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;所有静态资源走CDN&lt;/strong&gt;：开发阶段确定哪些文件属于静态资源&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;把静态资源与主页面置于不同域名下&lt;/strong&gt;：避免请求带上   &lt;code&gt;Cookie&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;  &lt;strong&gt;内容分发网络&lt;/strong&gt;简称  &lt;strong&gt;CDN&lt;/strong&gt;，指一组分布在各地存储数据副本并可根据就近原则满足数据请求的服务器。其核心特征是  &lt;code&gt;缓存&lt;/code&gt;和  &lt;code&gt;回源&lt;/code&gt;，缓存是把资源复制到  &lt;code&gt;CDN服务器&lt;/code&gt;里，回源是  &lt;code&gt;资源过期/不存在&lt;/code&gt;就向上层服务器请求并复制到  &lt;code&gt;CDN服务器&lt;/code&gt;里。&lt;/p&gt; &lt;p&gt;使用  &lt;code&gt;CDN&lt;/code&gt;可降低网络拥塞，提高用户访问响应速度和命中率。构建在现有网络基础上的智能虚拟网络，依靠部署在各地服务器，通过中心平台的调度、负载均衡、内容分发等功能模块，使用户就近获取所需资源，这就是  &lt;code&gt;CDN&lt;/code&gt;的终极使命。&lt;/p&gt; &lt;p&gt;基于  &lt;code&gt;CDN&lt;/code&gt;的  &lt;strong&gt;就近原则&lt;/strong&gt;所带来的优点，可将网站所有静态资源全部部署到  &lt;code&gt;CDN服务器&lt;/code&gt;里。那静态资源包括哪些文件？通常来说就是无需服务器产生计算就能得到的资源，例如不常变化的  &lt;code&gt;样式文件&lt;/code&gt;、  &lt;code&gt;脚本文件&lt;/code&gt;和  &lt;code&gt;多媒体文件(字体/图像/音频/视频)&lt;/code&gt;等。&lt;/p&gt; &lt;p&gt;若需单独配置  &lt;code&gt;CDN服务器&lt;/code&gt;，可考虑  &lt;a href="https://www.aliyun.com/product/oss" rel="nofollow noreferrer"&gt;阿里云OSS&lt;/a&gt;、  &lt;a href="https://www.163yun.com/product/nos" rel="nofollow noreferrer"&gt;网易树帆NOS&lt;/a&gt;和  &lt;a href="https://www.qiniu.com/products/kodo" rel="nofollow noreferrer"&gt;七牛云Kodo&lt;/a&gt;，当然配置起来还需购买该产品对应的  &lt;code&gt;CDN服务&lt;/code&gt;。由于篇幅问题，这些配置在购买后会有相关教程，可自行体会，在此就不再叙述了。&lt;/p&gt; &lt;p&gt;笔者推荐大家首选  &lt;a href="https://www.163yun.com/product/nos" rel="nofollow noreferrer"&gt;网易树帆NOS&lt;/a&gt;，毕竟对自家产品还是挺有信心的，不小心给自家产品打了个小广告了，哈哈！&lt;/p&gt; &lt;h5&gt;缓存策略&lt;/h5&gt; &lt;p&gt;该策略主要围绕  &lt;code&gt;浏览器缓存&lt;/code&gt;做相关处理，同时也使接入成本最低的  &lt;code&gt;性能优化策略&lt;/code&gt;。其显著减少网络传输所带来的损耗，提升网页访问速度，是一种很值得使用的  &lt;code&gt;性能优化策略&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;通过下图可知，为了让  &lt;code&gt;浏览器缓存&lt;/code&gt;发挥最大作用，该策略尽量遵循以下五点就能发挥  &lt;code&gt;浏览器缓存&lt;/code&gt;最大作用。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;考虑拒绝一切缓存策略&lt;/strong&gt;：   &lt;code&gt;Cache-Control:no-store&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;考虑资源是否每次向服务器请求&lt;/strong&gt;：   &lt;code&gt;Cache-Control:no-cache&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;考虑资源是否被代理服务器缓存&lt;/strong&gt;：   &lt;code&gt;Cache-Control:public/private&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;考虑资源过期时间&lt;/strong&gt;：   &lt;code&gt;Expires:t/Cache-Control:max-age=t,s-maxage=t&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;考虑协商缓存&lt;/strong&gt;：   &lt;code&gt;Last-Modified/Etag&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;  &lt;img alt="&amp;#32531;&amp;#23384;&amp;#21028;&amp;#26029;&amp;#26426;&amp;#21046;" src="https://segmentfault.com/img/remote/1460000040343060" title="&amp;#32531;&amp;#23384;&amp;#21028;&amp;#26029;&amp;#26426;&amp;#21046;"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;同时  &lt;code&gt;浏览器缓存&lt;/code&gt;也是高频面试题之一，笔者觉得上述涉及到的名词在不同语序串联下也能完全理解才能真正弄懂  &lt;code&gt;浏览器缓存&lt;/code&gt;在  &lt;code&gt;性能优化&lt;/code&gt;里起到的作用。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;缓存策略&lt;/code&gt;通过设置  &lt;code&gt;HTTP&lt;/code&gt;报文实现，在形式上分为  &lt;strong&gt;强缓存/强制缓存&lt;/strong&gt;和  &lt;strong&gt;协商缓存/对比缓存&lt;/strong&gt;。为了方便对比，笔者将某些细节使用图例展示，相信你有更好的理解。&lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#24378;&amp;#32531;&amp;#23384;.png" src="https://segmentfault.com/img/remote/1460000040343061" title="&amp;#24378;&amp;#32531;&amp;#23384;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#21327;&amp;#21830;&amp;#32531;&amp;#23384;.png" src="https://segmentfault.com/img/remote/1460000040343062" title="&amp;#21327;&amp;#21830;&amp;#32531;&amp;#23384;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;整个  &lt;code&gt;缓存策略&lt;/code&gt;机制很明了，  &lt;code&gt;先走强缓存，若命中失败才走协商缓存&lt;/code&gt;。若命中  &lt;code&gt;强缓存&lt;/code&gt;，直接使用  &lt;code&gt;强缓存&lt;/code&gt;；若未命中  &lt;code&gt;强缓存&lt;/code&gt;，发送请求到服务器检查是否命中  &lt;code&gt;协商缓存&lt;/code&gt;；若命中  &lt;code&gt;协商缓存&lt;/code&gt;，服务器返回304通知浏览器使用  &lt;code&gt;本地缓存&lt;/code&gt;，否则返回  &lt;code&gt;最新资源&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;有两种较常用的应用场景值得使用  &lt;code&gt;缓存策略&lt;/code&gt;一试，当然更多应用场景都可根据项目需求制定。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;频繁变动资源&lt;/strong&gt;：设置   &lt;code&gt;Cache-Control:no-cache&lt;/code&gt;，使浏览器每次都发送请求到服务器，配合   &lt;code&gt;Last-Modified/ETag&lt;/code&gt;验证资源是否有效&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;不常变化资源&lt;/strong&gt;：设置   &lt;code&gt;Cache-Control:max-age=31536000&lt;/code&gt;，对文件名哈希处理，当代码修改后生成新的文件名，当HTML文件引入文件名发生改变才会下载最新文件&lt;/li&gt;&lt;/ul&gt; &lt;h4&gt;渲染层面&lt;/h4&gt; &lt;p&gt;  &lt;strong&gt;渲染层面&lt;/strong&gt;的性能优化，无疑是如何让代码  &lt;code&gt;解析更好执行更快&lt;/code&gt;。因此笔者从以下五方面做出建议。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;CSS策略&lt;/strong&gt;：基于CSS规则&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;DOM策略&lt;/strong&gt;：基于DOM操作&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;阻塞策略&lt;/strong&gt;：基于脚本加载&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;回流重绘策略&lt;/strong&gt;：基于回流重绘&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;异步更新策略&lt;/strong&gt;：基于异步更新&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;上述五方面都是编写代码时完成，充满在整个项目流程的开发阶段里。因此在开发阶段需时刻注意以下涉及到的每一点，养成良好的开发习惯，  &lt;code&gt;性能优化&lt;/code&gt;也自然而然被使用上了。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;渲染层面&lt;/code&gt;的  &lt;code&gt;性能优化&lt;/code&gt;更多表现在编码细节上，而并非实体代码。简单来说就是遵循某些编码规则，才能将  &lt;code&gt;渲染层面&lt;/code&gt;的  &lt;code&gt;性能优化&lt;/code&gt;发挥到最大作用。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;回流重绘策略&lt;/strong&gt;在  &lt;code&gt;渲染层面&lt;/code&gt;的  &lt;code&gt;性能优化&lt;/code&gt;里占比较重，也是最常规的  &lt;code&gt;性能优化&lt;/code&gt;之一。上年笔者发布的掘金小册  &lt;a href="https://juejin.cn/book/6850413616484040711" rel="nofollow noreferrer"&gt;《玩转CSS的艺术之美》&lt;/a&gt;使用一整章讲解  &lt;code&gt;回流重绘&lt;/code&gt;，本章已开通试读，更多细节请戳  &lt;a href="https://juejin.cn/book/6850413616484040711/section/6850413616559194119" rel="nofollow noreferrer"&gt;这里&lt;/a&gt;。&lt;/p&gt; &lt;h5&gt;CSS策略&lt;/h5&gt; &lt;ul&gt;  &lt;li&gt;避免出现超过三层的   &lt;code&gt;嵌套规则&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;避免为   &lt;code&gt;ID选择器&lt;/code&gt;添加多余选择器&lt;/li&gt;  &lt;li&gt;避免使用   &lt;code&gt;标签选择器&lt;/code&gt;代替   &lt;code&gt;类选择器&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;避免使用   &lt;code&gt;通配选择器&lt;/code&gt;，只对目标节点声明规则&lt;/li&gt;  &lt;li&gt;避免重复匹配重复定义，关注   &lt;code&gt;可继承属性&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;h5&gt;DOM策略&lt;/h5&gt; &lt;ul&gt;  &lt;li&gt;缓存   &lt;code&gt;DOM计算属性&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;避免过多   &lt;code&gt;DOM操作&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;使用   &lt;code&gt;DOMFragment&lt;/code&gt;缓存批量化   &lt;code&gt;DOM操作&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;h5&gt;阻塞策略&lt;/h5&gt; &lt;ul&gt;  &lt;li&gt;脚本与   &lt;code&gt;DOM/其它脚本&lt;/code&gt;的依赖关系很强：对   &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;设置   &lt;code&gt;defer&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;脚本与   &lt;code&gt;DOM/其它脚本&lt;/code&gt;的依赖关系不强：对   &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;设置   &lt;code&gt;async&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;h5&gt;回流重绘策略&lt;/h5&gt; &lt;ul&gt;  &lt;li&gt;缓存   &lt;code&gt;DOM计算属性&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;使用类合并样式，避免逐条改变样式&lt;/li&gt;  &lt;li&gt;使用   &lt;code&gt;display&lt;/code&gt;控制   &lt;code&gt;DOM显隐&lt;/code&gt;，将   &lt;code&gt;DOM离线化&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;h5&gt;异步更新策略&lt;/h5&gt; &lt;ul&gt;  &lt;li&gt;在   &lt;code&gt;异步任务&lt;/code&gt;中修改   &lt;code&gt;DOM&lt;/code&gt;时把其包装成   &lt;code&gt;微任务&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;h3&gt;六大指标&lt;/h3&gt; &lt;p&gt;笔者根据  &lt;code&gt;性能优化&lt;/code&gt;的重要性和实际性划分出  &lt;code&gt;九大策略&lt;/code&gt;和  &lt;code&gt;六大指标&lt;/code&gt;，其实它们都是一条条活生生的  &lt;code&gt;性能优化建议&lt;/code&gt;。有些  &lt;code&gt;性能优化建议&lt;/code&gt;接不接入影响都不大，因此笔者将  &lt;code&gt;九大策略&lt;/code&gt;定位高于  &lt;code&gt;六大指标&lt;/code&gt;。针对  &lt;code&gt;九大策略&lt;/code&gt;还是建议在开发阶段和生产阶段接入，在项目复盘时可将  &lt;code&gt;六大指标&lt;/code&gt;的条条框框根据实际应用场景接入。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;六大指标&lt;/code&gt;基本囊括大部分  &lt;code&gt;性能优化&lt;/code&gt;细节，可作为  &lt;code&gt;九大策略&lt;/code&gt;的补充。笔者根据每条  &lt;code&gt;性能优化建议&lt;/code&gt;的特征将  &lt;code&gt;指标&lt;/code&gt;划分为以下六方面。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;加载优化&lt;/strong&gt;：资源在加载时可做的性能优化&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;执行优化&lt;/strong&gt;：资源在执行时可做的性能优化&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;渲染优化&lt;/strong&gt;：资源在渲染时可做的性能优化&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;样式优化&lt;/strong&gt;：样式在编码时可做的性能优化&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;脚本优化&lt;/strong&gt;：脚本在编码时可做的性能优化&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;V8引擎优化&lt;/strong&gt;：针对   &lt;code&gt;V8引擎&lt;/code&gt;特征可做的性能优化&lt;/li&gt;&lt;/ul&gt; &lt;h5&gt;加载优化&lt;/h5&gt; &lt;p&gt;  &lt;img alt="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#21152;&amp;#36733;&amp;#20248;&amp;#21270;.png" src="https://segmentfault.com/img/remote/1460000040343063" title="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#21152;&amp;#36733;&amp;#20248;&amp;#21270;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h5&gt;执行优化&lt;/h5&gt; &lt;p&gt;  &lt;img alt="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#25191;&amp;#34892;&amp;#20248;&amp;#21270;.png" src="https://segmentfault.com/img/remote/1460000040343064" title="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#25191;&amp;#34892;&amp;#20248;&amp;#21270;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h5&gt;渲染优化&lt;/h5&gt; &lt;p&gt;  &lt;img alt="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#28210;&amp;#26579;&amp;#20248;&amp;#21270;.png" src="https://segmentfault.com/img/remote/1460000040343065" title="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#28210;&amp;#26579;&amp;#20248;&amp;#21270;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h5&gt;样式优化&lt;/h5&gt; &lt;p&gt;  &lt;img alt="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#26679;&amp;#24335;&amp;#20248;&amp;#21270;.png" src="https://segmentfault.com/img/remote/1460000040343066" title="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#26679;&amp;#24335;&amp;#20248;&amp;#21270;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h5&gt;脚本优化&lt;/h5&gt; &lt;p&gt;  &lt;img alt="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#33050;&amp;#26412;&amp;#20248;&amp;#21270;.png" src="https://segmentfault.com/img/remote/1460000040343067" title="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#33050;&amp;#26412;&amp;#20248;&amp;#21270;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h5&gt;V8引擎优化&lt;/h5&gt; &lt;p&gt;  &lt;img alt="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-V8&amp;#24341;&amp;#25806;&amp;#20248;&amp;#21270;.png" src="https://segmentfault.com/img/remote/1460000040343068" title="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-V8&amp;#24341;&amp;#25806;&amp;#20248;&amp;#21270;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h3&gt;总结&lt;/h3&gt; &lt;p&gt;  &lt;strong&gt;性能优化&lt;/strong&gt;作为老生常谈的知识，必然会在工作或面试时遇上。很多时候不是想到某条  &lt;code&gt;性能优化建议&lt;/code&gt;就去做或答，而是要对这方面有一个整体认知，知道为何这样设计，这样设计的目的能达到什么效果。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;性能优化&lt;/code&gt;不是通过一篇文章就能全部讲完，若详细去讲可能要写两本书的篇幅才能讲完。本文能到给大家的就是一个方向一种态度，学以致用呗，希望阅读完本文会对你有所帮助。&lt;/p&gt; &lt;p&gt;最后，笔者将本文所有内容整理成一张高清脑图，由于体积太大无法上传，可关注笔者个人公众号  &lt;strong&gt;IQ前端&lt;/strong&gt;并回复  &lt;code&gt;性能优化&lt;/code&gt;获取口袋知识图谱吧！&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>前端 html css javascript 性能优化</category>
      <guid isPermaLink="true">https://itindex.net/detail/61610-%E5%89%8D%E7%AB%AF-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96-%E7%AD%96%E7%95%A5</guid>
      <pubDate>Wed, 14 Jul 2021 08:00:00 CST</pubDate>
    </item>
    <item>
      <title>HBase客户端访问超时原因及参数优化_Fang的博客-CSDN博客</title>
      <link>https://itindex.net/detail/61581-hbase-%E5%AE%A2%E6%88%B7%E7%AB%AF-%E8%AE%BF%E9%97%AE</link>
      <description>&lt;div&gt;    &lt;p&gt;默认的HBase客户端的参数配置是没有做过优化的，所以对于低延时响应的HBase集群，需要对客户端的参数进行优化。&lt;/p&gt;    &lt;h2&gt;hbase.rpc.timeout&lt;/h2&gt;    &lt;p&gt;以毫秒计算的所有HBase RPC超时，默认为60s。      &lt;br /&gt;该参数表示一次RPC请求的超时时间。如果某次RPC时间超过该值，客户端就会主动关闭socket。      &lt;br /&gt;如果经常出现java.io.IOException: Connection reset by peer异常问题，估计HBase集群出现了大量高并发读写业务或者服务器端发生了比较严重的Full GC等问题，导致某些请求无法得到及时处理，超过了设置的时间间隔。      &lt;br /&gt;根据实际情况，可以修改为5000，即5s。&lt;/p&gt;    &lt;h2&gt;hbase.client.retries.number&lt;/h2&gt;    &lt;p&gt;客户端重试最大次数。所有操作所使用的最大次数，例如，从根 RegionServer 获取根区域、获取单元格的值和启动行更新。      &lt;br /&gt;默认为35，可以设置为3。&lt;/p&gt;    &lt;h2&gt;hbase.client.pause&lt;/h2&gt;    &lt;p&gt;通用客户端暂停时间值(重试的休眠时间)。重试get失败或区域查找等操作前，经常使用的等待时间段。      &lt;br /&gt;HBase1.1版本开始此值默认为100ms，比较合理，如果你的版本不是，建议修改此值为100ms。&lt;/p&gt;    &lt;h2&gt;zookeeper.recovery.retry&lt;/h2&gt;    &lt;p&gt;zookeeper的重试次数，可调整为3次，zookeeper不轻易挂，且如果HBase集群出问题了，每次重试均会对zookeeper进行重试操作。      &lt;br /&gt;zookeeper的重试总次数是：&lt;/p&gt;    &lt;pre&gt;      &lt;code&gt;hbase.client.retries.number * zookeeper.recovery.retry。&lt;/code&gt;&lt;/pre&gt;    &lt;p&gt;并且每次重试的休眠时间均会呈2的指数级增长，每次访问HBase均会重试，在一次HBase操作中如果涉及多次zookeeper访问，则如果zookeeper不可用，则会出现很多次的zookeeper重试，非常浪费时间。&lt;/p&gt;    &lt;h2&gt;zookeeper.recovery.retry.intervalmill&lt;/h2&gt;    &lt;p&gt;zookeeper重试的休眠时间，默认为1s，可以减少，比如：200ms。&lt;/p&gt;    &lt;h2&gt;hbase.client.operation.timeout&lt;/h2&gt;    &lt;p&gt;该参数表示HBase客户端发起一次数据操作直至得到响应之间总的超时时间，数据操作类型包括get、append、increment、delete、put等。很显然，hbase.rpc.timeout表示一次RPC的超时时间，而hbase.client.operation.timeout则表示一次操作的超时时间，有可能包含多个RPC请求。      &lt;br /&gt;举个例子说明，比如一次Put请求，客户端首先会将请求封装为一个caller对象，该对象发送RPC请求到服务器，假如此时因为服务器端正好发生了严重的Full GC，导致这次RPC时间超时引起SocketTimeoutException，对应的就是hbase.rpc.timeout。那假如caller对象发送RPC请求之后刚好发生网络抖动，进而抛出网络异常，HBase客户端就会进行重试，重试多次之后如果总操作时间超时引起SocketTimeoutException，对应的就是hbase.client.operation.timeout。      &lt;br /&gt;默认为1200000，可以设置为30000，即30s。&lt;/p&gt;    &lt;h2&gt;hbase.regionserver.lease.period&lt;/h2&gt;    &lt;p&gt;hbase.client.operation.timeout参数规定的超时基本涉及到了HBase所有的数据操作，唯独没有scan操作。然而scan操作却是最有可能发生超时的，也是用户最为关心的。HBase特别考虑到了这点，并提供了一个单独的超时参数进行设置：hbase.client.scanner.timeout.period。      &lt;br /&gt;此参数指scan查询时每次与RegionServer交互的超时时间。      &lt;br /&gt;默认为60s，可不调整。HBase 1.1版本开始，此参数更名为hbase.client.scanner.timeout.period。      &lt;br /&gt;为了对这个参数更好地理解，我们演示一个scan的例子：&lt;/p&gt;    &lt;pre&gt; package com.zy.hbase;&lt;/pre&gt;    import java.io.IOException;
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.hbase.HBaseConfiguration;
    import org.apache.hadoop.hbase.KeyValue;
    import org.apache.hadoop.hbase.TableName;
    import org.apache.hadoop.hbase.client.Connection;
    import org.apache.hadoop.hbase.client.ConnectionFactory;
    import org.apache.hadoop.hbase.client.HTable;
    import org.apache.hadoop.hbase.client.Result;
    import org.apache.hadoop.hbase.client.ResultScanner;
    import org.apache.hadoop.hbase.client.Scan;
    import org.apache.hadoop.hbase.util.Bytes;
    public class KylinScan {
    /**
    * @param args
    * @throws IOException
    */
    public static void main(String[] args) throws IOException {
    scan(&amp;quot;kylin&amp;quot;);
    }

    @SuppressWarnings(&amp;quot;deprecation&amp;quot;)
    public static void scan(String tbl) throws IOException {
    Configurationconf = HBaseConfiguration.create();
    conf.set(&amp;quot;HADOOP_HOME&amp;quot;,&amp;quot;D:\\iangshouzhuang\\hadoop-2.6.0&amp;quot;);
    conf.set(&amp;quot;hbase.zookeeper.quorum&amp;quot;,&amp;quot;10.20.18.24,10.20.18.25,10.20.18.28&amp;quot;);
    conf.set(&amp;quot;hbase.zookeeper.property.clientPort&amp;quot;,&amp;quot;2181&amp;quot;);
    conf.set(&amp;quot;zookeeper.znode.parent&amp;quot;,&amp;quot;/hbase114&amp;quot;);
    conf.setInt(&amp;quot;hbase.rpc.timeout&amp;quot;,20000);
    conf.setInt(&amp;quot;hbase.client.operation.timeout&amp;quot;,30000);
    conf.setInt(&amp;quot;hbase.client.scanner.timeout.period&amp;quot;,20000);
    StringtableName = tbl;
    TableNametableNameObj = TableName.valueOf(tableName);
    Connectionconnection = ConnectionFactory.createConnection(conf);
    HTabletable = (HTable) connection.getTable(tableNameObj);
    Scanscan = new Scan();
    scan.setMaxResultSize(10000);
    scan.setCaching(500);
    ResultScannerrs = table.getScanner(scan);
    for(Result r : rs) {
    for(KeyValue kv : r.raw()) {
    System.out.println(String.format(&amp;quot;row:%s,family:%s, qualifier:%s, qualifiervalue:%s, timestamp:%s.&amp;quot;,
    Bytes.toString(kv.getRow()),Bytes.toString(kv.getFamily()),
    Bytes.toString(kv.getQualifier()),
    Bytes.toString(kv.getValue()),kv.getTimestamp()));
    }
    }
    }
    }    &lt;p&gt;输出结果为：&lt;/p&gt;    &lt;pre&gt;      &lt;code&gt;row:100001, family:info, qualifier:id,qualifiervalue:1, timestamp:1469930920802.

row:100001, family:info, qualifier:name,qualifiervalue:Hadoop, timestamp:1469930934184.&lt;/code&gt;&lt;/pre&gt;    &lt;p&gt;很多人都会误认为一次scan操作就是一次RPC请求，实际上，一次请求大量数据的scan操作可能会导致多个很严重的后果：服务器端可能因为大量io操作导致io利用率很高，影响其他正常业务请求；大量数据传输会导致网络带宽等系统资源被大量占用；客户端也可能因为内存无法缓存这些数据导致OOM。为了避免这些问题，HBase会将一次大的scan操作根据设置条件拆分为多个RPC请求，每次只返回规定数量的结果。上述代码中foreach(Result r ：rs)语句实际上等价于Result r = rs.next()，每执行一次next()操作就会调用客户端发送一次RPC请求，参数hbase.client.scanner.timeout.period就用来表示这么一次RPC请求的超时时间，默认为60000ms，一旦请求超时，就会抛出SocketTimeoutException异常。      &lt;br /&gt;根据上面的描述，我们引入两个问题来进行说明。&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;p&gt;一个Scan操作可能会被拆分为几个RPC          &lt;br /&gt;一次scan请求的RPC次数主要和两个因素相关，一个是本次scan的待检索条数，另一个是单次RPC请求的数据条数，很显然，两者的比值就是RPC请求次数。          &lt;br /&gt;一次scan的待检索条数由用户设置的条件决定，比如用户想一次获取某个用户最近一个月的所有操作信息，这些信息总和为10w条，那一次scan总扫瞄条数就是10w条。为了防止一次scan操作请求的数据量太大，额外提供了参数maxResultSize对总检索结果条数进行限制，该参数表示一次scan最多可以获取到的数据条数，默认为-1，表示无限制，如果用户设置了该参数，最后的返回结果条数就是该值与实际检索条数的更小者。          &lt;br /&gt;单次RPC请求的数据条数由参数caching设定，默认为100条。因为每次RPC请求获取到数据都会缓存到客户端，因此该值如果设置过大，可能会因为一次获取到的数据量太大导致客户端内存oom；而如果设置太小会导致一次大scan进行太多次RPC，网络成本高。&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;在scan过程中RegionServer端偶尔抛出leaseException&lt;/p&gt;        &lt;p&gt;看到leaseException就会想到租约机制，的确，HBase内部在一次完整的scan操作中引入了租约机制。为什么需要租约机制？这和整个scan操作流程有莫大的关系，上文讲到，一次完整的scan通常会被拆分为多个RPC请求，实际实现中，RegionServer接收到第一次RPC请求之后，会为该scan操作生成一个全局唯一的id，称为scanId。除此之外，RegionServer还会进行大量的准备工作，构建整个scan体系，构造需要用到的所有对象，后续的RPC请求只需要携带相同的scanId作为标示就可以直接利用这些已经构建好的资源进行检索。也就是说，在整个scan过程中，客户端其实都占用着服务器端的资源，此时如果此客户端意外宕机，是否就意味着这些资源永远都不能得到释放呢？租约机制就是为了解决这个问题。RegionServer接收到第一次RPC之后，除了生成全局唯一的scanId之外还会生成一个携带有超时时间的lease，超时时间可以通过参数hbase.regionserver.lease.period配置，一旦在超时时间内后续RPC请求没有到来（比如客户端处理太慢），RegionServer就认为客户端出现异常，此时会将该lease销毁并将整个scan所持有的资源全部释放，客户端在处理完成之后再发后续的RPC过来，检查到对应的lease已经不存在，就会抛出leaseExcption异常。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/61581-hbase-%E5%AE%A2%E6%88%B7%E7%AB%AF-%E8%AE%BF%E9%97%AE</guid>
      <pubDate>Fri, 02 Jul 2021 09:04:42 CST</pubDate>
    </item>
    <item>
      <title>高吞吐、低延迟 Java 应用的 GC 优化实践</title>
      <link>https://itindex.net/detail/61536-%E5%BB%B6%E8%BF%9F-java-%E5%BA%94%E7%94%A8</link>
      <description>&lt;div&gt;  &lt;p&gt;“以下信息节选自涤生的翻译内容”&lt;/p&gt;  &lt;p&gt;本篇原文作者是 LinkedIn 的 Swapnil Ghike，这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程，虽然文章写作于 April 8, 2014，但其中的很多内容和知识点非常有学习和参考意义。&lt;/p&gt;  &lt;h1&gt;背景&lt;/h1&gt;  &lt;p&gt;高性能应用构成了现代网络的支柱。LinkedIn 内部有许多高吞吐量服务来满足每秒成千上万的用户请求。为了获得最佳的用户体验，以低延迟响应这些请求是非常重要的。&lt;/p&gt;  &lt;p&gt;例如，我们的用户经常使用的产品是 Feed —— 它是一个不断更新的专业活动和内容的列表。Feed 在 LinkedIn 的系统中随处可见，包括公司页面、学校页面以及最重要的主页资讯信息。基础 Feed 数据平台为我们的经济图谱（会员、公司、群组等）中各种实体的更新建立索引，它必须高吞吐低延迟地实现相关的更新。如下图，LinkedIn Feeds 信息展示：   &lt;br /&gt;   &lt;img alt="5.jpg" src="https://a.perfma.net/img/233863"&gt;&lt;/img&gt;   &lt;br /&gt;为了将这些高吞吐量、低延迟类型的 Java 应用程序用于生产，开发人员必须确保在应用程序开发周期的每个阶段都保持一致的性能。确定最佳垃圾收集（Garbage Collection, GC）配置对于实现这些指标至关重要。&lt;/p&gt;  &lt;p&gt;这篇博文将通过一系列步骤来明确需求并优化 GC，它的目标读者是对使用系统方法进行 GC 优化来实现应用的高吞吐低延迟目标感兴趣的开发人员。在 LinkedIn 构建下一代 Feed 数据平台的过程中，我们总结了该方法。这些方法包括但不限于以下几点：并发标记清除（Concurrent Mark Sweep，CMS（参考[2]） 和 G1（参考 [3]） 垃圾回收器的 CPU 和内存开销、避免长期存活对象导致的持续 GC、优化 GC 线程任务分配提升性能，以及可预测 GC 停顿时间所需的 OS 配置。&lt;/p&gt;  &lt;h1&gt;优化 GC 的正确时机？&lt;/h1&gt;  &lt;p&gt;GC 的行为可能会因代码优化以及工作负载的变化而变化。因此，在一个已实施性能优化的接近完成的代码库上进行 GC 优化非常重要。而且在端到端的基本原型上进行初步分析也很有必要，该原型系统使用存根代码并模拟了可代表生产环境的工作负载。这样可以获取该架构延迟和吞吐量的真实边界，进而决定是否进行纵向或横向扩展。&lt;/p&gt;  &lt;p&gt;在下一代 Feed 数据平台的原型开发阶段，我们几乎实现了所有端到端的功能，并且模拟了当前生产基础设施提供的查询工作负载。这使我们在工作负载特性上有足够的多样性，可以在足够长的时间内测量应用程序性能和 GC 特征。&lt;/p&gt;  &lt;h1&gt;优化 GC 的步骤&lt;/h1&gt;  &lt;p&gt;下面是一些针对高吞吐量、低延迟需求优化 GC 的总体步骤。此外，还包括在 Feed 数据平台原型实施的具体细节。尽管我们还对 G1 垃圾收集器进行了试验，但我们发现 ParNew/CMS 具有最佳的 GC 性能。&lt;/p&gt;  &lt;h2&gt;1. 理解 GC 基础知识&lt;/h2&gt;  &lt;p&gt;由于 GC 优化需要调整大量的参数，因此理解 GC 工作机制非常重要。Oracle 的 Hotspot JVM 内存管理白皮书（参考 [4] ）是开始学习 Hotspot JVM GC 算法非常好的资料。而了解 G1 垃圾回收器的理论知识，可以参阅（参考 [3]）。&lt;/p&gt;  &lt;h2&gt;2. 仔细考量 GC 需求&lt;/h2&gt;  &lt;p&gt;为了降低对应用程序性能的开销，可以优化 GC 的一些特征。像吞吐量和延迟一样，这些 GC 特征应该在长时间运行的测试中观察到，以确保应用程序能够在经历多个 GC 周期中处理流量的变化。&lt;/p&gt;  &lt;ul&gt;   &lt;li&gt;Stop-the-world 回收器回收垃圾时会暂停应用线程。停顿的时长和频率不应该对应用遵守 SLA 产生不利的影响。&lt;/li&gt;   &lt;li&gt;并发 GC 算法与应用线程竞争 CPU 周期。这个开销不应该影响应用吞吐量。&lt;/li&gt;   &lt;li&gt;非压缩 GC 算法会引起堆碎片化，进而导致的 Full GC 长时间 Stop-the-world，因此，堆碎片应保持在最小值。&lt;/li&gt;   &lt;li&gt;垃圾回收工作需要占用内存。某些 GC 算法具有比其他算法更高的内存占用。如果应用程序需要较大的堆空间，要确保 GC 的内存开销不能太大。&lt;/li&gt;   &lt;li&gt;要清楚地了解 GC 日志和常用的 JVM 参数，以便轻松地调整 GC 行为。因为 GC 运行随着代码复杂性增加或工作负载特性的改变而发生变化&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt;我们使用 Linux 操作系统、Hotspot Java7u51、32GB 堆内存、6GB 新生代（Young Gen）和 -XX:CMSInitiatingOccupancyFraction 值为 70（Old GC 触发时其空间占用率）开始实验。设置较大的堆内存是用来维持长期存活对象的对象缓存。一旦这个缓存生效，晋升到 Old Gen 的对象速度会显著下降。&lt;/p&gt;  &lt;p&gt;使用最初的 JVM 配置，每 3 秒发生一次 80ms 的 Young GC 停顿，超过 99.9% 的应用请求延迟 100ms（999线）。这样的 GC 效果可能适合于 SLA 对延迟要求不太严格应用。然而，我们的目标是尽可能减少应用请求的 999 线。GC 优化对于实现这一目标至关重要。&lt;/p&gt;  &lt;h2&gt;3. 理解 GC 指标&lt;/h2&gt;  &lt;p&gt;衡量应用当前情况始终是优化的先决条件。了解 GC 日志的详细细节（参考 [5]）（使用以下选项）：&lt;/p&gt;  &lt;pre&gt;   &lt;code&gt;-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps 
-XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime&lt;/code&gt;复制&lt;/pre&gt;  &lt;p&gt;可以对该应用的 GC 特征有总体的把握。&lt;/p&gt;  &lt;p&gt;在 LinkedIn 的内部监控 inGraphs 和报表系统 Naarad，生成了各种有用的指标可视化图形，比如 GC 停顿时间百分比、一次停顿最大持续时间以及长时间内 GC 频率。除了 Naarad，有很多开源工具比如 gclogviewer 可以从 GC 日志创建可视化图形。在此阶段，可以确定 GC 频率和暂停持续时间是否满足应用程序满足延迟的要求。&lt;/p&gt;  &lt;h2&gt;4. 降低 GC 频率&lt;/h2&gt;  &lt;p&gt;在分代 GC 算法中，降低 GC 频率可以通过：(1) 降低对象分配/晋升率；(2) 增加各代空间的大小。&lt;/p&gt;  &lt;p&gt;在 Hotspot JVM 中，Young GC 停顿时间取决于一次垃圾回收后存活下来的对象的数量，而不是 Young Gen 自身的大小。增加 Young Gen 大小对于应用性能的影响需要仔细评估：&lt;/p&gt;  &lt;ul&gt;   &lt;li&gt;如果更多的数据存活而且被复制到 Survivor 区域，或者每次 GC 更多的数据晋升到 Old Gen，增加 Young Gen 大小可能导致更长的 Young GC 停顿。较长的 GC 停顿可能会导致应用程序延迟增加和(或)吞吐量降低。&lt;/li&gt;   &lt;li&gt;另一方面，如果每次垃圾回收后存活对象数量不会大幅增加，停顿时间可能不会延长。在这种情况下，降低 GC 频率可能会使整个应用总体延迟降低和(或)吞吐量增加。&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt;对于大部分为短期存活对象的应用，仅仅需要控制上述的参数；对于长期存活对象的应用，就需要注意，被晋升的对象可能很长时间都不能被 Old GC 周期回收。如果 Old GC 触发阈值（Old Gen 占用率百分比）比较低，应用将陷入持续的 GC 循环中。可以通过设置高的 GC 触发阈值可避免这一问题。&lt;/p&gt;  &lt;p&gt;由于我们的应用在堆中维持了长期存活对象的较大缓存，将 Old GC 触发阈值设置为&lt;/p&gt;  &lt;pre&gt;   &lt;code&gt;-XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSInitiatingOccupancyOnly&lt;/code&gt;复制&lt;/pre&gt;  &lt;p&gt;来增加触发 Old GC 的阈值。我们也试图增加 Young Gen 大小来减少 Young GC 频率，但是并没有采用，因为这增加了应用的 999 线。&lt;/p&gt;  &lt;h2&gt;5. 缩短 GC 停顿时间&lt;/h2&gt;  &lt;p&gt;减少 Young Gen 大小可以缩短 Young GC 停顿时间，因为这可能导致被复制到 Survivor 区域或者被晋升的数据更少。但是，正如前面提到的，我们要观察减少 Young Gen 大小和由此导致的 GC 频率增加对于整体应用吞吐量和延迟的影响。Young GC 停顿时间也依赖于 tenuring threshold （晋升阈值）和 Old Gen 大小（如步骤 6 所示）。&lt;/p&gt;  &lt;p&gt;在使用 CMS GC 时，应将因堆碎片或者由堆碎片导致的 Full GC 的停顿时间降低到最小。通过控制对象晋升比例和减小 -XX:CMSInitiatingOccupancyFraction 的值使 Old GC 在低阈值时触发。所有选项的细节调整和他们相关的权衡，请参考 Web Services 的 Java 垃圾回收（参考 [5] ）和 Java 垃圾回收精粹（参考 [6]）。&lt;/p&gt;  &lt;p&gt;我们观察到 Eden 区域的大部分 Young Gen 被回收，几乎没有 3-8 年龄对象在 Survivor 空间中死亡，所以我们将 tenuring threshold 从 8 降低到 2 (使用选项：-XX:MaxTenuringThreshold=2 ),以降低 Young GC 消耗在数据复制上的时间。&lt;/p&gt;  &lt;p&gt;我们还注意到 Young GC 暂停时间随着 Old Gen 占用率上升而延长。这意味着来自 Old Gen 的压力使得对象晋升花费更多的时间。为解决这个问题，将总的堆内存大小增加到 40GB，减小 -XX:CMSInitiatingOccupancyFraction 的值到 80，更快地开始 Old GC。尽管 -XX:CMSInitiatingOccupancyFraction 的值减小了，增大堆内存可以避免频繁的 Old GC。在此阶段，我们的结果是 Young GC 暂停 70ms，应用的 999 线在 80ms。&lt;/p&gt;  &lt;h2&gt;6. 优化 GC 工作线程的任务分配&lt;/h2&gt;  &lt;p&gt;为了进一步降低 Young GC 停顿时间，我们决定研究 GC 线程绑定任务的参数来进行优化。&lt;/p&gt;  &lt;p&gt;-XX:ParGCCardsPerStrideChunk 参数控制 GC 工作线程的任务粒度，可以帮助不使用补丁而获得最佳性能，这个补丁用来优化 Young GC 中的 Card table（卡表）扫描时间（参考[7]）。有趣的是，Young GC 时间随着 Old Gen 的增加而延长。将这个选项值设为 32678，Young GC 停顿时间降低到平均 50ms。此时应用的 999 线在 60ms。&lt;/p&gt;  &lt;p&gt;还有一些的参数可以将任务映射到 GC 线程，如果操作系统允许的话，-XX:+BindGCTaskThreadsToCPUs 参数可以绑定 GC 线程到个别的 CPU 核（见解释 [1]）。使用亲缘性 -XX:+UseGCTaskAffinity 参数可以将任务分配给 GC 工作线程（见解释 [2]）。然而，我们的应用并没有从这些选项带来任何好处。实际上，一些调查显示这些选项在 Linux 系统不起作用。&lt;/p&gt;  &lt;h2&gt;7. 了解 GC 的 CPU 和内存开销&lt;/h2&gt;  &lt;p&gt;并发 GC 通常会增加 CPU 使用率。虽然我们观察到 CMS 的默认设置运行良好，但是 G1 收集器的并发 GC 工作会导致 CPU 使用率的增加，显著降低了应用程序的吞吐量和延迟。与 CMS 相比，G1 还增加了内存开销。对于不受 CPU 限制的低吞吐量应用程序，GC 导致的高 CPU 使用率可能不是一个紧迫的问题。&lt;/p&gt;  &lt;p&gt;下图是 ParNew/CMS 和 G1 的 CPU 使用百分比：相对来说 CPU 使用率变化明显的节点使用 G1 参数 -XX:G1RSetUpdatingPauseTimePercent=20：   &lt;br /&gt;   &lt;img alt="6.jpg" src="https://a.perfma.net/img/233872"&gt;&lt;/img&gt;   &lt;br /&gt;下图是 ParNew/CMS 和 G1 每秒服务的请求数：吞吐量较低的节点使用 G1 参数 -XX:G1RSetUpdatingPauseTimePercent=20   &lt;br /&gt;   &lt;img alt="7.jpg" src="https://a.perfma.net/img/233885"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;h2&gt;8. 为 GC 优化系统内存和 I/O 管理&lt;/h2&gt;  &lt;p&gt;通常来说，GC 停顿有两种特殊情况：(1) 低 user time，高 sys time 和高 real time (2) 低 user time，低 sys time 和高 real time。这意味着基础的进程/OS设置存在问题。情况 (1) 可能意味着 JVM 页面被 Linux 窃取；情况 (2) 可能意味着 GC 线程被 Linux 用于磁盘刷新，并卡在内核中等待 I/O。在这些情况下，如何设置参数可以参考该 PPT（参考 [8]）。&lt;/p&gt;  &lt;p&gt;另外，为了避免在运行时造成性能损失，我们可以使用 JVM 选项 -XX:+AlwaysPreTouch 在应用程序启动时先访问所有分配给它的内存，让操作系统把内存真正的分配给 JVM。我们还可以将 vm.swappability 设置为0，这样操作系统就不会交换页面到 swap（除非绝对必要）。&lt;/p&gt;  &lt;p&gt;可能你会使用 mlock 将 JVM 页固定到内存中，这样操作系统就不会将它们交换出去。但是，如果系统用尽了所有的内存和交换空间，操作系统将终止一个进程来回收内存。通常情况下，Linux 内核会选择具有高驻留内存占用但运行时间不长的进程（OOM 情况下杀死进程的工作流（参考[9]）进行终止。在我们的例子中，这个进程很有可能就是我们的应用程序。优雅的降级是服务优秀的属性之一，不过服务突然终止的可能性对于可操作性来说并不好 —— 因此，我们不使用 mlock，只是通过 vm.swapability 来尽可能避免交换内存页到 swap 的惩罚。&lt;/p&gt;  &lt;p&gt;LinkedIn 动态信息数据平台的 GC 优化&lt;/p&gt;  &lt;p&gt;对于该 Feed 平台原型系统，我们使用 Hotspot JVM 的两个 GC 算法优化垃圾回收：&lt;/p&gt;  &lt;p&gt;Young GC 使用 ParNew，Old GC 使用 CMS。   &lt;br /&gt;Young Gen 和 Old Gen 使用 G1。G1 试图解决堆大小为 6GB 或更大时，暂停时间稳定且可预测在 0.5 秒以下的问题。在我们用 G1 实验过程中，尽管调整了各种参数，但没有得到像 ParNew/CMS 一样的 GC 性能或停顿时间的可预测值。我们查询了使用 G1 发生内存泄漏相关的一个 bug（见解释[3]），但还不能确定根本原因。   &lt;br /&gt;使用 ParNew/CMS，应用每三秒进行一次 40-60ms 的 Young GC 和每小时一个 CMS GC。JVM 参数如下：&lt;/p&gt;  &lt;pre&gt;   &lt;code&gt;// JVM sizing options
-server -Xms40g -Xmx40g -XX:MaxDirectMemorySize=4096m -XX:PermSize=256m -XX:MaxPermSize=256m   
// Young generation options
-XX:NewSize=6g -XX:MaxNewSize=6g -XX:+UseParNewGC -XX:MaxTenuringThreshold=2 -XX:SurvivorRatio=8 -XX:+UnlockDiagnosticVMOptions -XX:ParGCCardsPerStrideChunk=32768
// Old generation  options
-XX:+UseConcMarkSweepGC -XX:CMSParallelRemarkEnabled -XX:+ParallelRefProcEnabled -XX:+CMSClassUnloadingEnabled  -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly   
// Other options
-XX:+AlwaysPreTouch -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:-OmitStackTraceInFastThrow&lt;/code&gt;复制&lt;/pre&gt;  &lt;p&gt;使用这些参数，对于成千上万读请求的吞吐量，我们应用程序的 999 线降低到 60ms。&lt;/p&gt;  &lt;p&gt;解释&lt;/p&gt;  &lt;p&gt;[1] -XX:+BindGCTaskThreadsToCPUs 参数似乎在Linux 系统上不起作用，因为 hotspot/src/os/linux/vm/oslinux.cpp 的 distributeprocesses 方法在 JDK7 或 JDK8 中没有实现。&lt;/p&gt;  &lt;p&gt;[2] -XX:+UseGCTaskAffinity 参数在 JDK7 和 JDK8 的所有平台似乎都不起作用，因为任务的亲缘性属性永远被设置为 sentinelworker = (uint) -1。源码见 hotspot/src/share/vm/gcimplementation/parallelScavenge/{gcTaskManager.cpp，gcTaskThread.cpp, gcTaskManager.cpp}。&lt;/p&gt;  &lt;p&gt;[3] G1 存在一些内存泄露的 bug，可能 Java7u51 没有修改。这个 bug 仅在 Java 8 修正了。&lt;/p&gt;&lt;/div&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>dev</category>
      <guid isPermaLink="true">https://itindex.net/detail/61536-%E5%BB%B6%E8%BF%9F-java-%E5%BA%94%E7%94%A8</guid>
      <pubDate>Tue, 15 Jun 2021 00:00:00 CST</pubDate>
    </item>
    <item>
      <title>TCP之网络优化</title>
      <link>https://itindex.net/detail/61530-tcp-%E7%BD%91%E7%BB%9C-%E4%BC%98%E5%8C%96</link>
      <description>&lt;div&gt;  &lt;p&gt;   &lt;a href="https://juejin.cn/post/6971710168413241375" target="_blank"&gt;上一篇文章我提到了Nagle算法&lt;/a&gt;，是为了解决报头大数据小从而导致网络利用率低的问题，这其实会带来新的问题。除此之外我们一起来看看tcp还会有什么优化策略呢！本文纯属学习记录，不完善或错误之处若指正将不胜感激。如有被误导的朋友，望海涵。&lt;/p&gt;
  &lt;hr&gt;&lt;/hr&gt;
  &lt;p&gt;首先我们先康康Nagle算法&lt;/p&gt;
  &lt;h3&gt;Nagle算法规则&lt;/h3&gt;
  &lt;ul&gt;
   &lt;li&gt;（1）如果包长度达到MSS，则允许发送；&lt;/li&gt;
   &lt;li&gt;（2）如果该包含有FIN，则允许发送；&lt;/li&gt;
   &lt;li&gt;（3）设置了TCP_NODELAY选项，则允许发送；&lt;/li&gt;
   &lt;li&gt;（4）未设置TCP_CORK选项时，若所有发出去的小数据包（包长度小于MSS）均被确认，则允许发送；&lt;/li&gt;
   &lt;li&gt;（5）上述条件都未满足，但发生了超时（一般为200ms），则立即发送。&lt;/li&gt;
&lt;/ul&gt;
  &lt;h3&gt;延迟ACK&lt;/h3&gt;
  &lt;p&gt;ACK机制中，在接收方收到一个包之后会先检查是否需要立即回应ACK，否则进入延迟ACK逻辑。   &lt;a href="https://blog.csdn.net/sinat_20184565/article/details/90181191" rel="nofollow noopener noreferrer" target="_blank"&gt;参考&lt;/a&gt;，   &lt;br /&gt;
优点显而易见，提高了网络信道的利用率&lt;/p&gt;
  &lt;h3&gt;当Nagle遇见延迟ACK&lt;/h3&gt;
  &lt;p&gt;假想一个场景  MSS为8个中文字（最大报文长度）   &lt;br /&gt;
甲需要发送两个应用层报文给乙——“你好”！“我是甲”。显然这两个报文都是不足8的，但是由于“你好”满足Nagel的第4条规则所以会第一时间发送出去，由于ACK延迟，甲迟迟收不到乙的确认。于是等到超时发送。   &lt;br /&gt;
这会产生一个明显的延迟   &lt;br /&gt;
   &lt;strong&gt;解决&lt;/strong&gt;&lt;/p&gt;
  &lt;ul&gt;
   &lt;li&gt;关闭Nagle算法，使用TCP套接字选项TCP_NODELAY可以关闭套接字选项（不推荐，有更多的优化策略）&lt;/li&gt;
   &lt;li&gt;使用writev，而不是两次调用write，单个writev调用会使tcp输出一次而不是两次，只产生一个tcp分节，这是首选方法&lt;/li&gt;
&lt;/ul&gt;
  &lt;h3&gt;流量控制之滑动窗口&lt;/h3&gt;
  &lt;p&gt;滑动窗口是为了平衡发送方和接收方速率不匹配，发送窗口在连接建立时由双方商定。但在通信的过程中，一般由接收方反馈本身的缓冲区大小从而动态调节发送窗口（缓冲区）大小&lt;/p&gt;
  &lt;h3&gt;拥塞控制&lt;/h3&gt;
  &lt;p&gt;为了方便，我们假设主机A给主机B传输数据   &lt;br /&gt;
我们知道，两台主机在传输数据包的时候，如果发送方迟迟没有收到接收方反馈的ACK，那么发送方就会认为它发送的数据包丢失了，进而会重新传输这个丢失的数据包。
然而实际情况有可能此时有太多主机正在使用信道资源，导致网络拥塞了，而A发送的数据包被堵在了半路，迟迟没有到达B。这个时候A误认为是发生了丢包情况，会重新传输这个数据包。   &lt;br /&gt;
结果就是不仅浪费了信道资源，还会使网络更加拥塞。因此，我们需要进行拥塞控制&lt;/p&gt;
  &lt;h4&gt;慢开始和拥塞避免&lt;/h4&gt;
  &lt;ul&gt;
   &lt;li&gt;在建立连接之后是如何确定拥塞窗口的大小？(拥塞窗口和滑动窗口注意区别)
    &lt;ul&gt;
     &lt;li&gt;第一种策略，第一次发送一个包，如果没有丢失就+1，以此类推&lt;/li&gt;
     &lt;li&gt;第二种策略，第一次发送一个包，如果没有丢失就乘以2，以此类推&lt;/li&gt;
&lt;/ul&gt;
实际上第一种方式增长过于缓慢，难以快速适应网络拥塞情况，而第二种方式指数型增长，很容易到达拥塞阈值。    &lt;br /&gt;
所以二者取其长，在前期使用指数增长，当到达某一个数值之后进行线性增长    &lt;br /&gt;
但是无论是指数增长还是线性增长最终都会到达一个MAX值，此时会重新以1开始启动并把阈值设置为MAX/2    &lt;br /&gt;
我们把确定拥塞窗口大小的过程中指数增长阶段称之为    &lt;strong&gt;慢开始&lt;/strong&gt;，线性增长阶段称之为    &lt;strong&gt;拥塞避免&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
  &lt;h4&gt;快速重传与快速恢复&lt;/h4&gt;
  &lt;p&gt;前面说过了，当出现网络拥塞时会重启慢开始过程&lt;/p&gt;
  &lt;ul&gt;
   &lt;li&gt;怎么判断网络拥塞？
    &lt;ul&gt;
     &lt;li&gt;当网络拥塞时，会出现大量的丢包&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
   &lt;li&gt;超时重发（丢包）一定是网络拥塞吗？
    &lt;ul&gt;
     &lt;li&gt;网络拥塞会导致大量的包触发超时重发事件。而当一个单独的包出现损坏或者丢包时，也会导致超时重发，所以超时重发不一定是网络拥塞&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
   &lt;li&gt;如何判断超时重发的原因
    &lt;ul&gt;
     &lt;li&gt;网络拥塞会导致大量的丢包&lt;/li&gt;
     &lt;li&gt;单个的丢包，由于延迟ACK规则，后序到达的每到达一个包，接收端都会回复相同的ACK。故当发送方接收到三个相同的ACK时，表明发生了单个的丢包&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
  &lt;p&gt;单个丢包事件，当发送方接收到三个相同的ACK时，此时发送方不必等待序号为ACK-1包的超时，会立即重发。并把当前的阈值设置为MAX，新的阈值为MAX/2，以 拥塞窗口 = MAX/2 进行增长   &lt;br /&gt;
重发阶段我们称之为   &lt;strong&gt;快速重传&lt;/strong&gt; ，窗口参数的调整阶段称之为   &lt;strong&gt;快速恢复&lt;/strong&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/61530-tcp-%E7%BD%91%E7%BB%9C-%E4%BC%98%E5%8C%96</guid>
      <pubDate>Thu, 10 Jun 2021 08:12:49 CST</pubDate>
    </item>
    <item>
      <title>从源码中来，到业务中去，React性能优化终极指南</title>
      <link>https://itindex.net/detail/61467-%E6%BA%90%E7%A0%81-%E4%B8%9A%E5%8A%A1-react</link>
      <description>&lt;blockquote&gt;
  &lt;p&gt;前言：我们从React源码入手，结合有道精品课大前端的具体业务，运用三大原则对系统进行外科手术式的优化。同时介绍React Profiler这款工具如何帮我们定位性能瓶颈前言：我们从React源码入手，结合有道精品课大前端的具体业务，运用三大原则对系统进行外科手术式的优化。同时介绍React Profiler这款工具如何帮我们定位性能瓶颈&lt;/p&gt;
  &lt;p&gt;作者/ 安增平&lt;/p&gt;
  &lt;p&gt;编辑/ Ein&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;React性能优化是在业务迭代过程中不得不考虑的问题，大部分情况是由于项目启动之初，没有充分考虑到项目的复杂度，定位该产品的用户体量及技术场景并不复杂，那么我们在业务前期可能并不需要考虑性能优化。但是随着业务场景的复杂化，性能优化就变得格外重要。&lt;/p&gt;
 &lt;p&gt;我们从React源码入手，结合有道精品课大前端的具体业务，运用优化技巧对系统进行外科手术式的优化。同时介绍一下React Profiler这款性能优化的利器是如何帮我们定位性能瓶颈的。&lt;/p&gt;
 &lt;p&gt;本文中的项目代码全部是在有道大前端组开发项目中的工作记录，如有不足欢迎在留言区讨论交流，笔芯❤  &lt;br /&gt;
&lt;/p&gt;
 &lt;h1&gt;页面加载流程&lt;/h1&gt;
 &lt;p&gt;  &lt;img alt="" src="https://oscimg.oschina.net/oscnet/up-052ec483a7d51c31efd59949610500b6fc0.JPEG"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;假设用户首次打开页面（无缓存），这个时候页面是完全空白的；&lt;/li&gt;
  &lt;li&gt;html 和引用的 css 加载完毕，浏览器进行   &lt;strong&gt;首次渲染&lt;/strong&gt;；&lt;/li&gt;
  &lt;li&gt;react、react-dom、业务代码加载完毕，应用第一次渲染，或者说   &lt;strong&gt;首次内容渲染&lt;/strong&gt;；&lt;/li&gt;
  &lt;li&gt;应用的代码开始执行，拉取数据、进行动态import、响应事件等等，完毕后页面进入   &lt;strong&gt;可交互&lt;/strong&gt;状态；&lt;/li&gt;
  &lt;li&gt;接下来 lazyload 的图片等多媒体内容开始逐渐加载完毕；&lt;/li&gt;
  &lt;li&gt;直到页面的其它资源（如错误上报组件、打点上报组件等）加载完毕，整个页面加载完成。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;  &lt;strong&gt;我们主要来针对React进行剖析&lt;/strong&gt;：&lt;/p&gt;
 &lt;p&gt;React 针对渲染性能优化的三个方向，也适用于其他软件开发领域，这三个方向分别是:&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;   &lt;strong&gt;减少计算的量&lt;/strong&gt;：React 中就是减少渲染的节点或通过索引减少渲染复杂度；&lt;/li&gt;
  &lt;li&gt;   &lt;strong&gt;利用缓存&lt;/strong&gt;：React 中就是避免重新渲染（利用 memo 方式来避免组件重新渲染）；&lt;/li&gt;
  &lt;li&gt;   &lt;strong&gt;精确重新计算的范围&lt;/strong&gt;：React 中就是绑定组件和状态关系, 精确判断更新的’时机’和’范围’. 只重新渲染变更的组件（减少渲染范围）。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;如何做到这三点呢？我们从React本身的特性入手分析。&lt;/p&gt;
 &lt;h1&gt;React 工作流&lt;/h1&gt;
 &lt;p&gt;React 是声明式 UI 库，负责将 State 转换为页面结构（虚拟 DOM 结构）后，再转换成真实 DOM 结构，交给浏览器渲染。State 发生改变时，React 会先进行Reconciliation，结束后立刻进入Commit阶段，Commit结束后，新 State 对应的页面才被展示出来。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://oscimg.oschina.net/oscnet/up-72128c7e5014784753ac28496bbdcfac480.JPEG"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;React 的  &lt;strong&gt;Reconciliation&lt;/strong&gt;需要做两件事：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;计算出目标 State 对应的虚拟 DOM 结构。&lt;/li&gt;
  &lt;li&gt;寻找「将虚拟 DOM 结构修改为目标虚拟 DOM 结构」的最优方案。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;React 按照深度优先遍历虚拟 DOM 树的方式，在一个虚拟 DOM 上完成Render和Diff的计算后，再计算下一个虚拟 DOM。Diff 算法会记录虚拟 DOM 的更新方式（如：Update、Mount、Unmount），为Commit做准备。&lt;/p&gt;
 &lt;p&gt;React 的  &lt;strong&gt;Commit&lt;/strong&gt;也需要做两件事：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;将Reconciliation结果应用到 DOM 中。&lt;/li&gt;
  &lt;li&gt;调用暴露的hooks如：componentDidUpdate、useLayoutEffect 等。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;  &lt;strong&gt;下面我们将针对三个优化方向进行精准分析。&lt;/strong&gt;&lt;/p&gt;
 &lt;h1&gt;减少计算的量&lt;/h1&gt;
 &lt;p&gt;关于以上  &lt;strong&gt;Reconciliation&lt;/strong&gt;与  &lt;strong&gt;Commit&lt;/strong&gt;两个阶段的优化办法，我在实现的过程中遵循  &lt;strong&gt;减少计算量&lt;/strong&gt;的方法进行优化（  &lt;strong&gt;列表项使用 key 属性&lt;/strong&gt;)该过程是优化的重点，React 内部的 Fiber 结构和并发模式也是在减少该过程的耗时阻塞。对于  &lt;strong&gt;Commit&lt;/strong&gt;在执行hooks时，开发者应保证hooks中的代码尽量轻量，避免耗时阻塞，同时应避免在   &lt;strong&gt;CDM、CDU&lt;/strong&gt;周期中更新组件。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;列表项使用 key 属性&lt;/strong&gt;&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;特定框架中，提示也做的十分友好。假如你没有在列表中添加key属性，控制台会为你展示一片大红&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;  &lt;img alt="" src="https://oscimg.oschina.net/oscnet/up-4fe69e5a342647e8cc122c015bafec0c5cd.JPEG"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;系统会时刻提醒你记得加Key哦~~&lt;/p&gt;
 &lt;h2&gt;优化Render 过程&lt;/h2&gt;
 &lt;p&gt;Render 过程：即Reconciliation中计算出目标 State 对应的虚拟 DOM 结构这一阶段 。&lt;/p&gt;
 &lt;p&gt;触发 React 组件的 Render 过程目前有三种方式：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;forceUpdate、&lt;/li&gt;
  &lt;li&gt;State 更新、&lt;/li&gt;
  &lt;li&gt;父组件 Render 触发子组件 Render 过程。&lt;/li&gt;
&lt;/ol&gt;
 &lt;h3&gt;优化技巧&lt;/h3&gt;
 &lt;h4&gt;PureComponent、React.memo&lt;/h4&gt;
 &lt;p&gt;在 React 工作流中，如果只有父组件发生状态更新，即使父组件传给子组件的所有 Props 都没有修改，也会引起子组件的 Render 过程。&lt;/p&gt;
 &lt;p&gt;从 React 的声明式设计理念来看，如果子组件的 Props 和 State 都没有改变，那么其生成的 DOM 结构和副作用也不应该发生改变。当子组件符合声明式设计理念时，就可以忽略子组件本次的 Render 过程。&lt;/p&gt;
 &lt;p&gt;PureComponent 和 React.memo 就是应对这种场景的，PureComponent 是对类组件的 Props 和 State 进行浅比较，React.memo 是对函数组件的 Props 进行浅比较。&lt;/p&gt;
 &lt;h5&gt;useMemo、useCallback 实现稳定的 Props 值&lt;/h5&gt;
 &lt;p&gt;如果传给子组件的派生状态或函数，每次都是新的引用，那么 PureComponent 和 React.memo 优化就会失效。所以需要使用 useMemo 和 useCallback 来生成稳定值，并结合 PureComponent 或 React.memo 避免子组件重新 Render。&lt;/p&gt;
 &lt;h5&gt;useMemo 减少组件 Render 过程耗时&lt;/h5&gt;
 &lt;p&gt;useMemo 是一种缓存机制提速，当它的依赖未发生改变时，就不会触发重新计算。一般用在「计算派生状态的代码」非常耗时的场景中，如：遍历大列表做统计信息。&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-10becf1cb1b4ad17a47f5b5a9155a30e6cf.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;显然useMemo的作用是缓存昂贵的计算(避免在每次渲染时都进行高开销的计算)，在业务中使用它去控制变量来更新表格&lt;/p&gt;
 &lt;h4&gt;shouldComponentUpdate&lt;/h4&gt;
 &lt;p&gt;在类组件中，例如要往数组中添加一项数据时，当时的代码很可能是 state.push(item)，而不是 const newState = [...state, item]。&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-57bfb127368f9b484905e5f2401af06211e.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;在此背景下，当时的开发者经常使用&lt;/p&gt;
 &lt;p&gt;shouldComponentUpdate 来深比较 Props，只在 Props 有修改才执行组件的 Render 过程。如今由于数据不可变性和函数组件的流行，这样的优化场景已经不会再出现了。&lt;/p&gt;
 &lt;p&gt;为了贴合shouldComponentUpdate的思想：给子组件传props的时候一定只传其需要的而并非一股脑全部传入：&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-32d99c3457639dfd3d0b611e54205f86381.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;传入到子组件的参数一定保证其在自组件中被使用到。&lt;/p&gt;
 &lt;h2&gt;批量更新，减少 Render 次数&lt;/h2&gt;
 &lt;p&gt;在 React 管理的事件回调和生命周期中，setState 是异步的，而其他时候 setState 都是同步的。这个问题根本原因就是 React 在自己管理的事件回调和生命周期中，对于 setState 是批量更新的，而在其他时候是立即更新的。&lt;/p&gt;
 &lt;p&gt;批量更新 setState 时，多次执行 setState 只会触发一次 Render 过程。相反在立即更新 setState 时，每次 setState 都会触发一次 Render 过程，就存在性能影响。&lt;/p&gt;
 &lt;p&gt;假设有如下组件代码，该组件在 getData() 的 API 请求结果返回后，分别更新了两个 State 。&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-6c86a84cf309cd9b1dc8ef8e9a4d33c5cb5.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;该组件会在 setList(data.list) 后触发组件的 Render 过程，然后在 setInfo(data.info) 后再次触发 Render 过程，造成性能损失。那我们该如何解决呢：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;将多个 State 合并为单个 State。例如 useState({ list: null, info: null }) 替代 list 和 info 两个 State。&lt;/li&gt;
  &lt;li&gt;使用 React 官方提供的 unstable_batchedUpdates 方法，将多次 setState 封装到 unstable_batchedUpdates 回调中。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;修改后代码如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-71947c1183764c4da5d49847ea4027b36a7.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h1&gt;精细化渲染阶段&lt;/h1&gt;
 &lt;h2&gt;按优先级更新，及时响应用户&lt;/h2&gt;
 &lt;p&gt;优先级更新是批量更新的逆向操作，其思想是：优先响应用户行为，再完成耗时操作。常见的场景是：页面弹出一个 Modal，当用户点击 Modal 中的确定按钮后，代码将执行两个操作：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;关闭 Modal。&lt;/li&gt;
  &lt;li&gt;页面处理 Modal 传回的数据并展示给用户。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;当操作2需要执行500ms时，用户会明显感觉到从点击按钮到 Modal 被关闭之间的延迟。&lt;/p&gt;
 &lt;p&gt;以下为一般的实现方式，将 slowHandle 函数作为用户点击按钮的回调函数。&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-5c3584e2db57a51a496e970376b68f72c1b.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;slowHandle() 执行过程耗时长，用户点击按钮后会明显感觉到页面卡顿。&lt;/p&gt;
 &lt;p&gt;如果让页面优先隐藏输入框，用户便能立刻感知到页面更新，不会有卡顿感。&lt;/p&gt;
 &lt;p&gt;实现优先级更新的要点是将耗时任务移动到下一个宏任务中执行，优先响应用户行为。&lt;/p&gt;
 &lt;p&gt;例如在该例中，将 setNumbers 移动到 setTimeout 的回调中，用户点击按钮后便能立即看到输入框被隐藏，不会感知到页面卡顿。mhd项目中优化后的代码如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-c85c495b7a0468465a41a3eace75eb7267f.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;发布者订阅者跳过中间组件 Render 过程&lt;/h2&gt;
 &lt;p&gt;React 推荐将公共数据放在所有「需要该状态的组件」的公共祖先上，但将状态放在公共祖先上后，该状态就需要层层向下传递，直到传递给使用该状态的组件为止。&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-faf8fcc068d87181e8586507ed0ba649927.JPEG" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;每次状态的更新都会涉及中间组件的 Render 过程，但中间组件并不关心该状态，它的 Render 过程只负责将该状态再传给子组件。在这种场景下可以将状态用发布者订阅者模式维护，只有关心该状态的组件才去订阅该状态，不再需要中间组件传递该状态。&lt;/p&gt;
 &lt;p&gt;当状态更新时，发布者发布数据更新消息，只有订阅者组件才会触发 Render 过程，中间组件不再执行 Render 过程。&lt;/p&gt;
 &lt;p&gt;只要是发布者订阅者模式的库，都可以使用useContext进行该优化。比如：redux、use-global-state、React.createContext 等。&lt;/p&gt;
 &lt;p&gt;业务代码中的使用如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-b5ab9990cdc525f6fbac21f0b0517a4c9ea.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;从图中可看出，优化后只有使用了公共状态的组件renderTable才会发生更新，由此可见这样做可以大大减少父组件和 其他renderSon… 组件的 Render 次数(减少叶子节点的重渲染)。&lt;/p&gt;
 &lt;h2&gt;useMemo 返回虚拟 DOM 可跳过该组件 Render 过程&lt;/h2&gt;
 &lt;p&gt;利用 useMemo 可以缓存计算结果的特点，如果 useMemo 返回的是组件的虚拟 DOM，则将在 useMemo 依赖不变时，跳过组件的 Render 阶段。&lt;/p&gt;
 &lt;p&gt;该方式与 React.memo 类似，但与 React.memo 相比有以下优势：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;更方便。React.memo 需要对组件进行一次包装，生成新的组件。而 useMemo 只需在存在性能瓶颈的地方使用，不用修改组件。&lt;/li&gt;
  &lt;li&gt;更灵活。useMemo 不用考虑组件的所有 Props，而只需考虑当前场景中用到的值，也可使用 useDeepCompareMemo 对用到的值进行深比较。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;该例子中，父组件状态更新后，不使用 useMemo 的子组件会执行 Render 过程，而使用 useMemo 的子组件会按需执行更新。业务代码中的使用方法：&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-9bcf696026f50a3af10f9a707e5c0bf458f.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h1&gt;精确判断更新的’时机’和’范围’&lt;/h1&gt;
 &lt;h2&gt;debounce、throttle 优化频繁触发的回调&lt;/h2&gt;
 &lt;p&gt;在搜索组件中，当 input 中内容修改时就触发搜索回调。当组件能很快处理搜索结果时，用户不会感觉到输入延迟。&lt;/p&gt;
 &lt;p&gt;但实际场景中，中后台应用的列表页非常复杂，组件对搜索结果的 Render 会造成页面卡顿，明显影响到用户的输入体验。&lt;/p&gt;
 &lt;p&gt;在搜索场景中一般使用 useDebounce+ useEffect 的方式获取数据。&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-b588a3994697509bb54ddc62e040b861d87.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;在搜索场景中，只需响应用户最后一次输入，无需响应用户的中间输入值，debounce 更适合。而 throttle 更适合需要实时响应用户的场景中更适合，如通过拖拽调整尺寸或通过拖拽进行放大缩小（如：window 的 resize 事件）。&lt;/p&gt;
 &lt;h2&gt;懒加载&lt;/h2&gt;
 &lt;p&gt;在 SPA 中，懒加载优化一般用于从一个路由跳转到另一个路由。&lt;/p&gt;
 &lt;p&gt;还可用于用户操作后才展示的复杂组件，比如点击按钮后展示的弹窗模块（大数据量弹窗）。&lt;/p&gt;
 &lt;p&gt;在这些场景下，结合 Code Split 收益较高。懒加载的实现是通过 Webpack 的动态导入和 React.lazy 方法。&lt;/p&gt;
 &lt;p&gt;实现懒加载优化时，不仅要考虑加载态，还需要对加载失败进行容错处理。&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-70d53594254239dd81003a13b4b68f6991f.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;懒渲染&lt;/h2&gt;
 &lt;p&gt;懒渲染指当组件进入或即将进入可视区域时才渲染组件。常见的组件 Modal/Drawer 等，当 visible 属性为 true 时才渲染组件内容，也可以认为是懒渲染的一种实现。懒渲染的使用场景有：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;页面中出现多次的组件，且组件渲染费时、或者组件中含有接口请求。如果渲染多个带有请求的组件，由于浏览器限制了同域名下并发请求的数量，就可能会阻塞可见区域内的其他组件中的请求，导致可见区域的内容被延迟展示。&lt;/li&gt;
  &lt;li&gt;需用户操作后才展示的组件。这点和懒加载一样，但懒渲染不用动态加载模块，不用考虑加载态和加载失败的兜底处理，实现上更简单。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;懒渲染的实现中判断组件是否出现在可视区域内借助react-visibility-observer依赖：&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-b49f8195abd6c725f32285c1b3a2b0f833b.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;虚拟列表&lt;/h2&gt;
 &lt;p&gt;虚拟列表是懒渲染的一种特殊场景。虚拟列表的组件有 react-window和 react-virtualized，它们都是同一个作者开发的。&lt;/p&gt;
 &lt;p&gt;react-window 是 react-virtualized 的轻量版本，其 API 和文档更加友好。推荐使用 react-window，只需要计算每项的高度即可：&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-67ce95b55519173c426d648f4bda5ca4c3a.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;如果每项的高度是变化的，可给 itemSize 参数传一个函数。&lt;/p&gt;
 &lt;p&gt;所以在开发过程中，遇到接口返回的是所有数据时，需提前预防这类会有展示的性能瓶颈的需求时，推荐使用虚拟列表优化。使用示例：  &lt;a href="https://react-window.vercel.app/#/examples/list/fixed-size" title="react-window&amp;#8203;react-window.vercel.app"&gt;react-window​react-window.vercel.app&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;动画库直接修改 DOM 属性，跳过组件 Render 阶段&lt;/h2&gt;
 &lt;p&gt;这个优化在业务中应该用不上，但还是非常值得学习的，将来可以应用到组件库中。&lt;/p&gt;
 &lt;p&gt;参考 react-spring 的动画实现，当一个动画启动后，每次动画属性改变不会引起组件重新 Render ，而是直接修改了 dom 上相关属性值：&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-3154268122929bd891f2679b2015ae8690b.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;避免在 didMount、didUpdate 中更新组件 State&lt;/h2&gt;
 &lt;p&gt;这个技巧不仅仅适用于 didMount、didUpdate，还包括 willUnmount、useLayoutEffect 和特殊场景下的 useEffect（当父组件的 cDU/cDM 触发时，子组件的 useEffect 会同步调用），本文为叙述方便将他们统称为「提交阶段钩子」。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;React 工作流commit&lt;/strong&gt;阶段的第二步就是执行提交阶段钩子，它们的执行会阻塞浏览器更新页面。&lt;/p&gt;
 &lt;p&gt;如果在提交阶段钩子函数中更新组件 State，会再次触发组件的更新流程，造成两倍耗时。一般在提交阶段的钩子中更新组件状态的场景有：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;计算并更新组件的派生状态（Derived State）。在该场景中，类组件应使用    &lt;strong&gt;getDerivedStateFromProps&lt;/strong&gt; 钩子方法代替，函数组件应使用函数调用时执行    &lt;strong&gt;setState&lt;/strong&gt; 的方式代替。使用上面两种方式后，React 会将新状态和派生状态在一次更新内完成。&lt;/li&gt;
  &lt;li&gt;根据 DOM 信息，修改组件状态。在该场景中，除非想办法不依赖 DOM 信息，否则两次更新过程是少不了的，就只能用其他优化技巧了。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;use-swr 的源码就使用了该优化技巧。当某个接口存在缓存数据时，use-swr 会先使用该接口的缓存数据，并在 requestIdleCallback 时再重新发起请求，获取最新数据。模拟一个swr：&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-e3d6a721a15a3c031398737fa555c5dbe70.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;它的第二个参数 deps，是为了在请求带有参数时，如果参数改变了就重新发起请求。&lt;/li&gt;
  &lt;li&gt;暴露给调用方的 fetch 函数，可以应对主动刷新的场景，比如页面上的刷新按钮。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;如果 use-swr 不做该优化的话，就会在 useLayoutEffect 中触发重新验证并设置 isValidating 状态为 true·，引起组件的更新流程，造成性能损失。&lt;/p&gt;
 &lt;h1&gt;工具介绍——React Profiler&lt;/h1&gt;
 &lt;h2&gt;React Profiler 定位 Render 过程瓶颈&lt;/h2&gt;
 &lt;p&gt;React Profiler 是 React 官方提供的性能审查工具，本文只介绍笔者的使用心得，详细的使用手册请移步官网文档。&lt;/p&gt;
 &lt;p&gt;Note：react-dom 16.5+ 在 DEV 模式下才支持 Profiling，同时生产环境下也可以通过一个 profiling bundle react-dom/profiling 来支持。请在 fb.me/react-profi… 上查看如何使用这个 bundle。&lt;/p&gt;
 &lt;p&gt;“Profiler” 的面板在刚开始的时候是空的。你可以点击 record 按钮来启动 profile：&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-24112e88844bd37cbf0bfbbe42391c142c4.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;Profiler 只记录了 Render 过程耗时&lt;/h2&gt;
 &lt;p&gt;不要通过 Profiler 定位非 Render 过程的性能瓶颈问题&lt;/p&gt;
 &lt;p&gt;通过 React Profiler，开发者可以查看组件 Render 过程耗时，但无法知晓提交阶段的耗时。&lt;/p&gt;
 &lt;p&gt;尽管 Profiler 面板中有 Committed at 字段，但这个字段是相对于录制开始时间，根本没有意义。&lt;/p&gt;
 &lt;p&gt;通过在 React v16 版本上进行实验，同时开启 Chrome 的 Performance 和 React Profiler 统计。&lt;/p&gt;
 &lt;p&gt;如下图，在 Performance 面板中，Reconciliation和Commit阶段耗时分别为 642ms 和 300ms，而 Profiler 面板中只显示了 642ms：&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-ae3c815e9b3e80c2b42ed83a2bdfd99e38b.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;开启「记录组件更新原因」&lt;/h2&gt;
 &lt;p&gt;点击面板上的齿轮，然后勾选「Record why each component rendered while profiling.」，如下图：&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-a9442d9e79b59acc37aac4f1b7f0a3e72a6.JPEG" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;然后点击面板中的虚拟 DOM 节点，右侧便会展示该组件重新 Render 的原因。&lt;/p&gt;
 &lt;h2&gt;定位产生本次 Render 过程原因&lt;/h2&gt;
 &lt;p&gt;由于 React 的批量更新（Batch Update）机制，产生一次 Render 过程可能涉及到很多个组件的状态更新。那么如何定位是哪些组件状态更新导致的呢？&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-db44264ec4a18285bad1fa100f1928f1dc8.JPEG" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;在 Profiler 面板左侧的虚拟 DOM 树结构中，从上到下审查每个发生了渲染的（不会灰色的）组件。&lt;/p&gt;
 &lt;p&gt;如果组件是由于 State 或 Hook 改变触发了 Render 过程，那它就是我们要找的组件，如下图：&lt;/p&gt;
 &lt;p&gt;  &lt;img align="middle" src="https://oscimg.oschina.net/oscnet/up-10e0a2826e6a558259e77a6ab6bbfa54570.png" width="700"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h1&gt;站在巨人的肩膀上&lt;/h1&gt;
 &lt;p&gt;  &lt;a href="https://react.docschina.org/docs/optimizing-performance.html" title="Optimizing Performance React"&gt;Optimizing Performance React&lt;/a&gt; 官方文档，最好的教程, 利用好 React 的性能分析工具。&lt;/p&gt;
 &lt;p&gt;  &lt;a href="https://medium.com/@paularmstrong/twitter-lite-and-high-performance-react-progressive-web-apps-at-scale-d28a00e780a3" title="Twitter Lite and High Performance React Progressive Web Apps at Scale "&gt;Twitter Lite and High Performance React Progressive Web Apps at Scale &lt;/a&gt;看看 Twitter 如何优化的。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;-END-&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>技术分享 大前端 react</category>
      <guid isPermaLink="true">https://itindex.net/detail/61467-%E6%BA%90%E7%A0%81-%E4%B8%9A%E5%8A%A1-react</guid>
      <pubDate>Thu, 27 May 2021 11:03:15 CST</pubDate>
    </item>
  </channel>
</rss>

