写点什么

浏览器应该使用所有的可用内存吗?

  • 2021-08-24
  • 本文字数:5029 字

    阅读完需:约 16 分钟

浏览器应该使用所有的可用内存吗?

本文最初发布于 Julio Merino 的个人博客,经原作者授权由 InfoQ 中文站翻译并分享。


这篇文章在我的草稿箱里呆了 3 年了。我之所以一直犹豫着没发,一部分原因是本文探讨的内容是一个比较难以达成共识的想法。另一部分原因是,如果只看这个激进的标题,可能会引发不小的争议。无论如何,现在是时候发布出来了!


根据Betteridge的标题法则,标题中的问题,答案一般是“不“。我同意,但有一些点需要注意。


我们都见过类似这样的讨论,尤其是在 Hacker News 上:首先是有人抱怨,像谷歌 Chrome 这样的应用程序很浪费资源,因为它会消耗好几 GB 的内存。然后就有人过来说,这些内存是用来提升速度的,因此,这是正确的行为:如果计算机有数 GB 的空闲内存,像 Chrome 这样的应用程序应该将所有可用内存用作缓存,从而尽可能地提高响应速度。好像很有道理,是吗?


是的,有道理,但前提得是只有 Chrome 一个应用程序在运行。然而,这种情况并不常有,不是吗?总是会有多个程序同时运行(考虑下系统服务,还有像 Teams 这样的重量级程序),也就是说,那也许不是最好的主意。


本文将探讨两个问题,一是为什么最好不要允许应用程序占用所有的可用内存作为缓存,二是关于这一问题的可能的解决方案。我将以 Chrome 为例,因为它经常成为抱怨的对象,但本文探讨的内容也同样适用于所有其他的浏览器,以及大多数现代化的大型程序。但是,在此之前,让我们先回顾一些内存管理的基础知识,以确保我讲的和你想的是同一件事。

内存分页回顾


对于运行在其上的每个应用程序(进程),现代计算机提供的内存地址空间基本上可以说是无限的。每个进程都认为只有自己在运行,有海量的内存可供自己使用,从地址 0 到 2^64(在 64 位的机器上)。


显然,事实并非如此:还没有计算机有 2^64 物理字节的内存。计算机处理器将物理内存划分成固定大小的页帧(page frames)(通常是 4KB),将每个进程的虚拟内存划分成页(pages)。然后,操作系统内核负责将虚拟内存页子集映射到物理页帧,对于每一次内存访问,处理器会处理虚拟地址和物理地址之间的转换。


在这种设计下,虚拟地址空间比物理内存要大许多,多个进程(这样会有多个虚拟地址空间)可以同时运行。换句话说:物理内存被故意过度使用(over-committed)。当一个进程在其虚拟地址空间中分配了较多的内存,可能会没有足够的物理内存页来支撑新分配的虚拟页。这样就会导致内存紧张(memory pressure),内核将做些事情来缓解这种情况,尝试满足新的内存请求。


在内存紧张的情况下,内核会做什么,这取决于操作系统,但通常来说,内核必须找到已经映射的页(可以是任何进程的)并把它们驱逐,从而释放页帧,为新的内存请求腾出空间。


我们可以将内存页分成以下两类。要了解更多背景信息,我建议你读下NetBSD关于统一缓冲区缓存的论文

  • 文件页对应的内存块直接来自磁盘文件。如果没有修改的话,这些页可以随意丢弃,如果修改了,则可以刷写到它们的后备文件。例如:用于运行可执行代码的页总是可以丢弃,因为它们是只读的,而且磁盘上有后备文件,而通过 mmap(2) 使用的页可能需要,也可能不需要刷写到磁盘,这要视它们的脏状态而定。

  • 匿名页对应分配给应用程序的内存块(考虑下 mallocnew )。在内核看来,这些内存的内容是无意义的,因为它们是由程序逻辑 "动态 "填充的,没有后备资源。这样一来,如果要驱逐这些页,就没有一个文件可供刷写,所以内核就得把它们放在其他某个地方。那个某个地方就是交换区(swap area)

重要提示:关于页驱逐的一个关键细节,也是你阅读本文接下来的内容时必须留意的一个关键细节,就是页的原始来源不同,页驱逐进程会有所不同。

