HankChow's Blog


  • 首页

  • 归档

  • 关于

  • 标签

  • 搜索

如何处理删除文件后磁盘空间未被释放的问题

发表于 2019-05-05
字数统计: 264 | 阅读时长 ≈ 1

在磁盘空间即将耗尽的时候,需要清理一些文件。在找到需要删除的文件之后,如果直接使用 rm 删除文件,可能会发现删除文件后磁盘空间并没有被清理出来的情况。

这是由于刚刚删除的文件还在被某个进程打开,导致几时文件看起来被删除了,但仍然是打开状态,因此仍然会占用磁盘空间。

如果需要彻底清理出磁盘空间,可以通过 lsof | grep -i delete 查看这种“在打开状态下被删除了”的文件,会显示出这个文件正在被哪个进程所打开,只需要将进程 kill 掉,对应的文件空间就被释放出来了。

但如果由于某些原因,不能终止某个进程,还可以采用另一个折中一点的办法。使用 echo > foo(需要删除的文件) 将文件内容置空,同样可以达到清理磁盘空间的目标。

在 Bash 中使用[方括号](二)

发表于 2019-04-22
字数统计: 1.4k | 阅读时长 ≈ 5

square brackets

我们继续来看方括号的用法,它们甚至还可以在 Bash 当中作为一个命令使用。

欢迎回到我们的方括号专题。在前一篇文章当中,我们介绍了方括号在命令行中可以用于通配操作,如果你已经读过前一篇文章,就可以从这里继续了。

方括号还可以以一个命令的形式使用,就像这样:

1
[ "a" = "a" ]

上面这种 [ ... ] 的形式就可以看成是一个可执行的命令。要注意,方括号内部的内容 "a" = "a" 和方括号 [、] 之间是有空格隔开的。因为这里的方括号被视作一个命令,因此要用空格将命令和它的参数隔开。

上面这个命令的含义是“判断字符串 "a" 和字符串 "a" 是否相同”,如果判断结果为真,那么 [ ... ] 就会以状态码status code 0 退出,否则以状态码 1 退出。在之前的文章中,我们也有介绍过状态码的概念,可以通过 $? 变量获取到最近一个命令的状态码。

分别执行

1
2
[ "a" = "a" ]
echo $?

以及

1
2
[ "a" = "b" ]
echo $?

这两段命令中,前者会输出 0(判断结果为真),后者则会输出 1(判断结果为假)。在 Bash 当中,如果一个命令的状态码是 0,表示这个命令正常执行完成并退出,而且其中没有出现错误,对应布尔值 true;如果在命令执行过程中出现错误,就会返回一个非零的状态码,对应布尔值 false。而 [ ... ] 也同样遵循这样的规则。

因此,[ ... ] 很适合在 if ... then、while 或 until 这种在代码块结束前需要判断是否达到某个条件结构中使用。

对应使用的逻辑判断运算符也相当直观:

1
2
3
4
5
6
7
8
9
[ STRING1 = STRING2 ] => 检查字符串是否相等
[ STRING1 != STRING2 ] => 检查字符串是否不相等
[ INTEGER1 -eq INTEGER2 ] => 检查整数 INTEGER1 是否等于 INTEGER2
[ INTEGER1 -ge INTEGER2 ] => 检查整数 INTEGER1 是否大于等于 INTEGER2
[ INTEGER1 -gt INTEGER2 ] => 检查整数 INTEGER1 是否大于 INTEGER2
[ INTEGER1 -le INTEGER2 ] => 检查整数 INTEGER1 是否小于等于 INTEGER2
[ INTEGER1 -lt INTEGER2 ] => 检查整数 INTEGER1 是否小于 INTEGER2
[ INTEGER1 -ne INTEGER2 ] => 检查整数 INTEGER1 是否不等于 INTEGER2
等等……

方括号的这种用法也可以很有 shell 风格,例如通过带上 -f 参数可以判断某个文件是否存在:

1
2
3
4
5
6
7
8
9
10
for i in {000..099}; \
do \
if [ -f file$i ]; \
then \
echo file$i exists; \
else \
touch file$i; \
echo I made file$i; \
fi; \
done

如果你在上一篇文章使用到的测试目录中运行以上这串命令,其中的第 3 行会判断那几十个文件当中的某个文件是否存在。如果文件存在,会输出一条提示信息;如果文件不存在,就会把对应的文件创建出来。最终,这个目录中会完整存在从 file000 到 file099 这一百个文件。

上面这段命令还可以写得更加简洁:

1
2
3
4
5
6
7
8
for i in {000..099};\
do\
if [ ! -f file$i ];\
then\
touch file$i;\
echo I made file$i;\
fi;\
done

