Docker 的诅咒:曾以为它是终极解法,最后却是“罪大恶极”?

系统管理中的基础问题

打包软件一直是系统管理中的一大基础问题。它非常重要,对系统的使用方式有着巨大影响,甚至让包管理器成为区分操作系统的一项重要指标。

以 Windows 为例:在很多“Linux 派”眼中,这款操作系统最不讨喜的就是缺乏包管理机制,微软先后多次尝试引入这个概念也都没能得到广泛认可。相比之下,Linux 世界中各发行版间的主要区别,就集中在其管理软件 repo 的方式上。我所指的不只是 dpkb 和 rpm 之间的差异,而是在强调更底层的设计,例如硬性指定还是上游配置、稳定 repo 还是滚动发布。拿 RHEL 和 Arch 来说,二者虽然共享绝大多数实现,但管理风格却截然不同。

大多数 Linux 发行版都会明确强调某种软件应该如何打包(甚至规定了多久打包一次),而且这些系统也都有着一项共通理念:将依赖项集中起来。应该把库声明为依赖项,并把所依赖的包安装在公共位置以供链接器使用。但这也可能带来挑战,因为不同的软件往往依赖于不同的库版本,而各版本之间可能并不兼容。这也是在维护 Linux 发行版时最典型的老大难问题:如何提供能够良好协作的软件 repo 版本。像 RHEL 这类稳定发行版的一大优势,就是它们在这方面表现得相当可靠;至于缺点,就是这种可靠性其实是通过拒绝大部分新版本软件来实现的。

由于需要提供大量可以相互兼容的软件版本,并确保发行版遵守各种构建规范(例如支持自由软件等开发理念、以及配置文件布局等具体规则),所以向 Linux 发行版引入新软件时往往极度麻烦且繁琐。对于软件维护人员来说,他们需要面对一大堆有着特定构建和配置差异的旧版本,并想办法把它们塞进发行版中去。而在发行版和软件包维护者这边,则需要全盘考虑各种上游软件是否符合发行版策略,并解决版本和依赖项问题。虽然行业中已经出台了一系列相关规范,但具体操作仍然令人头痛,庞大的工作量也几近疯狂。

这就形成了一种双输的诡异局面:希望自己的软件能够广泛传播的开发者必须忍受发行版的怪癖,而想要壮大自身软件生态的发行版也得顺应开发者。每个人都不开心,每个人都很疲惫。

有问题,自然有人尝试解决。业界已经在这方面做出了各种探索,但也正是因为方法多种多样,所以至今没有真正出现一种能够统领全局的解决方案。在桌面环境中,常见的软件分发选项有 Flatpak、Snap 和 AppImage 等。这些系统的镜像或应用程序能够将软件及其依赖项共同打包,提供一套完整的独立环境,从而保证在任何发行版上都能正常工作。

但在实际使用时,我自己曾不止一次对 flatpak 文件进行逆向工程以修改其中的依赖项,所以说上述工具的宣传效果在日常应用时并不一定能发挥作用。但公平地讲,软件在与各种要素的交互过程中,难免会出现意料之外的状况——比如运行时无法正确将各种要素彼此隔离开来。视频技术栈就是个典型,我们往往需要删除或替换掉包中错误的 OpenGL 库,才能使其跟特定图形驱动程序共同运行。

尽管如此,我还是承认上述工具的运行效果不错,甚至值得进一步扩展并普遍使用。它们所依托的桌面应用形式主要强调与用户交互,并通过自身界面接收用户发来的配置选项。尽管在与文件系统交互时仍然不够顺畅丝滑,但这种将交互界面限制在 GUI 形式内的作法确实让沙箱变得既可行、又极具现实意义。

需要强调的是,本文不会对沙箱问题做过多延伸。沙箱是一项重要的安全和稳定性保障技术,但这里讨论的主要是软件打包和分发问题。毕竟沙箱软件也可以通过更传统的方式进行分发,在其现代外壳之下的打包机制其实远没有人们想象的那么先进。

让我无法忍受的是什么?

总而言之,我想抱怨的其实是服务器端的软件运行体系。这里的软件打包方案基本只有一个:Docker。当然,人们偶尔也会使用 Podman 等兼容性的工具选项。