如果系统无法找到足够的页来驱逐(例如,已经驱逐了所有文件页,也已经没有交换空间来驱逐剩余的匿名页),内核就会发生严重错误,或者开始终止进程,尝试释放已使用的内存。在 Linux 上,这是通过备受喜爱的 Out Of Memory killer 机制完成的。


至于如何决定从内存中驱逐哪些页,每个内核都有自己的算法。一般来说,内核会实现一个 LRU 算法驱逐最近最少使用的页。但是,它也会考虑每次驱逐的“成本”:

  1. 成本最低的做法是首先驱逐只读文件页,因为它们不需要通过写磁盘实现持久化,而且可以快速恢复。

  2. 成本第二低的是驱逐脏文件页,因为它们位于文件系统中,可以覆写。

  3. 成本最高的是驱逐匿名页,因为这不可避免地要写交换区——这反过来又可能会涉及某种空间分配。你永远不会希望系统到达必须将页移到交换区的程度,这么做时,性能就很糟糕了。

不过,这里就不介绍具体细节了,那和本文接下来的内容没什么关系。

同时运行 Chrome 和 Bazel


在介绍完背景信息后,让我们回到最初的讨论。


为了证明 Chrome 大量使用内存的合理性,通常人们给出的论据是,浏览器使用所有内存作为缓存。持有这种观点的人认为,这是好事,因为人们想要快速的浏览体验,尽量多地缓存数据有助于实现那种体验。没错,不过他们忽略了一个小事实,就是 Chrome 不是在真空中运行。


我将采用 Raymond Chen 的分析方法“如果两个程序这样做会怎么样?”,说明这为什么是个坏主意。饥饿的浏览器可能与其他程序同时运行,而其中某些程序也可能是内存密集型的。为了使示例场景更真实,我们把 Bazel 加入进来,这个应用程序也喜欢占用大量的内存,以缓存工作空间中数 GB 的构建图。在这个场景中,我们有一名程序员使用 Chrome 在线研究一些信息,编写一些代码(使用某种重量级 IDE),最后使用 Bazel 构建生成的项目。


在这种情况下,程序员可能首先会密集地使用 Chrome 做些研究。在这个过程中,Chrome 的内存使用可能会逐渐增加,占用所有可用的内存来缓存页面和图片。然后,程序员可能会运行 Bazel 构建项目。而 Bazel 可能需要额外消耗大量的内存来加载完全依赖图。但是,此时,Bazel 可能没有找到足够的可用内存,所以操作系统将需要换出 Chrome 的缓存内存。这可能会导致后续切回 Chrome 的时候反应速度慢很多,因为浏览器缓存的东西都被换出了。


这里的问题是,像 Chrome 和 Bazel 这样的应用程序使用匿名内存来运作它们自己的缓存。按定义,缓存内存可以在任意时间随意丢弃,当出现内存压力时,内核唯一能看到的是这些应用程序分配了大量的匿名内存。内核并不知道这些页中是包含必须持久化的宝贵数据,还是可随意丢弃的易失性数据。由此导致的恶果是,内核可能会决定将缓存数据移到交换区,我前面提到过,一旦用到了交换区,从性能的角度来说,你已经输了。


关于这一点,我们有什么可以做的吗?

协作型内存分配


你会说,“好吧,我们显然不能允许应用程序把所有内存都拿来自己用。我们应该限制他们最多使用 X%的内存,要留一些内存给其他应用程序使用”。(事实上,这就 Bazel 采用的方案。)


这无法解决任何问题:如果应用程序本身负责查看当前可用的内存,然后独自决定应该使用多少内存,那么要么我们最终还会面临上面的情况,要么就是应用程序没有足够的内存可以使用。设想一下,如果我们允许 Chrome 使用所有内存的 80%,因为它知道要留下 20%。然后,Bazel 要运行了,按照配置,它也可以使用可用内存的 80%……是 20% 的 80%,已经很小了。Bazel 受到了巨大的惩罚,只因为它是第二个运行的。


你可以调整这个模型,提出一种有效的方案,但是,通过分布式决策决定如何使用内存是不够的。首先,你需要一个能在整个机器上做出明智决策的神使(内核);其次,你不能相信所有的应用程序都会遵守规则。毕竟,那就是几十年前我们从协作型多任务转到抢占式多任务的原因。

文件缓存


我们考虑下这样一个场景:有很多应用程序,它们几乎不使用匿名内存,但会大量使用文件系统。这些应用程序每个都会打开许多非常大的文件,对它们执行随机读写操作,而且还打开很长时间。我们同时运行着这些应用程序的多个实例。