其中 ! 运算符表示将判断结果取反,因此第 3 行的含义就是“如果文件 file$i 不存在”。

可以尝试一下将测试目录中那几十个文件随意删除几个,然后运行上面的命令,你就可以看到它是如何把被删除的文件重新创建出来的。

除了 -f 之外,还有很多有用的参数。-d 参数可以判断某个目录是否存在,-h 参数可以判断某个文件是不是一个符号链接。可以用 -G 参数判断某个文件是否属于某个用户组,用 -ot 参数判断某个文件的最后更新时间是否早于另一个文件,甚至还可以判断某个文件是否为空文件。

运行下面的几条命令,可以向几个文件中写入一些内容:

1
2
3
echo "Hello World" >> file023
echo "This is a message" >> file065
echo "To humanity" >> file010

然后运行:

1
2
3
4
5
6
7
8
for i in {000..099};\
do\
if [ ! -s file$i ];\
then\
rm file$i;\
echo I removed file$i;\
fi;\
done

你就会发现所有空文件都被删除了,只剩下少数几个非空的文件。

如果你还想了解更多别的参数,可以执行 man test 来查看 test 命令的 man 手册(test 是 [ ... ] 的命令别名)。

有时候你还会看到 [[ ... ]] 这种双方括号的形式,使用起来和单方括号差别不大。但双方括号支持的比较运算符更加丰富:例如可以使用 == 来判断某个字符串是否符合某个模式pattern,也可以使用 <、> 来判断两个字符串的出现顺序。

可以在 Bash 表达式文档中了解到双方括号支持的更多运算符。

下一集

在下一篇文章中,我们会开始介绍圆括号 () 在 Linux 命令行中的用法,敬请关注!

更多

  • Linux 工具:点的含义
  • 理解 Bash 中的尖括号
  • Bash 中尖括号的更多用法
  • Linux 中的 &
  • Bash 中的 & 符号和文件描述符
  • Bash 中的逻辑和(&)
  • 浅析 Bash 中的 {花括号}
  • 在 Bash 中使用[方括号] (一)

via: https://www.linux.com/blog/learn/2019/4/using-square-brackets-bash-part-2

在 Bash 中使用[方括号] (一)

发表于 2019-04-13
字数统计: 1k | 阅读时长 ≈ 3

square brackets

这篇文章将要介绍方括号及其在命令行中的不同用法。

看完花括号在命令行中的用法之后,现在我们继续来看方括号([])在上下文中是如何发挥作用的。

通配

方括号最简单的用法就是通配。你可能在知道“Globbing”这个概念之前就已经通过通配来匹配内容了,列出具有相同特征的多个文件就是一个很常见的场景,例如列出所有 JPEG 文件:

1
ls *.jpg

使用通配符wildcard来得到符合某个模式的所有内容,这个过程就叫通配。

在上面的例子当中,星号(*)就代表“0 个或多个字符”。除此以外,还有代表“有且仅有一个字符”的问号(?)。因此

1
ls d*k*

可以列出 darkly 和 ducky,而且 dark 和 duck 也是可以被列出的,因为 * 可以匹配 0 个字符。而

1
ls d*k?

则只能列出 ducky,不会列出 darkly、dark 和 duck。

方括号也可以用于通配。为了便于演示,可以创建一个用于测试的目录,并在这个目录下创建文件:

1
touch file0{0..9}{0..9}

(如果你还不清楚上面这个命令的原理,可以看一下另一篇介绍花括号的文章)

执行上面这个命令之后,就会创建 file000、file001、……、file099 这 100 个文件。

如果要列出这些文件当中第二位数字是 7 或 8 的文件,可以执行:

1
ls file0[78]?

如果要列出 file022、file027、file028、file052、file057、file058、file092、file097、file098,可以执行:

1
ls file0[259][278]

当然,不仅仅是 ls,很多其它的命令行工具都可以使用方括号来进行通配操作。但在删除文件、移动文件、复制文件的过程中使用通配,你需要有一点横向思维。

例如将 file010 到 file029 这 30 个文件复制成 archive010 到 archive029 这 30 个副本,不可以这样执行:

1
cp file0[12]? archive0[12]?

因为通配只能针对已有的文件,而 archive 开头的文件并不存在,不能进行通配。

而这条命令

1
cp file0[12]? archive0[1..2][0..9]

也同样不行,因为 cp 并不允许将多个文件复制到多个文件。在复制多个文件的情况下,只能将多个文件复制到一个指定的目录下:

1
2
mkdir archive
cp file0[12]? archive

这条命令是可以正常运行的,但它只会把这 30 个文件以同样的名称复制到 archive/ 目录下,而这并不是我们想要的效果。

