2017年12月

从图像中生成的 SVG 可以用作占位符。请继续阅读!

我对怎么去让 web 性能更优化和图像加载的更快充满了热情。在这些感兴趣的领域中的其中一项研究就是占位符:当图像还没有被加载的时候应该去展示些什么?

在前些天,我偶然发现了使用 SVG 的一些加载技术,我将在这篇文章中谈论它。

在这篇文章中我们将涉及如下的主题:

  • 不同的占位符类型的概述
  • 基于 SVG 的占位符(边缘、形状和轮廓)
  • 自动化处理

不同的占位符类型的概述

之前 我写过一篇关于图像占位符和 延迟加载 lazy-loading 的文章以及 关于它的讨论。当进行一个图像的延迟加载时,一个很好的办法是提供一个东西作为占位符,因为它可能会很大程度上影响用户的感知体验。之前我提供了几个选择:

在图像被加载之前,有几种办法去填充图像区域:

  • 在图像区域保持空白:在一个响应式设计的环境中,这种方式防止了内容的跳跃。从用户体验的角度来看,那些布局的改变是非常差的作法。但是,它是为了性能的考虑,否则,每次为了获取图像尺寸,浏览器就要被迫进行布局重新计算,以便为它留下空间。
  • 占位符:在图像那里显示一个用户配置的图像。我们可以在背景上显示一个轮廓。它一直显示直到实际的图像被加载完成,它也被用于当请求失败或者当用户根本没有设置头像图像的情况下。这些图像一般都是矢量图,并且由于尺寸非常小,可以作为内联图片。
  • 单一颜色:从图像中获取颜色,并将其作为占位符的背景颜色。这可能是图像的主要颜色、最具活力的颜色 … 这个想法是基于你正在加载的图像,并且它将有助于在没有图像和图像加载完成之间进行平滑过渡。
  • 模糊的图像:也被称为模糊技术。你提供一个极小版本的图像,然后再去过渡到完整的图像。最初显示的图像的像素和尺寸是极小的。为去除 细节 artifacts ,该图像会被放大并模糊化。我在前面写的 Medium 是怎么做的渐进加载图像使用 WebP 去创建极小的预览图像、和渐进加载图像的更多示例 中讨论过这方面的内容。

此外还有其它的更多的变种,许多聪明的人也开发了其它的创建占位符的技术。

其中一个就是用梯度图代替单一的颜色。梯度图可以创建一个更精确的最终图像的预览,它整体上非常小(提升了有效载荷)。

使用梯度图作为背景。这是来自 Gradify 的截屏,它现在已经不在线了,代码 在 GitHub

另外一种技术是使用基于 SVG 的技术,它在最近的实验和研究中取得到了一些进展。

基于 SVG 的占位符

我们知道 SVG 是完美的矢量图像。而在大多数情况下我们是希望加载一个位图,所以,问题是怎么去矢量化一个图像。其中一些方法是使用边缘、形状和轮廓。

边缘

前面的文章中,我解释了怎么去找出一个图像的边缘并创建一个动画。我最初的目标是去尝试绘制区域,矢量化该图像,但是我并不知道该怎么去做到。我意识到使用边缘也可能是一种创新,我决定去让它们动起来,创建一个 “绘制” 的效果。

使用边缘检测绘制图像和 SVG 动画

在以前,很少使用和支持 SVG。一段时间以后,我们开始用它去作为一个某些图标的传统位图的替代品……

形状

SVG 也可以用于根据图像绘制区域而不是边缘/边界。用这种方法,我们可以矢量化一个位图来创建一个占位符。

在以前,我尝试去用三角形做类似的事情。你可以在 CSSConfRender Conf 上我的演讲中看到它。

上面的 codepen 是一个由 245 个三角形组成的基于 SVG 占位符的概念验证。生成的三角形是基于 Delaunay triangulation 的,使用了 Possan’s polyserver。正如预期的那样,使用更多的三角形,文件尺寸就更大。

Primitive 和 SQIP,一个基于 SVG 的 LQIP 技术

Tobias Baldauf 正在致力于另一个使用 SVG 的低质量图像占位符技术,它被称为 SQIP。在深入研究 SQIP 之前,我先简单介绍一下 Primitive,它是基于 SQIP 的一个库。

Primitive 是非常吸引人的,我强烈建议你去了解一下。它讲解了一个位图怎么变成由重叠形状组成的 SVG。它尺寸比较小,适合于直接内联放置到页面中。当步骤较少时,在初始的 HTML 载荷中作为占位符是非常有意义的。

Primitive 基于三角形、长方形、和圆形等形状生成一个图像。在每一步中它增加一个新形状。很多步之后,图像的结果看起来非常接近原始图像。如果你输出的是 SVG,它意味着输出代码的尺寸将很大。

为了理解 Primitive 是怎么工作的,我通过几个图像来跑一下它。我用 10 个形状和 100 个形状来为这个插画生成 SVG:

使用 Primitive 处理 ,使用 10 个形状100 形状原图
使用 Primitive 处理,使用 10 形状100 形状原图

当在图像中使用 10 个形状时,我们基本构画出了原始图像。在图像占位符这种使用场景里,我们可以使用这种 SVG 作为潜在的占位符。实际上,使用 10 个形状的 SVG 代码已经很小了,大约是 1030 字节,当通过 SVGO 传输时,它将下降到约 640 字节。

<svg xmlns=”http://www.w3.org/2000/svg" width=”1024" height=”1024"><path fill=”#817c70" d=”M0 0h1024v1024H0z”/><g fill-opacity=”.502"><path fill=”#03020f” d=”M178 994l580 92L402–62"/><path fill=”#f2e2ba” d=”M638 894L614 6l472 440"/><path fill=”#fff8be” d=”M-62 854h300L138–62"/><path fill=”#76c2d9" d=”M410–62L154 530–62 38"/><path fill=”#62b4cf” d=”M1086–2L498–30l484 508"/><path fill=”#010412" d=”M430–2l196 52–76 356"/><path fill=”#eb7d3f” d=”M598 594l488–32–308 520"/><path fill=”#080a18" d=”M198 418l32 304 116–448"/><path fill=”#3f201d” d=”M1086 1062l-344–52 248–148"/><path fill=”#ebd29f” d=”M630 658l-60–372 516 320"/></g></svg>

正如我们预计的那样,使用 100 个形状生成的图像更大,在 SVGO(之前是 8kB)之后,大小约为 5kB。它们在细节上已经很好了,但是仍然是个很小的载荷。使用多少三角形主要取决于图像类型和细腻程度(如,对比度、颜色数量、复杂度)。

还可以创建一个类似于 cpeg-dssim 的脚本,去调整所使用的形状的数量,以满足 结构相似 的阈值(或者最差情况中的最大数量)。

这些生成的 SVG 也可以用作背景图像。因为尺寸约束和矢量化,它们在展示 超大题图 hero image 和大型背景图像时是很好的选择。

SQIP

Tobias 自己的话说

SQIP 尝试在这两个极端之间找到一种平衡:它使用 Primitive 去生成一个 SVG,由几种简单的形状构成,近似于图像中可见的主要特征,使用 SVGO 优化 SVG,并且为它增加高斯模糊滤镜。产生的最终的 SVG 占位符后大小仅为约 800~1000 字节,在屏幕上看起来更为平滑,并提供一个图像内容的视觉提示。

这个结果和使用一个用了模糊技术的极小占位符图像类似。(看看 Medium其它站点 是怎么做的)。区别在于它们使用了一个位图图像,如 JPG 或者 WebP,而这里是使用的占位符是 SVG。

如果我们使用 SQIP 而不是原始图像,我们将得到这样的效果:

第一张图像第二张图像 使用了 SQIP 后的输出图像。

输出的 SVG 约 900 字节,并且通过检查代码,我们可以发现 feGaussianBlur 过滤被应用到该组形状上:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2000 2000"><filter id="b"><feGaussianBlur stdDeviation="12" /></filter><path fill="#817c70" d="M0 0h2000v2000H0z"/><g filter="url(#b)" transform="translate(4 4) scale(7.8125)" fill-opacity=".5"><ellipse fill="#000210" rx="1" ry="1" transform="matrix(50.41098 -3.7951 11.14787 148.07886 107 194.6)"/><ellipse fill="#eee3bb" rx="1" ry="1" transform="matrix(-56.38179 17.684 -24.48514 -78.06584 205 110.1)"/><ellipse fill="#fff4bd" rx="1" ry="1" transform="matrix(35.40604 -5.49219 14.85017 95.73337 16.4 123.6)"/><ellipse fill="#79c7db" cx="21" cy="39" rx="65" ry="65"/><ellipse fill="#0c1320" cx="117" cy="38" rx="34" ry="47"/><ellipse fill="#5cb0cd" rx="1" ry="1" transform="matrix(-39.46201 77.24476 -54.56092 -27.87353 219.2 7.9)"/><path fill="#e57339" d="M271 159l-123–16 43 128z"/><ellipse fill="#47332f" cx="214" cy="237" rx="242" ry="19"/></g></svg>

SQIP 也可以输出一个带有 Base64 编码的 SVG 内容的图像标签:

<img width="640" height="640" src="example.jpg” alt="Add descriptive alt text" style="background-size: cover; background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAw…<stripped base 64>…PjwvZz48L3N2Zz4=);">

轮廓

我们刚才看了使用了边缘和原始形状的 SVG。另外一种矢量化图像的方式是 “描绘” 它们。在几天前 Mikael Ainalem 分享了一个 codepen 代码,展示了怎么去使用两色轮廓作为一个占位符。结果非常漂亮:

SVG 在这种情况下是手工绘制的,但是,这种技术可以用工具快速生成并自动化处理。

如果感兴趣,可以去看一下 Emil 的 webpack 加载器 (基于 potrace) 和 Mikael 的手工绘制 SVG 之间的比较。

这里我假设该输出是使用默认选项的 potrace 生成的。但是可以对它们进行优化。查看 图像描绘加载器的选项传递给 potrace 的选项非常丰富。

总结

我们看到了从图像中生成 SVG 并使用它们作为占位符的各种不同的工具和技术。与 WebP 是一个用于缩略图的奇妙格式 一样,SVG 也是一个用于占位符的有趣格式。我们可以控制细节的级别(和它们的大小),它是高可压缩的,并且很容易用 CSS 和 JS 进行处理。

额外的资源

这篇文章上到了 Hacker News 热文。对此以及在该页面的评论中分享的其它资源的链接,我表示非常感谢。下面是其中一部分。


via: https://medium.freecodecamp.org/using-svg-as-placeholders-more-image-loading-techniques-bed1b810ab2c

作者:José M. Pérez 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

前言

之前工作时候,一台引流测试机器的一个 ngx\_lua 服务突然出现了一些 HTTP/500 响应,从错误日志打印的堆栈来看,是不久前新发布的版本里添加的一个 Lua table 不存在,而有代码向其进行索引导致的。这令人百思不得其解,如果是版本回退导致的,那么为什么使用这个 Lua table 的代码没有被回退,偏偏定义这个 table 的代码被回退了呢?

经过排查发现,当时 nginx 刚刚完成热更新操作,旧的 master 进程还存在,因为要准备机器重启,先切掉了引流流量(但有些请求还在),同时系统触发了 nginx -s stop,这才导致了这个问题。

场景复现

下面我将使用一个原生的 nginx,在我的安装了 fedora26 的虚拟机上复现这个过程,我使用的 nginx 版本是目前最新的 1.13.4

首先启动 nginx:

alex@Fedora26-64: ~/bin_install/nginx
./sbin/nginx
alex@Fedora26-64: ~/bin_install/nginx
ps auxf | grep nginx
alex      6174  0.0  0.0  28876   428 ?        Ss   14:35   0:00 nginx: master process ./sbin/nginx
alex      6175  0.0  0.2  29364  2060 ?        S    14:35   0:00  \\_ nginx: worker process

可以看到 master 和 worker 都已经在运行。

接着我们向 master 发送一个 SIGUSR2 信号,当 nginx 核心收到这个信号后,就会触发热更新。

alex@Fedora26-64: ~/bin_install/nginx
kill -USR2 6174
alex@Fedora26-64: ~/bin_install/nginx
ps auxf | grep nginx
alex      6174  0.0  0.1  28876  1996 ?        Ss   14:35   0:00 nginx: master process ./sbin/nginx
alex      6175  0.0  0.2  29364  2060 ?        S    14:35   0:00  \\_ nginx: worker process
alex      6209  0.0  0.2  28876  2804 ?        S    14:37   0:00  \\_ nginx: master process ./sbin/nginx
alex      6213  0.0  0.1  29364  2004 ?        S    14:37   0:00      \\_ nginx: worker process

可以看到新的 master 和该 master fork 出来的 worker 已经在运行了,此时我们接着向旧 master 发送一个 SIGWINCH 信号,旧 master 收到这个信号后,会向它的 worker 发送 SIGQUIT,于是旧 master 的 worker 进程就会退出:

alex@Fedora26-64: ~/bin_install/nginx
kill -WINCH 6174
alex@Fedora26-64: ~/bin_install/nginx
ps auxf | grep nginx
alex      6174  0.0  0.1  28876  1996 ?        Ss   14:35   0:00 nginx: master process ./sbin/nginx
alex      6209  0.0  0.2  28876  2804 ?        S    14:37   0:00  \\_ nginx: master process ./sbin/nginx
alex      6213  0.0  0.1  29364  2004 ?        S    14:37   0:00      \\_ nginx: worker process

此时只剩下旧的 master,新的 master 和新 master 的 worker 在运行,这和当时线上运行的情况类似。

接着我们使用 stop 命令:

alex@Fedora26-64: ~/bin_install/nginx
./sbin/nginx -s stop
alex@Fedora26-64: ~/bin_install/nginx
ps auxf | grep nginx
alex      6174  0.0  0.1  28876  1996 ?        Ss   14:35   0:00 nginx: master process ./sbin/nginx
alex      6301  0.0  0.2  29364  2124 ?        S    14:49   0:00  \\_ nginx: worker process

我们会发现,新的 master 和它的 worker 都已经退出,而旧的 master 还在运行,并产生了 worker 出来。这就是当时线上的情况了。

事实上,这个现象和 nginx 自身的设计有关:当旧的 master 准备产生 fork 新的 master 之前,它会把 nginx.pid 这个文件重命名为 nginx.pid.oldbin,然后再由 fork 出来的新的 master 去创建新的 nginx.pid,这个文件将会记录新 master 的 pid。nginx 认为热更新完成之后,旧 master 的使命几乎已经结束,之后它随时会退出,因此之后的操作都应该由新 master 接管。当然,在旧 master 没有退出的情况下通过向新 master 发送 SIGUSR2 企图再次热更新是无效的,新 master 只会忽略掉这个信号然后继续它自己的工作。

问题分析

更不巧的是,我们上面提到的这个 Lua table,定义它的 Lua 文件早在运行 init\_by\_lua 这个 hook 的时候,就已经被 LuaJIT 加载到内存并编译成字节码了,那么显然旧的 master 必然没有这个 Lua table,因为它加载那部分 Lua 代码是旧版本的。