Docker 的出现被广泛视为服务器运营最佳实践的里程碑事件。尽管 Docker 是一种软件分发方式,但其最初似乎主要是为了将容器编排引入大规模可扩展环境。但最终随着不断发展和思想融合,Docker 成为一种面向开发者和单节点用例的常见软件分发方式。

现如今,Docker 也成为 Linux 上最常见的服务器端软件分发选项。而我,对它恨之入骨。

千万别误会,我在这里要批评的并不是容器技术本身。容器化非常精妙、有着诸多优点,虽然未必像炒作中说的那样全面碾压轻量化虚拟机,但它的亮点还是非常突出。我不太确定 Docker 帮助节约的时间有没有超过对它的管理成本,但公平地讲,身为一名 DevOps 顾问,实践经验告诉我正确运行 Docker 镜像并不算特别麻烦。

真正让我无法忍受的,就是在非 DevOps 环境中盲目使用 Docker 镜像。镜像需要集中规划和管理,也就是说 Docker 应该是向用户分发软件时的最小公共集,应该是种最低的保障性选项。而每当看到开源服务器端软件以 Docker 镜像的形式提交过来,甚至是更糟糕的 Docker Compose 栈时,我的第一反应就是愤怒。跟传统 Linux 软件包分发、或者使用源代码直接构建的软件相比,Docker 镜像总是需要耗费更多时间才能正常起效。

很多朋友可能不太理解,Docker 不是把所有元素都独立隔离开来,降低了部署难度吗?确实,但接下来我打算聊几个常见问题,也就是“Docker 不适用的情况”。

 配置

Docker 在分发中最大的问题之一,就是缺少统一的配置约定。

绝大多数服务器端 Linux 软件需要读取文本文件来获取配置,这种古老的方式当然有自己的问题……但至少它有着统一的框架和准则。

可 Docker 镜像就不同了。

如果大家听说过 12 因素应用原则,就会意识到 Docker 镜像的最佳配置方式应该是通过环境变量。这样做的好处在于,启动容器时可以在命令行上轻松实现;至于缺点,就是环境变量不太适合传递结构化数据,而且由于大多需要通过 shell 脚本进行交互,这些脚本在处理长值或复杂值也显得比较笨拙。

DevOps 环境中使用的许多 Docker 镜像确实会从环境变量中获取配置,但出于前面这些现实问题,它们往往要通过避免复杂配置(例如假设 TLS 将被「他方」终止)或者控制信息获取量来实现。而配置内容,则来自网络上的数据库或服务。

不过对于大多数最终用户软件来说,其配置过于复杂或冗长,单靠环境变量根本无法容纳。这时他们就只能求助于配置文件,即以某种方式将配置文件纳入容器的文件系统当中。Docker 倒是提供了多种操作执行方式,于是不同软件包的文档会根据相关推荐而有所区别,甚至经常触发关于所有权和权限的警告。

更糟糕的是,很多 Docker 镜像还试图通过提供某种入口点 shell 脚本来降低配置难度,这些脚本负责向容器提供更简单的文档以生成完整配置。这种抽象级别在实践中往往缺少相应的记录痕迹,这就让故障排查变得更加困难。

我自己就无数次经历过软件无法正常启动的“惊喜”,原因就是脚本引用了一些配置中未提供的键,导致我们必须查阅 Docker 镜像构建说明和入口点脚本来反推它的启动过程。这好吗?这一点也不好。

最要命的是,很多配置入口点脚本还有自己的设计倾向性。别看“设计倾向性”这词好像比较中性,但它的真实含义就是“除了开发者自己,别人都弄不明白”。我至少有十几次都被迫自己构建 Docker 镜像版本,来替换掉那些没有公开底层软件参数的入口点脚本。

更夸张的是,某些 Docker 镜像甚至根本不提供任何说明文档。用户必须深入研究、四下探寻,才能找出当前软件所使用的配置文件的具体位置。我认为 Docker 镜像至少也得给出最基本的 README 信息,帮助我们了解打包的软件到底应该如何配置。

 文件系统

Docker 提供的沙箱或者说隔离机制肯定是个优点,但这也代表着它必然有着一切沙箱所面临的共同问题。

