现代图片性能优化及体验优化指南

Posted ChokCoco

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了现代图片性能优化及体验优化指南相关的知识,希望对你有一定的参考价值。

之前,整个《现代图片性能优化及体验优化指南》分了 5 篇来发,本文是系列合集,方便大家收藏及连贯阅读。

图片资源,在我们的业务中可谓是占据了非常大头的一环,尤其是其对带宽的消耗是十分巨大的。

对图片的性能优化及体验优化在今天就显得尤为重要。本文,就将从各个方面阐述,在各种新特性满头飞的今天,我们可以如何尽可能的对我们的图片资源,进行性能优化及体验优化。

图片类型的选取及 Picture 标签的使用

首先,从图片的类型上而言,除了常见的 PNG-8/PNG-24,JPEG,GIF 之外,我们更多的关注另外几个较新的图片格式:

  • WebP
  • JPEG XL
  • AVIF

首先,通过一张表格,快速过一下这几个图片,我们将从图片类型、透明通道、动画、编解码性能、压缩算法、颜色支持、内存占用、兼容性方面,对比它们:

图片类型 Alpha 通道 动画 编解码性能 压缩算法 颜色支持 内存占用 兼容性
GIF 支持 支持 较高 无损压缩 索引色(256) 基本一致 ALL
PNG-8/PNG-24 支持 不支持 较高 无损压缩 索引色(256)\\直接色 基本一致 ALL
JPEG 不支持 不支持 较高 有损压缩 直接色 基本一致 ALL
WebP 支持 支持 编解码性能差(低配设备更为显著) 有损压缩\\无损压缩 直接色 基本一致 高版本 Chrome\\Opera\\Android
JPEG XL 支持 支持 渐进式解码 有损压缩\\无损压缩 直接色 基本一致 部分高版本 Chrome\\Opera\\Firefox\\Edge
AVIF 支持 支持 编解码性能一般 有损压缩\\无损压缩 直接色 基本一致 高版本 Chrome\\Opera\\Android\\Edge

首先,了解了解上述的一些参数含义:

  • Alpha 通道:图片是否支持透明的特性

当然,需要指出的是,Alpha 没有透明度的意思,不代表透明度。opacity 和 transparency 才和透明度有关,前者是不透明度,后者是透明度。比如 css 中的「opacity: 0.5」就是设定元素有 50% 的不透明度。后来 Alvy Ray Smith 提出每个像素再增加一个 Alpha 通道,取值为0到1,用来储存这个像素是否对图片有「贡献」,0代表透明、1代表不透明。也就是说,「Alpha 通道」储存一个值,其外在表现是「透明度」,Alpha 和透明度没啥关系

  • 动画:很好理解,图片是否支持多帧率动态图片,类似于 GIF
  • 编解码性能:图像的解码与编码。这个很关键,很多人对待图片容易忽视图片的编解码性能,解码图像主要从图像文件中读出图像数据,而编码则是将图像数据写入图像文件。解码与编码的过程正好相反。而这两者的性能耗时会影响我们页面的的展示性能。
  • 压缩算法:该图片格式是否支持压缩,支持的话,图片的压缩又会分为无损压缩与有损压缩

有损压缩算法是一种数据压缩方法,经过此方法压缩、解压的数据会与原始数据不同但是非常接近。原理是借由将次要的信息数据舍弃,牺牲一些质量来减少数据量、提高压缩比

无损压缩指数据经过压缩后,信息不受损失,还能完全恢复到压缩前的原样。无损压缩通常用于严格要求“经过压缩、解压缩的数据必须与原始数据一致”的场合。

  • 颜色支持:会分为索引色与直接色,在过去,为了节省存储空间,并非所有图片都能支持所有颜色值,因此存在索引色这种优化方式。

索引颜色是一种以有限的方式管理数字图像颜色的技术,以节省计算机内存和文件存储,同时加速显示刷新和文件传输。即用一个数字来代表(索引)一种颜色,在存储图片的时候,存储一个数字的组合,同时存储数字到图片颜色的映射。这种方式只能存储有限种颜色。索引色常见有1位(即黑白),8位(即灰阶/256色),16位(即高彩),24位(即真彩),30/36/48位(即全彩)。

直接色使用四个数字来代表一种颜色,这四个数字分别代表这个颜色中红色、绿色、蓝色以及透明度(即 RGBA)。现在流行的显示设备可以在这四个维度分别支持256种变化,所以直接色可以表示2的32次方种颜色。

  • 内存占用:图片对内存资源的占用
  • 兼容性:影响图片格式能否大规模推广的核心要素之一

WebP vs JPEG XL vs AVIF: JPEG 替代之战

因为传统的 PNG-8/PNG-24,JPEG,GIF 各自或多或少都存在一些问题,近些年来它们的替代方案之争也愈演愈烈,核心领跑者可能是 WebPJPEG XLAVIF

再简单了解了解它们:

WebP

WebP 最初由 Google 在 2010 年 9 月发布,其特性总结如下:

  1. 可以同时提供无损/有损压缩(像 JPEG 一样)和支持透明度(像 PNG 一样)的图片文件格式
  2. 支持动画效果(像 GIF 一样)
  3. WebP 主要优势在于有损编码,其无损编码的性能和压缩比表现一般
  4. WebP 的缺点在于其编解码性能不是特别理想
  5. 在兼容性方面,除了 IE,基本已经得到了全系列浏览器支持

对于复杂的图像(比如照片)来说,WebP 无损编码表现并不好,但有损编码表现却非常棒。相近质量的图片解码速度 WebP 相距 JPEG 也已经相差不大了,而文件压缩比却能提升不少。

下图是我之前还在 TX 的时候做的一个测试对比:

加载同样张数的 JPEG 与 WebP 的耗时对比:

对于 WebP 图片格式,简单做个总结:

  1. 目前 WebP 与 JPEG 相比较,据资料考证,编码速度慢 10 倍,解码速度慢 1.5 倍
  2. WebP 虽然会增加额外的解码时间,但由于大幅减少了文件体积,缩短了加载的时间,大页面图片量较多的场景下,页面的渲染速度是有较大加快的
  3. 目前而言,是 WebP、JPEG XL、AVIF 三者中兼容性最好的

截止至(2023-02-05)的兼容性图:

JPEG XL

JPEG XL 由联合图像专家组(开发原始 JPEG 标准的同一组织)于 2021 年发布,旨在成为传统 JPEG 的长期替代品。作为一种免版税的开源标准,JPEG XL 的创建者希望其格式的开放性能够吸引网络开发人员采用该标准,该格式的扩展名为 .jxl,JXL 核心比特流于 2021 年 1 月冻结,文件格式于 2021 年 4 月定稿。:

JPEG XL 中的 X 指 2000 年以来的多个 JPEG 标准的名称:JPEG XTJPEG XRJPEG XS,而 L 表示 \'long-term\',表示“长期”。创建这种格式是为替换旧的JPEG文件格式,并使用足够长的时间。

其主要特点有:

  • 与传统图像格式(例如JPEG、GIF和PNG)相比,有着更佳的效率与更丰富的功能
  • 全面支持广色域和 HDR,支持 Alpha 通道,支持多帧(也就是动画支持)
  • 有损压缩时:相同的视觉质量,比 JPEG 小约 60%。
  • 无损压缩时:比 PNG 减小约 35%(对于 HDR,减小 50%)。
  • 支持无损 JPEG 转码,减小约 20% 文件大小。
  • 渐进式解码,专为支持不同显示分辨率的响应式加载
  • 开源免费:具有使用三条款版BSD许可证开源参考实现的免版税格式

看看同一张图片,相同质量下的大小表现:

数据来源:技术周刊 2021-04-15:2021最值得期待的新技术 JPEG XL

JPEG XL 是目前而言,最有可能全面替代传统图片格式(Gif、PNG、JPEG)的下一代标准,当然,在今天,需要看看其兼容性:

好吧,目前的兼容有点离谱。Chrome 从 91 版本开始已经实验室性质支持了 .jxl 格式的图片,需要通过 --enable-features=JXL 配置开启,遗憾的是,从 Chrome 110 开始,Chrome 又不再支持 JPEG XL 。

有趣的是,Chrome 从 110 版本开始中弃用了对 JPEG-XL 的支持,谷歌的回答是,人们对 JPEG-XL 没有足够的兴趣,并且与现有格式相比也没有足够的优势。谷歌之前一直对 JPEG 的支持都是实验性的性质的,他们认为 JPEG XL 缺乏生态系统支持,并且该格式没有足够多的好处(相对 WebP 和 AVIF)。也就是说,目前而言,Chrome 对 WebP 和 AVIF 等替代格式更感兴趣。

AVIF

最后,我们再来看看 AVIF 格式图片。

AVIF 是由开放媒体联盟 (AOM) 开发并于 2019 年发布的另一种最新图像格式。该格式基于 AV1 视频编解码器,源自视频帧。其特点如下:

  • 同样的,与传统图像格式(例如JPEG、GIF和PNG)相比,有着更佳的效率与更丰富的功能
  • 支持 Alpha 通道,支持动态图像动画
  • 支持有损、无损压缩。AVIF 文件在低保真有损图像压缩方面表现出色(比 JPEG XL 更优)。压缩的 AVIF 图像保留了很高的图片质量,避免了恼人的压缩伪影等问题
  • 相对而言,AVIF 的解码和编码速度不如 JPEG XL,它不支持渐进式渲染
  • 最后,再看看兼容性,目前(2023-02-05)它的兼容性介于 WebP 与 JPEG XL 之间

看看 CaniUse 的数据:

下图是 WebP vs JPEG XL vs AVIF 三者在图片解码上的性能表现:

图片来源于:Encode.su -- JPEG XL vs. AVIF

从图中可以看到,对于解码性能的对比,结果居然是 WebP > AVIF > JPEG XL 。JPEG XL 的编解码性能并没有其描述的那么强大。

图片格式总结

总结一下,WebP、AVIF 和 JPEG XL 都是浏览器不广泛支持的新型图像格式。虽然 WebP、AVIF 已经存在很长时间,但到今天,影响它们大规模使用的依旧是兼容问题。它们各自有各自的特点与优势,谁能胜出仍未知晓。

虽然 AVIF、JPEG XL 等新型图片格式未得到任何浏览器的完全支持,但是在新版本的 Chrome、Firefox 和 Edge Chromium,可以使用配置标志启用对应图像格式,配合 HTML 的 Picture 标签,我们还是可以一定程度上对我们的图片进行格式选择上的优化的。

这,就可以引出我们要说的第二部分 -- HTML Picture 标签的使用。

Picture 元素的使用

HTML5 规范新增了 Picture Element。那么 <picture> 元素的作用是什么呢?

<picture> 元素通过包含零或多个 <source> 元素和一个 <img> 元素来为不同的显示/设备场景提供图像版本。浏览器会选择最匹配的子 <source> 元素,如果没有匹配的,就选择 <img> 元素的 src 属性中的 URL。然后,所选图像呈现在 <img> 元素占据的空间中。