而索引该 table 的 Lua 代码并没有在 init\_by\_lua 的时候使用到,这些代码都是在 worker 进程里被加载起来的,这时候项目目录里的代码都是最新的,所以 worker 进程加载的都是最新的代码,如果这些 worker 进程处理到相关的请求,就会出现 Lua 运行时错误,外部表现则是对应的 HTTP 500。

吸收了这个教训之后,我们需要更加合理地关闭我们的 nginx 服务。 所以一个更加合理的 nginx 服务启动关闭脚本是必需的,网上流传的一些脚本并没有对这个现象做处理,我们更应该参考 NGINX 官方提供的脚本。

stop() {
    echo -n $"Stopping $prog: "
    killproc $prog -QUIT
    retval=$?
    echo
    [ $retval -eq 0 ] && rm -f $lockfile
    return $retval
}

这段代码引自 NGINX 官方的 /etc/init.d/nginx

nginx 信号集

接下来我们来全面梳理下 nginx 信号集,这里不会涉及到源码细节,感兴趣的同学可以自行阅读相关源码。

我们有两种方式来向 master 进程发送信号,一种是通过 nginx -s signal 来操作,另一种是通过 kill 命令手动发送。

第一种方式的原理是,产生一个新进程,该进程通过 nginx.pid 文件得到 master 进程的 pid,然后把对应的信号发送到 master,之后退出,这种进程被称为 signaller。

第二种方式要求我们了解 nginx -s signal 到真实信号的映射。下表是它们的映射关系:

operationsignal
reloadSIGHUP
reopenSIGUSR1
stopSIGTERM
quitSIGQUIT
hot updateSIGUSR2 & SIGWINCH & SIGQUIT

stop vs quit

stop 发送 SIGTERM 信号,表示要求强制退出,quit 发送 SIGQUIT,表示优雅地退出。 具体区别在于,worker 进程在收到 SIGQUIT 消息(注意不是直接发送信号,所以这里用消息替代)后,会关闭监听的套接字,关闭当前空闲的连接(可以被抢占的连接),然后提前处理所有的定时器事件,最后退出。没有特殊情况,都应该使用 quit 而不是 stop。

reload

master 进程收到 SIGHUP 后,会重新进行配置文件解析、共享内存申请,等一系列其他的工作,然后产生一批新的 worker 进程,最后向旧的 worker 进程发送 SIGQUIT 对应的消息,最终无缝实现了重启操作。

reopen

master 进程收到 SIGUSR1 后,会重新打开所有已经打开的文件(比如日志),然后向每个 worker 进程发送 SIGUSR1 信息,worker 进程收到信号后,会执行同样的操作。reopen 可用于日志切割,比如 NGINX 官方就提供了一个方案:

$ mv access.log access.log.0
$ kill -USR1 `cat master.nginx.pid`
$ sleep 1
$ gzip access.log.0    # do something with access.log.0

这里 sleep 1 是必须的,因为在 master 进程向 worker 进程发送 SIGUSR1 消息到 worker 进程真正重新打开 access.log 之间,有一段时间窗口,此时 worker 进程还是向文件 access.log.0 里写入日志的。通过 sleep 1s,保证了 access.log.0 日志信息的完整性(如果没有 sleep 而直接进行压缩,很有可能出现日志丢失的情况)。

hot update

某些时候我们需要进行二进制热更新,nginx 在设计的时候就包含了这种功能,不过无法通过 nginx 提供的命令行完成,我们需要手动发送信号。

通过上面的问题复现,大家应该已经了解到如何进行热更新了,我们首先需要给当前的 master 进程发送 SIGUSR2,之后 master 会重命名 nginx.pidnginx.pid.oldbin,然后 fork 一个新的进程,新进程会通过 execve 这个系统调用,使用新的 nginx ELF 文件替换当前的进程映像,成为新的 master 进程。新 master 进程起来之后,就会进行配置文件解析等操作,然后 fork 出新的 worker 进程开始工作。

接着我们向旧的 master 发送 SIGWINCH 信号,然后旧的 master 进程则会向它的 worker 进程发送 SIGQUIT 信息,从而使得 worker 进程退出。向 master 进程发送 SIGWINCHSIGQUIT 都会使得 worker 进程退出,但是前者不会使得 master 进程也退出。

最后,如果我们觉得旧的 master 进程使命完成,就可以向它发送 SIGQUIT 信号,让其退出了。

worker 进程如何处理来自 master 的信号消息

实际上,master 进程再向 worker 进程通讯,不是使用 kill 函数,而是使用了通过管道实现的 nginx channel,master 进程向管道一端写入信息(比如信号信息),worker 进程则从另外一端收取信息,nginx channel 事件,在 worker 进程刚刚起来的时候,就被加入事件调度器中(比如 epoll,kqueue),所以当有数据从 master 发来时,即可被事件调度器通知到。

nginx 这么设计是有理由的,作为一个优秀的反向代理服务器,nginx 追求的就是极致的高性能,而 signal handler 会中断 worker 进程的运行,使得所有的事件都被暂停一个时间窗口,这对性能是有一定损失的。

很多人可能会认为当 master 进程向 worker 进程发送信息之后,worker 进程立刻会有对应操作回应,然而 worker 进程是非常繁忙的,它不断地处理着网络事件和定时器事件,当调用 nginx channel 事件的 handler 之后,nginx 仅仅只是处理了一些标志位。真正执行这些动作是在一轮事件调度完成之后。所以这之间存在一个时间窗口,尤其是业务复杂且流量巨大的时候,这个窗口就有可能被放大,这也就是为什么 NGINX 官方提供的日志切割方案里要求 sleep 1s 的原因。

当然,我们也可以绕过 master 进程,直接向 worker 进程发送信号,worker 可以处理的信号有

signaleffect
SIGINT强制退出
SIGTERM强制退出
SIGQUIT优雅退出
SIGUSR1重新打开文件

总结

nginx 信号操作在日常运维中是最常见的,也是非常重要的,这个环节如果出现失误则可能造成业务异常,带来损失。所以理清楚 nginx 信号集是非常必要的,能帮助我们更好地处理这些工作。

另外,通过这次的经验教训和对 nginx 信号集的认知,我们认为以下几点是比较重要的:

  • 慎用 nginx -s stop,尽可能使用 nginx -s quit
  • 热更新之后,如果确定业务没问题,尽可能让旧的 master 进程退出
  • 关键性的信号操作完成后,等待一段时间,避免时间窗口的影响
  • 不要直接向 worker 进程发送信号

LibreOffice on Flathub

LibreOffice 现在可以从集中化的 Flatpak 应用商店 Flathub 进行安装。

它的到来使任何运行现代 Linux 发行版的人都能只点击一两次即可安装 LibreOffice 的最新稳定版本,而无需搜索 PPA,纠缠于 tar 包或等待发行版将其打包。

自去年 8 月份 LibreOffice 5.2 发布以来,LibreOffice Flatpak 已经可供用户下载和安装。

这里“新”的是指发行方法。 文档基金会 Document Foundation 选择使用 Flathub 而不是专门的服务器来发布更新。

这对于终端用户来说是一个很好的消息,因为这意味着不需要在新安装时担心仓库,但对于 Flatpak 的倡议者来说也是一个好消息:LibreOffice 是开源软件里最流行的生产力套件。它对该格式和应用商店的支持肯定会受到热烈的欢迎。

在撰写本文时,你可以从 Flathub 安装 LibreOffice 5.4.2。新的稳定版本将在发布时添加。

在 Ubuntu 上启用 Flathub

要在 Ubuntu 上启动并运行 Flatpak,首先必须安装它:

sudo apt install flatpak gnome-software-plugin-flatpak

为了能够从 Flathub 安装应用程序,你需要添加 Flathub 远程服务器:

flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo

这就行了。只需注销并重新登录(以便 Ubuntu Software 刷新其缓存),之后你应该能够通过 Ubuntu Software 看到 Flathub 上的任何 Flatpak 程序了。

Fedora、Arch 和 Linux Mint 18.3 用户已经安装了 Flatpak,随时可以开箱即用。Mint 甚至预启用了 Flathub remote。

在本例中,搜索 “LibreOffice” 并在结果中找到下面有 Flathub 提示的结果。(请记住,Ubuntu 已经调整了客户端,来将 Snap 程序显示在最上面,所以你可能需要向下滚动列表来查看它)。

从 Flathub 安装 LibreOffice

从 flatpakref 中安装 Flatpak 程序有一个 bug,所以如果上面的方法不起作用,你也可以使用命令行从 Flathub 中安装 Flathub 程序。

Flathub 网站列出了安装每个程序所需的命令。切换到“命令行”选项卡来查看它们。

Flathub 上更多的应用

如果你经常看这个网站,你就会知道我喜欢 Flathub。这是我最喜欢的一些应用(Corebird、Parlatype、GNOME MPV、Peek、Audacity、GIMP 等)的家园。我无需等待就能获得这些应用程序的最新、稳定版本(加上它们需要的所有依赖)。

而且,在我 twiiter 上发布一周左右后,大多数 Flatpak 应用现在看起来有很棒 GTK 主题 - 不再需要临时方案了!


via: http://www.omgubuntu.co.uk/2017/11/libreoffice-now-available-flathub-flatpak-app-store

作者:JOEY SNEDDON 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

使用 伯克利包过滤器 Berkeley Packet Filter (BPF) 编译器集合 Compiler Collection (BCC)工具深度探查你的 Linux 代码。

在 Linux 中出现的一种新技术能够为系统管理员和开发者提供大量用于性能分析和故障排除的新工具和仪表盘。它被称为 增强的伯克利数据包过滤器 enhanced Berkeley Packet Filter (eBPF,或 BPF),虽然这些改进并不是由伯克利开发的,而且它们不仅仅是处理数据包,更多的是过滤。我将讨论在 Fedora 和 Red Hat Linux 发行版中使用 BPF 的一种方法,并在 Fedora 26 上演示。

BPF 可以在内核中运行由用户定义的沙盒程序,可以立即添加新的自定义功能。这就像按需给 Linux 系统添加超能力一般。 你可以使用它的例子包括如下:

  • 高级性能跟踪工具:对文件系统操作、TCP 事件、用户级事件等的可编程的低开销检测。
  • 网络性能: 尽早丢弃数据包以提高对 DDoS 的恢复能力,或者在内核中重定向数据包以提高性能。
  • 安全监控: 7x24 小时的自定义检测和记录内核空间与用户空间内的可疑事件。

在可能的情况下,BPF 程序必须通过一个内核验证机制来保证它们的安全运行,这比写自定义的内核模块更安全。我在此假设大多数人并不编写自己的 BPF 程序,而是使用别人写好的。在 GitHub 上的 BPF Compiler Collection (bcc) 项目中,我已发布许多开源代码。bcc 为 BPF 开发提供了不同的前端支持,包括 Python 和 Lua,并且是目前最活跃的 BPF 工具项目。

7 个有用的 bcc/BPF 新工具

为了了解 bcc/BPF 工具和它们的检测内容,我创建了下面的图表并添加到 bcc 项目中。

Linux bcc/BPF 跟踪工具图

这些是命令行界面工具,你可以通过 SSH 使用它们。目前大多数分析,包括我的老板,都是用 GUI 和仪表盘进行的。SSH 是最后的手段。但这些命令行工具仍然是预览 BPF 能力的好方法,即使你最终打算通过一个可用的 GUI 使用它。我已着手向一个开源 GUI 添加 BPF 功能,但那是另一篇文章的主题。现在我想向你分享今天就可以使用的 CLI 工具。

1、 execsnoop

从哪儿开始呢?如何查看新的进程。那些会消耗系统资源,但很短暂的进程,它们甚至不会出现在 top(1) 命令或其它工具中的显示之中。这些新进程可以使用 execsnoop 进行检测(或使用行业术语说,可以 被追踪 traced )。 在追踪时,我将在另一个窗口中通过 SSH 登录:

# /usr/share/bcc/tools/execsnoop
PCOMM            PID    PPID   RET ARGS
sshd             12234  727      0 /usr/sbin/sshd -D -R
unix_chkpwd      12236  12234    0 /usr/sbin/unix_chkpwd root nonull
unix_chkpwd      12237  12234    0 /usr/sbin/unix_chkpwd root chkexpiry
bash             12239  12238    0 /bin/bash
id               12241  12240    0 /usr/bin/id -un
hostname         12243  12242    0 /usr/bin/hostname
pkg-config       12245  12244    0 /usr/bin/pkg-config --variable=completionsdir bash-completion
grepconf.sh      12246  12239    0 /usr/libexec/grepconf.sh -c
grep             12247  12246    0 /usr/bin/grep -qsi ^COLOR.*none /etc/GREP_COLORS
tty              12249  12248    0 /usr/bin/tty -s
tput             12250  12248    0 /usr/bin/tput colors
dircolors        12252  12251    0 /usr/bin/dircolors --sh /etc/DIR_COLORS
grep             12253  12239    0 /usr/bin/grep -qi ^COLOR.*none /etc/DIR_COLORS
grepconf.sh      12254  12239    0 /usr/libexec/grepconf.sh -c
grep             12255  12254    0 /usr/bin/grep -qsi ^COLOR.*none /etc/GREP_COLORS
grepconf.sh      12256  12239    0 /usr/libexec/grepconf.sh -c
grep             12257  12256    0 /usr/bin/grep -qsi ^COLOR.*none /etc/GREP_COLORS

哇哦。 那是什么? 什么是 grepconf.sh? 什么是 /etc/GREP_COLORS? 是 grep 在读取它自己的配置文件……由 grep 运行的? 这究竟是怎么工作的?

欢迎来到有趣的系统追踪世界。 你可以学到很多关于系统是如何工作的(或者根本不工作,在有些情况下),并且发现一些简单的优化方法。 execsnoop 通过跟踪 exec() 系统调用来工作,exec() 通常用于在新进程中加载不同的程序代码。

2、 opensnoop

接着上面继续,所以,grepconf.sh 可能是一个 shell 脚本,对吧? 我将运行 file(1) 来检查它,并使用opensnoop bcc 工具来查看打开的文件:

# /usr/share/bcc/tools/opensnoop
PID    COMM               FD ERR PATH
12420  file                3   0 /etc/ld.so.cache
12420  file                3   0 /lib64/libmagic.so.1
12420  file                3   0 /lib64/libz.so.1
12420  file                3   0 /lib64/libc.so.6
12420  file                3   0 /usr/lib/locale/locale-archive
12420  file               -1   2 /etc/magic.mgc
12420  file                3   0 /etc/magic
12420  file                3   0 /usr/share/misc/magic.mgc
12420  file                3   0 /usr/lib64/gconv/gconv-modules.cache
12420  file                3   0 /usr/libexec/grepconf.sh
1      systemd            16   0 /proc/565/cgroup
1      systemd            16   0 /proc/536/cgroup