沙箱隔离机制跟 Linux 的文件系统兼容性很差,哪怕不涉及 UID 行为、单纯在 Docker Compose 栈中使用命名分卷就足以引发意外。在需要使用虚拟容器跟命名分卷中的文件进行交互时,比如执行备份之类的日常操作(都不说故障排查这类更复杂的需求),结果都很可能让人头痛欲裂。随着时间推移,命名分卷得到了大幅改进,但看似简单的操作在不同 Docker 版本之间仍经常出现奇怪的冲突,更不用说还得考虑如何兼容 Podman 等其他工具了。

当然,UID 也有自己的问题。Docker 的一大原罪,就是要求以 root 身份运行软件。

没错,Docker 确实提供一定程度的隔离,但从纵深防御的角度来看,以 root 身份公开运行用户操作仍然不是明智之举。为此,我需要频繁重构软件来适应 Docker 的这种怪癖,而且整个过程相当复杂。而且在稍微繁琐的环境中使用 Docker,都有很大概率引发涉及 UID 分配的 NFS 难题。使用命名分卷当然能缓解这些问题,但分卷本身又有自己的痛点,简直是要人老命。

 容器可移植性很差

对于强调分布式特性的 Docker(特别是 Docker Compose)来说,最讽刺的还在于很多常规实践会大大影响其可移植性。

在 Docker Compose 中对网络执行的任何非默认操作,都有可能导致其在网络设置比较复杂的计算机上无法正常工作。很多 Docker Compose 栈都会将那些众所周知的端口默认为可供侦听器使用。它们还会启用各种底层软件功能,又不提供禁用的方法,甚至用到很多在具体环境中并不可用的通用值。

就个人而言,最让我恼火的就是 TLS。前文已经提到,我认为 Docker 容器不应该终止掉 TLS。只有接受 TLS 连接,才允许访问私钥信息。尽管长达 90 天的临时 TLS 证书和普遍懈怠的安全意识已经破坏了保障能力,但我仍然认为私钥信息应该受到严密保护。这部分信息只应存储在一个位置,且只能由一名主体进行访问。当然,能不能实现这种安全破坏还在其次,很多用户可能根本就搞不定 TLS 配置。

不少自行托管软件的朋友都会选择 SNI 或者虚拟托管的方式,其中往往存在涉及多个子域的通用证书。这一切最好都能在少量、甚至是单一专用点上处理。而一旦遇到在构建中假设在各个点上单独处理 TLS 的 Docker 镜像,可就倒了大霉了。即使完全不考虑 TLS,我个人也永远不想把 Docker 应用容器直接暴露在互联网上,在前面部署反向代理才是正确的选择。

此外,Docker Compose 栈还总想要使用 ACME 为最终用户软件颁发自有证书,我们得深入研究说明文档才能搞清如何禁用这一行为。

 单一用途计算设备

我前面说的这些问题在业余人士开发的软件中最为常见,我现在脑海中就浮现了 HomeAssistant 和 Nextcloud 这两个例子。请别误会,我所说的“业余”并不质疑软件本身,而是在强调普通用户的使用习惯。

遗憾的是,由于 Raspberry Pi 设备的价格越来越低廉,很多业余爱好者失去了那份严谨态度。可能我说的有点夸张,但他们在专用硬件上运行的“自托管”软件包已经多到了荒谬的程度。在我看来,软件名称中带有“pi”基本就是个危险信号,代表着开发者“没考虑过在共享设备上运行需要做哪些改动”。大家可以说是我太守旧,但我认为计算设备就不该只能执行一项任务,特别是那些需要 24/7 全天候开机的设备。

HomeAsistant 可能就是其中最大的罪魁祸首,我自己就在一台设备上通过 Docker 运行它,还有其他几款应用程序。但 HomeAsistantr 明显不想跟其他软件共存,每次更新后都会弹出“检测到不支持软件”的提醒。这也太过分了,有必要管得这么宽吗?

后来我决定尝试一下 Nextcloud,大概花了两个小时想让打包的 Docker 镜像在自己的环境上正常运行。最终,我决定放弃并转为手动安装,结果发现它就是一款平平无奇的 PHP 应用,跟十几年前的程序没有任何区别。这么简单的东西,你把它打包成 Docker 镜像干什么嘛?直接用 config.php 不好吗?

“罪大恶极,罄竹难书”