什么意思呢?怎么使用 <picture> 元素呢?

假设,没有 <picture> ,只有 <img> 元素,我们想尽可能在支持一些现代图片格式的浏览器上使用类似于上述我们提到的 WebP、AVIF 和 JPEG XL 等图片格式,而不支持的浏览器回退使用常规的 JPEG、PNG 等。没错,就是一种渐进增强的思想,该怎么办呢?

只能是 JavaScript 去写对应的逻辑,通过 JS 脚本进行特性查询,动态赋值给 <img> 的 src。

而有了 <picture> 后,浏览器将原生支持上述的一些列操作,我们来看看对应的语法:

<picture>
  <!-- 可能是一些对兼容性有要求的,但是性能表现更好的现代图片格式-->
  <source src="image.avif" type="image/avif">
  <source src="image.jxl" type="image/jxl">
  <source src="image.webp" type="image/webp">

   <!-- 最终的兜底方案-->
  <img src="image.jpg" type="image/jpeg">
</picture> 

上述代码的含义是:

  • 第 1 个 source 元素指向新 AVIF 格式的图像。如果浏览器支持 AVIF 图像,那么它会选择该图像文件。否则,它将移动到下一个 source 元素。
  • 第 2个 source 元素指向新 JPEG XL 格式的图像。如果浏览器支持 JPEG XL 图像,那么它会选择该图像文件。否则,它将移动到下一个 source 元素。
  • 第 3 个 source 元素指向一张WebP 格式的图像。如果浏览器能够渲染 WebP 图像,它将使用该图像文件。
  • 否则浏览器将回退到使用 img 元素 src 属性中的图像文件。img 元素指向的是 JPEG 格式的图片,它是最终的兜底方案。

这意味着现在我们可以在不牺牲向后兼容性的情况下开始使用新的图像格式。

简而言之,<picture> 元素的作用:

  1. 通过 <source> 给出一系列对兼容性有所要求的现代图片格式选项
  2. 通过 <img> 给出兜底的高兼容性图片格式选项
  3. 浏览器通过对给出的图片格式做特性检测,要决定加载哪个 URL,user agent 检查每个 <source> 的 srcset、media 和 type 属性,来选择最匹配页面当前布局、显示设备特征等的兼容图像。
  4. 最终,所选图像呈现在 <img> 元素占据的空间中

模块总结

总结一下,本文对常见的图片格式以及最新的几种未被大规模兼容的图片格式进行的对比,它们分别是:

  • PNG-8/PNG-24
  • JPEG
  • GIF
  • WebP
  • JPEG XL
  • AVIF

其后,着重介绍了 3 种现代图片格式:WebP、JPEG XL、AVIF。相对于 JPEG 等传统格式,它们在色彩表现、动画支持、是否支持无损有损压缩、压损比率、编解码性能上有着更进一步的提升,正在成为下一阶段 Web 图像的标准。

最后,介绍了 <picture> 元素,借助它,我们能更好的实现图片的渐进增强。

适配不同的屏幕尺寸及 DPR

下一个模块,我们来看看图片资源如何更好的适配不同的屏幕尺寸。

这里首先会涉及一个预备知识,屏幕的 DPR 值,那么,什么是 DPR 呢?要了解 DPR,又需要知道什么是设备独立像素 以及 物理像素

设备独立像素

以 iPhone6/7/8为例,这里我们打开 Chrome 开发者工具:

这里的 375 * 667 表示的是什么呢,表示的是设备独立像素(DIP),也可以理解为 CSS 像素,也称为逻辑像素:

设备独立像素 = CSS 像素 = 逻辑像素

如何记忆呢?这里使用 CSS 像素来记忆,也就是说。我们设定一个宽度为 375px 的 div,刚好可以充满这个设备的一行,配合高度 667px ,则 div 的大小刚好可以充满整个屏幕。

物理像素

OK,那么,什么又是物理像素呢。我们到电商网站购买手机,都会看一看手机的参数,以 JD 上的 iPhone7 为例:

可以看到,iPhone7 的分辨率是 1334 x 750,这里描述的就是屏幕实际的物理像素。

物理像素,又称为设备像素。显示屏是由一个个物理像素点组成的,1334 x 750 表示手机分别在垂直和水平上所具有的像素点数。通过控制每个像素点的颜色,就可以使屏幕显示出不同的图像,屏幕从工厂出来那天起,它上面的物理像素点就固定不变了,单位为pt。

设备像素 = 物理像素

DPR(Device Pixel Ratio) 设备像素比

OK,有了上面两个概念,就可以顺理成章引出下一个概念。DPR(Device Pixel Ratio) 设备像素比,这个与我们通常说的视网膜屏(多倍屏,Retina屏)有关。

设备像素比描述的是未缩放状态下,物理像素和设备独立像素的初始比例关系。

简单的计算公式:

DPR = 物理像素 / 设备独立像素

我们套用一下上面 iPhone7 的数据(取设备的物理像素宽度与设备独立像素宽度进行计算):

iPhone7’s DPR = iPhone7’s 物理像素宽度 / iPhone7\'s 设备独立像素宽度 = 2

750 / 375 = 2
或者是 1334 / 667 = 2

可以得到 iPhone7 的 dpr 为 2。也就是我们常说的视网膜屏幕。

视网膜(Retina)屏幕是苹果公司"发明"的一个营销术语。 苹果公司将 dpr > 1 的屏幕称为视网膜屏幕。

在视网膜屏幕中,以 dpr = 2 为例,把 4(2x2) 个像素当 1 个像素使用,这样让屏幕看起来更精致,但是元素的大小本身却不会改变:

OK,我们再来看看 iPhone XS Max:

它的物理像素如上图是 2688 x 1242

它的 CSS 像素是 896 x 414,很容易得出 iPhone XS Max 的 dpr 为 3。

为不同 DPR 屏幕,提供恰当的图片

那么,DPR 和图片适配有什么关系呢?

举个例子,同样的 CSS 像素大小下,屏幕如果有不同 DPR,同样大小的图片渲染出来的效果不尽相同。

我们以 dpr = 3 的手机为例子,在 300 x 389 CSS 像素大小的范围内,渲染 1倍/2倍/3倍 图的效果如下:

实际图片所占的物理像素为 900 x 1167。

可以看到,在高 DPR 设备下提供只有 CSS 像素大小的图片,是非常模糊的。

因此,为了在不同的 DPR 屏幕下,让图片看起来都不失真,我们需要为不同 DPR 的图片,提供不同大小的图片。

那么,有哪些可行的解决方案呢?

方案一:无脑多倍图

假设,在移动端假设我们需要一张 CSS 像素为 300 x 200 的图像,考虑到现在已经有了 dpr = 3 的设备,那么要保证图片在 dpr = 3 的设备下也正常高清展示,我们最大可能需要一张 900 x 600 的原图。

这样,不管设备的 dpr 是否为 3,我们统一都使用 3 倍图。这样即使在 dpr = 1,dpr = 2 的设备上,也能非常好的展示图片。

当然这样并不可取,会造成大量带宽的浪费。

现代浏览器,提供了更好的方式,让我们能够根据设备 dpr 的不同,提供不同尺寸的图片。

方案二:媒体查询

方案二,我们可以考虑使用媒体查询。到今天,我们可以通过相应的媒体查询,得知当前的设备的 DPR 值,这样,我们就可以在对应的媒体查询中,使用对应的图片。

像是这样:

#id  
    background: url(xxx@2x.png) 

@media (device-pixel-ratio: 2) 
    #id  
        background: url(xxx@2x.png) 
    

@media (device-pixel-ratio: 3) 
    #id  
        background: url(xxx@3x.png) 
    

这个方案的缺点在于:

  1. 要写的代码可能太多了,而且,可能存在一些介于 12,23 之间的 DPR 值,不好穷举出所有场景
  2. 需要注意语法需要的兼容性,需要添加前缀,譬如 -webkit-min-device-pixel-ratio,当然这个可以由 autoprefixer 辅助解决

方案三:CSS 配合 image-set 语法

image-set 属于 CSS background 中的一种语法,image-set() 函数为设备提供最合适的图像分辨率,它提供一组图像选项,每个选项都有一个相关的 DPR 声明,浏览器将从中选择最适合设备的图像进行设置。

什么意思呢,来看看代码:

.img 
    /* 不支持 image-set 的浏览器*/
    background-image: url(\'../photo@2x.png\');

    /* 支持 image-set 的浏览器*/
    background-image: image-set(
        url(\'./photo@2x.png\') 2x,
        url(\'./photo@3x.png\') 3x
    );

这样一看,作用应该很清晰了。对于支持 image-set 语法的浏览器:

  1. 如果其设备对应的 DPR 为 2,会选取这条 url(\'./photo@2x.png\') 2x 记录,也就是最终生效的 URL 是 \'./photo@2x.png\'
  2. 如果其设备对应的 DPR 为 3,会选取这条 url(\'./photo@3x.png\') 3x 记录,也就是最终生效的 URL 是 \'./photo@3x.png\'

其中的 2x3x 就是用于匹配 DRP的。

使用 image-set 的一些痛点与媒体查询方案类似。代码量与兼容性语法,而且难以匹配所有情况。

方案四:srcset 配合 1x 2x 像素密度描述符

简单来说,srcset 可以根据不同的 dpr 拉取对应尺寸的图片:

<div class=\'illustration\'>
   <img src=\'illustration-small.png\'
       srcset=\'images/illustration-small.png 1x,
               images/illustration-big.png 2x\'
   >
</div>

上面 srcset 里的 1x,2x 表示 像素密度描述符,表示

  • 当屏幕的 dpr = 1 时,使用 images/illustration-small.png 这张图
  • 当屏幕的 dpr = 2 时,使用 images/illustration-big.png 这张图
  • 如果不支持 srcset 语法,src=\'illustration-small.png\' 将会是最终的兜底方案

方案五:srcset 属性配合 sizes 属性 w 宽度描述符

上面 1x,2x 的写法比较容易接受易于理解。

但是,上述 3 种方案都存在统一的问题,只考虑了 DPR,但是忽略了响应性布局的复杂性与屏幕的多样性

因此,规范还推出了一种方案 -- srcset 属性配合 sizes 属性 w 宽度描述符

srcset 属性还有一个 w 宽度描述符,配合 sizes 属性一起使用,可以覆盖更多的面。

sizes 属性怎么理解呢?它定义图像元素在不同的视口宽度时,可能的大小值。

以下面这段代码为例子:

<img 
        sizes = “(min-width: 600px) 600px, 300px" 
        src = "photo.png" 
        srcset = “photo@1x.png 300w,
                       photo@2x.png 600w,
                       photo@3x.png 1200w,
>

解析一下:

sizes = “(min-width: 600px) 600px, 300px" 的意思是:

  1. 如果屏幕当前的 CSS 像素宽度大于或者等于 600px,则图片的 CSS 宽度为 600px
  2. 反之,则图片的 CSS 宽度为 300px

也就是 sizes 属性声明了在不同宽度下图片的 CSS 宽度表现。这里可以理解为,大屏幕下图片宽度为 600px,小屏幕下图片宽度为 300px。

需要注意的是,这里大屏、小屏下图片具体的宽度表现,还是需要借助媒体查询代码,经由 CSS 实现的

srcset = “photo@1x.png 300w, photo@2x.png 600w, photo@3x.png 1200w 里面的 300w,600w,900w 叫宽度描述符。

那么,怎么确定当前场景会选取哪张图片呢?

当前屏幕 dpr = 2 ,CSS 宽度为 375px

当前屏幕 CSS 宽度为 375px,则图片 CSS 宽度为 300px。分别用上述 3 个宽度描述符的数值除以 300。

  1. 300 / 300 = 1
  2. 600 / 300 = 2
  3. 1200 / 300 = 4

上面计算得到的 1、 2、 4 即是算出的有效的像素密度,换算成和 x 描述符等价的值 。这里 600w 算出的 2 即满足 dpr = 2 的情况,选择此张图。

当前屏幕 dpr = 3 ,CSS 宽度为 414px

当前屏幕 CSS 宽度为 414px,则图片 CSS 宽度仍为 300px。再计算一次:

  1. 300 / 300 = 1
  2. 600 / 300 = 2
  3. 1200 / 300 = 4

因为 dpr = 3,2 已经不满足了,则此时会选择 1200w 这张图。

当前屏幕 dpr = 1 ,CSS 宽度为 1920px

当前屏幕 CSS 宽度为 1920px,则图片 CSS 宽度变为了 600px。再计算一次:

  1. 300 / 600 = .5
  2. 600 / 600 = 1
  3. 1200 / 600 = 2

因为 dpr = 1,所以此时会选择 600w 对应的图片。

具体的可以试下这个 Demo:CodePen Demo -- srcset属性配合w宽度描述符配合sizes属性

此方案的意义在于考虑到了响应性布局的复杂性与屏幕的多样性,利用上述规则,可以一次适配 PC 端大屏幕和移动端高清屏,一箭多雕。

嗯,总结一下,在实现响应式图像时,我们同时使用 srcsetsizes 属性。它们的作用是:

  • srcset:定义多个不同宽度的图像源,让浏览器在 HTML 解析期间选择最合适的图像源
  • sizes:定义图像元素在不同的视口宽度时,可能的大小值

有了这些属性后,浏览器就会根据 srcset/size 来创建一个分辨率切换器的响应式图片,可以在不同的分辨率的情况下,提供相同尺寸的图像,或者在不同的视图大小的情况下,提供不同尺寸大小的图像。

模块总结

本章节一共列举了 5 种实现响应式图片,适配不同屏幕大小,不同 DPR 的方式,它们分别是:

  1. 无脑多倍图的方式
  2. DRP 媒体查询
  3. CSS Background 中的使用 image-set
  4. srcset 配合 1x 2x 像素密度描述符
  5. srcset 属性配合 sizes 属性 w 宽度描述符

合理使用它们,可以有效的为不同屏幕,提供最为恰当的图片资源,在保证用户体验的同时,尽可能节省带宽。

它们各有优缺点,可以根据自己实际的业务场景,选取合适相对成本最低的方案,并且适当的配合 Autoprefixer 以及一些 PostCSS 等工具,简化代码量。

图片的宽高比、裁剪与缩放

OK,下面进入到我们的第三个模块,图片的宽高比、裁剪与缩放。我们会介绍 4 个新的特性:

  • aspect-ratio
  • object-fit
  • object-position
  • image-rendering

使用 aspect-ratio 避免布局偏移

很多时候,只能使用固定尺寸大小的图片,我们的布局可能是这样:

对应的布局:

<ul class="g-container">
    <li>
        <img src="http://placehold.it/150x100">
        <p>图片描述</p>
    </li>
</ul>
ul li img 
    width: 150px;

当然,万一假设后端接口出现一张非正常大小的图片,上述不加保护的布局就会出问题:

所以对于图片,我们总是建议同时写上高和宽,避免因为图片尺寸错误带来的布局问题:

ul li img 
    width: 150px;
    height: 100px;

同时,给 <img> 标签同时写上高宽,可以在图片未加载之前提前占住位置,避免图片从未加载状态到渲染完成状态高宽变化引起的重排问题。

当然,到今天,我们还可以使用 aspect-ratio 设定图片的高宽比。

aspect-ratio CSS 属性为容器规定了一个期待的宽高比,这个宽高比可以用来计算自动尺寸以及为其他布局函数服务。

像是上面的代码,我们就可以替换成:

ul li img 
    width: 150px;
    aspect-ratio: 3 / 2;

当然,有的时候,我们的布局是响应式动态在变化的,容器的宽度也是不确定的,因此,有了 aspect-ratio 之后,我们的写法就可以更佳的舒服。

ul li img 
    width: 100%;
    aspect-ratio: 3 / 2;

这里,容器基于 Flex 弹性布局或者响应式布局,其宽度是不固定的,但是图片的宽高比是固定的,使用 aspect-ratio: 3 / 2 就能非常好的适配这种情况。

我们借助了 aspect-ratio 这个 CSS 中较新的属性来始终自动获得正确的宽高比,无论其父元素的宽度如何变化。

当然,aspect-ratio 不仅仅只是能运用在这里,在 aspect-ratio 出现之前,我们只能通过一些其它的 Hack 方式,譬如设置 padding-top 等方式模拟固定的宽高比。在 aspect-ratio 之后,我们终于有了设定容器固定宽高比的能力。

object-fit 避免图片拉伸

当然,限制高宽也会出现问题,譬如图片被拉伸了,非常的难看:

这个时候,我们可以借助 object-fit,它能够指定可替换元素的内容(也就是图片)该如何适应它的父容器的高宽。

ul li img 
    width: 150px;
    aspect-ratio: 3 / 2;
    object-fit: cover;

利用 object-fit: cover,使图片内容在保持其宽高比的同时填充元素的整个内容框。

object-fit 的取值有 fillnonecontaincover,与 background-size 类似,可以类比记忆。

也可以看看这张图,很易于理解:

object-fit 还有一个配套属性 object-position,它可以控制图片在其内容框中的位置。(类似于 background-position),默认是 object-position: 50% 50%,如果你不希望图片居中展示,可以使用它去改变图片实际展示的 position。

ul li img 
    width: 150px;
    aspect-ratio: 3 / 2;
    object-fit: cover;
    object-position: 50% 100%;

像是这样,object-position: 100% 50% 指明从底部开始展示图片。这里有一个很好的 Demo 可以帮助你理解 object-position

CodePen Demo -- Object position

使用 image-rendering 设置图片缩放算法

相对于上面几个新特性,image-rendering 会更为冷门。

很多时候,我们设置一个图片在页面上的展示大小为 200px x 200px,但是图片的原始尺寸可能是 800px x 800px,也可能是 50px x 50px

这个时候,我们就可以利用 image-rendering,设置图片在缩放状态下的展示算法。

image-rendering 在特定的场景下,能够起到奇效。

来看这样一个有意思的 DEMO,假设我们有这样一个原图效果,它是一个二维码,大小为 100px x 100px

如果我们将它放大,放到很大,明显,这个二维码会失真,像是这样:

OK,在这种放大失真的情况想,可以使用 image-rendering 改变图片缩放算法,这里我们试一下 image-rendering: pixelated

.img 
  image-rendering: pixelated;

效果变化,如下图所示:

可以看到,image-rendering: pixelated 处理过的图像,竟然变得如此清晰!

CodePen Demo -- QrCode Image-rendering demo

来看看 image-rendering 的几个取值:

  • image-rendering: auto:自 Gecko 1.9(Firefox 3.0)起,Gecko 使用双线性(bilinear)算法进行重新采样(高质量)。
  • image-rendering: smooth:使用能最大化图像客观观感的算法来缩放图像
  • image-rendering: high-quality:与 smooth 相同,但更倾向于高质量的缩放。
  • image-rendering: crisp-edges:必须使用可有效保留对比度和图像中的边缘的算法来对图像进行缩放,并且,该算法既不会平滑颜色,又不会在处理过程中为图像引入模糊。合适的算法包括最近邻居(nearest-neighbor)算法和其他非平滑缩放算法,比如 2×SaI 和 hqx-* 系列算法。此属性值适用于像素艺术作品,例如一些网页游戏中的图像。
  • image-rendering: pixelated:放大图像时,使用最近邻居算法,因此,图像看着像是由大块像素组成的。缩小图像时,算法与 auto 相同。

虽然规范定义了挺多值,但是实际上,现代浏览器基本暂时只支持:autopixelated、以及 -webkit-optimize-contrast(Chrome 下的 smooth)。

看描述都会挺懵逼的,实际使用的时候,最好每个都试一下验证一下效果。总结而言,image-rendering 的作用是在图像缩放时,提供不一样的渲染方式,让图片的展示形态更为多样化,或者说是尽可能的去减少图片的失真带来的信息损耗

我们再看一个 DEMO,原图如下(例子来源于 W3C 规范文档):

实际效果:

当然,看上去 pixelated 的效果挺好,这是由于这是一张偏向于矢量的图片,细节不多,对于高精度的人物图,就不太适用于 pixelated,容易把图片马赛克化。

真正规范希望的在放大后让图片尽可能不失真的 crisp-edges 效果,目前暂时没有得到浏览器的实现。后面可以期待一下。

CodePen Demo -- Image-rendering demo

模块总结

这一章,我们介绍了 4 个较新的 CSS 特性:

  • aspect-ratio:控制容器的宽高比,避免产生布局偏移及抖动
  • object-fit:设定内容应该如何适应到其使用高度和宽度确定的框,避免图片拉伸
  • object-position:基于 object-fit,设置图片实际展示的 position 范围
  • image-rendering:控制图片在缩放状态下的展示算法

合理利用它们,可以给用户在图片上以更好的体验。

懒加载/异步图像解码方案

继续下一个章节。本章节,我们来讨论下图片的懒加载与异步图像解码方案。

图片的懒加载

懒加载是一种网页性能优化的常见方式,它能极大的提升用户体验。到今天,现在一张图片超过几 M 已经是常见事了。如果每次进入页面都需要请求页面上的所有的图片资源,会较大的影响用户体验,对用户的带宽也是一种极大的损耗。

所以,图片懒加载的意义即是,当页面未滚动到相应区域,该区域内的图片资源(网络请求)不会被加载。反之,当页面滚动到相应区域,相关图片资源的请求才会被发起。

在过去,我们通常都是使用 JavaScript 方案进行图片的懒加载。而今天,我们在图片的懒加载实现上,有了更多不一样的选择。

JavaScript 方案实现图片的懒加载

首先,回顾一下过往最常见的,使用 JavaScript 方案实现图片的懒加载。

通过 JavaScript 实现的懒加载,主要是两种方式:

  1. 监听 onscroll 事件,通过 getBoundingClientRect API 获取元素图片距离视口顶部的距离,配合当前可视区域的位置实现图片的懒加载
  2. 通过 HTML5 的 IntersectionObserver API,Intersection Observer(交叉观察器) 配合监听元素的 isIntersecting 属性,判断元素是否在可视区内,能够实现比监听 onscroll 性能更佳的图片懒加载方案

但是,JavaScript 方案的一个劣势在于,不管如何,需要引入一定量的 JavaScript 代码,进行一定量的运算。

到今天,其实我们有更多的其他便捷的方式去实现图片的懒加载。

使用 content-visibility: auto 实现图片内容的延迟渲染

首先,介绍一个非常有用,但是相对较为冷门的属性 -- content-visibility

content-visibility:属性控制一个元素是否渲染其内容,它允许用户代理(浏览器)潜在地省略大量布局和渲染工作,直到需要它为止。

利用 content-visibility 的特性,我们可以实现如果该元素当前不在屏幕上,则不会渲染其后代元素

假设我们有这样一个 DEMO:

<div class="g-wrap">
    // 模块 1
    <div class="paragraph">
        <p>Lorem Start!</p>   
        <img src="https://s1.ax1x.com/2023/02/20/pSX1xMV.png"  />
        <p>Lorem End!</p>  
    </div>
    // 模块 2
    <div class="paragraph">
        <p>Lorem Start!</p>   
        <img src="https://s1.ax1x.com/2023/02/20/pSX1xMV.png"  />
        <p>Lorem End!</p>  
    </div>
    // ... 连续几十个上述类似的结构
</div>

只需要给需要延迟(实时)渲染的元素,设置简单的 CSS 样式:

.paragraph 
    content-visibility: auto;

我们来看一下,设置了 content-visibility: auto 与没设置的区别。

如果,不添加上述的 content-visibility: auto 代码,页面的滚动条及滚动效果如下:

那么,在添加了 content-visibility: auto 之后,注意观察页面的滚动条及滚动效果:

可以看到滚动条在向下滚动在不断的抽搐,这是由于下面不在可视区域内的内容,一开始是没有被渲染的,在每次滚动的过程中,才逐渐渲染,以此来提升性能。

Codepen Deom -- content-visibility: auto Image Load Demo

content-visibility: auto VS 图片懒加载

当然,其实使用 content-visibility: auto 并不能真正意义上实现图片的懒加载。

这是因为,即便当前页面可视区域外的内容未被渲染,但是图片资源的 HTTP/HTTPS 请求,依然会在页面一开始被触发!

因此,这也得到了一个非常重要的结论:

content-visibility: auto 无法直接替代图片懒加载,设置了 content-visibility: auto 的元素在可视区外只是未被渲染,但是其中的静态资源仍旧会在页面初始化的时候被全部加载。因此,它更像是一个虚拟列表的替代方案。

关于 content-visibility 本文限于篇幅,没有完全展开,但是它是一个非常有意思且对渲染性能有帮助的属性,完整的教程,你可以看我的这篇文章 -- 使用 content-visibility 优化渲染性能

使用 loading=lazy HTML 属性实现图片懒加载

OK,content-visibility 很不错,但是略有瑕疵。但是,我们还有其他方式。

HTML5 新增了一个 loading 属性。

到今天,除了 IE 系列浏览器,目前都支持通过 loading 属性实现延迟加载。此属性可以添加到 <img> 元素中,也可以添加到 <iframe> 元素中。

属性的值为 loading=lazy 会告诉浏览器,如果图像位于可视区时,则立即加载图像,并在用户滚动到它们附近时获取其他图像。

我们可以像是这样使用它:

<img src="xxx.png" loading="lazy">

这样,便可以非常便捷的实现图片的懒加载,省去了添加繁琐的 JavaScript 代码的过程

看看 loading=lazy 到今天(2023-02-26)的兼容性,还是非常不错的:

使用 decoding=async 实现图片的异步解码

除了 loading=lazy,HTML5 还新增了一个非常有意思的属性增强图片的用户体验。那就是 decoding 属性。

HTMLImageElement 接口的 decoding 属性用于告诉浏览器使用何种方式解析图像数据。

它的可选取值如下:

  • sync: 同步解码图像,保证与其他内容一起显示。
  • async: 异步解码图像,加快显示其他内容。
  • auto: 默认模式,表示不偏好解码模式。由浏览器决定哪种方式更适合用户。

上文其实也提及了,浏览器在进行图片渲染展示的过程中,是需要对图片文件进行解码的,这一个过程快慢与图片格式有关。

而如果我们不希望图片的渲染解码影响页面的其他内容的展示,可以使用 decoding=async 选项,像是这样:

<img src="xxx.png" decoding="async">

这样,浏览器便会异步解码图像,加快显示其他内容。这是图片优化方案中可选的一环。

同样的,我们来看看到今天(2023-02-26),decoding="async" 的兼容性,整体还是非常不错的,作为渐进增强方案使用,是非常好的选择。

实际检验 loading=lazydecoding=async 效果

OK,下面我们制作一个简单的 DEMO,试一下 loading=lazydecoding=async 的效果。

我们准备一个拥有 339 个图片的 HTML 页面,每个图片文件的 src 大小不一。

<div class="g-container">
    <img src="image1.jpeg">
    <img src="image2.jpeg">
    // ... 339 个
</div>

CSS 的设置也很重要,由于是纯图片页面,如果不给图片设置默认高宽,最页面刷新的一瞬间,<img> 元素的高宽都是 0,会导致所有 <img> 元素都在可视区内,所以,我们需要给 <img> 设置一个默认的高宽:

img 
    margin: 8px;
    width: 300px;
    height: 200px;
    object-fit: cover;

这样,再不添加 loading=lazydecoding=async 的状态下,看看 Network 的表现:

我这里没有模拟弱网环境,网速非常快,可以看到,发送了 339 个图片资源请求,也就是全部的图片资源在页面加载的过程中都请求了,页面 Load 事件完成的时间为 1.28s。

好,我们给所有的图片元素,添加上 loading=lazydecoding=async

<div class="g-container">
    <img src="image1.jpeg" loading="lazy" decoding="async">
    <img src="image2.jpeg" loading="lazy" decoding="async">
    // ... 339 个
</div>

看看效果:

可以看到,这一次只发送了 17 个图片资源请求,页面 Load 事件完成的时间为 26ms。

优化前 优化后
1.28s 26 ms

1.28s 到 26ms,效果是非常明显的,如果是弱网环境,对首屏加载性能的提升,会更为明显

当然,实际我测试的过程也,也单独试过 decoding="async" 的作用,只是由于是纯图片页面,效果不那么明显。感兴趣的同学,可以自行尝试。

模块总结

在本章节中,我们介绍了不同的方式实现图片的懒加载、延迟渲染、异步解码,它们分别是:

  1. 通过 onscroll 事件与 getBoundingClientRect API 实现图片的懒加载方案
  2. 通过 Intersection Observer(交叉观察器)实现比监听 onscroll 性能更佳的图片懒加载方案
  3. 通过 content-visibility: auto 实现图片资源的延迟渲染
  4. 通过 loading=lazy HTML 属性实现图片懒加载
  5. 通过 decoding=async HTML 属性实现图片的异步解码

图片资源的容错及可访问性处理

OK,最后一个章节,我们简单聊一聊图片资源的容错及可访问性处理。

图片的可访问性处理

可访问性(A11Y),在我们的网站中,属于非常重要的一环,但是大部分同学都容易忽视它。

在一些重交互、重逻辑的网站中,我们需要考虑用户的使用习惯、使用场景,从高可访问性的角度考虑,譬如假设用户没有鼠标,仅仅使用键盘,能否顺畅的使用我们的网站?

非常重要的一点是,提高可访问性也能让普通用户更容易理解 Web 内容

基于 Usability & Web Accessibility - image

对于图像信息,我们需要大致遵循如下可访问性原则:

  • 所有有意义的 img 元素必须有 alt 属性
  • 提供替代 alt 属性的其他方式
  • 使用辅助技术隐藏装饰图像

第一点非常好理解,所有的有意义的图片元素都必须要提供 alt 属性。

第二点比较有意思,在 A11Y 中,其实有一套 WAI-ARIA 标准。WAI-ARIA 是一个为残疾人士等提供无障碍访问动态、可交互Web内容的技术规范。

简单来说,它提供了一些属性,增强标签的语义及行为:

  • 可以使用 tabindex 属性控制元素是否可以聚焦,以及它是否/在何处参与顺序键盘导航
  • 可以使用 role 属性,来标识元素的语义及作用,譬如使用 <div id="saveChanges" tabindex="0" role="button">Save</div>来模拟一个按钮
  • 还有大量的 aria-* 属性,表示元素的属性或状态,帮助我们进一步地识别以及实现元素的语义化,优化无障碍体验

上述第二点,提供替代 alt 属性的其他方式 的含义就是使用 WAR-ARIA 规范提供的诸如 aria-labelaria-labelledby 属性为图像提供可访问的名称。

当存在这些属性时,辅助技术(屏幕阅读器)将忽略图像的 alt 属性并读取 ARIA 标签。

而第三点,使用辅助技术隐藏装饰图像,又是什么意思呢?

上面第一点 所有有意义的 img 元素必须有 alt 属性,反过来说,页面上也会存在无意义的装饰性的图片,这些图片内容对辅助技术(屏幕阅读器)而言,其实是可以忽略的。

对于没有任何功能或信息内容的装饰图像,可以通过多种方式对屏幕阅读器隐藏:

  • 使用空的 alt 属性
  • 使用 ARIA 属性 role="presentation" 标明图片元素是装饰可忽略图片
  • 使用 CSS background 的方式呈现这些图片

alt 不要与 title 混淆

OK,下面来讲一些有意思的细节内容。

有一个非常基础的知识,简单过一下,也就是图片元素中,alttitle 的差异:

  • 图片中的 alt 属性是在图片不能正常显示时出现的文本提示。
  • 图片中的 title 属性是在鼠标在移动到元素上的文本提示。

正确使用 alt 属性

对于使用屏幕阅读器的用户而言,图片是无法正常展示或者被的浏览的,基于此,我们需要利用好 alt 属性,或者是上述的aria-labelaria-labelledby 属性。

那么,这些属性内的内容应该填充什么呢?我们需要基于图片的功能加以区分:

  • 信息性图像:以图形方式表示概念和信息的图像,通常是图片、照片和插图。alt 替代文本应该至少是一个简短的描述,传达图像所呈现的基本信息。

  • 装饰性图像:当图像的唯一目的是为页面添加视觉装饰,而不是传达对理解页面很重要的信息时,如上述所言,使用空的 alt,譬如

  • 功能图像:用作链接或按钮的图像的替代文本应该描述链接或按钮的功能,而不是视觉图像。此类图像的示例是表示打印功能的打印机图标或提交表单的按钮。

  • 文本图像:可读文本有时会出现在图像中。如果图片不是徽标,请避免图片中出现文字。但是,如果使用文本图像,替代文本应包含与图像中相同的词。

  • 图形和图表等复杂图像:为了传达数据或详细信息,提供与图像中提供的数据或信息等效的完整文本作为替代文本。

  • 图像组:如果多张图像传达一条信息,则一张图像的替代文本应传达整组信息。

  • 图像映射:包含多个可点击区域的图像的替代文本应该为链接集提供整体上下文。此外,每个可单独点击的区域都应该有替代文本来描述链接的目的或目的地。

其实 alt 的学问是非常之多的,如果我们的页面能做到这一点,那真的算是从根上开始思考,开始优化用户体验。

img 元素与 background 元素的取舍

OK,那么,讲到这里,还有一个有意思的点就很自然的应该被提及。

那就是我们应该什么时候使用 <img> 元素,什么时候使用 background 内嵌图片?

我们可以从性能功能两个方面进行考虑:

类型 img backgroud-image
图层位置 前景 背景
默认初始尺寸 不定 固定
是否会产生回流重绘 不会
图片加载失败 可以触发元素的 onerror 事件,展示 alt 属性 无法有效设置异常处理场景
使用场景 Logo、产品图片、广告图片 装饰性无语义内容等

其实性能上并不是核心考虑的点,因为上文我们也讲到了在今天可以大规模使用是 loading="lazy" 属性,图片可以进行原生支持的懒加载。

我们在考虑选取 <img> 还是 backgroud-image 的时候,更多的还是从图片功能上进行考虑。一般来说,作为修饰的且无语义的装饰性图片选择使用 background-image,而比较重要的与网页内容相关的就使用 <img> 标签。

由于有语义的图片使用 <img> 展示,它的一个好处在于,当图片加载失败的时候,可以触发元素的 onerror 事件,我们可以有效的利用这一点,对图片进行异常处理。

图片的异常处理

当图片链接挂了,加载失败了,我们比较好的处理方式应该是怎么样呢?

处理的方式有很多种。在张鑫旭老师的这篇文章中 -- 图片加载失败后CSS样式处理最佳实践 有一个不错的实践。

核心思路为:

  1. 利用图片加载失败,触发 <img> 元素的 onerror 事件,给加载失败的 <img> 元素新增一个样式类
  2. 利用新增的样式类,配合 <img> 元素的伪元素,在展示默认兜底图的同时,还能一起展示 <img> 元素的 alt 信息
<img src="test.png"  onerror="this.classList.add(\'error\');">
img.error 
    position: relative;
    display: inline-block;


img.error::before 
    content: "";
    /** 定位代码 **/
    background: url(error-default.png);


img.error::after 
    content: attr(alt);
    /** 定位代码 **/

我们利用伪元素 before ,加载默认错误兜底图,利用伪元素 after,展示图片的 alt 信息:

OK,到此,完整的对图片的处理就算完成了,这也比较好的阐述了为什么,对有语义,有 alt 信息的图片,我们应该使用 <img> 元素来实现。这是因为,我们可以在错误发生的时候,比较好的对图片进行兜底展示,让用户直观的能够看到 alt 内容。

完整的 Demo 你可以戳这里看看:

CodePen Demo -- 图片处理

当然,上述方案存在两个小问题:

  1. 对于每一个 <img> 元素,我

    Linux性能优化的全景指南

    Linux 性能优化

    性能优化

    性能指标

    高并发和响应快对应着性能优化的两个核心指标:吞吐和延时

    Linux性能优化的全景指南_系统调用

    • 应用负载角度:直接影响了产品终端的用户体验
    • 系统资源角度:资源使用率、饱和度等

    性能问题的本质就是系统资源已经到达瓶颈,但请求的处理还不够快,无法支撑更多的请求。性能分析实际上就是找出应用或系统的瓶颈,设法去避免或缓解它们。

    • 选择指标评估应用程序和系统性能
    • 为应用程序和系统设置性能目标
    • 进行性能基准测试
    • 性能分析定位瓶颈
    • 性能监控和告警

    对于不同的性能问题要选取不同的性能分析工具。下面是常用的Linux Performance Tools以及对应分析的性能问题类型。

    Linux性能优化的全景指南_僵尸进程_02

    到底应该怎么理解”平均负载”

    平均负载:单位时间内,系统处于可运行状态和不可中断状态的平均进程数,也就是平均活跃进程数。它和我们传统意义上理解的CPU使用率并没有直接关系。

    其中不可中断进程是正处于内核态关键流程中的进程(如常见的等待设备的I/O响应)。不可中断状态实际上是系统对进程和硬件设备的一种保护机制。

    平均负载多少时合理

    实际生产环境中将系统的平均负载监控起来,根据历史数据判断负载的变化趋势。当负载存在明显升高趋势时,及时进行分析和调查。当然也可以当设置阈值(如当平均负载高于CPU数量的70%时)

    现实工作中我们会经常混淆平均负载和CPU使用率的概念,其实两者并不完全对等:

    • CPU 密集型进程,大量 CPU 使用会导致平均负载升高,此时两者一致
    • I/O 密集型进程,等待 I/O 也会导致平均负载升高,此时 CPU 使用率并不一定高
    • 大量等待 CPU 的进程调度会导致平均负载升高,此时 CPU 使用率也会比较高

    平均负载高时可能是 CPU 密集型进程导致,也可能是 I/O 繁忙导致。具体分析时可以结合 mpstat/pidstat 工具辅助分析负载来源。

    CPU

    CPU上下文切换(上)

    CPU 上下文切换,就是把前一个任务的 CPU 上下文(CPU 寄存器和 PC)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的位置,运行新任务。其中,保存下来的上下文会存储在系统内核中,待任务重新调度执行时再加载,保证原来的任务状态不受影响。

    按照任务类型,CPU 上下文切换分为:

    • 进程上下文切换
    • 线程上下文切换
    • 中断上下文切换
    进程上下文切换

    Linux 进程按照等级权限将进程的运行空间分为内核空间和用户空间。从用户态向内核态转变时需要通过系统调用来完成。

    一次系统调用过程其实进行了两次 CPU 上下文切换:

    • CPU 寄存器中用户态的指令位置先保存起来,CPU 寄存器更新为内核态指令的位置,跳转到内核态运行内核任务;
    • 系统调用结束后,CPU 寄存器恢复原来保存的用户态数据,再切换到用户空间继续运行。

    系统调用过程中并不会涉及虚拟内存等进程用户态资源,也不会切换进程。和传统意义上的进程上下文切换不同。因此系统调用通常称为特权模式切换。

    进程是由内核管理和调度的,进程上下文切换只能发生在内核态。因此相比系统调用来说,在保存当前进程的内核状态和CPU寄存器之前,需要先把该进程的虚拟内存,栈保存下来。再加载新进程的内核态后,还要刷新进程的虚拟内存和用户栈。

    进程只有在调度到CPU上运行时才需要切换上下文,有以下几种场景:CPU时间片轮流分配,系统资源不足导致进程挂起,进程通过sleep函数主动挂起,高优先级进程抢占时间片,硬件中断时CPU上的进程被挂起转而执行内核中的中断服务。

    线程上下文切换

    线程上下文切换分为两种:

    • 前后线程同属于一个进程,切换时虚拟内存资源不变,只需要切换线程的私有数据,寄存器等;
    • 前后线程属于不同进程,与进程上下文切换相同。

    同进程的线程切换消耗资源较少,这也是多线程的优势。

    中断上下文切换

    中断上下文切换并不涉及到进程的用户态,因此中断上下文只包括内核态中断服务程序执行所必须的状态(CPU寄存器,内核堆栈,硬件中断参数等)。

    中断处理优先级比进程高,所以中断上下文切换和进程上下文切换不会同时发生

    CPU上下文切换(下)

    通过 vmstat 可以查看系统总体的上下文切换情况

    vmstat 5         #每隔5s输出一组数据
    procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
    r b swpd free buff cache si so bi bo in cs us sy id wa st
    1 0 0 103388 145412 511056 0 0 18 60 1 1 2 1 96 0 0
    0 0 0 103388 145412 511076 0 0 0 2 450 1176 1 1 99 0 0
    0 0 0 103388 145412 511076 0 0 0 8 429 1135 1 1 98 0 0
    0 0 0 103388 145412 511076 0 0 0 0 431 1132 1 1 98 0 0
    0 0 0 103388 145412 511076 0 0 0 10 467 1195 1 1 98 0 0
    1 0 0 103388 145412 511076 0 0 0 2 426 1139 1 0 99 0 0
    4 0 0 95184 145412 511108 0 0 0 74 500 1228 4 1 94 0 0
    0 0 0 103512 145416 511076 0 0 0 455 723 1573 12 3 83 2 0
    • cs (context switch) 每秒上下文切换次数
    • in (interrupt) 每秒中断次数
    • r (runnning or runnable)就绪队列的长度,正在运行和等待CPU的进程数
    • b (Blocked) 处于不可中断睡眠状态的进程数

    要查看每个进程的详细情况,需要使用pidstat来查看每个进程上下文切换情况

    pidstat -w 5
    14时51分16秒 UID PID cswch/s nvcswch/s Command
    14时51分21秒 0 1 0.80 0.00 systemd
    14时51分21秒 0 6 1.40 0.00 ksoftirqd/0
    14时51分21秒 0 9 32.67 0.00 rcu_sched
    14时51分21秒 0 11 0.40 0.00 watchdog/0
    14时51分21秒 0 32 0.20 0.00 khugepaged
    14时51分21秒 0 271 0.20 0.00 jbd2/vda1-8
    14时51分21秒 0 1332 0.20 0.00 argusagent
    14时51分21秒 0 5265 10.02 0.00 AliSecGuard
    14时51分21秒 0 7439 7.82 0.00 kworker/0:2
    14时51分21秒 0 7906 0.20 0.00 pidstat
    14时51分21秒 0 8346 0.20 0.00 sshd
    14时51分21秒 0 20654 9.82 0.00 AliYunDun
    14时51分21秒 0 25766 0.20 0.00 kworker/u2:1
    14时51分21秒 0 28603 1.00 0.00 python3
    • cswch 每秒自愿上下文切换次数(进程无法获取所需资源导致的上下文切换)
    • nvcswch 每秒非自愿上下文切换次数(时间片轮流等系统强制调度)
    vmstat 1 1    #新终端观察上下文切换情况
    此时发现cs数据明显升高,同时观察其他指标:
    r列:远超系统CPU个数,说明存在大量CPU竞争
    us和sy列:sy列占比80%,说明CPU主要被内核占用
    in列:中断次数明显上升,说明中断处理也是潜在问题

    说明运行/等待CPU的进程过多,导致大量的上下文切换,上下文切换导致系统的CPU占用率高

    pidstat -w -u 1  #查看到底哪个进程导致的问题

    从结果中看出是 sysbench 导致 CPU 使用率过高,但是 pidstat 输出的上下文次数加起来也并不多。分析 sysbench 模拟的是线程的切换,因此需要在 pidstat 后加 -t 参数查看线程指标。

    另外对于中断次数过多,我们可以通过 /proc/interrupts 文件读取

    watch -d cat /proc/interrupts

    发现次数变化速度最快的是重调度中断(RES),该中断用来唤醒空闲状态的CPU来调度新的任务运行。分析还是因为过多任务的调度问题,和上下文切换分析一致。

    某个应用的CPU使用率达到100%,怎么办?

    Linux作为多任务操作系统,将CPU时间划分为很短的时间片,通过调度器轮流分配给各个任务使用。为了维护CPU时间,Linux通过事先定义的节拍率,触发时间中断,并使用全局变了jiffies记录开机以来的节拍数。时间中断发生一次该值+1.

    CPU使用率,除了空闲时间以外的其他时间占总CPU时间的百分比。可以通过/proc/stat中的数据来计算出CPU使用率。因为/proc/stat时开机以来的节拍数累加值,计算出来的是开机以来的平均CPU使用率,一般意义不大。可以间隔取一段时间的两次值作差来计算该段时间内的平均CPU使用率。性能分析工具给出的都是间隔一段时间的平均CPU使用率,要注意间隔时间的设置。

    CPU使用率可以通过top 或 ps来查看。分析进程的CPU问题可以通过perf,它以性能事件采样为基础,不仅可以分析系统的各种事件和内核性能,还可以用来分析指定应用程序的性能问题。

    perf top / perf record / perf report (-g 开启调用关系的采样)

    sudo docker run --name nginx -p 10000:80 -itd feisky/nginx
    sudo docker run --name phpfpm -itd --network container:nginx feisky/php-fpm


    ab -c 10 -n 100 http://XXX.XXX.XXX.XXX:10000/ #测试Nginx服务性能

    发现此时每秒可承受请求给长少,此时将测试的请求数从100增加到10000。在另外一个终端运行top查看每个CPU的使用率。发现系统中几个php-fpm进程导致CPU使用率骤升。

    接着用perf来分析具体是php-fpm中哪个函数导致该问题。

    perf top -g -p XXXX #对某一个php-fpm进程进行分析

    发现其中 sqrt 和 add_function 占用 CPU 过多, 此时查看源码找到原来是sqrt中在发布前没有删除测试代码段,存在一个百万次的循环导致。将该无用代码删除后发现nginx负载能力明显提升

    系统的CPU使用率很高,为什么找不到高CPU的应用?

    sudo docker run --name nginx -p 10000:80 -itd feisky/nginx:sp
    sudo docker run --name phpfpm -itd --network container:nginx feisky/php-fpm:sp
    ab -c 100 -n 1000 http://XXX.XXX.XXX.XXX:10000/ #并发100个请求测试

    实验结果中每秒请求数依旧不高,我们将并发请求数降为5后,nginx负载能力依旧很低。

    此时用top和pidstat发现系统CPU使用率过高,但是并没有发现CPU使用率高的进程。

    出现这种情况一般时我们分析时遗漏的什么信息,重新运行top命令并观察一会。发现就绪队列中处于Running状态的进行过多,超过了我们的并发请求次数5. 再仔细查看进程运行数据,发现nginx和php-fpm都处于sleep状态,真正处于运行的却是几个stress进程。

    下一步就利用pidstat分析这几个stress进程,发现没有任何输出。用ps aux交叉验证发现依旧不存在该进程。说明不是工具的问题。再top查看发现stress进程的进程号变化了,此时有可能时以下两种原因导致:

    • 进程不停的崩溃重启(如段错误/配置错误等),此时进程退出后可能又被监控系统重启;
    • 短时进程导致,即其他应用内部通过 exec 调用的外面命令,这些命令一般只运行很短时间就结束,很难用top这种间隔较长的工具来发现

    可以通过pstree来查找 stress 的父进程,找出调用关系。

    pstree | grep stress

    发现是php-fpm调用的该子进程,此时去查看源码可以看出每个请求都会调用一个stress命令来模拟I/O压力。之前top显示的结果是CPU使用率升高,是否真的是由该stress命令导致的,还需要继续分析。代码中给每个请求加了verbose=1的参数后可以查看stress命令的输出,在中断测试该命令结果显示stress命令运行时存在因权限问题导致的文件创建失败的bug。

    此时依旧只是猜测,下一步继续通过perf工具来分析。性能报告显示确实时stress占用了大量的CPU,通过修复权限问题来优化解决即可。

    系统中出现大量不可中断进程和僵尸进程怎么办?

    进程状态

    R Running/Runnable,表示进程在CPU的就绪队列中,正在运行或者等待运行;
    D Disk Sleep,不可中断状态睡眠,一般表示进程正在跟硬件交互,并且交互过程中不允许被其他进程中断;
    Z Zombie,僵尸进程,表示进程实际上已经结束,但是父进程还没有回收它的资源;
    S Interruptible Sleep,可中断睡眠状态,表示进程因为等待某个事件而被系统挂起,当等待事件发生则会被唤醒并进入R状态;
    I Idle,空闲状态,用在不可中断睡眠的内核线程上。该状态不会导致平均负载升高;
    T Stop/Traced,表示进程处于暂停或跟踪状态(SIGSTOP/SIGCONT, GDB调试);
    X Dead,进程已经消亡,不会在top/ps中看到。

    对于不可中断状态,一般都是在很短时间内结束,可忽略。但是如果系统或硬件发生故障,进程可能会保持不可中断状态很久,甚至系统中出现大量不可中断状态,此时需注意是否出现了I/O性能问题。

    僵尸进程一般多进程应用容易遇到,父进程来不及处理子进程状态时子进程就提前退出,此时子进程就变成了僵尸进程。大量的僵尸进程会用尽PID进程号,导致新进程无法建立。

    磁盘O_DIRECT问题

    sudo docker run --privileged --name=app -itd feisky/app:iowait
    ps aux | grep /app

    可以看到此时有多个app进程运行,状态分别时Ss+和D+。其中后面s表示进程是一个会话的领导进程,+号表示前台进程组。

    其中进程组表示一组相互关联的进程,子进程是父进程所在组的组员。会话指共享同一个控制终端的一个或多个进程组。

    用top查看系统资源发现:1)平均负载在逐渐增加,且1分钟内平均负载达到了CPU个数,说明系统可能已经有了性能瓶颈;2)僵尸进程比较多且在不停增加;3)us和sys CPU使用率都不高,iowait却比较高;4)每个进程CPU使用率也不高,但有两个进程处于D状态,可能在等待IO。

    分析目前数据可知:iowait过高导致系统平均负载升高,僵尸进程不断增长说明有程序没能正确清理子进程资源。

    用dstat来分析,因为它可以同时查看CPU和I/O两种资源的使用情况,便于对比分析。

    dstat 1 10    #间隔1秒输出10组数据

    可以看到当wai(iowait)升高时磁盘请求read都会很大,说明iowait的升高和磁盘的读请求有关。接下来分析到底时哪个进程在读磁盘。

    之前 Top 查看的处于 D 状态的进程号,用 pidstat -d -p XXX 展示进程的 I/O 统计数据。发现处于 D 状态的进程都没有任何读写操作。在用 pidstat -d 查看所有进程的 I/O统计数据,看到 app 进程在进行磁盘读操作,每秒读取 32MB 的数据。进程访问磁盘必须使用系统调用处于内核态,接下来重点就是找到app进程的系统调用。

    sudo strace -p XXX #对app进程调用进行跟踪

    报错没有权限,因为已经时 root 权限了。所以遇到这种情况,首先要检查进程状态是否正常。ps 命令查找该进程已经处于Z状态,即僵尸进程。

    这种情况下top pidstat之类的工具无法给出更多的信息,此时像第5篇一样,用 perf record -d和 perf report 进行分析,查看app进程调用栈。

    看到 app 确实在通过系统调用 sys_read() 读取数据,并且从 new_sync_read和 blkdev_direct_IO看出进程时进行直接读操作,请求直接从磁盘读,没有通过缓存导致iowait升高。

    通过层层分析后,root cause 是 app 内部进行了磁盘的直接I/O。然后定位到具体代码位置进行优化即可。

    僵尸进程

    上述优化后 iowait 显著下降,但是僵尸进程数量仍旧在增加。首先要定位僵尸进程的父进程,通过pstree -aps XXX,打印出该僵尸进程的调用树,发现父进程就是app进程。

    查看app代码,看看子进程结束的处理是否正确(是否调用wait()/waitpid(),有没有注册SIGCHILD信号的处理函数等)。

    碰到iowait升高时,先用dstat pidstat等工具确认是否存在磁盘I/O问题,再找是哪些进程导致I/O,不能用strace直接分析进程调用时可以通过perf工具分析。

    对于僵尸问题,用pstree找到父进程,然后看源码检查子进程结束的处理逻辑即可。

    CPU性能指标

    • CPU使用率
    • 用户CPU使用率, 包括用户态(user)和低优先级用户态(nice). 该指标过高说明应用程序比较繁忙.
    • 系统CPU使用率, CPU在内核态运行的时间百分比(不含中断). 该指标高说明内核比较繁忙.
    • 等待I/O的CPU使用率, iowait, 该指标高说明系统与硬件设备I/O交互时间比较长.
    • 软/硬中断CPU使用率, 该指标高说明系统中发生大量中断.
    • steal CPU / guest CPU, 表示虚拟机占用的CPU百分比.
    • 平均负载
    • 理想情况下平均负载等于逻辑CPU个数,表示每个CPU都被充分利用. 若大于则说明系统负载较重.

    • 进程上下文切换

    • 包括无法获取资源的自愿切换和系统强制调度时的非自愿切换. 上下文切换本身是保证Linux正常运行的一项核心功能. 过多的切换则会将原本运行进程的CPU时间消耗在寄存器,内核占及虚拟内存等数据保存和恢复上

    • CPU缓存命中率

    • CPU缓存的复用情况,命中率越高性能越好. 其中L1/L2常用在单核,L3则用在多核中

    性能工具

    • 平均负载案例
    • 先用uptime查看系统平均负载
    • 判断负载在升高后再用mpstat和pidstat分别查看每个CPU和每个进程CPU使用情况.找出导致平均负载较高的进程.
    • 上下文切换案例
    • 先用vmstat查看系统上下文切换和中断次数

    • 再用pidstat观察进程的自愿和非自愿上下文切换情况

    • 最后通过pidstat观察线程的上下文切换情况

    • 进程CPU使用率高案例

    • 先用top查看系统和进程的CPU使用情况,定位到进程

    • 再用perf top观察进程调用链,定位到具体函数

    • 系统CPU使用率高案例

    • 先用top查看系统和进程的CPU使用情况,top/pidstat都无法找到CPU使用率高的进程

    • 重新审视top输出

    • 从CPU使用率不高,但是处于Running状态的进程入手

    • perf record/report发现短时进程导致 (execsnoop工具)

    • 不可中断和僵尸进程案例

    • 先用top观察iowait升高,发现大量不可中断和僵尸进程

    • strace无法跟踪进程系统调用

    • perf分析调用链发现根源来自磁盘直接I/O

    • 软中断案例

    • top观察系统软中断CPU使用率高

    • 查看/proc/softirqs找到变化速率较快的几种软中断

    • sar命令发现是网络小包问题

    • tcpdump找出网络帧的类型和来源,确定SYN FLOOD攻击导致

    根据不同的性能指标来找合适的工具:

    Linux性能优化的全景指南_上下文切换_03

    先运行几个支持指标较多的工具,如 top/vmstat/pidstat,根据它们的输出可以得出是哪种类型的性能问题。定位到进程后再用 strace/perf 分析调用情况进一步分析。如果是软中断导致用 /proc/softirqs

    Linux性能优化的全景指南_系统调用_04

    CPU优化

    • 应用程序优化
    • 编译器优化:编译阶段开启优化选项,如gcc -O2
    • 算法优化
    • 异步处理:避免程序因为等待某个资源而一直阻塞,提升程序的并发处理能力。(将轮询替换为事件通知)
    • 多线程代替多进程:减少上下文切换成本
    • 善用缓存:加快程序处理速度
    • 系统优化
    • CPU绑定:将进程绑定要1个/多个CPU上,提高CPU缓存命中率,减少CPU调度带来的上下文切换

    • CPU独占:CPU亲和性机制来分配进程

    • 优先级调整:使用nice适当降低非核心应用的优先级

    • 为进程设置资源显示: cgroups设置使用上限,防止由某个应用自身问题耗尽系统资源

    • NUMA优化: CPU尽可能访问本地内存

    • 中断负载均衡: irpbalance,将中断处理过程自动负载均衡到各个CPU上

    • TPS、QPS、系统吞吐量的区别和理解

    • QPS(TPS)

    • 并发数

    • 响应时间

    • QPS(TPS)=并发数/平均相应时间

    • 用户请求服务器

    • 服务器内部处理

    • 服务器返回给客户
      QPS 类似 TPS,但是对于一个页面的访问形成一个 TPS,但是一次页面请求可能包含多次对服务器的请求,可能计入多次 QPS

    • QPS(Queries Per Second)每秒查询率,一台服务器每秒能够响应的查询次数.

    • TPS(Transactions Per Second)每秒事务数,软件测试的结果.

    • 系统吞吐量,包括几个重要参数:

    内存

    Linux内存是怎么工作的

    内存映射

    大多数计算机用的主存都是动态随机访问内存(DRAM),只有内核才可以直接访问物理内存。Linux内核给每个进程提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。这样进程就可以很方便的访问内存(虚拟内存)。

    虚拟地址空间的内部分为内核空间和用户空间两部分,不同字长的处理器地址空间的范围不同。32位系统内核空间占用1G,用户空间占3G。64位系统内核空间和用户空间都是128T,分别占内存空间的最高和最低处,中间部分为未定义。

    并不是所有的虚拟内存都会分配物理内存,只有实际使用的才会。分配后的物理内存通过内存映射管理。为了完成内存映射,内核为每个进程都维护了一个页表,记录虚拟地址和物理地址的映射关系。页表实际存储在CPU的内存管理单元MMU中,处理器可以直接通过硬件找出要访问的内存。

    当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存,更新进程页表,再返回用户空间恢复进程的运行。

    MMU以页为单位管理内存,页大小4KB。为了解决页表项过多问题Linux提供了多级页表和HugePage的机制。

    虚拟内存空间分布

    用户空间内存从低到高是五种不同的内存段:

    • 只读段 代码和常量等
    • 数据段 全局变量等
    •  动态分配的内存,从低地址开始向上增长
    • 文件映射 动态库、共享内存等,从高地址开始向下增长
    •  包括局部变量和函数调用的上下文等,栈的大小是固定的。一般8MB
    内存分配与回收

    分配

    malloc 对应到系统调用上有两种实现方式:

    • brk() 针对小块内存(<128K),通过移动堆顶位置来分配。
      内存释放后不立即归还内存,而是被缓存起来。
    • mmap()针对大块内存(>128K),直接用内存映射来分配,即在文件映射段找一块空闲内存分配。

    前者的缓存可以减少缺页异常的发生,提高内存访问效率。但是由于内存没有归还系统,在内存工作繁忙时,频繁的内存分配/释放会造成内存碎片。

    后者在释放时直接归还系统,所以每次mmap都会发生缺页异常。

    在内存工作繁忙时,频繁内存分配会导致大量缺页异常,使内核管理负担增加。

    上述两种调用并没有真正分配内存,这些内存只有在首次访问时,才通过缺页异常进入内核中,由内核来分配


    回收

    内存紧张时,系统通过以下方式来回收内存:

    • 回收缓存:LRU算法回收最近最少使用的内存页面;
    • 回收不常访问内存:把不常用的内存通过交换分区写入磁盘
    • 杀死进程:OOM内核保护机制(进程消耗内存越大 oom_score 越大,占用 CPU 越多 oom_score 越小,可以通过 /proc 手动调整 oom_adj)
    echo -16 > /proc/$(pidof XXX)/oom_adj

    如何查看内存使用情况

    free来查看整个系统的内存使用情况

    top/ps来查看某个进程的内存使用情况

    • VIRT 进程的虚拟内存大小
    • RES 常驻内存的大小,即进程实际使用的物理内存大小,不包括swap和共享内存
    • SHR 共享内存大小,与其他进程共享的内存,加载的动态链接库以及程序代码段
    • %MEM 进程使用物理内存占系统总内存的百分比

    怎样理解内存中的Buffer和Cache?

    buffer是对磁盘数据的缓存,cache是对文件数据的缓存,它们既会用在读请求也会用在写请求中

    如何利用系统缓存优化程序的运行效率

    缓存命中率

    缓存命中率是指直接通过缓存获取数据的请求次数,占所有请求次数的百分比。命中率越高说明缓存带来的收益越高,应用程序的性能也就越好。

    安装bcc包后可以通过cachestat和cachetop来监测缓存的读写命中情况。

    安装pcstat后可以查看文件在内存中的缓存大小以及缓存比例

    #首先安装Go
    export GOPATH=~/go
    export PATH=~/go/bin:$PATH
    go get golang.org/x/sys/unix
    go ge github.com/tobert/pcstat/pcstat

    dd缓存加速

    dd if=/dev/sda1 of=file bs=1M count=512 #生产一个512MB的临时文件
    echo 3 > /proc/sys/vm/drop_caches #清理缓存
    pcstat file #确定刚才生成文件不在系统缓存中,此时cached和percent都是0
    cachetop 5
    dd if=file of=/dev/null bs=1M #测试文件读取速度
    #此时文件读取性能为30+MB/s,查看cachetop结果发现并不是所有的读都落在磁盘上,读缓存命中率只有50%。
    dd if=file of=/dev/null bs=1M #重复上述读文件测试
    #此时文件读取性能为4+GB/s,读缓存命中率为100%
    pcstat file #查看文件file的缓存情况,100%全部缓存

    O_DIRECT选项绕过系统缓存

    cachetop 5
    sudo docker run --privileged --name=app -itd feisky/app:io-direct
    sudo docker logs app #确认案例启动成功
    #实验结果表明每读32MB数据都要花0.9s,且cachetop输出中显示1024次缓存全部命中

    但是凭感觉可知如果缓存命中读速度不应如此慢,读次数时1024,页大小为4K,五秒的时间内读取了1024*4KB数据,即每秒0.8MB,和结果中32MB相差较大。说明该案例没有充分利用缓存,怀疑系统调用设置了直接I/O标志绕过系统缓存。因此接下来观察系统调用。

    strace -p $(pgrep app)
    #strace 结果可以看到openat打开磁盘分区/dev/sdb1,传入参数为O_RDONLY|O_DIRECT

    这就解释了为什么读32MB数据那么慢,直接从磁盘读写肯定远远慢于缓存。找出问题后我们再看案例的源代码发现flags中指定了直接IO标志。删除该选项后重跑,验证性能变化。

    内存泄漏,如何定位和处理?

    对应用程序来说,动态内存的分配和回收是核心又复杂的一个逻辑功能模块。管理内存的过程中会发生各种各样的“事故”:

    • 没正确回收分配的内存,导致了泄漏
    • 访问的是已分配内存边界外的地址,导致程序异常退出

    内存的分配与回收

    虚拟内存分布从低到高分别是只读段,数据段,堆,内存映射段,栈五部分。其中会导致内存泄漏的是:

    • 堆:由应用程序自己来分配和管理,除非程序退出这些堆内存不会被系统自动释放。
    • 内存映射段:包括动态链接库和共享内存,其中共享内存由程序自动分配和管理

    内存泄漏的危害比较大,这些忘记释放的内存,不仅应用程序自己不能访问,系统也不能把它们再次分配给其他应用。内存泄漏不断累积甚至会耗尽系统内存。

    如何检测内存泄漏

    预先安装systat,docker,bcc

    sudo docker run --name=app -itd feisky/app:mem-leak
    sudo docker logs app
    vmstat 3

    可以看到free在不断下降,buffer和cache基本保持不变。说明系统的内存一致在升高。但并不能说明存在内存泄漏。此时可以通过memleak工具来跟踪系统或进程的内存分配/释放请求

    /usr/share/bcc/tools/memleak -a -p $(pidof app)

    从 memleak 输出可以看到,应用在不停地分配内存,并且这些分配的地址并没有被回收。通过调用栈看到是 fibonacci 函数分配的内存没有释放。定位到源码后查看源码来修复增加内存释放函数即可。

    为什么系统的 Swap 变高

    系统内存资源紧张时通过内存回收和OOM杀死进程来解决。其中可回收内存包括:

    • 缓存/缓冲区,属于可回收资源,在文件管理中通常叫做文件页
    • 在应用程序中通过fsync将脏页同步到磁盘
    • 交给系统,内核线程pdflush负责这些脏页的刷新
    • 被应用程序修改过暂时没写入磁盘的数据(脏页),要先写入磁盘然后才能内存释放
    • 内存映射获取的文件映射页,也可以被释放掉,下次访问时从文件重新读取

    对于程序自动分配的堆内存,也就是我们在内存管理中的匿名页,虽然这些内存不能直接释放,但是 Linux 提供了 Swap 机制将不常访问的内存写入到磁盘来释放内存,再次访问时从磁盘读取到内存即可。

    Swap原理

    Swap本质就是把一块磁盘空间或者一个本地文件当作内存来使用,包括换入和换出两个过程:

    • 换出:将进程暂时不用的内存数据存储到磁盘中,并释放这些内存
    • 换入:进程再次访问内存时,将它们从磁盘读到内存中

    Linux如何衡量内存资源是否紧张?

    • 直接内存回收新的大块内存分配请求,但剩余内存不足。
      此时系统会回收一部分内存;
    • kswapd0 内核线程定期回收内存。
      为了衡量内存使用情况,定义了pages_min,pages_low,pages_high 三个阈值,并根据其来进行内存的回收操作。
    • 剩余内存 < pages_min,进程可用内存耗尽了,只有内核才可以分配内存
    • pages_min < 剩余内存 < pages_low,内存压力较大,kswapd0执行内存回收,直到剩余内存 > pages_high
    • pages_low < 剩余内存 < pages_high,内存有一定压力,但可以满足新内存请求
    • 剩余内存 > pages_high,说明剩余内存较多,无内存压力
      pages_low = pages_min 5 / 4 pages_high = pages_min 3 / 2
    NUMA 与 SWAP

    很多情况下系统剩余内存较多,但 SWAP 依旧升高,这是由于处理器的 NUMA 架构。

    在NUMA架构下多个处理器划分到不同的 Node,每个Node都拥有自己的本地内存空间。在分析内存的使用时应该针对每个Node单独分析

    numactl --hardware #查看处理器在Node的分布情况,以及每个Node的内存使用情况

    内存三个阈值可以通过 /proc/zoneinfo 来查看,该文件中还包括活跃和非活跃的匿名页/文件页数。

    当某个Node内存不足时,系统可以从其他Node寻找空闲资源,也可以从本地内存中回收内存。通过/proc/sys/vm/zone_raclaim_mode来调整。

    • 0表示既可以从其他Node寻找空闲资源,也可以从本地回收内存
    • 1,2,4 表示只回收本地内存,2表示可以会回脏数据回收内存,4表示可以用Swap方式回收内存。
    swappiness

    在实际回收过程中Linux根据 /proc/sys/vm/swapiness 选项来调整使用Swap的积极程度,从 0-100,数值越大越积极使用 Swap,即更倾向于回收匿名页;数值越小越消极使用 Swap,即更倾向于回收文件页。

    注意:这只是调整 Swap 积极程度的权重,即使设置为0,当剩余内存+文件页小于页高阈值时,还是会发生 Swap。

    Swap升高时如何定位分析

    free #首先通过free查看swap使用情况,若swap=0表示未配置Swap
    #先创建并开启swap
    fallocate -l 8G /mnt/swapfile
    chmod 600 /mnt/swapfile
    mkswap /mnt/swapfile
    swapon /mnt/swapfile


    free #再次执行free确保Swap配置成功


    dd if=/dev/sda1 of=/dev/null bs=1G count=2048 #模拟大文件读取
    sar -r -S 1 #查看内存各个指标变化 -r内存 -S swap
    #根据结果可以看出,%memused在不断增长,剩余内存kbmemfress不断减少,缓冲区kbbuffers不断增大,由此可知剩余内存不断分配给了缓冲区
    #一段时间之后,剩余内存很小,而缓冲区占用了大部分内存。此时Swap使用之间增大,缓冲区和剩余内存只在小范围波动


    停下sar命令
    cachetop5 #观察缓存
    #可以看到dd进程读写只有50%的命中率,未命中数为4w+页,说明正式dd进程导致缓冲区使用升高
    watch -d grep -A 15 ‘Normal’ /proc/zoneinfo #观察内存指标变化
    #发现升级内存在一个小范围不停的波动,低于页低阈值时会突然增大到一个大于页高阈值的值

    说明剩余内存和缓冲区的波动变化正是由于内存回收和缓存再次分配的循环往复。有时候 Swap 用的多,有时候缓冲区波动更多。此时查看 swappiness 值为60,是一个相对中和的配置,系统会根据实际运行情况来选去合适的回收类型。

    如何“快准狠”找到系统内存存在的问题

    内存性能指标

    • 系统内存指标
    • 已用内存/剩余内存
    • 共享内存 (tmpfs实现)
    • 可用内存:包括剩余内存和可回收内存
    • 缓存:磁盘读取文件的页缓存,slab分配器中的可回收部分
    • 缓冲区:原始磁盘块的临时存储,缓存将要写入磁盘的数据

    进程内存指标

    • 虚拟内存:5大部分
    • 常驻内存:进程实际使用的物理内存,不包括Swap和共享内存
    • 共享内存:与其他进程共享的内存,以及动态链接库和程序的代码段
    • Swap 内存:通过Swap换出到磁盘的内存

    缺页异常

    • 可以直接从物理内存中分配,次缺页异常
    • 需要磁盘 IO 介入(如 Swap),主缺页异常。此时内存访问会慢很多
    内存性能工具

    根据不同的性能指标来找合适的工具:

    Linux性能优化的全景指南_上下文切换_05

    内存分析工具包含的性能指标:

    Linux性能优化的全景指南_系统调用_06

    如何迅速分析内存的性能瓶颈

    通常先运行几个覆盖面比较大的性能工具,如 free,top,vmstat,pidstat 等

    • 先用 free 和 top 查看系统整体内存使用情况
    • 再用 vmstat 和 pidstat,查看一段时间的趋势,从而判断内存问题的类型
    • 最后进行详细分析,比如内存分配分析,缓存/缓冲区分析,具体进程的内存使用分析等

    常见的优化思路:

    • 最好禁止 Swap,若必须开启则尽量降低 swappiness 的值
    • 减少内存的动态分配,如可以用内存池,HugePage 等
    • 尽量使用缓存和缓冲区来访问数据。如用堆栈明确声明内存空间来存储需要缓存的数据,或者用 Redis 外部缓存组件来优化数据的访问
    • cgroups 等方式来限制进程的内存使用情况,确保系统内存不被异常进程耗尽
    • /proc/pid/oom_adj 调整核心应用的 oom_score,保证即使内存紧张核心应用也不会被OOM杀死

    vmstat 使用详解

    vmstat 命令是最常见的 Linux/Unix 监控工具,可以展现给定时间间隔的服务器的状态值,包括服务器的 CPU 使用率,内存使用,虚拟内存交换情况,IO读写情况。可以看到整个机器的 CPU,内存,IO 的使用情况,而不是单单看到各个进程的 CPU 使用率和内存使用率(使用场景不一样)。

    vmstat 2
    procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
    r b swpd free buff cache si so bi bo in cs us sy id wa st
    1 0 0 1379064 282244 11537528 0 0 3 104 0 0 3 0 97 0 0
    0 0 0 1372716 282244 11537544 0 0 0 24 4893 8947 1 0 98 0 0
    0 0 0 1373404 282248 11537544 0 0 0 96 5105 9278 2 0 98 0 0
    0 0 0 1374168 282248 11537556 0 0 0 0 5001 9208 1 0 99 0 0
    0 0 0 1376948 282248 11537564 0 0 0 80 5176 9388 2 0 98 0 0
    0 0 0 1379356 282256 11537580 0 0 0 202 5474 9519 2 0 98 0 0
    1 0 0 1368376 282256 11543696 0 0 0 0 5894 8940 12 0 88 0 0
    1 0 0 1371936 282256 11539240 0 0 0 10554 6176 9481 14 1 85 1 0
    1 0 0 1366184 282260 11542292 0 0 0 7456 6102 9983 7 1 91 0 0
    1 0 0 1353040 282260 11556176 0 0 0 16924 7233 9578 18 1 80 1 0
    0 0 0 1359432 282260 11549124 0 0 0 12576 5495 9271 7 0 92 1 0
    0 0 0 1361744 282264 11549132 0 0 0 58 8606 15079 4 2 95 0 0
    1 0 0 1367120 282264 11549140 0 0 0 2 5716 9205 8 0 92 0 0
    0 0 0 1346580 282264 11562644 0 0 0 70 6416 9944 12 0 88 0 0
    0 0 0 1359164 282264 11550108 0 0 0 2922 4941 8969 3 0 97 0 0
    1 0 0 1353992 282264 11557044 0 0 0 0 6023 8917 15 0 84 0 0


    # 结果说明
    - r 表示运行队列(就是说多少个进程真的分配到CPU),我测试的服务器目前CPU比较空闲,没什么程序在跑,当这个值超过了CPU数目,就会出现CPU瓶颈了。这个也和top的负载有关系,一般负载超过了3就比较高,超过了5就高,超过了10就不正常了,服务器的状态很危险。top的负载类似每秒的运行队列。如果运行队列过大,表示你的CPU很繁忙,一般会造成CPU使用率很高。


    - b 表示阻塞的进程,这个不多说,进程阻塞,大家懂的。


    - swpd 虚拟内存已使用的大小,如果大于0,表示你的机器物理内存不足了,如果不是程序内存泄露的原因,那么你该升级内存了或者把耗内存的任务迁移到其他机器。


    - free 空闲的物理内存的大小,我的机器内存总共8G,剩余3415M。


    - buff Linux/Unix系统是用来存储,目录里面有什么内容,权限等的缓存,我本机大概占用300多M


    - cache cache直接用来记忆我们打开的文件,给文件做缓冲,我本机大概占用300多M(这里是Linux/Unix的聪明之处,把空闲的物理内存的一部分拿来做文件和目录的缓存,是为了提高 程序执行的性能,当程序使用内存时,buffer/cached会很快地被使用。)


    - si 每秒从磁盘读入虚拟内存的大小,如果这个值大于0,表示物理内存不够用或者内存泄露了,要查找耗内存进程解决掉。我的机器内存充裕,一切正常。


    - so 每秒虚拟内存写入磁盘的大小,如果这个值大于0,同上。


    - bi 块设备每秒接收的块数量,这里的块设备是指系统上所有的磁盘和其他块设备,默认块大小是1024byte,我本机上没什么IO操作,所以一直是0,但是我曾在处理拷贝大量数据(2-3T)的机器上看过可以达到140000/s,磁盘写入速度差不多140M每秒


    - bo 块设备每秒发送的块数量,例如我们读取文件,bo就要大于0。bi和bo一般都要接近0,不然就是IO过于频繁,需要调整。


    - in 每秒CPU的中断次数,包括时间中断


    - cs 每秒上下文切换次数,例如我们调用系统函数,就要进行上下文切换,线程的切换,也要进程上下文切换,这个值要越小越好,太大了,要考虑调低线程或者进程的数目,例如在apache和nginx这种web服务器中,我们一般做性能测试时会进行几千并发甚至几万并发的测试,选择web服务器的进程可以由进程或者线程的峰值一直下调,压测,直到cs到一个比较小的值,这个进程和线程数就是比较合适的值了。系统调用也是,每次调用系统函数,我们的代码就会进入内核空间,导致上下文切换,这个是很耗资源,也要尽量避免频繁调用系统函数

    以上是关于现代图片性能优化及体验优化指南的主要内容,如果未能解决你的问题,请参考以下文章

    现代图片性能优化及体验优化指南

    现代图片性能优化及体验优化指南

    现代图片性能优化及体验优化指南

    Linux性能优化的全景指南

    Android界面性能分析及优化

    前端性能优化之路——图片篇。