execsnoopopensnoop 这样的工具会将每个事件打印一行。上图显示 file(1) 命令当前打开(或尝试打开)的文件:返回的文件描述符(“FD” 列)对于 /etc/magic.mgc 是 -1,而 “ERR” 列指示它是“文件未找到”。我不知道该文件,也不知道 file(1) 正在读取的 /usr/share/misc/magic.mgc 文件是什么。我不应该感到惊讶,但是 file(1) 在识别文件类型时没有问题:

# file /usr/share/misc/magic.mgc /etc/magic
/usr/share/misc/magic.mgc: magic binary file for file(1) cmd (version 14) (little endian)
/etc/magic:                magic text file for file(1) cmd, ASCII text

opensnoop 通过跟踪 open() 系统调用来工作。为什么不使用 strace -feopen file 命令呢? 在这种情况下是可以的。然而,opensnoop 的一些优点在于它能在系统范围内工作,并且跟踪所有进程的 open() 系统调用。注意上例的输出中包括了从 systemd 打开的文件。opensnoop 应该系统开销更低:BPF 跟踪已经被优化过,而当前版本的 strace(1) 仍然使用较老和较慢的 ptrace(2) 接口。

3、 xfsslower

bcc/BPF 不仅仅可以分析系统调用。xfsslower 工具可以跟踪大于 1 毫秒(参数)延迟的常见 XFS 文件系统操作。