当然,大家可能觉得前面的问题都是操作细节,只要认真制作 Docker 镜像就能避免。没错,确实可以!也许 Docker 最大的问题就是它“门槛太低”了。创建 RPM 或者 Debian 软件包就有一定的技术难度,即使是经验丰富的开发者需要多次尝试才能让 rpmbuild 顺利运行起来(这里建议只用 copr 和 rpkg)。

我的抱怨在于,纯以 Docker 镜像的形式做分发,往往代表着开发者并没有对自己的项目投入足够的心力、或者至少没有专门做过项目分发设计。要想让自己的成果在各种非标环境中运行起来,就一定得预先考虑到可能出现的意外。

当然,大家应该能明白,我的用词只是种夸张和讽刺。我曾经把 Docker 誉为稳定可靠的终极解决方案,只是后来发现必须得有一定的配置经验和管理水平才能实现。

写在最后

当然,以上只是我的一点个人观点,相信很多朋友会抱有完全不同的意见——比如我坚信 Docker Compose 是容器时代最大的错误之一。

15 年前我曾写过一篇类似的文章,讲述自己在开发小型项目时在 RPM 中遇到的各种问题。Docker 最让我惊讶的一点,就是它能让项目发展到很大的规划、获得企业的广泛支持,同时还通过 Docker Compose 栈大大降低了分发的技术门槛。

我在前面提到的很多问题也确实源自这种“简单性”,因为很多项目根本就没有固定的分布式工程人员。面对不断变化的软件发展格局,他们只是想用廉价的单片机搭配上 Docker 容器技术,回避掉相对更麻烦的虚拟机镜像。当然,我也承认随着年纪愈长,我这人说话也越来越不中听了。

原文链接

https://computer.rip/2023-11-25-the-curse-of-docker.pdf

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/174642.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

docker 安装oracle 11,配置客户端远程连接

最近由于工作需要,oracle11数据库的导入导出,所以自己在电脑上模拟个数据库环境, 1.docker的安装,可以参考之前文档,也可以直接yum install 包名字安装 2.下载镜像 docker pull registry.cn-hangzhou.aliyuncs…

数据结构 | 堆排序

数据结构 | 堆排序 文章目录 数据结构 | 堆排序建立大堆排序结果以及全部代码 如果没有看过堆的实现的话可以先看前面的一章堆的实现,然后再来看这个堆排序,都是比较简单的~~ 这里堆排序首先建堆,建堆是要建小堆还是大堆呢? 在堆排…

案例029:基于微信小程序的阅读网站设计与实现

文末获取源码 开发语言:Java 框架:SSM JDK版本:JDK1.8 数据库:mysql 5.7 开发软件:eclipse/myeclipse/idea Maven包:Maven3.5.4 小程序框架:uniapp 小程序开发软件:HBuilder X 小程序…

鸿蒙应用开发-初见:ArkUI

编程范式:命令式->声明式 以一个卡片的实现做下讲解 命令式 简单讲就是需要开发用代码一步一步进行布局,这个过程需要开发全程参与。 Objective-C UIView *cardView [[UIView alloc] init]; cardView.backgroundColor [UIColor whiteColor]; ca…

Qt_一个由单例引发的崩溃

Qt_一个由单例引发的崩溃 文章目录 Qt_一个由单例引发的崩溃摘要关于 Q_GLOBAL_STATIC代码测试布局管理器源码分析Demo 验证关于布局管理器析构Qt 类声明周期探索更新代码获取父类分析Qt 单例宏源码 关键字: Qt、 Q_GLOBAL_STATIC、 单例、 UI、 崩溃 摘要 今…

R语言实操记录——R包无法安装,报错:Warning in system(cmd) : ‘make‘ not found

R语言 R语言实操记录——R包无法安装,报错:Warning in system(cmd) : ‘make‘ not found 文章目录 R语言一、起因二、具体步骤2.1、确认问题源2.2、安装RTools2.3、与R(/Rstudio)绑定2.4、验证可行性 三、疑惑 一、起因 R语言在包的安装上是真的方便&…

西北大学计算机844考研-23年计网计算题详细解析

