diff --git a/containerize-everything/attachments/tissue-in-docker.png b/containerize-everything/attachments/tissue-in-docker.png new file mode 100644 index 0000000..a026b70 Binary files /dev/null and b/containerize-everything/attachments/tissue-in-docker.png differ diff --git a/containerize-everything/index.md b/containerize-everything/index.md index 26cdf48..f14cbc9 100644 --- a/containerize-everything/index.md +++ b/containerize-everything/index.md @@ -20,26 +20,14 @@ 容器一般意义上是指一个应用与它所需的运行环境的组合。对于没有环境依赖的、使用二进制可执行文件打包的应用来说,容器可以仅仅是它本身;对于一个 Next.js 项目来说,容器可以是 Node.js 运行环境、其在生产环境下所需要的依赖与其构建之后得到的针对生产环境优化的 JS 代码;对于一个 Python 项目来说,容器可以是 Python 环境、其所需的依赖与其的源代码。 -但容器的概念也并不绝对,广义上的容器可以说是使用容器技术封装的一切东西。一些项目会将更多的东西打包进容器,例如 GitLab 会将整套运行环境打包进去,而像是 Docker-OSX 这种项目则更是利用容器实现了整套的 Hackintosh 。也有项目会将一些资源数据打包成容器,再在需要使用的时候映射出来使用。 +但容器的概念也并不绝对,广义上的容器可以说是使用容器技术封装的一切东西。一些项目会将更多的东西打包进容器,例如 GitLab 会将整套运行环境打包进去,而像是 Docker-OSX 这种项目则更是利用容器实现了整套的 Hackintosh 。 -为了避免概念上的混乱,我们这里就还是讲一般意义上的容器。 +为了避免概念上的混乱,我们这里就还是讲一般意义上用于封装单个应用程序的容器。 ## 开始使用容器 我们以 Docker 这个容器解决方案为例,逐步开始了解容器相关的内容。 -::: danger root 权限 - -如果您在 Linux 环境下运行今天讲的 Docker 内容,那么您可能需要 root 权限。这不是一个安全的做法。如果您担心可能会存在安全隐患,您可能需要创建一个虚拟机来完成今天的内容。 - -Docker 也提供了 rootless 的运行模式,但我暂时还没有尝试过它。未来我会进一步了解更多内容。 - -请**尽可能避免**在生产环境下使用 root 权限。 - -在 Windows 和 MacOS 环境下, Docker Desktop 会自行管理相关需要的权限。可以不用在意它。 - -::: - ::: details 安装 Docker Docker 官网提供了安装 Docker 的完整流程说明。针对图形界面环境他们提供了一个 GUI 工具 Docker Desktop 方便使用,而对于 Linux 服务器这种命令行环境则直接安装引擎即可。 @@ -54,6 +42,18 @@ bash <(curl -L -s https://get.docker.com) ::: +::: danger root 权限 + +如果您在 Linux 环境下运行今天讲的 Docker 内容,那么您可能需要 root 权限。这不是一个安全的做法。如果您担心可能会存在安全隐患,您可能需要创建一个虚拟机来完成今天的内容。 + +Docker 也提供了 rootless 的运行模式,但我暂时还没有尝试过它。未来我会进一步了解更多内容。 + +请**尽可能避免**在生产环境下使用 root 权限。 + +在 Windows 和 MacOS 环境下, Docker Desktop 会自行管理相关需要的权限。可以不用在意它。 + +::: + ### 容器与镜像 如果您安装过操作系统,那么您一定知道镜像是指什么:用于解开得到目标操作系统实例的通用资源。 @@ -462,8 +462,20 @@ services: - "127.0.0.1:1323:1323" ``` +具体的指令也与直接 docker 运行差不多,例如: + +- 拉取所有镜像: `docker compose pull` +- 构建镜像(如果有服务指定了 `build` 字段的话): `docker compose build` +- 创建并启动: `docker compose up -d` ( `-d` 参数表示启动后在后台运行) +- 停止: `docker compose stop` +- 启动: `docker compose start` +- 重启: `docker compose restart` +- 销毁: `docker compose down` (如果有创建卷映射的话,可以指定 `-v` 参数来销毁它们) + 您可能会觉得它有点简陋——对于这样的小项目来说, compose 并不能发挥它的全部功效。那么,我们来展示一个我们可能会在 Day 7 里用到的*稍微有点*复杂的 compose 文件(可以先猜猜功能是什么?): +{#挑战传送门} + ```yaml version: '3.9' services: @@ -546,7 +558,7 @@ networks: 有时,我们会遇到一个容器的性能不足以支撑起逐渐增长的业务需求的场景。除了挑战让人头秃的多线程编程之外,有没有什么简单易行的性能提升方式呢? -多启动几个容器怎么样?如果我们的程序是无状态的(即运算与存储分离,程序本身不存储状态,而是通过外部的组件如 Redis 或其他数据库来存储信息),那么我们可以通过简单的水平扩展来快速利用系统资源,提升业务性能。 +多启动几个容器怎么样?如果我们的程序是无状态的(即运算与存储分离,程序本身不存储状态,而是通过外部的组件如 Redis 或 Postgres 来存储信息),那么我们可以通过简单的水平扩展来快速利用系统资源,提升业务性能。 通过 docker compose 中对应部分的定义,我们可以快速地实现这个需求。只需要在对应服务的声明部分加上这样的设置就可以: @@ -583,7 +595,33 @@ services: memory: 32M // [!code ++] ``` -更多的 `deploy` 字段相关的用法可以参见 [Compose Deploy Specification] ,这里就不更多展开了。 +如果这些容器都需要监听端口,我们可以开放多个连续的端口给这些容器使用。就像这样: + +```yaml +version: "3.9" +services: + some-service: + ... + ports: + - "127.0.0.1:3000:3000" // [!code --] + - "127.0.0.1:3001-3008:3000" // [!code ++] + deploy: + mode: replicated + replicas: 8 + resources: + limits: + cpus: '0.60' + memory: 128M + reservations: + cpus: '0.15' + memory: 32M +``` + +我们会在 Day 7 的内容中讲到负载均衡相关的内容。 + +唯一值得注意的是,在启动容器时这些端口会被随机分配给不同后缀的容器,并不意味着 3001 端口就一定对应着 1 号容器。虽然这样可能会让强迫症感到不开心,但它实际上不会影响任何功能——只要容器之间都是等价的就可以。 + +更多的 `deploy` 字段相关的用法可以参见 [Compose Deploy Specification] ,这里就不多展开了。 [Compose Deploy Specification]: https://docs.docker.com/compose/compose-file/deploy/ @@ -593,25 +631,43 @@ services: ### 好处 +容器化有不少好处,例如: + #### 方便管理 使用容器的一大好处是方便管理。不用刻意去记哪些服务是手动启动的、哪些服务是系统自动启动的,也不需要在 system log 里为了出错的条目拼命翻找,当不确定哪些容器正在运行、哪些容器已经停止的时候,最次也不过是 `docker ps -a` 一下的事,更不用说市面上有不少图形化的管理工具可以进一步优化相关流程。 在使用 docker 之前,我比较常用的 systemd 之外的服务进程管理工具是 pm2 。它也曾伴我经过了不少时光,但它的开源版本仅仅拥有非常精简的核心功能,稍微想要进阶一些就需要购买他们的商业服务 pm2+ ,这让我感到有些膈应。也有听说过使用 Supervisor 的,但我并没有了解太多。而在曾经使用~~拖库塔~~管理服务器的蛮荒年代,也使用过面板自带的一些管理功能,但随着个中原因最后发现还是手搓脚本和命令行来得方便(真香)。 +而在开发的时候,对于不同的项目设置不同的依赖 compose 容器组,能快速在不同的数据环境中切换,这让开发工作变得更加迅速流畅。 + #### 跨平台支持 容器的另一大好处就是可以无视平台支持,只考虑指令集就行。无论运行在 Windows 还是 Linux 上,只要是同样指令集(如 AMD64 )的,那通通可以用同样的镜像,具体的平台间差异则由容器引擎自己来拉平。妈妈再也不怕我写的程序 Only works on my machine 啦。 +包括对于一些依赖繁复或是构建流程复杂的项目来说也是如此。只要能提供一个有效的 Docker 容器,无论流程再怎么繁杂,一切都可以被快速稳定地复现出来(网络问题除外),那么终端用户就不需要和冗长的说明文档打架,直接启动一个容器——一切均已就绪。 + +::: tip 妙妙用法:新瓶装旧酒 + +还记得上面提到的那个 Docker-OSX 吗?既然 OSX 都可以被装进 Docker ,那些维护价格高昂但性能早已惨不忍睹的上古系统呢? + +我印象里听说过这个用法(忘了是调侃还是个新闻),但想去搜却没搜到。仔细想了想好像也不太对:何必要折腾打包成 Docker 呢,直接把硬盘数据 dump 出来封个虚拟机是不是会更好一点?以及 Docker 不能解决指令集不兼容的问题,有些老系统没法迁移的原因是没有兼容指令集的新硬件了,于是只能花高价修修补补,也不敢运行在转译层上怕万一出点问题就完蛋。这也不是 Docker 能解决的。 + +或许是我记错了,总之我先在这里记一笔。当作个无稽之谈看看也好吧。 + +::: + #### 安全隔离 -这就要提到我之前经历过的一次小事故了。起初是监控系统告警服务状态异常,打开网页看到服务在爆 500 ,一路排查发现是 Redis 容器在不停重启导致服务错误。再检查 Redis 日志,发现它在尝试往系统核心层写入数据(恶意代码),但因为权限不够所以触发了 panic ,在容器挂了之后再被引擎的重启策略拉起来,于是不停重复这个仰卧起坐的过程。虽然最后发现是面板自带的防火墙参数没法应用到 Docker 网络层上,导致看似会被防火墙阻挡连接但其实并没有生效(配置完后也没有测试),因而还是庆幸使用了容器没有酿成更大的后果。 +这就要提到我之前经历过的一次小事故了。起初是监控系统告警服务状态异常,打开网页看到服务在爆 500 ,一路排查发现是 Redis 容器在不停重启导致服务错误。再检查 Redis 日志,发现它在尝试往系统核心层写入数据(恶意代码),但因为权限不够所以触发了 panic ,在容器挂了之后再被引擎的重启策略拉起来,于是不停重复这个仰卧起坐的过程。最后发现是面板自带的防火墙参数没法应用到 Docker 网络层上,导致看似会被防火墙阻挡连接但其实并没有生效(配置完后也没有测试),因而还是庆幸使用了容器没有酿成更大的后果。 -容器里的环境与宿主机是隔离的,只能通过一些给定的权限进行通讯;对于一些没必要开放的端口,还是应该尽可能把访问权限往小限制。至于防火墙相关的设置问题,这还是应该在设置完成后再仔细检查一遍,避免因为疏忽导致再有类似的事件发生。 +容器里的环境与宿主机是隔离的,只能通过一些给定的权限进行通讯;对于一些没必要开放的端口,还是应该尽可能把访问权限往小限制。如果使用的是传统部署方案的话,说不准系统就已经被攻破了。至于防火墙设置相关的问题,这还是应该在按流程走完后再仔细检查一遍,避免因为疏忽导致再有类似的事件发生。 以及,**别在晚上干精细活**。人累的时候脑子一糊涂做出来什么鬼东西就完全不知道了。 -### 坏处 +### 不足 + +那么古尔丹,代价是什么呢? #### 高级权限 @@ -643,30 +699,125 @@ ProxmoxVE 也是一个非常好用的虚拟化解决方案,用它开出来 LXC 我们知道,包的层数越多,那么性能损失越严重,这是无法避免的问题。 Docker 也是如此,虽然因它损失的性能比起带来的好处来说基本可以忽略不计,但它确确实实会带来性能损失(主要是网络处理的 I/O 方面)。 -如果您有兴趣,或许可以参考这篇来自 IBM Research 的研究报告: [An Updated Performance Comparison of Virtual Machines and Linux Containers] 。虽然已经是十年前的报告,如今的日新月异的技术发展让相关的内容早已完全不同,但它依然可以作为一份较为权威的参考资料。 +如果您有兴趣,或许可以参考这篇来自 IBM Research 的研究报告: [An Updated Performance Comparison of Virtual Machines and Linux Containers] 。虽然已经是十年前的报告,如今的日新月异的技术发展让相关的内容早已完全不同,但它依然可以作为一份较为权威的资料来参考,其中的测试方法也值得拿来做复现实验使用。 [An Updated Performance Comparison of Virtual Machines and Linux Containers]: https://dominoweb.draco.res.ibm.com/reports/rc25482.pdf #### 基础组件更新时容器重启 -这个其实是我个人的痛点。因为 Docker 算是一个类似虚拟化的层级,那么在更新它(尤其是它的底层运行环境 containerd )的时候势必会让运行在它上面的容器都不得不重启。这对于非业务关键型应用来说倒是还好,重启了也就重启了;对于生产环境的一些业务关键型应用(尤其是有些启动还贼慢的东西),这个只能用噩梦的来形容。 +这个其实是我个人的痛点。因为 Docker 算是一个类似虚拟化的层级,那么在更新它(尤其是它的底层运行环境)的时候势必会让运行在它上面的容器都不得不重启。这对于非业务关键型应用来说倒是还好,重启了也就重启了;对于生产环境的一些业务关键型应用(尤其是有些启动还贼慢的东西),这个只能用噩梦的来形容。 要么在夜深人静的时候悄悄更新一把,希望没人发现(并且千万别更新爆炸!半夜真的很不适合干活),要么就只能早早贴出更新公告,期望着用户不要生气,然后期望着能正常更新完千万别出什么幺蛾子吧。 (买包乖乖放机箱上不知道会不会有用) -### 妙妙用法:新瓶装旧酒 +## 大规模的容器化 -还记得上面提到的那个 Docker-OSX 吗?既然 OSX 都可以被装进 Docker ,那些维护价格高昂但性能早已惨不忍睹的上古系统呢? +如果您接触过现代化运维相关的概念,那么您一定不会对 `Kubernetes (k8s)` 感到陌生。这是一个大型的容器化应用管理系统,旨在为多地域、多节点的复杂部署环境提供一个高可用性、高扩展性的解决方案。~~虽然现在不少提供商将它简单封装之后便一股脑把所有东西塞在一起其实有违其设计的初衷便是了。~~ -我印象里听说过这个用法(忘了是调侃还是个新闻),但想去搜却没搜到。仔细想了想好像也不太对:何必要折腾打包成 Docker 呢,直接把硬盘数据 dump 出来封个虚拟机是不是会更好一点?以及 Docker 不能解决指令集不兼容的问题,有些老系统没法迁移的原因是没有兼容指令集的新硬件了,于是只能花高价修修补补,也不敢运行在转译层上怕万一出点问题就完蛋。这也不是 Docker 能解决的。 +简单来说就是,它能把一群服务器抽象成一个大型的容器系统,让运行在上面的容器能自由灵活地活动,允许一部分节点出现异常之后依然保持系统的正常运转。它非常适用于大型系统的部署,而对于小规模部署的场景来说它有些资源浪费——它的设计里为了容错会加入很多冗余机制,如果只是小规模部署的话,可能其本身系统占用的资源就会超过其上所有服务的总和。 -或许是我记错了,总之我先在这里记一笔。当作个无稽之谈看看也好吧。 +那么,什么时候需要用到它呢?对于我自己部署的项目来说,目前的 docker compose 部署项目已经足够,加上一些存储和备份也足以应付数据冗余和灾备相关的需求。但在工作中,因为经常接触规模较大的项目,所以我们的业务是部署在 k8s 上的,我也需要不断接触并学习与 k8s 相关的一些概念。如果不知道从哪里入手的话,可以试试 `k3s` 这个轻量级的解决方案,它的使用和管理与完整形态的 k8s 很接近,但相对来说更加简单一些,不需要考虑控制面板与运算节点分离之类的复杂需求,完全交给它自己来管理就可以。在熟悉了 docker 相关的操作之后,可以把它当作是一种 DLC ,多了一些新的概念,也加入了不少强大的功能。 -## 大规模的容器化 +## 今日总结 -K8s +灵活利用容器能让我们的开发与生产工作变得流畅而优雅。但这也意味着我们必须承担与之对应的一些损失与风险。**永远选择最适合自己的**,而不是最多人追捧的方案。 -## 今日总结 +- 例如,如果您的项目非常需要把握每一丝一毫的性能(例如一些嵌入式开发),那么 docker 就是减分项,因为虚拟化会带来更多不必要的开销。 +- 或者,比如您需要运行一大堆复杂的系统,并且随时可能面临着服务在不同设备间的迁移的需求,那么使用 compose 甚至 k8s 来组织项目,或许会是一个比在服务器上把东西放得到处都是、在迁移的时候会不自觉地忘记一些导致数据意外丢失(我经历过)更好的选择。 + +还有一件事,不要在深夜干活。 + +::: details 一个玩笑 + +Docker 可以封装任何东西,~~甚至是餐巾纸~~ + +![Docker 里的餐巾纸](./attachments/tissue-in-docker.png) + +::: ## 课后挑战 + +今天的课后挑战就是分析[那个巨大的 docker-compose.yml 文件](#挑战传送门) 。先不要偷看答案哦! + +::: details 参考解析 + +我们可以从网络为入口来分析这个 docker compose 文件。 + +先看 networks 字段。可以看到这个项目使用了两个网络, `internal_network` 和 `external_network` ,其中 `internal_network` 限制为内部网络——意味着如果某个容器在这个网络中,那么它是没有外部网络访问权限的。 + +然后我们再来看 services 。一共有 `prometheus` `influxdb` `grafana` `grafana-db` `caddy` 这五个容器。 + +- `grafana` **depends_on** `grafana-db` :这意味着只有在 grafana-db 启动后, grafana 才会启动。 +- `influxdb` 和 `grafana-db` 只有 internal_network :这意味着这两个容器没法直接与外界网络交互。 +- `caddy` 容器暴露了 443 端口:这意味着它能接受来自外部的请求。 + +那么,具体到每个容器,它们都做了些什么呢? + +- `prometheus` 容器映射了两个卷,分别是: + + | 宿主机 | 容器 | 只读模式 | + | ------------------- | --------------- | ------------------ | + | ./config/prometheus | /etc/prometheus | :white_check_mark: | + | ./data/prometheus | /prometheus | | + +- `influxdb` 容器也映射了两个卷: + + | 宿主机 | 容器 | + | ---------------------------- | ------------------------- | + | ./config/influxdb/config.yml | /etc/influxdb2/config.yml | + | ./data/influxdb | /var/lib/influxdb2 | + +- `grafana` 容器映射了一个卷: + + | 宿主机 | 容器 | + | -------------------- | ------------------------ | + | ./config/grafana.ini | /etc/grafana/grafana.ini | + +- `grafana-db` 容器也映射了一个卷: + + | 宿主机 | 容器 | + | --------- | ------------------------ | + | ./data/db | /var/lib/postgresql/data | + + 同时,它还指定了这些环境变量: + + | 变量名称 | 变量值 | + | -------------------- | --------------------------------------------------- | + | POSTGRES_USER | grafana | + | POSTGRES_PASSWORD | password | + | POSTGRES_DB | grafana | + | POSTGRES_INITDB_ARGS | "--encoding='UTF8' --lc-collate='C' --lc-ctype='C'" | + + 同时,它还指定了一个每 `30s` 执行一次,超时时间为 `20s` ,最多重试 `3` 次的健康检查指令: + + ```sh + pg_isready -U grafana -d grafana + ``` + +- `caddy` 容器映射了好多卷: + + | 宿主机 | 容器 | 只读模式 | + | ------------------- | -------------------- | ------------------ | + | ./config/Caddyfile | /etc/caddy/Caddyfile | | + | ./ssl | /ssl | :white_check_mark: | + | ./data/caddy/data | /data | | + | ./data/caddy/config | /config | | + +并且,每一个容器的重启策略都是 `always` ,即在遇到错误或 docker 环境重启后,也都会自动重新启动。 + +那么,聪明的您猜到这个 docker compose 项目是做什么用的了吗? + +不卖关子了,这个是我用来监测设备和容器运行状态使用的组合。 + +- prometheus 因为是主动发出采集请求,所以它需要外网访问权限; grafana 需要 SSO 登录所以也需要; caddy 是整个组合的流量入口,所以也需要外网权限。其他两个容器就不需要外网权限了。 +- influxdb 是接收发送进来的请求数据,所以直接让 caddy 接收之后反向代理给它就可以。 +- grafana 的数据存储在 grafana-db ,所以它需要依赖 grafana-db 启动之后才能启动。 +- 映射的一大堆卷主要是配置和数据的存储,因为是 docker compose 项目,所以可以用相对路径的写法。 +- grafana-db 容器的环境变量就是它的配置:它只需要在初始启动的时候使用配置初始化数据库就可以。健康检查是为了检查容器是否健康,而其他组件的健康检查很多我用的是一个外部的检查系统来执行。 + +至于整套系统的完整用法,我会在 Day 7 的对应部分再继续详细解释。今天就先这样吧。 + +对了,最后再补充一个细节:在 docker compose 里写卷映射的时候,它不知道宿主机上的卷应该是文件还是目录。如果需要是一个文件而不是目录,那么请在使用 `docker compose up -d` 启动前先手动创建好它,由 docker compose 来创建的话默认是会创建成一个目录的。 + +:::