# /usr/share/bcc/tools/xfsslower 1
Tracing XFS operations slower than 1 ms
TIME     COMM           PID    T BYTES   OFF_KB   LAT(ms) FILENAME
14:17:34 systemd-journa 530    S 0       0           1.69 system.journal
14:17:35 auditd         651    S 0       0           2.43 audit.log
14:17:42 cksum          4167   R 52976   0           1.04 at
14:17:45 cksum          4168   R 53264   0           1.62 [
14:17:45 cksum          4168   R 65536   0           1.01 certutil
14:17:45 cksum          4168   R 65536   0           1.01 dir
14:17:45 cksum          4168   R 65536   0           1.17 dirmngr-client
14:17:46 cksum          4168   R 65536   0           1.06 grub2-file
14:17:46 cksum          4168   R 65536   128         1.01 grub2-fstest
[...]

在上图输出中,我捕获到了多个延迟超过 1 毫秒 的 cksum(1) 读取操作(字段 “T” 等于 “R”)。这是在 xfsslower 工具运行的时候,通过在 XFS 中动态地检测内核函数实现的,并当它结束的时候解除该检测。这个 bcc 工具也有其它文件系统的版本:ext4slowerbtrfsslowerzfsslowernfsslower

这是个有用的工具,也是 BPF 追踪的重要例子。对文件系统性能的传统分析主要集中在块 I/O 统计信息 —— 通常你看到的是由 iostat(1) 工具输出,并由许多性能监视 GUI 绘制的图表。这些统计数据显示的是磁盘如何执行,而不是真正的文件系统如何执行。通常比起磁盘来说,你更关心的是文件系统的性能,因为应用程序是在文件系统中发起请求和等待。并且,文件系统的性能可能与磁盘的性能大为不同!文件系统可以完全从内存缓存中读取数据,也可以通过预读算法和回写缓存来填充缓存。xfsslower 显示了文件系统的性能 —— 这是应用程序直接体验到的性能。通常这对于排除整个存储子系统的问题是有用的;如果确实没有文件系统延迟,那么性能问题很可能是在别处。

4、 biolatency

虽然文件系统性能对于理解应用程序性能非常重要,但研究磁盘性能也是有好处的。当各种缓存技巧都无法挽救其延迟时,磁盘的低性能终会影响应用程序。 磁盘性能也是容量规划研究的目标。

iostat(1) 工具显示了平均磁盘 I/O 延迟,但平均值可能会引起误解。 以直方图的形式研究 I/O 延迟的分布是有用的,这可以通过使用 [biolatency] 来实现18

# /usr/share/bcc/tools/biolatency
Tracing block device I/O... Hit Ctrl-C to end.
^C
     usecs               : count     distribution
         0 -> 1          : 0        |                                        |
         2 -> 3          : 0        |                                        |
         4 -> 7          : 0        |                                        |
         8 -> 15         : 0        |                                        |
        16 -> 31         : 0        |                                        |
        32 -> 63         : 1        |                                        |
        64 -> 127        : 63       |****                                    |
       128 -> 255        : 121      |*********                               |
       256 -> 511        : 483      |************************************    |
       512 -> 1023       : 532      |****************************************|
      1024 -> 2047       : 117      |********                                |
      2048 -> 4095       : 8        |                                        |

这是另一个有用的工具和例子;它使用一个名为 maps 的 BPF 特性,它可以用来实现高效的内核摘要统计。从内核层到用户层的数据传输仅仅是“计数”列。 用户级程序生成其余的。

值得注意的是,这种工具大多支持 CLI 选项和参数,如其使用信息所示:

# /usr/share/bcc/tools/biolatency -h
usage: biolatency [-h] [-T] [-Q] [-m] [-D] [interval] [count]

Summarize block device I/O latency as a histogram

positional arguments:
  interval            output interval, in seconds
  count               number of outputs

optional arguments:
  -h, --help          show this help message and exit
  -T, --timestamp     include timestamp on output
  -Q, --queued        include OS queued time in I/O time
  -m, --milliseconds  millisecond histogram
  -D, --disks         print a histogram per disk device

examples:
    ./biolatency            # summarize block I/O latency as a histogram
    ./biolatency 1 10       # print 1 second summaries, 10 times
    ./biolatency -mT 1      # 1s summaries, milliseconds, and timestamps
    ./biolatency -Q         # include OS queued time in I/O time
    ./biolatency -D         # show each disk device separately

它们的行为就像其它 Unix 工具一样,以利于采用而设计。

5、 tcplife

另一个有用的工具是 tcplife ,该例显示 TCP 会话的生命周期和吞吐量统计。

# /usr/share/bcc/tools/tcplife
PID   COMM       LADDR           LPORT RADDR           RPORT TX_KB RX_KB MS
12759 sshd       192.168.56.101  22    192.168.56.1    60639     2     3 1863.82
12783 sshd       192.168.56.101  22    192.168.56.1    60640     3     3 9174.53
12844 wget       10.0.2.15       34250 54.204.39.132   443      11  1870 5712.26
12851 curl       10.0.2.15       34252 54.204.39.132   443       0    74 505.90

在你说 “我不是可以只通过 tcpdump(8) 就能输出这个?” 之前请注意,运行 tcpdump(8) 或任何数据包嗅探器,在高数据包速率的系统上的开销会很大,即使 tcpdump(8) 的用户层和内核层机制已经过多年优化(要不可能更差)。tcplife 不会测试每个数据包;它只会有效地监视 TCP 会话状态的变化,并由此得到该会话的持续时间。它还使用已经跟踪了吞吐量的内核计数器,以及进程和命令信息(“PID” 和 “COMM” 列),这些对于 tcpdump(8) 等线上嗅探工具是做不到的。

6、 gethostlatency

之前的每个例子都涉及到内核跟踪,所以我至少需要一个用户级跟踪的例子。 这就是 gethostlatency,它检测用于名称解析的 gethostbyname(3) 和相关的库调用:

# /usr/share/bcc/tools/gethostlatency
TIME      PID    COMM                  LATms HOST
06:43:33  12903  curl                 188.98 opensource.com
06:43:36  12905  curl                   8.45 opensource.com
06:43:40  12907  curl                   6.55 opensource.com
06:43:44  12911  curl                   9.67 opensource.com
06:45:02  12948  curl                  19.66 opensource.cats
06:45:06  12950  curl                  18.37 opensource.cats
06:45:07  12952  curl                  13.64 opensource.cats
06:45:19  13139  curl                  13.10 opensource.cats

是的,总是有 DNS 请求,所以有一个工具来监视系统范围内的 DNS 请求会很方便(这只有在应用程序使用标准系统库时才有效)。看看我如何跟踪多个对 “opensource.com” 的查找? 第一个是 188.98 毫秒,然后更快,不到 10 毫秒,毫无疑问,这是缓存的作用。它还追踪多个对 “opensource.cats” 的查找,一个不存在的可怜主机名,但我们仍然可以检查第一个和后续查找的延迟。(第二次查找后是否有一些否定缓存的影响?)

7、 trace

好的,再举一个例子。 trace 工具由 Sasha Goldshtein 提供,并提供了一些基本的 printf(1) 功能和自定义探针。 例如:

# /usr/share/bcc/tools/trace 'pam:pam_start "%s: %s", arg1, arg2'
PID    TID    COMM         FUNC             -
13266  13266  sshd         pam_start        sshd: root

在这里,我正在跟踪 libpam 及其 pam_start(3) 函数,并将其两个参数都打印为字符串。 libpam 用于插入式身份验证模块系统,该输出显示 sshd 为 “root” 用户调用了 pam_start()(我登录了)。 其使用信息中有更多的例子(trace -h),而且所有这些工具在 bcc 版本库中都有手册页和示例文件。 例如 trace_example.txttrace.8

通过包安装 bcc

安装 bcc 最佳的方法是从 iovisor 仓储库中安装,按照 bcc 的 INSTALL.md 进行即可。IO Visor 是包括了 bcc 的 Linux 基金会项目。4.x 系列 Linux 内核中增加了这些工具所使用的 BPF 增强功能,直到 4.9 添加了全部支持。这意味着拥有 4.8 内核的 Fedora 25 可以运行这些工具中的大部分。 使用 4.11 内核的 Fedora 26 可以全部运行它们(至少在目前是这样)。

如果你使用的是 Fedora 25(或者 Fedora 26,而且这个帖子已经在很多个月前发布了 —— 你好,来自遥远的过去!),那么这个通过包安装的方式是可以工作的。 如果您使用的是 Fedora 26,那么请跳至“通过源代码安装”部分,它避免了一个已修复的已知错误。 这个错误修复目前还没有进入 Fedora 26 软件包的依赖关系。 我使用的系统是:

# uname -a
Linux localhost.localdomain 4.11.8-300.fc26.x86_64 #1 SMP Thu Jun 29 20:09:48 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
# cat /etc/fedora-release
Fedora release 26 (Twenty Six)

以下是我所遵循的安装步骤,但请参阅 INSTALL.md 获取更新的版本:

# echo -e '[iovisor]\nbaseurl=https://repo.iovisor.org/yum/nightly/f25/$basearch\nenabled=1\ngpgcheck=0' | sudo tee /etc/yum.repos.d/iovisor.repo
# dnf install bcc-tools
[...]
Total download size: 37 M
Installed size: 143 M
Is this ok [y/N]: y

安装完成后,您可以在 /usr/share 中看到新的工具:

# ls /usr/share/bcc/tools/
argdist       dcsnoop              killsnoop       softirqs    trace
bashreadline  dcstat               llcstat         solisten    ttysnoop
[...]

试着运行其中一个:

# /usr/share/bcc/tools/opensnoop
chdir(/lib/modules/4.11.8-300.fc26.x86_64/build): No such file or directory
Traceback (most recent call last):
  File "/usr/share/bcc/tools/opensnoop", line 126, in 
    b = BPF(text=bpf_text)
  File "/usr/lib/python3.6/site-packages/bcc/__init__.py", line 284, in __init__
    raise Exception("Failed to compile BPF module %s" % src_file)
Exception: Failed to compile BPF module

运行失败,提示 /lib/modules/4.11.8-300.fc26.x86_64/build 丢失。 如果你也遇到这个问题,那只是因为系统缺少内核头文件。 如果你看看这个文件指向什么(这是一个符号链接),然后使用 dnf whatprovides 来搜索它,它会告诉你接下来需要安装的包。 对于这个系统,它是:

# dnf install kernel-devel-4.11.8-300.fc26.x86_64
[...]
Total download size: 20 M
Installed size: 63 M
Is this ok [y/N]: y
[...]

现在:

# /usr/share/bcc/tools/opensnoop
PID    COMM               FD ERR PATH
11792  ls                  3   0 /etc/ld.so.cache
11792  ls                  3   0 /lib64/libselinux.so.1
11792  ls                  3   0 /lib64/libcap.so.2
11792  ls                  3   0 /lib64/libc.so.6
[...]

运行起来了。 这是捕获自另一个窗口中的 ls 命令活动。 请参阅前面的部分以使用其它有用的命令。

通过源码安装

如果您需要从源代码安装,您还可以在 INSTALL.md 中找到文档和更新说明。 我在 Fedora 26 上做了如下的事情:

sudo dnf install -y bison cmake ethtool flex git iperf libstdc++-static \
  python-netaddr python-pip gcc gcc-c++ make zlib-devel \
  elfutils-libelf-devel
sudo dnf install -y luajit luajit-devel  # for Lua support
sudo dnf install -y \
  http://pkgs.repoforge.org/netperf/netperf-2.6.0-1.el6.rf.x86_64.rpm
sudo pip install pyroute2
sudo dnf install -y clang clang-devel llvm llvm-devel llvm-static ncurses-devel

netperf 外一切妥当,其中有以下错误:

Curl error (28): Timeout was reached for http://pkgs.repoforge.org/netperf/netperf-2.6.0-1.el6.rf.x86_64.rpm [Connection timed out after 120002 milliseconds]

不必理会,netperf 是可选的,它只是用于测试,而 bcc 没有它也会编译成功。

以下是余下的 bcc 编译和安装步骤:

git clone https://github.com/iovisor/bcc.git
mkdir bcc/build; cd bcc/build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr
make
sudo make install

现在,命令应该可以工作了:

# /usr/share/bcc/tools/opensnoop
PID    COMM               FD ERR PATH
4131   date                3   0 /etc/ld.so.cache
4131   date                3   0 /lib64/libc.so.6
4131   date                3   0 /usr/lib/locale/locale-archive
4131   date                3   0 /etc/localtime
[...]

写在最后和其他的前端

这是一个可以在 Fedora 和 Red Hat 系列操作系统上使用的新 BPF 性能分析强大功能的快速浏览。我演示了 BPF 的流行前端 bcc ,并包括了其在 Fedora 上的安装说明。bcc 附带了 60 多个用于性能分析的新工具,这将帮助您充分利用 Linux 系统。也许你会直接通过 SSH 使用这些工具,或者一旦 GUI 监控程序支持 BPF 的话,你也可以通过它们来使用相同的功能。

此外,bcc 并不是正在开发的唯一前端。plybpftrace,旨在为快速编写自定义工具提供更高级的语言支持。此外,SystemTap 刚刚发布版本 3.2,包括一个早期的实验性 eBPF 后端。 如果这个继续开发,它将为运行多年来开发的许多 SystemTap 脚本和 tapset(库)提供一个安全和高效的生产级引擎。(随同 eBPF 使用 SystemTap 将是另一篇文章的主题。)

如果您需要开发自定义工具,那么也可以使用 bcc 来实现,尽管语言比 SystemTap、ply 或 bpftrace 要冗长得多。我的 bcc 工具可以作为代码示例,另外我还贡献了用 Python 开发 bcc 工具的教程。 我建议先学习 bcc 的 multi-tools,因为在需要编写新工具之前,你可能会从里面获得很多经验。 您可以从它们的 bcc 存储库funccountfunclatencyfuncslowerstackcounttraceargdist 的示例文件中研究 bcc。

感谢 Opensource.com 进行编辑。

关于作者

Brendan Gregg 是 Netflix 的一名高级性能架构师,在那里他进行大规模的计算机性能设计、分析和调优。


via:https://opensource.com/article/17/11/bccbpf-performance

作者:Brendan Gregg 译者:yongshouzhang 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Suplemon 是一个 CLI 中的现代文本编辑器,它模拟 Sublime Text 的多光标行为和其它特性。它是轻量级的,非常易于使用,就像 Nano 一样。

使用 CLI 编辑器的好处之一是,无论你使用的 Linux 发行版是否有 GUI,你都可以使用它。这种文本编辑器也很简单、快速和强大。

你可以在其官方仓库中找到有用的信息和源代码。

功能

这些是一些它有趣的功能:

  • 多光标支持
  • 撤销/重做
  • 复制和粘贴,带有多行支持
  • 鼠标支持
  • 扩展
  • 查找、查找所有、查找下一个
  • 语法高亮
  • 自动完成
  • 自定义键盘快捷键

安装

首先,确保安装了最新版本的 python3 和 pip3。

然后在终端输入:

$ sudo pip3 install suplemon

使用

在当前目录中创建一个新文件

打开一个终端并输入:

$ suplemon

你将看到如下:

suplemon new file

打开一个或多个文件

打开一个终端并输入:

$ suplemon <filename1> <filename2> ... <filenameN>

例如:

$ suplemon example1.c example2.c

主要配置

你可以在 ~/.config/suplemon/suplemon-config.json 找到配置文件。

编辑这个文件很简单,你只需要进入命令模式(进入 suplemon 后)并运行 config 命令。你可以通过运行 config defaults 来查看默认配置。

键盘映射配置

我会展示 suplemon 的默认键映射。如果你想编辑它们,只需运行 keymap 命令。运行 keymap default 来查看默认的键盘映射文件。

操作快捷键
退出Ctrl + Q
复制行到缓冲区Ctrl + C
剪切行缓冲区Ctrl + X
插入缓冲区Ctrl + V
复制行Ctrl + K
跳转Ctrl + G。 你可以跳转到一行或一个文件(只需键入一个文件名的开头)。 另外,可以输入类似于 exam:50 跳转到 example.c50 行。
用字符串或正则表达式搜索Ctrl + F
搜索下一个Ctrl + D
去除空格Ctrl + T
在箭头方向添加新的光标Alt + 方向键
跳转到上一个或下一个单词或行Ctrl + 左/右
恢复到单光标/取消输入提示Esc
向上/向下移动行Page Up / Page Down
保存文件Ctrl + S
用新名称保存文件F1
重新载入当前文件F2
打开文件Ctrl + O
关闭文件Ctrl + W
切换到下一个/上一个文件Ctrl + Page Up / Ctrl + Page Down
运行一个命令Ctrl + E
撤消Ctrl + Z
重做Ctrl + Y
触发可见的空格F7
切换鼠标模式F8
显示行号F9
显示全屏F11

鼠标快捷键

  • 将光标置于指针位置:左键单击
  • 在指针位置添加一个光标:右键单击
  • 垂直滚动:向上/向下滚动滚轮

总结

在尝试 Suplemon 一段时间后,我改变了对 CLI 文本编辑器的看法。我以前曾经尝试过 Nano,是的,我喜欢它的简单性,但是它的现代特征的缺乏使它在日常使用中变得不实用。

这个工具有 CLI 和 GUI 世界最好的东西……简单性和功能丰富!所以我建议你试试看,并在评论中写下你的想法 :-)