在这种情况下,我们可能会看到,系统总体的内存使用率接近 100%,和之前一样,但交换区仍然是空的。更重要的是,虽然系统可能因为 CPU 和 I/O 使用率高而变慢,但其性能是可预测的:从命令行执行一条简单的命令 ls 瞬间就可以完成,不会受内存抖动拖累。


这里的情况是,内核现在将所有内存作为其文件缓冲区缓存的一部分;应用程序本身不控制内存。这种内核级的缓存可以跟踪文件页(不是匿名页),通过优化随机 I/O 和顺序访问(通过预取)来提升 I/O 性能。通常,该缓存可以占用所有可用的内存。


与前面介绍的 Chrome 和 Bazel 所采用的方案相比,这种基于文件的方案有一个很大的不同,就是由内核控制一个统一的跨应用程序的缓存,内核对缓存中的内容了如指掌。内核可以针对缓存中的内容从整体视角做出决策,尽量保证所有应用程序的正常运行:如果只有一个应用程序在运行,那么所有的文件缓冲区缓存将都供它使用;但是,如果有两个或两个以上的应用程序在运行,那么它们将“公平地”共享缓存——我这里之所以加引号,是因为确实存在相互干扰的问题。

根本原因


那么,允许 Chrome 使用所有内存作为缓存,真正的问题在哪里?


简单来说,就是操作系统无法查看应用程序的匿名内存,不能自己做决策。由此导致的结果是,当出现内存压力时,内核唯一能做的事情是,将匿名内存页推出到磁盘——即使那些页包含了易失性缓存——后续再从交换区还原它们成本很高。

有什么解决方案吗?


设想一下,如果内核和应用程序之间有一种可以回收内存的反馈机制。内核可以说,“嗨,Chrome,我内存不够用了,把你不是特别需要的内存释放出来一些吧”,以此请求回收内存。


遗憾的是,这是不可行的,因为这得要求所有应用程序配合,在所有情况下都不能做错。流氓应用程序或是存在缺陷的应用程序会囤积内存,导致更糟糕的情况。


尽管如此,Android 就实现了这样一种方案。Android 的设计就是,系统可以彻底驱逐应用程序的某些部分(活动)。其原理是,系统和应用程序之间有一种协议,通过它可以实现受控的驱逐动作:系统首先会友好地请求应用程序释放内存,并允许应用程序刷写数据,但是,即使应用程序没有按照要求释放内存,系统也会销毁那部分内存。这两种情况都有可能出现,因此,在设计应用程序时必须保证,不管是被优雅地关闭还是强制关闭,它都可以重建状态——就像它从未退出过。Android 之所以有这样一种设计,一个原因是它首先是面向移动设备的,这类设备的内存很小,另一个原因是移动设备同一时间主要运行一个应用。


另一种解决方案是,有一个系统级的缓存服务,可以处理运行应用程序产生的任意内存对象。这样一个服务可以跨应用程序就内存使用做出协商一致的决策,均匀地删除缓存条目。但是,和前面的解决方案一样,需要所有应用程序的配合。否则,一个不合格的应用程序可能会囤积内存,使得其他所有按规则行事的应用程序都受到惩罚。


这就说到了另一种解决方案:划分内存,预先指定每个程序可以使用的内存大小。容器就是这么做的,但对于个人计算机来说这并不是一个好方案:硬性划分无法动态适应用户的行为。有时候,你就是只想浏览网页,在那种情况下,你会希望浏览器使用所有可用的资源。


最后,我们可以尝试将应用程序都塞进现有系统的设施中。如果应用程序使用文件而不是匿名内存来实现缓存,那么系统的文件缓冲区缓存就可以正常运行。设想一下,每个应用程序都使用单个的文件来存储可缓存的对象,而不是使用 malloc 来获取匿名内存。这时,应用程序会使用打开/读取/关闭循环来访问那些缓存的对象。在这种情况下,内核中的文件缓存就可以跨应用程序做该做的事:经常使用的缓存条目(文件)会驻留内存,如果内存紧张,就可以把它们驱逐到后备文件中,而且成本很低。性能可能会因为额外的系统调用而受影响,但总的结果还是要好些。事实上,Varnish就是这样做的