如果你阅读过我关于花括号的文章,你大概会记得可以使用 % 来截掉字符串的末尾部分,而使用 # 则可以截掉字符串的开头部分。

例如:

1
2
myvar="Hello World"
echo Goodbye Cruel ${myvar#Hello}

就会输出 Goodbye Cruel World,因为 #Hello 将 myvar 变量中开头的 Hello 去掉了。

在通配的过程中,也可以使用这一个技巧。

1
2
3
4
for i in file0[12]?;\
do\
cp $i archive${i#file};\
done

上面的第一行命令告诉 Bash 需要对所有 file01 开头或者 file02 开头,且后面只跟一个任意字符的文件进行操作,第二行的 do 和第四行的 done 代表需要对这些文件都执行这一块中的命令。

第三行就是实际的复制操作了,这里使用了两次 $i 变量:第一次在 cp 命令中直接作为源文件的文件名使用,第二次则是截掉文件名开头的 file 部分,然后在开头补上一个 archive,也就是这样:

1
"archive" + "file019" - "file" = "archive019"

最终整个 cp 命令展开为:

1
cp file019 archive019

最后,顺带说明一下反斜杠 \ 的作用是将一条长命令拆分成多行,这样可以方便阅读。

在下一节,我们会了解方括号的更多用法,敬请关注。


via: https://www.linux.com/blog/2019/3/using-square-brackets-bash-part-1

10 个 Python 图像编辑工具

发表于 2019-04-03
字数统计: 2.1k | 阅读时长 ≈ 8

以下提到的这些 Python 工具在编辑图像、操作图像底层数据方面都提供了简单直接的方法。

当今的世界充满了数据,而图像数据就是其中很重要的一部分。但只有经过处理和分析,提高图像的质量,从中提取出有效地信息,才能利用到这些图像数据。

常见的图像处理操作包括显示图像,基本的图像操作,如裁剪、翻转、旋转;图像的分割、分类、特征提取;图像恢复;以及图像识别等等。Python 作为一种日益风靡的科学编程语言,是这些图像处理操作的最佳选择。同时,在 Python 生态当中也有很多可以免费使用的优秀的图像处理工具。

下文将介绍 10 个可以用于图像处理任务的 Python 库,它们在编辑图像、查看图像底层数据方面都提供了简单直接的方法。

1、scikit-image

scikit-image 是一个结合 NumPy 数组使用的开源 Python 工具,它实现了可用于研究、教育、工业应用的算法和应用程序。即使是对于刚刚接触 Python 生态圈的新手来说,它也是一个在使用上足够简单的库。同时它的代码质量也很高,因为它是由一个活跃的志愿者社区开发的,并且通过了同行评审peer review。

资源

scikit-image 的文档非常完善,其中包含了丰富的用例。

示例

可以通过导入 skimage 使用,大部分的功能都可以在它的子模块中找到。

图像滤波image filtering:

1
2
3
4
5
6
7
8
import matplotlib.pyplot as plt
%matplotlib inline

from skimage import data,filters

image = data.coins() # ... or any other NumPy array!
edges = filters.sobel(image)
plt.imshow(edges, cmap='gray')

Image filtering in scikit-image

使用 match_template() 方法实现模板匹配template matching:

Template matching in scikit-image

在展示页面可以看到更多相关的例子。

2、NumPy

NumPy 提供了对数组的支持,是 Python 编程的一个核心库。图像的本质其实也是一个包含像素数据点的标准 NumPy 数组,因此可以通过一些基本的 NumPy 操作(例如切片、掩膜mask、花式索引fancy indexing等),就可以从像素级别对图像进行编辑。通过 NumPy 数组存储的图像也可以被 skimage 加载并使用 matplotlib 显示。

资源

在 NumPy 的官方文档中提供了完整的代码文档和资源列表。

示例

使用 NumPy 对图像进行掩膜mask操作:

1
2
3
4
5
6
7
8
9
10
11
12
import numpy as np
from skimage import data
import matplotlib.pyplot as plt
%matplotlib inline

image = data.camera()
type(image)
numpy.ndarray #Image is a NumPy array:

mask = image < 87
image[mask]=255
plt.imshow(image, cmap='gray')

NumPy

3、SciPy

像 NumPy 一样,SciPy 是 Python 的一个核心科学计算模块,也可以用于图像的基本操作和处理。尤其是 SciPy v1.1.0 中的 scipy.ndimage 子模块,它提供了在 n 维 NumPy 数组上的运行的函数。SciPy 目前还提供了线性和非线性滤波linear and non-linear filtering、二值形态学binary morphology、B 样条插值B-spline interpolation、对象测量object measurements等方面的函数。

资源

在官方文档中可以查阅到 scipy.ndimage 的完整函数列表。

示例

使用 SciPy 的高斯滤波对图像进行模糊处理:

1
2
3
4
5
6
7
8
from scipy import misc,ndimage

face = misc.face()
blurred_face = ndimage.gaussian_filter(face, sigma=3)
very_blurred = ndimage.gaussian_filter(face, sigma=5)

#Results
plt.imshow(<image to be displayed>)

Using a Gaussian filter in SciPy

4、PIL/Pillow

PIL (Python Imaging Library) 是一个免费 Python 编程库,它提供了对多种格式图像文件的打开、编辑、保存的支持。但在 2009 年之后 PIL 就停止发布新版本了。幸运的是,还有一个 PIL 的积极开发的分支 Pillow,它的安装过程比 PIL 更加简单,支持大部分主流的操作系统,并且还支持 Python 3。Pillow 包含了图像的基础处理功能,包括像素点操作、使用内置卷积内核进行滤波、颜色空间转换等等。

资源

Pillow 的官方文档提供了 Pillow 的安装说明自己代码库中每一个模块的示例。

示例

使用 Pillow 中的 ImageFilter 模块实现图像增强:

1
2
3
4
5
6
7
8
9
from PIL import Image,ImageFilter
#Read image
im = Image.open('image.jpg')
#Display image
im.show()

from PIL import ImageEnhance
enh = ImageEnhance.Contrast(im)
enh.enhance(1.8).show("30% more contrast")

Enhancing an image in Pillow using ImageFilter

  • 源码

5、OpenCV-Python

OpenCV(Open Source Computer Vision 库)是计算机视觉领域最广泛使用的库之一,OpenCV-Python 则是 OpenCV 的 Python API。OpenCV-Python 的运行速度很快,这归功于它使用 C/C++ 编写的后台代码,同时由于它使用了 Python 进行封装,因此调用和部署的难度也不大。这些优点让 OpenCV-Python 成为了计算密集型计算机视觉应用程序的一个不错的选择。

资源

入门之前最好先阅读 OpenCV2-Python-Guide 这份文档。

示例

使用 OpenCV-Python 中的金字塔融合Pyramid Blending将苹果和橘子融合到一起:

Image blending using Pyramids in OpenCV-Python

  • 源码

6、SimpleCV

SimpleCV 是一个开源的计算机视觉框架。它支持包括 OpenCV 在内的一些高性能计算机视觉库,同时不需要去了解位深度bit depth、文件格式、色彩空间color space之类的概念,因此 SimpleCV 的学习曲线要比 OpenCV 平缓得多,正如它的口号所说,“将计算机视觉变得更简单”。SimpleCV 的优点还有:

  • 即使是刚刚接触计算机视觉的程序员也可以通过 SimpleCV 来实现一些简易的计算机视觉测试
  • 录像、视频文件、图像、视频流都在支持范围内

资源

官方文档简单易懂,同时也附有大量的学习用例。

示例

SimpleCV

7、Mahotas

Mahotas 是另一个 Python 图像处理和计算机视觉库。在图像处理方面,它支持滤波和形态学相关的操作;在计算机视觉方面,它也支持特征计算feature computation、兴趣点检测interest point detection、局部描述符local descriptors等功能。Mahotas 的接口使用了 Python 进行编写,因此适合快速开发,而算法使用 C++ 实现,并针对速度进行了优化。Mahotas 尽可能做到代码量少和依赖项少,因此它的运算速度非常快。可以参考官方文档了解更多详细信息。

资源

文档包含了安装介绍、示例以及一些 Mahotas 的入门教程。

示例

Mahotas 力求使用少量的代码来实现功能。例如这个 Finding Wally 游戏:

Finding Wally problem in Mahotas

Finding Wally problem in Mahotas

  • 源码

8、SimpleITK

ITK(Insight Segmentation and Registration Toolkit)是一个为开发者提供普适性图像分析功能的开源、跨平台工具套件,SimpleITK 则是基于 ITK 构建出来的一个简化层,旨在促进 ITK 在快速原型设计、教育、解释语言中的应用。SimpleITK 作为一个图像分析工具包,它也带有大量的组件,可以支持常规的滤波、图像分割、图像配准registration功能。尽管 SimpleITK 使用 C++ 编写,但它也支持包括 Python 在内的大部分编程语言。

资源

有很多 Jupyter Notebooks 用例可以展示 SimpleITK 在教育和科研领域中的应用,通过这些用例可以看到如何使用 Python 和 R 利用 SimpleITK 来实现交互式图像分析。

示例

使用 Python + SimpleITK 实现的 CT/MR 图像配准过程:

SimpleITK animation

  • 源码

9、pgmagick

pgmagick 是使用 Python 封装的 GraphicsMagick 库。GraphicsMagick 通常被认为是图像处理界的瑞士军刀,因为它强大而又高效的工具包支持对多达 88 种主流格式图像文件的读写操作,包括 DPX、GIF、JPEG、JPEG-2000、PNG、PDF、PNM、TIFF 等等。

资源

pgmagick 的 GitHub 仓库中有相关的安装说明、依赖列表,以及详细的使用指引。

示例

图像缩放:

Image scaling in pgmagick

  • 源码

边缘提取:

Edge extraction in pgmagick

  • 源码

10、Pycairo

Cairo 是一个用于绘制矢量图的二维图形库,而 Pycairo 是用于 Cairo 的一组 Python 绑定。矢量图的优点在于做大小缩放的过程中不会丢失图像的清晰度。使用 Pycairo 可以在 Python 中调用 Cairo 的相关命令。

资源

Pycairo 的 GitHub 仓库提供了关于安装和使用的详细说明,以及一份简要介绍 Pycairo 的入门指南。

示例

使用 Pycairo 绘制线段、基本图形、径向渐变radial gradients:

Pycairo

  • 源码

总结

以上就是 Python 中的一些有用的图像处理库,无论你有没有听说过、有没有使用过,都值得试用一下并了解它们。


via: https://opensource.com/article/19/3/python-image-manipulation-tools

如何打造更小巧的容器镜像

发表于 2019-03-27
字数统计: 4.5k | 阅读时长 ≈ 16

五种优化 Linux 容器大小和构建更小的镜像的方法。

Docker 近几年的爆炸性发展让大家逐渐了解到容器和容器镜像的概念。尽管 Linux 容器技术在很早之前就已经出现,但这项技术近来的蓬勃发展却还是要归功于 Docker 对用户友好的命令行界面以及使用 Dockerfile 格式轻松构建镜像的方式。纵然 Docker 大大降低了入门容器技术的难度,但构建一个兼具功能强大、体积小巧的容器镜像的过程中,有很多技巧需要了解。

第一步:清理不必要的文件

这一步和在普通服务器上清理文件没有太大的区别,而且要清理得更加仔细。一个小体积的容器镜像在传输方面有很大的优势,同时,在磁盘上存储不必要的数据的多个副本也是对资源的一种浪费。因此,这些技术对于容器来说应该比有大量专用内存的服务器更加需要。

清理容器镜像中的缓存文件可以有效缩小镜像体积。下面的对比是使用 dnf 安装 Nginx 构建的镜像,分别是清理和没有清理 yum 缓存文件的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Dockerfile with cache
FROM fedora:28
LABEL maintainer Chris Collins <collins.christopher@gmail.com>

RUN dnf install -y nginx

-----

# Dockerfile w/o cache
FROM fedora:28
LABEL maintainer Chris Collins <collins.christopher@gmail.com>

RUN dnf install -y nginx \
&& dnf clean all \
&& rm -rf /var/cache/yum

-----

[chris@krang] $ docker build -t cache -f Dockerfile .
[chris@krang] $ docker images --format "{{.Repository}}: {{.Size}}"
| head -n 1
cache: 464 MB

[chris@krang] $ docker build -t no-cache -f Dockerfile-wo-cache .
[chris@krang] $ docker images --format "{{.Repository}}: {{.Size}}" | head -n 1
no-cache: 271 MB

从上面的结果来看,清理缓存文件的效果相当显著。和清除了元数据和缓存文件的容器镜像相比,不清除的镜像体积接近前者的两倍。除此以外,包管理器缓存文件、Ruby gem 的临时文件、nodejs 缓存文件,甚至是下载的源码 tarball 最好都全部清理掉。

层:一个潜在的隐患

很不幸(当你往下读,你会发现这是不幸中的万幸),根据容器中的层的概念,不能简单地向 Dockerfile 中写一句 RUN rm -rf /var/cache/yum 就完事儿了。因为 Dockerfile 的每一条命令都以一个层的形式存储,并一层层地叠加。所以,如果你是这样写的:

1
2
3
RUN dnf install -y nginx
RUN dnf clean all
RUN rm -rf /var/cache/yum

你的容器镜像就会包含三层,而 RUN dnf install -y nginx 这一层仍然会保留着那些缓存文件,然后在另外两层中被移除。但缓存实际上仍然是存在的,当你把一个文件系统挂载在另外一个文件系统之上时,文件仍然在那里,只不过你见不到也访问不到它们而已。

在上一节的示例中,你会看到正确的做法是将几条命令链接起来,在产生缓存文件的同一条 Dockerfile 指令里把缓存文件清理掉:

1
2
3
RUN dnf install -y nginx \
&& dnf clean all \
&& rm -rf /var/cache/yum

这样就把几条命令连成了一条命令,在最终的镜像中只占用一个层。这样只会浪费一点缓存的好处,稍微多耗费一点点构建容器镜像的时间,但被清理掉的缓存文件就不会留存在最终的镜像中了。作为一个折衷方法,只需要把一些相关的命令(例如 yum install 和 yum clean all、下载文件、解压文件、移除 tarball 等等)连接成一个命令,就可以在最终的容器镜像中节省出大量体积,你也能够利用 Docker 的缓存加快开发速度。

层还有一个更隐蔽的特性。每一层都记录了文件的更改,这里的更改并不仅仅已有的文件累加起来,而是包括文件属性在内的所有更改。因此即使是对文件使用了 chmod 操作也会被在新的层创建文件的副本。

下面是一次 docker images 命令的输出内容。其中容器镜像 layer_test_1 是在 CentOS 基础镜像中增加了一个 1GB 大小的文件后构建出来的镜像,而容器镜像 layer_test_2 是使用了 FROM layer_test_1 语句创建出来的,除了执行一条 chmod u+x 命令没有做任何改变。

1
2
layer_test_2        latest       e11b5e58e2fc           7 seconds ago           2.35 GB
layer_test_1 latest 6eca792a4ebe 2 minutes ago 1.27 GB

如你所见,layer_test_2 镜像比 layer_test_1 镜像大了 1GB 以上。尽管事实上 layer_test_1 只是 layer_test_2 的前一层,但隐藏在这第二层中有一个额外的 1GB 的文件。在构建容器镜像的过程中,如果在单独一层中进行移动、更改、删除文件,都会出现类似的结果。

专用镜像和公用镜像

有这么一个亲身经历:我们部门重度依赖于 Ruby on Rails,于是我们开始使用容器。一开始我们就建立了一个正式的 Ruby 的基础镜像供所有的团队使用,为了简单起见(以及在“这就是我们自己在服务器上瞎鼓捣的”想法的指导下),我们使用 rbenv 将 Ruby 最新的 4 个版本都安装到了这个镜像当中,目的是让开发人员只用这个单一的镜像就可以将使用不同版本 Ruby 的应用程序迁移到容器中。我们当时还认为这是一个虽然非常大但兼容性相当好的镜像,因为这个镜像可以同时满足各个团队的使用。

实际上这是费力不讨好的。如果维护独立的、版本略微不同的镜像中,可以很轻松地实现镜像的自动化维护。同时,选择特定版本的特定镜像,还有助于在引入破坏性改变,在应用程序接近生命周期结束前提前做好预防措施,以免产生不可控的后果。庞大的公用镜像也会对资源造成浪费,当我们后来将这个庞大的镜像按照 Ruby 版本进行拆分之后,我们最终得到了共享一个基础镜像的多个镜像,如果它们都放在一个服务器上,会额外多占用一点空间,但是要比安装了多个版本的巨型镜像要小得多。

这个例子也不是说构建一个灵活的镜像是没用的,但仅对于这个例子来说,从一个公共镜像创建根据用途而构建的镜像最终将节省存储资源和维护成本,而在受益于公共基础镜像的好处的同时,每个团队也能够根据需要来做定制化的配置。

从零开始:将你需要的内容添加到空白镜像中

有一些和 Dockerfile 一样易用的工具可以轻松创建非常小的兼容 Docker 的容器镜像,这些镜像甚至不需要包含一个完整的操作系统,就可以像标准的 Docker 基础镜像一样小。

我曾经写过一篇关于 Buildah 的文章,我想在这里再一次推荐一下这个工具。因为它足够的灵活,可以使用宿主机上的工具来操作一个空白镜像并安装打包好的应用程序,而且这些工具不会被包含到镜像当中。

Buildah 取代了 docker build 命令。可以使用 Buildah 将容器的文件系统挂载到宿主机上并进行交互。

下面来使用 Buildah 实现上文中 Nginx 的例子(现在忽略了缓存的处理):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env bash
set -o errexit

# Create a container
container=$(buildah from scratch)

# Mount the container filesystem
mountpoint=$(buildah mount $container)

# Install a basic filesystem and minimal set of packages, and nginx
dnf install --installroot $mountpoint --releasever 28 glibc-minimal-langpack nginx --setopt install_weak_deps=false -y

# Save the container to an image
buildah commit --format docker $container nginx

# Cleanup
buildah unmount $container

# Push the image to the Docker daemon’s storage
buildah push nginx:latest docker-daemon:nginx:latest

你会发现这里使用的已经不再是 Dockerfile 了,而是普通的 Bash 脚本,而且是从框架(或空白)镜像开始构建的。上面这段 Bash 脚本将容器的根文件系统挂载到了宿主机上,然后使用宿主机的命令来安装应用程序,这样的话就不需要把软件包管理器放置到容器镜像中了。

这样所有无关的内容(基础镜像之外的部分,例如 dnf)就不再会包含在镜像中了。在这个例子当中,构建出来的镜像大小只有 304 MB,比使用 Dockerfile 构建的镜像减少了 100 MB 以上。

1
2
[chris@krang] $ docker images |grep nginx
docker.io/nginx buildah 2505d3597457 4 minutes ago 304 MB

注:这个镜像是使用上面的构建脚本构建的,镜像名称中前缀的 docker.io 只是在推送到镜像仓库时加上的。

对于一个 300MB 级别的容器基础镜像来说,能缩小 100MB 已经是很显著的节省了。使用软件包管理器来安装 Nginx 会带来大量的依赖项,如果能够使用宿主机直接从源代码对应用程序进行编译然后构建到容器镜像中,节省出来的空间还可以更多,因为这个时候可以精细的选用必要的依赖项,非必要的依赖项一概不构建到镜像中。

Tom Sweeney 有一篇文章《用 Buildah 构建更小的容器》,如果你想在这方面做深入的优化,不妨参考一下。

通过 Buildah 可以构建一个不包含完整操作系统和代码编译工具的容器镜像,大幅缩减了容器镜像的体积。对于某些类型的镜像,我们可以进一步采用这种方式,创建一个只包含应用程序本身的镜像。

使用静态链接的二进制文件来构建镜像

按照这个思路,我们甚至可以更进一步舍弃容器内部的管理和构建工具。例如,如果我们足够专业,不需要在容器中进行排错调试,是不是可以不要 Bash 了?是不是可以不要 GNU 核心套件了?是不是可以不要 Linux 基础文件系统了?如果你使用的编译型语言支持静态链接库,将应用程序所需要的所有库和函数都编译成二进制文件,那么程序所需要的函数和库都可以复制和存储在二进制文件本身里面。

这种做法在 Golang 社区中已经十分常见,下面我们使用由 Go 语言编写的应用程序进行展示:

以下这个 Dockerfile 基于 golang:1.8 镜像构建一个小的 Hello World 应用程序镜像:

1
2
3
4
5
6
7
8
9
10
11
12
FROM golang:1.8

ENV GOOS=linux
ENV appdir=/go/src/gohelloworld

COPY ./ /go/src/goHelloWorld
WORKDIR /go/src/goHelloWorld

RUN go get
RUN go build -o /goHelloWorld -a

CMD ["/goHelloWorld"]

构建出来的镜像中包含了二进制文件、源代码以及基础镜像层,一共 716MB。但对于应用程序运行唯一必要的只有编译后的二进制文件,其余内容在镜像中都是多余的。

如果在编译的时候通过指定参数 CGO_ENABLED=0 来禁用 cgo,就可以在编译二进制文件的时候忽略某些函数的 C 语言库:

1
GOOS=linux CGO_ENABLED=0 go build -a goHelloWorld.go

编译出来的二进制文件可以加到一个空白(或框架)镜像:

1
2
3
FROM scratch
COPY goHelloWorld /
CMD ["/goHelloWorld"]

来看一下两次构建的镜像对比:

1
2
3
4
[ chris@krang ] $ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
goHello scratch a5881650d6e9 13 seconds ago 1.55 MB
goHello builder 980290a100db 14 seconds ago 716 MB

从镜像体积来说简直是天差地别了。基于 golang:1.8 镜像构建出来带有 goHelloWorld 二进制的镜像(带有 builder 标签)体积是基于空白镜像构建的只包含该二进制文件的镜像的 460 倍!后者的整个镜像大小只有 1.55MB,也就是说,有 713MB 的数据都是非必要的。

正如上面提到的,这种缩减镜像体积的方式在 Golang 社区非常流行,因此不乏这方面的文章。Kelsey Hightower 有一篇文章专门介绍了如何处理这些库的依赖关系。

压缩镜像层

除了前面几节中讲到的将多个命令链接成一个命令的技巧,还可以对镜像进行压缩。镜像压缩的实质是导出它,删除掉镜像构建过程中的所有中间层,然后保存镜像的当前状态为单个镜像层。这样可以进一步将镜像缩小到更小的体积。

在 Docker 1.13 之前,压缩镜像层的的过程可能比较麻烦,需要用到 docker-squash 之类的工具来导出容器的内容并重新导入成一个单层的镜像。但 Docker 在 Docker 1.13 中引入了 --squash 参数,可以在构建过程中实现同样的功能:

1
2
3
4
5
6
7
8
9
10
FROM fedora:28
LABEL maintainer Chris Collins <collins.christopher@gmail.com>

RUN dnf install -y nginx
RUN dnf clean all
RUN rm -rf /var/cache/yum

[chris@krang] $ docker build -t squash -f Dockerfile-squash --squash .
[chris@krang] $ docker images --format "{{.Repository}}: {{.Size}}" | head -n 1
squash: 271 MB

通过这种方式使用 Dockerfile 构建出来的镜像有 271MB 大小,和上面连接多条命令的方案构建出来的镜像体积一样,因此这个方案也是有效的,但也有一个潜在的问题,而且是另一种问题。

“什么?还有另外的问题?”

好吧,有点像以前一样的问题,以另一种方式引发了问题。

过头了:过度压缩、太小太专用了

容器镜像之间可以共享镜像层。基础镜像或许大小上有几 Mb,但它只需要拉取/存储一次,并且每个镜像都能复用它。所有共享基础镜像的实际镜像大小是基础镜像层加上每个特定改变的层的差异内容,因此,如果有数千个基于同一个基础镜像的容器镜像,其体积之和也有可能只比一个基础镜像大不了多少。

因此,这就是过度使用压缩或专用镜像层的缺点。将不同镜像压缩成单个镜像层,各个容器镜像之间就没有可以共享的镜像层了,每个容器镜像都会占有单独的体积。如果你只需要维护少数几个容器镜像来运行很多容器,这个问题可以忽略不计;但如果你要维护的容器镜像很多,从长远来看,就会耗费大量的存储空间。

回顾上面 Nginx 压缩的例子,我们能看出来这种情况并不是什么大的问题。在这个镜像中,有 Fedora 操作系统和 Nginx 应用程序,没有缓存,并且已经被压缩。但我们一般不会使用一个原始的 Nginx,而是会修改配置文件,以及引入其它代码或应用程序来配合 Nginx 使用,而要做到这些,Dockerfile 就变得更加复杂了。

如果使用普通的镜像构建方式,构建出来的容器镜像就会带有 Fedora 操作系统的镜像层、一个安装了 Nginx 的镜像层(带或不带缓存)、为 Nginx 作自定义配置的其它多个镜像层,而如果有其它容器镜像需要用到 Fedora 或者 Nginx,就可以复用这个容器镜像的前两层。

1
2
3
[   App 1 Layer (  5 MB) ]          [   App 2 Layer (6 MB) ]
[ Nginx Layer ( 21 MB) ] ------------------^
[ Fedora Layer (249 MB) ]

如果使用压缩镜像层的构建方式,Fedora 操作系统会和 Nginx 以及其它配置内容都被压缩到同一层里面,如果有其它容器镜像需要使用到 Fedora,就必须重新引入 Fedora 基础镜像,这样每个容器镜像都会额外增加 249MB 的大小。

1
[ Fedora + Nginx + App 1 (275 MB)]      [ Fedora + Nginx + App 2 (276 MB) ]

当你构建了大量在功能上趋于分化的的小型容器镜像时,这个问题就会暴露出来了。

就像生活中的每一件事一样,关键是要做到适度。根据镜像层的实现原理,如果一个容器镜像变得越小、越专用化,就越难和其它容器镜像共享基础的镜像层,这样反而带来不好的效果。

对于仅在基础镜像上做微小变动构建出来的多个容器镜像,可以考虑共享基础镜像层。如上所述,一个镜像层本身会带有一定的体积,但只要存在于镜像仓库中,就可以被其它容器镜像复用。这种情况下,数千个镜像也许要比单个镜像占用更少的空间。

1
2
3
[ specific app   ]      [ specific app 2 ]
[ customizations ]--------------^
[ base layer ]

一个容器镜像变得越小、越专用化,就越难和其它容器镜像共享基础的镜像层,最终会不必要地占用越来越多的存储空间。

1
[ specific app 1 ]     [ specific app 2 ]      [ specific app 3 ]

总结

减少处理容器镜像时所需的存储空间和带宽的方法有很多,其中最直接的方法就是减小容器镜像本身的大小。在使用容器的过程中,要经常留意容器镜像是否体积过大,根据不同的情况采用上述提到的清理缓存、压缩到一层、将二进制文件加入在空白镜像中等不同的方法,将容器镜像的体积缩减到一个有效的大小。


via: https://opensource.com/article/18/7/building-container-images

12…17
HankChow

HankChow

84 日志
74 标签
0%
© 2019 HankChow | Site words total count: 111.3k