via: https://linoxide.com/tools/suplemon-cli-text-editor-multi-cursor/

作者:Ivo Ursino 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在 2017 年红帽峰会上,有几个人问我“我们通常用完整的虚拟机来隔离如 DNS 和 DHCP 等网络服务,那我们可以用容器来取而代之吗?”答案是可以的,下面是在当前红帽企业版 Linux 7 系统上创建一个系统容器的例子。

我们的目的

创建一个可以独立于任何其它系统服务而更新的网络服务,并且可以从主机端容易地管理和更新。

让我们来探究一下在容器中建立一个运行在 systemd 之下的 BIND 服务器。在这一部分,我们将了解到如何建立自己的容器以及管理 BIND 配置和数据文件。

在本系列的第二部分,我们将看到如何整合主机中的 systemd 和容器中的 systemd。我们将探究如何管理容器中的服务,并且使它作为一种主机中的服务。

创建 BIND 容器

为了使 systemd 在一个容器中轻松运行,我们首先需要在主机中增加两个包:oci-register-machineoci-systemd-hookoci-systemd-hook 这个钩子允许我们在一个容器中运行 systemd,而不需要使用特权容器或者手工配置 tmpfs 和 cgroups。oci-register-machine 这个钩子允许我们使用 systemd 工具如 systemctlmachinectl 来跟踪容器。

[root@rhel7-host ~]# yum install oci-register-machine oci-systemd-hook  

回到创建我们的 BIND 容器上。红帽企业版 Linux 7 基础镜像包含了 systemd 作为其初始化系统。我们可以如我们在典型的系统中做的那样安装并激活 BIND。你可以从 git 仓库中下载这份 Dockerfile

[root@rhel7-host bind]# vi Dockerfile

# Dockerfile for BIND
FROM registry.access.redhat.com/rhel7/rhel
ENV container docker
RUN yum -y install bind && \
    yum clean all && \
    systemctl enable named
STOPSIGNAL SIGRTMIN+3
EXPOSE 53
EXPOSE 53/udp
CMD [ "/sbin/init" ]  

因为我们以 PID 1 来启动一个初始化系统,当我们告诉容器停止时,需要改变 docker CLI 发送的信号。从 kill 系统调用手册中 (man 2 kill):

唯一可以发送给 PID 1 进程(即 init 进程)的信号,是那些初始化系统明确安装了 信号处理器 signal handler 的信号。这是为了避免系统被意外破坏。

对于 systemd 信号处理器,SIGRTMIN+3 是对应于 systemd start halt.target 的信号。我们也需要为 BIND 暴露 TCP 和 UDP 端口号,因为这两种协议可能都要使用。

管理数据

有了一个可以工作的 BIND 服务,我们还需要一种管理配置文件和区域文件的方法。目前这些都放在容器里面,所以我们任何时候都可以进入容器去更新配置或者改变一个区域文件。从管理的角度来说,这并不是很理想。当要更新 BIND 时,我们将需要重建这个容器,所以镜像中的改变将会丢失。任何时候我们需要更新一个文件或者重启服务时,都需要进入这个容器,而这增加了步骤和时间。

相反的,我们将从这个容器中提取出配置文件和数据文件,把它们拷贝到主机上,然后在运行的时候挂载它们。用这种方式我们可以很容易地重启或者重建容器,而不会丢失所做出的更改。我们也可以使用容器外的编辑器来更改配置和区域文件。因为这个容器的数据看起来像“该系统所提供服务的特定站点数据”,让我们遵循 Linux 文件系统层次标准 File System Hierarchy ,并在当前主机上创建 /srv/named 目录来保持管理权分离。

[root@rhel7-host ~]# mkdir -p /srv/named/etc

[root@rhel7-host ~]# mkdir -p /srv/named/var/named     

提示:如果你正在迁移一个已有的配置文件,你可以跳过下面的步骤并且将它直接拷贝到 /srv/named 目录下。你也许仍然要用一个临时容器来检查一下分配给这个容器的 GID。