遗憾的是,上面所有这些解决方案都需要某种跨程序协同,并且需要所有程序都按规则行事。这在设计新系统时也许可行(就像 Android 所做的那样),但将这些东西加装到目前的系统中,肯定是不行的,虽然我希望它可以。


最后,你可能会认为,在现如今的世界里,上面这样的情况没什么问题,因为计算机有足够的内存。但它们确实还是问题。我之前在谷歌的时候,就想着要让人们能够在 16GB 的笔记本上愉快地使用 Chrome 和 Bazel。每次我的 Surface Go 2 变慢(我一年前新买的机器,但只有 8GB 内存),我就会想起这些问题。


那么,让我们回到最初的问题:“浏览器应该使用所有可用的内存吗?”不应该,不能像现在这样做。但是,如果有更好的机制可以实现有效的跨应用程序缓存,答案就是 Yes 了。


原文链接:


Should the browser use all available memory?

2021-08-24 11:237362

评论

发布
暂无评论
发现更多内容

阿里P9重磅分享内部绝密《百亿级并发系统设计》手册!

程序员高级码农

Java 程序员 高并发 架构设计 架构师

OpenAI从传统发布会改成12天直播:OpenAI Day1 带来了哪些惊喜?

测试人

Java后端最全面试攻略,吃透25个技术栈,阿里十万字内部面试题总结全网开源

架构师之道

编程 java面试

想提高查询性能,用GaussDB(DWS) in表达式还是or表达式?

华为云开发者联盟

数据库 GaussDB 表达式 大数据‘’ #SQL

科大讯飞P20 Plus词典笔 怎么样

妙龙

科大讯飞 词典笔

面试必刷:阿里巴巴 内部 Java 高级架构师 1080 道面试题

采菊东篱下

Java 编程 计算机

币安独霸,okx,bitget共享天下交易所新格局

区块链项目一站式包装孵化

Java面试突击手册,一周刷完这300道面试题,你也可以当架构师!

Summer

Java 程序员 面试 架构师 大厂

让我们一起来建设 Fluent Editor 开源富文本编辑器吧!

OpenTiny社区

富文本 OpenTiny 前端开源

猿辅导和作业帮哪个更好

妙龙

作业帮 学习机 猿辅导

DevOps研发效能建设的六大“雷区”:你中招了吗?

嘉为蓝鲸

DevOps 研发度量 效能度量 研发效能管理

AI与数据分析|使用机器学习,轻松解决复杂的情感分析问题

Altair RapidMiner

机器学习 AI 数据分析 情感分析 altair

Mysql优化

EquatorCoco

MySQL

作业帮X58和X28区别对比选哪个

妙龙

作业帮 学习机

科大讯飞T30 Lite和T30 Pro 对比

妙龙

科大讯飞 学习机

AI Agent:未来高效螺丝钉,谁用得好,谁先赚到钱

博文视点Broadview

新金景集团:二十载专注做好女性私密

新消费日报

ChatGPT在功能测试用例生成方面的优势

不在线第一只蜗牛

ChatGPT

GitHub下载破千万!这份Java大厂面试指南,竟是阿里面试官上传的

Summer

Java 程序员 面试 架构师 大厂

科大讯飞学习机和作业帮学习机哪个好

妙龙

科大讯飞 作业帮 学习机

精选的掘金文章汇总[2024.11月-12月]

安全乐谷

GitHub 架构 算法 前端 后端

CCF-CV企业交流会—走进合合信息顺利举办,打造大模型时代的可信AI

合合技术团队

人工智能 信息安全 图像安全

编写 Java 单元测试最佳实践

腾讯云 AI 代码助手

Java行情崩盘了?传智播客收入下滑严重,Java之父和金角大王的IT课程白菜价贱卖

陆通

2025上海国际机器人展(Tech G)

AIOTE智博会

消费电子展 消费电子展会 消费电子博览会 消费电子展览会

阿里Spring Security OAuth2.0认证授权笔记震撼开源!原理+实战+源码三飞

采菊东篱下

编程 java面试

怎么把域名解析到IP地址?流程有哪些?一文讲清域名解析那些事

国科云

RWA代币:下一波财富增长的密码?

TechubNews

阿里大牛强力推荐:springboot实战派文档,从入门到实战,样样具备

架构师之道

Java 编程

浏览器应该使用所有的可用内存吗?_语言 & 开发_Julio Merino_InfoQ精选文章