西北大学计算机844考研-23年计网计算题详细解析 1.计算无传输差错状态下停止—等待ARQ协议效率,电磁波传播速率为2*10^8m/s,链路长为2000m,帧长度为1000比特,计算传输速率10kbps及10Mbps时的协议效率(即信道利用率) …

中低压MOSFET 2N7002KW 60V 300mA 双N通道 SOT-323封装

2N7002KW小电流双N通道MOSFET,电压60V电流300mA,采用SOT-323封装形式。超高密度电池设计,适用于极低的ros (on),具有导通电阻和最大直流电流能力,ESD保护。可应用于笔记本中的电源管理,电池供电系统等产品应…

外观设计模式

package com.jmj.pattern.facade;public class Light {public void on(){System.out.println("打开电灯...");}public void off(){System.out.println("关闭电灯...");} }package com.jmj.pattern.facade;public class AirCondition {public void on(){S…

R语言阶段复习一

创建一个长度为7的字符向量,元素为"A", "B", "C", "D", "E", "F", "G",并命名为vec1。 创建一个因子,包含6个水果:"apple", "banana"…

stack和queue

目录 1.什么是stack 2.容器适配器 3.stack的使用 top push pop 4.模拟实现stack 1.什么是stack 1. stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行 元素的插入与提取操作。(后进先出) 2. stack是作为容…

C语言:一个数如果恰好等于除它本身外的因子之和,这个数就称为完数。例如6=1+2+3。编程找出1000以内的所有完数。

分析: 在主函数 main 中,程序首先定义三个整型变量 m、s 和 i,并用于计算和判断完数。然后使用 printf 函数输出提示信息。 接下来,程序使用 for 循环结构,从 2 到 999 遍历所有的数。对于每个遍历到的数 m&#xff0c…

如何通过nginx进行反向代理

简单介绍 正向代理 正向代理服务器是一个位于客户端和原始服务器(origin server)之间的服务器,为了能够从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。正向…

Ubuntu+Tesla V100环境配置

系统基本信息 nvidia-smi’ nvidia-smi 470.182.03 driver version:470.182.03 cuda version: 11.4 查看系统体系结构 uname -aUTC 2023 x86_64 x86_64 x86_64 GNU/Linux 下载miniconda https://mirrors.tuna.tsinghua.edu.cn/anaconda/miniconda/?CM&OA https://mi…

redis报错3

INFO: Initializing SpringDispatcherServletdispatcherServlet

OpenCV快速入门:移动物体检测和目标跟踪

文章目录 前言一、移动物体检测和目标跟踪简介1.1 移动物体检测的基本概念1.2 移动物体检测算法的类型1.3 目标跟踪的基本概念1.4 目标跟踪算法的类型 二、差值法检测移动物体2.1 差值法原理2.2 差值法公式2.3 代码实现2.3.1 视频或摄像头检测移动物体2.3.2 随机动画生成的移动…

串口数据包收发的思路和流程-stm32入门

本节主要内容: 如何去规定一个合理的数据包格式如何收发数据包 1. 数据包格式规定/定义 1.1 HEX 数据包定义 固定包长,含包头包尾 可变包长,含包头包尾 首先数据包的作用是把一个个单独的数据给打包起来,方便我们进行多字节…

Java LeetCode篇-深入了解关于数组的经典解法

🔥博客主页: 【小扳_-CSDN博客】 ❤感谢大家点赞👍收藏⭐评论✍ 文章目录 1.0 轮转数组 1.1 使用移位的方式 1.2 使用三次数组逆转法 2.0 消失的数字 2.1 使用相减法 2.2 使用异或的方式 3.0 合并两个有序数组 3.1 使用三指针方式 3.2 使用合…

Spring Cache(缓存框架)

学习的最大理由是想摆脱平庸,早一天就多一份人生的精彩;迟一天就多一天平庸的困扰。各位小伙伴,如果您: 想系统/深入学习某技术知识点… 一个人摸索学习很难坚持,想组团高效学习… 想写博客但无从下手,急需…

vue 中 js 金额数字转中文

参考:js工具函数之数字转为中文数字和大写金额_js封装工具类函数金额大写-CSDN博客 我使用的框架vol.core。 客户需求要将录入框的金额数字转换成中文在旁边显示,换了几种函数,最终确定如下函数 function changeToChineseMoney(Num) {//判断…