让我们建立并运行一个临时容器来检查 BIND。在将 init 进程以 PID 1 运行时,我们不能交互地运行这个容器来获取一个 shell。我们会在容器启动后执行 shell,并且使用 rpm 命令来检查重要文件。

[root@rhel7-host ~]# docker build -t named . 

[root@rhel7-host ~]# docker exec -it $( docker run -d named ) /bin/bash

[root@0e77ce00405e /]# rpm -ql bind

对于这个例子来说,我们将需要 /etc/named.conf/var/named/ 目录下的任何文件。我们可以使用 machinectl 命令来提取它们。如果注册了一个以上的容器,我们可以在任一机器上使用 machinectl status 命令来查看运行的是什么。一旦有了这些配置,我们就可以终止这个临时容器了。

如果你喜欢,资源库中也有一个样例 named.conf 和针对 example.com 的区域文件

[root@rhel7-host bind]# machinectl list

MACHINE                          CLASS     SERVICE
8824c90294d5a36d396c8ab35167937f container docker 

[root@rhel7-host ~]# machinectl copy-from 8824c90294d5a36d396c8ab35167937f /etc/named.conf /srv/named/etc/named.conf

[root@rhel7-host ~]# machinectl copy-from 8824c90294d5a36d396c8ab35167937f /var/named /srv/named/var/named

[root@rhel7-host ~]# docker stop infallible_wescoff

最终的创建

为了创建和运行最终的容器,添加卷选项以挂载:

  • 将文件 /srv/named/etc/named.conf 映射为 /etc/named.conf
  • 将目录 /srv/named/var/named 映射为 /var/named

因为这是我们最终的容器,我们将提供一个有意义的名字,以供我们以后引用。

[root@rhel7-host ~]# docker run -d -p 53:53 -p 53:53/udp -v /srv/named/etc/named.conf:/etc/named.conf:Z -v /srv/named/var/named:/var/named:Z --name named-container named

在最终容器运行时,我们可以更改本机配置来改变这个容器中 BIND 的行为。这个 BIND 服务器将需要在这个容器分配的任何 IP 上监听。请确保任何新文件的 GID 与来自这个容器中的其余的 BIND 文件相匹配。

[root@rhel7-host bind]# cp named.conf /srv/named/etc/named.conf 

[root@rhel7-host ~]# cp example.com.zone /srv/named/var/named/example.com.zone

[root@rhel7-host ~]# cp example.com.rr.zone  /srv/named/var/named/example.com.rr.zone
很好奇为什么我不需要在主机目录中改变 SELinux 上下文? 注1

我们将运行这个容器提供的 rndc 二进制文件重新加载配置。我们可以使用 journald 以同样的方式检查 BIND 日志。如果运行出现错误,你可以在主机中编辑该文件,并且重新加载配置。在主机中使用 hostdig,我们可以检查来自该容器化服务的 example.com 的响应。

[root@rhel7-host ~]# docker exec -it named-container rndc reload       
server reload successful

[root@rhel7-host ~]# docker exec -it named-container journalctl -u named -n
-- Logs begin at Fri 2017-05-12 19:15:18 UTC, end at Fri 2017-05-12 19:29:17 UTC. --
May 12 19:29:17 ac1752c314a7 named[27]: automatic empty zone: 9.E.F.IP6.ARPA
May 12 19:29:17 ac1752c314a7 named[27]: automatic empty zone: A.E.F.IP6.ARPA
May 12 19:29:17 ac1752c314a7 named[27]: automatic empty zone: B.E.F.IP6.ARPA
May 12 19:29:17 ac1752c314a7 named[27]: automatic empty zone: 8.B.D.0.1.0.0.2.IP6.ARPA
May 12 19:29:17 ac1752c314a7 named[27]: reloading configuration succeeded
May 12 19:29:17 ac1752c314a7 named[27]: reloading zones succeeded
May 12 19:29:17 ac1752c314a7 named[27]: zone 1.0.10.in-addr.arpa/IN: loaded serial 2001062601
May 12 19:29:17 ac1752c314a7 named[27]: zone 1.0.10.in-addr.arpa/IN: sending notifies (serial 2001062601)
May 12 19:29:17 ac1752c314a7 named[27]: all zones loaded
May 12 19:29:17 ac1752c314a7 named[27]: running

[root@rhel7-host bind]# host www.example.com localhost
Using domain server:
Name: localhost
Address: ::1#53
Aliases: 
www.example.com is an alias for server1.example.com.
server1.example.com is an alias for mail
你的区域文件没有更新吗?可能是因为你的编辑器,而不是序列号。 注2

终点线

我们已经达成了我们打算完成的目标,从容器中为 DNS 请求和区域文件提供服务。我们已经得到一个持久化的位置来管理更新和配置,并且更新后该配置不变。

在这个系列的第二部分,我们将看到怎样将一个容器看作为主机中的一个普通服务来运行。


关注 RHEL 博客,通过电子邮件来获得本系列第二部分和其它新文章的更新。


附加资源

你可能已经注意到当我从容器向本地主机拷贝文件时,我没有运行 chcon 将主机中的文件类型改变为 svirt_sandbox_file_t。为什么它没有出错?将一个文件拷贝到 /srv 会将这个文件标记为类型 var_t。我 setenforce 0 (关闭 SELinux)了吗?

当然没有,这将让 Dan Walsh 大哭(LCTT 译注:RedHat 的 SELinux 团队负责人,倡议不要禁用 SELinux)。是的,machinectl 确实将文件标记类型设置为期望的那样,可以看一下:

启动一个容器之前:

[root@rhel7-host ~]# ls -Z /srv/named/etc/named.conf
-rw-r-----. unconfined_u:object_r:var_t:s0   /srv/named/etc/named.conf

不过,运行中我使用了一个卷选项可以使 Dan Walsh 先生高兴起来,:Z-v /srv/named/etc/named.conf:/etc/named.conf:Z 命令的这部分做了两件事情:首先它表示这需要使用一个私有卷的 SELiunx 标记来重新标记;其次它表明以读写挂载。

启动容器之后:

[root@rhel7-host ~]# ls -Z /srv/named/etc/named.conf 
-rw-r-----. root 25 system_u:object_r:svirt_sandbox_file_t:s0:c821,c956 /srv/named/etc/named.conf
  • 注2: VIM 备份行为能改变 inode

如果你在本地主机中使用 vim 来编辑配置文件,而你没有看到容器中的改变,你可能不经意的创建了容器感知不到的新文件。在编辑时,有三种 vim 设定影响备份副本:backupwritebackupbackupcopy

我摘录了 RHEL 7 中的来自官方 VIM backup\_table 中的默认配置。

backup    writebackup
off      on backup current file, deleted afterwards (default)

所以我们不创建残留下的 ~ 副本,而是创建备份。另外的设定是 backupcopyauto 是默认的设置:

"yes" make a copy of the file and overwrite the original one
"no" rename the file and write a new one
"auto" one of the previous, what works best

这种组合设定意味着当你编辑一个文件时,除非 vim 有理由(请查看文档了解其逻辑),你将会得到一个包含你编辑内容的新文件,当你保存时它会重命名为原先的文件。这意味着这个文件获得了新的 inode。对于大多数情况,这不是问题,但是这里容器的 绑定挂载 bind mount 对 inode 的改变很敏感。为了解决这个问题,你需要改变 backupcopy 的行为。

不管是在 vim 会话中还是在你的 .vimrc中,请添加 set backupcopy=yes。这将确保原先的文件被清空并覆写,维持了 inode 不变并且将该改变传递到了容器中。


via: http://rhelblog.redhat.com/2017/07/19/containing-system-services-in-red-hat-enterprise-linux-part-1/

作者:Matt Micene 译者:liuxinyu123 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出