容器之路(1)-认识容器

“容器”源起

提到容器这两个字,任何一个人都能举出一两个称得上容器的物件,例如烟灰缸,水果盘,容器简单来说就是用于承载其他物品的器皿。

那么容器怎么和计算机搭上边的呢?这里有一个非常好的类比:运输业。

想象在很久很久以前,物流、交通还没有像现在这样复杂繁忙,农民收获了粮食,将其装在布袋子中,果农则按照种植水果的不同,将其装在大小不一的篮子中,几乎每一种交易物都有自己不同类型的“盒子”。

如果要进行商品交易,农民则将自己的作物通过小车运输到车站,集中批发出售,批发商会用更大的盒子来容纳多个农民的作物,不同的作物用不同的盒子,最终再运输到目的地,分发给零售商。

整个过程虽然有序但并不高效,商品也可能在装卸及运输过程中发生损坏、丢失等问题。直到集装箱的出现,改变了这个行业。

集装箱我们经常会在高速路上看到,现在几乎任何的商品运输都会使用集装箱。

使用集装箱有很多好处,其中最重要的是标准化,无论是什么样的商品,都可以装在这个大箱子中,又因为大箱子有着国际认可的规格,无论是汽车、货船、还是火车均可以进行运输,集装箱在不同运输工具间切换时只需要用装卸机把集装箱挪个位子,非常快速。同时集装箱又有一定的安全性,在运输物品时坚固的集装箱可以有效保护其承载的货物,即使有多个集装箱同时被一个运输工具,例如货船进行运输,集装箱之间也是由隔离性,不会相互影响(废话,集装箱间怎么相互影响…)

计算机里的容器

再回到计算机领域,无论是软件开发者、发布者还是使用者,经常会被一件事困扰,软件怎么快速安全的交付给最终用户使用。

对于普通的面向个人软件而言,很多 app store 很好地解决了这个问题,开发者按照 app store 的规范开发应用,app store 审核并托管应用,使用者直接下载使用。

而对于企业应用,尤其是提供服务的应用(比如某宝后端的服务),问题就没这么好解决,因为一般企业应用很多均是定制化开发,又有很多知识产权信息,很难公开地放在受信任的平台上统一托管或交付。这时候大家只能用传统的手段:操作文档+二进制程序。这样的软件分发并没有什么大的问题,只是效率和安全性上没有太多保证。

但如果要考虑到软件的部署,这个问题就会变得比较复杂。因为几乎所有软件均是在指定的平台上开发,只能运行在指定的环境之上,环境稍有差异软件可能就无法运行起来,所以一般软件也会有兼容性手册,告诉最终用户将软件部署在什么样的环境上才能正常运行,但现实是并不是每个人都能完全按照指南完成,甚至于有些软件指南都是残缺不全的。那么如何让应用开发者看到的可以正常运行的软件与最终用户看到的一致,成了一个需要解决的问题。容器技术就是这个问题的最佳答案。

和前面讲的集装箱一样,计算机里的容器就是一种标准化技术,只要人人都按照容器标准来封装自己的软件(镜像封装,可理解为将商品装载到集装箱中),就能保证用户看到的和自己发布的一致,而且可以快速运行起来(基于收到的镜像运行程序,可理解为从集装箱中卸货)。

那么具体怎么做到的呢?

第一,封装。在容器的世界里,应用和其依赖的库文件、配置文件等均会被放在一个封闭的盒子里,这样就天然地屏蔽了容器与底层环境的依赖。这个架构特别类似于虚拟化,虚拟化可以屏蔽 OS 与底层硬件的依赖,使得 OS 可以较为标准化地跑在任意的 X86 物理服务器上,容器则是更深一层,屏蔽应用与 OS 的依赖。

第二,运行。单纯的软件打包意义并不大,让容器能在生产环境使用的最大价值,就是容器可以直接跑起来。容器封装后的形态叫 image,将 image 放在目标环境中,使用简单一条 run 命令即可以让容器跑起来。举个稍具体的例子,在没有容器时如果我想部署一个博客,那第一步是去官方网站下载二进制包,第二步是上传到我预先申请好的服务器上,第三步按照安装指南安装,第四部进行配置,第五步启动服务。有了容器之后,最简单的方式是,直接在预先申请好的服务器中 docker run xx,系统会自动下载指定的镜像并运行。

第三,隔离。和虚拟化类似,一个 OS 上可以运行多个容器的实例,那势必要保证多个容器之间不要相互影响,好在 OS 自己有很多技术来做隔离,例如 Linux namespace 就可以实现进程间的计算、网络、存储等隔离。

容器还有其他一些功能和好处,不知道是设计之初就具备还是逐渐演化的,现罗列如下:

快捷:前面说到一个 OS 可以运行多个容器实例,而每个容器说到底最终的形态就是进程,这使得容器的运行和操作系统上启停一个程序一样,无比快捷。

轻量:又因容器本身只包含应用二进制和其依赖的文件,相比传统的 VM 式的镜像封装整整省去了一个 OS 层,非常精简。而容器自身又有传统虚拟化的隔离功能、解耦合功能,这使得很多人会直接用“裸金属服务器+OS+容器”这一组合来替代“裸金属服务器+hypervisior+OS+应用二进制”。

版本更新:容器自身是松耦合架构,你可以将应用的配置文件挂在容器外面,这样就能实现应用二进制和配置的松耦合。这时候如果要进行软件版本升级,直接换个 image 就行。

分层文件系统:这是一个很好玩的设计,类似于叠罗汉,每次做容器镜像的构建时,变更的内容都会放在一个新的层里。最终一个容器镜像就是多个层合并的结果。对一般使用者而言,开源社区会做一些基础镜像,比如 Ubuntu18.04、Centos7.8,使用者可以基于这些基础镜像,叠罗汉一样把自己的应用程序加在这些镜像之上,封装成镜像后使用。

镜像仓库:顾名思义,存放容器镜像的地方。和普通的 app store 类似,在互联网上也有开放的容器镜像仓库,例如 Dockerhub,企业也可以搭建私有的镜像仓库,例如使用 Harbor 搭建。无论是哪种类型,只要是标准的镜像都可以进行存取。

有了以上种种技术后,企业应用的分发和运行就变成异常简单,开发者写好代码,通过容器封装成镜像,通过容器平台运行测试功能,测试完毕后将成品镜像传到镜像仓库或直接发送给使用者,使用者通过容器平台运行容器,业务上线。

所以容器的领头羊 Docker 有着这样一句口号:Build, Ship, Run, Any App Anywhere.

🍿5Ax01 Tanzu 1 容器基础1 - 图1

硬币的另一面

再好的东西,也会有背对的一面。谈完容器的优点,说说它的缺点。

第一、松耦合。松耦合的设计或许是无奈之举,前面提到容器会将应用的二进制、依赖、配置文件等都封装在一起,但封装之后需要变化的部分怎么办?比如开放的端口,使用的 IP,密码等,这些在不同环境中大概率是不同的。为了让容器能够支持“变量”,出现了很多概念。

比如数据卷,这就相当于用户的一个移动硬盘,用户可以存放自己专属的文件,而在这里容器本身就类似于公共电脑,大家都是共用的。听着挺美好,但为容器创建的数据卷一般都是空的,万一有些默认数据必须预先存在数据卷中怎么办?弄个初始化容器吧,专门用来把一些默认内容写在数据卷中。

又如容器的环境变量,应用的配置文件都是写死在容器里的,如何改变其中的内容呢?那就通过环境变量透传给容器,然后容器里有个脚本来读环境变量,读完后再修改容器里的配置文件吧。同时,有可能环境变量只会在容器第一次启动进行初始化时才需要,一旦初始化完成用户的配置都写到数据卷中去了,那还得有些判断的语句来决定是不是每次容器重启都要执行相关脚本。

再如网络,传统网络很好理解,说到隔离大家想到的肯定是 VLAN 隔离,物理隔离,在容器里就没这么简单了,多个容器都在同一个 OS 里,OS 一般也只有一个 IP 啊,容器里的网络隔离需要通过操作系统的一些特性来实现,比如 Linux Network Namespace,ok,隔离实现了,容器要通信怎么办?最常用的方式是“桥接”,即把容器的网卡连接到 OS 的网卡上,这样就能对外提供服务了。

你看,简单的一个松耦合,带来的就是一堆需要考虑的事情。

第二、开源。开源的世界有时候也挺无奈,还记得之前看油管上一位 Linux 爱好者举办的年度“Linux sucks”聚会,虽然大家都很热爱 Linux,但还是有不少每年都能吐槽的事情,其中一件就是分裂,就拿包封装来说,一个软件在不同的 Linux 发行版下都要有人来测试打包,不能通用。容器虽然不存在这个问题,但分裂的问题依然存在,任何一个细小的可插拔的模块,都有多种实现方案。开源世界的作风就是,不喜欢,我就造一个,不够好,我就来改改。

第三、生产部署。当要将容器运行在生产环境中时,你将会发现有很多功能的缺失,比如最重要的高可用,一个容器只能跑在一个节点上,当这个节点故障,上面的容器就会故障。当然这个问题并不是无解,未来讲的 Kubernetes 将可以很好地解决这个问题,甚至会带来很多其他更好用的功能,但,也将带来更多复杂的功能和更多的解决方案。

最后献上 CNCF 的全景图,感受下我们将要面对的市场。

🍿5Ax01 Tanzu 1 容器基础1 - 图2