
【4】同业务开发的关系及交互模式
前面我们介绍了容器和K8S技术的诞生缘由和背景,接下来我们就进入最贴近实际的部分:K8S&容器技术到底和业务开发有什么关系?日常工作可能会以何种形式被影响?在实际工作实践中到底是如何和这一类技术进行具体交互的?
K8S&容器技术和业务开发的关系
一般来说,作为一个未曾接触过或者略微听闻过容器&K8S技术的业务开发来说,他第一次真正在实践中切实接触到它的场景一般分为两类:
- 被动使用:你进入了公司/实验室,发现周边的大家构建、部署程序都有专门的CI/CD及发布平台,只需要在页面上点几下按钮,去接杯水喝两口,刚刚提交的代码就被自动部署发布到期望的开发/生产环境中。这时,享受到如此便利的你可能会产生一些好奇:这些平台底下到底是什么样的系统和架构,实现了如此便捷丝滑的程序部署体验?这就是你所处的开发团队已经实现了容器和K8S化后的效果,在这种场景下的你,一开始就成为了容器&K8S技术的受众和用户
- 主动使用:你作为一名乐于了解新兴技术及趋势的业务开发程序员,很早就听过容器&K8S技术的优势和名声了,甚至你可能已经用过了Docker部署过你的个人实验小程序。你进入了公司/实验室/某个技术团队,发现周边的大家总是在各类部署和环境问题中焦头烂额,这时候你突然想起了容器和K8S技术,于是你号召大家主动尝试和拥抱它。一切进展的很顺利,因为有云服务,即使团队的大部分人并不是很懂相关技术细节,你们还是快速地用上了K8S和容器,解决了你们之前所遇到的环境依赖和程序部署的问题
上述两种场景是个人所能遇到的最典型的两类情况,那么这类技术到底是如何和业务开发产生关系的呢?其实也很简单,就是我在之前两篇文章中介绍过的,一门技术的诞生和应用就是为了解决问题,因为要解决实践中开发&运维&部署&资源利用等事项效率低的问题,所以我们便采用了容器&K8S技术来提高效率,展开来讲,在这里看待所解决的问题有两个方面的视角:
- 对个人和技术团队:这类技术能够提高开发&运维乃至整个软件工程/产品项目的效率,并且个人和团队均能享受到这一类红利
- 对一个企业和组织:在提高企业和组织内部人员的生产力和效率的同时,这类技术还能提升硬件资源的利用率,进而帮助整个企业或组织降本增效,因此这也是企业经营者所愿意见到的
所以,从某种程度上来讲,容器&K8S技术会越来越来流行并被不断广泛地采用,和互联网&IT行业的千万从业者产生关系,也是一种必然的趋势。我们能做的,就是要顺应并利用这种趋势,就像是历史上发生的各类技术革新一样,而不是拒绝接受新事物,渐渐落后于时代。
业务开发如何与K8S&容器技术打交道
程序是如何运行在K8S&容器环境中的
程序运行在哪
我们的程序一般都是跑在某一台机器上的,不管是虚拟机还是物理机,一般来讲都是有硬件、驱动程序、OS等作为底层基础设施来支持程序的运行。
而在容器&K8S环境中,自然也不会脱离这种模式,只是在传统的这类运行环境模式的基础上,又多了一层抽象实体:容器和Pod。
容器很简单,之前也解释过,就是一个独立的程序运行环境,程序跑在其中,可以和其他程序隔离,享受独占的运行环境和依赖库,保持运维部署运行的一致性和可重复性。
而K8S则在此基础上抽象出了Pod的概念,作为K8S管理容器的最小单元,Pod即是一组容器的集合(因此可以翻译为容器组,Pod的本意是豆荚,这个命名很形象)。
那为什么是一组容器而不是一个呢?因为我们作为基本部署单位的业务服务有些时候并不仅是单个程序,而是一组程序,比如一套业务服务有业务主程序,还有运维、日志收集、鉴权等辅助程序,而这些辅助程序也是和业务服务主程序紧密依赖的,因此需要和主程序打包部署使用,这也就是所谓的sidecar模式
那么其实就很清楚了,我们的程序是如何运行在K8S&容器环境中的?首先当然是在一台机器(集群中的一个机器节点)上的,然后机器上跑了很多Pod,Pod中可能有多个容器,我们的程序就在某一个容器中运行。
程序如何启动
那么我们知道了运行态的程序是运行在哪里的,那这些程序是怎么启动的?
我们在前面也提到过容器一般都是需要依赖容器镜像作为基础跑起来的,我们将软件和程序产物打包为镜像,然后容器运行时就能以镜像为基础,创建程序运行的容器环境实例,再在容器中启动需要运行的目标程序。
容器化的CI/CD系统所做的事情其实就是完成上述的打包工作,你可以指定代码仓库和版本,将代码从仓库拉取到后,CI/CD系统会根据你的编译指令(一般是通过Dockerfile内定义运行脚本)进行程序编译,最后根据你的指令把程序构建产物拷贝到镜像的特定目录中,这样后面容器启动的时候你就可以通过编写运行指定目录程序的指令,并传入需要的参数来把程序跑起来。
因此,也可以这样理解:镜像是静态的容器,容器是动态的镜像。
简要总结
在K8S&容器技术生态中,容器和Pod这两个概念成为了屏蔽机器、集群节点概念等底层基础设施的新的实体,而这也是正是这一类技术想要达成的目的:让容器和Pod的概念成为一等公民,而非虚拟机、物理机;推动程序的开发者无需再关注具体硬件设备、OS等基础设施上的复杂细节,把精力聚焦在仅关注程序自身就足够了;基础设施的各类复杂细节都被隐藏到了K8S&容器层次以下,交由更专业的分工团队(内部基础架构、运维团队或云服务商)来关注。
甚至若干年后,那时的程序员和开发者会把Pod当成一种习以为常的事物,而逐渐遗忘掉所谓的机器的概念,出了问题,他们不会问机器有什么问题,而是Pod有什么问题。
运行环境改变后应该清楚的事
对日常工作的影响
对任何程序员及技术人员来说来说,一旦底层的技术架构被切换到了K8S&容器类的架构,对日常工作带来的影响和变化也是较为显著的,因此更好地了解这一技术的本质性原理和使用场景,能够更好地帮助我们在日常工作中提高效率。
试想,如果你的程序都日常都会跑到K8S&容器环境中运行,万一出了问题,你连程序跑在哪都不知道,你觉得你能很快地解决并避免后续重复问题的发生吗?你还有充足的时间完成你其他堆积如山的需求和BUG修复工作吗?
很显然,出了问题只能干瞪眼是不行的,即使你无法独立解决问题,但如果你能懂一些思想、原理和某些重要的细节,也能够帮你更高效地解决问题:比如至少你知道大概是哪里有问题,你该去找哪个专业领域的人协助,或是怎么迅速地恢复故障等等,这些都会帮你避免大量的麻烦,节省大量的时间。
另外,如果你所处的底层技术架构已经变成了K8S&容器环境,那么kubectl(K8S集群的命令行操作工具)、Docker这些软件工具你也得或多或少地学习一些常见操作和用法,毕竟对这些软件的使用来说,20%的内容就能覆盖80%的场景。再加上你对这类技术中原理的一些了解,你也完全可以做出很多hack的操作去提升你自己的开发效率,节省下来的时间便都是你的,后面我会实际举一些这方面的例子。
程序能够享受到哪些好处
程序部署运行到K8S&容器环境中,自然会享受到一些属于这类环境的特殊优势,程序开发者了解到这些好处和优势后,要尽量使用这些已有的机制和功能,发挥其作用,而不是无所觉察地重复造一遍轮子:
- 底层基础设施的复杂性被屏蔽:这就是上一部分所阐述过的,由于在容器&K8S技术生态视角下,Pod和容器变为了基础设施,使得上层程序开发者免于关注复杂的底层基础设施中的各类细节,让开发者专注于程序本身;而底层基础设施的各类问题和复杂性交给更加专业的团队来做,从本质上为应用开发者减轻了负担(但如果没有这样的团队另说)
- 可重复的独立隔离运行环境:这是容器本质特性所带来的天然结果,也是其被开发出来的最核心的目的效果;程序可以不需要再关注复杂的运行环境问题,做到一次构建,处处运行,极大地提升部署效率
- 自动化运维:这是K8S最本质性的优势功能,程序在Pod中运行,作为K8S独立的调度和运维单元,可以在各种情况下被自动化地进行调度、安排和调整,程序挂了会自动重启,资源匮乏会自动扩容,资源浪费会自动缩容等等,充分发挥了弹性的特点
- 使用K8S&容器生态技术提供的其他功能:前面提到了,K8S&容器技术和微服务的关系非常密切,微服务天生就适合运行在K8S&容器环境中,以至于K8S本身就提供了很多微服务相关的能力:比如语言中立的服务发现机制(K8S Service/DNS)、简单轻量化的配置管理功能(Configmap、Secret)、基础资源供给的声明式API(通过编写YAML文件来申请和使用各类资源)等等,灵活运用这类机制会让业务服务程序的运行和运维更加高效方便
程序必须承受哪些成本
天下没有免费的午餐,凡是享受到好处,就必然要付出成本,任何技术都不能例外。因此,将程序跑在容器&K8S中的开发和运维者必须要清楚的了解到需要付出的对应的成本,并尽可能地规避:
- 高特权操作有所限制:由于容器本身是依靠OS内核提供的一系列基础能力进行实现的,因此出于安全、实现成本、内核架构及各方面的考虑,在许多内核相关功能上和直接跑在裸机上是有些许差异和Gap的:比如各类Linux特权、sysctl系统参数均需要提前声明,而无法在容器中直接修改操作,同时还有部分内核配置并不支持在容器层面修改,必须整体修改容器所在宿主机器的配置来完成
- 潜在问题的引入概率更高、问题排查更加复杂:由于程序的运行新叠加了K8S&容器这一层架构,因此这一层架构的引入会给各类程序的运行、性能带来更多潜在问题,且排查路径更加复杂,因为触发问题的各类变量和影响因素都变多了,因此可能必须找到较为熟悉这一层次技术的人来参与排查,才能更快地解决问题。很多时候,如果使用K8S&容器技术的团队规模较小,且自身对这类技术不够熟悉,从整体收益上讲反而是不如直接抛弃K8S&容器技术的使用的,或是诉诸于云服务厂商提供的产品化能力
- 开发、测试及运维模式有所变动:因为容器和K8S在机器上额外封装一层的特殊性,很多原有的基于传统机器的开发、测试及运维流程均失去了效果。比如:因为容器不是完全真实的OS环境,而且大部分时候环境本身相当精简,因此很多调试、编译和观测工具都无法直接使用,需要用其他的方式来做到(比如使用临时容器或是到宿主机器上直接操作)
- 特定case下的性能下降:因为容器&K8S额外一层架构的引入,在某些特定的case下,某些方面的性能可能出现一定的下降,比如:最经典的容器网络CNI插件,常用overlay的网络方案,即通过各类虚拟设备、设置额外的路由、转发规则等方式实现以Pod/容器作为基本通信单位的网络架构,这种基于底层物理真实网络之上又架了一层虚拟网络的模式,使得在高带宽、低延时要求的高性能网络场景的性能问题得到了突显;再比如因为启动程序前还需要进行准备镜像、拉起容器、设置网络存储资源等额外工作,程序的启动时间一定是被拉长的,在要求程序启动速度较高的特定场景下,这可能又是一个无法避免的问题
是否契合程序本身的特性
当前的互联网上存在着各种各样类型的业务服务类型,每种服务的程序都具备各种各样的特性特质,并且彼此之间的差异性也非常大。某一些程序可能天然适合在K8S&容器环境中运行,而另一些程序可能就相对来说没有那么适合(但也不是不合适)地在K8S&容器环境中运行了。
- 无状态服务:最典型的如Web后端服务,通常会常驻运行来处理用户的业务请求,然后读取和修改缓存和持久化数据(大部分是DB),最常见的就是CRUD类服务。这种服务程序本身是没有状态,什么时候启动停止,运行在哪里,有多少个副本等等对其的运行都没有什么影响。这类程序就非常适合运行在K8S&容器环境中,因为大部分K8S系统能够覆盖的自动化运维都能完美适用于这类服务,并且基本不会有什么负面效果
- 有状态服务:最典型的如各类涉及到数据存储的服务,比如数据库、缓存中间件;以及网络确定性要求较高的服务,比如承载长连接的负载均衡网关、消息队列等等。这类服务的特点是确定性要求较高,包括启停时间、运行的位置、副本数等等,均有较为确定性的要求,一般都是长时间跑在一台固定的机器上,并且短时间内的频繁变动会让这类服务表现非常不稳定,进而对用户造成不良影响。这类程序就不一定非常适合运行在K8S&容器环境中了(但如果周边工作做的非常到位,也是比较合适的),因为其本身的部署运维的频次并不高,且K8S当前提供的默认自动化运维能力对其带来的收益有限(除非面向特定场景开发了针对性的增加特性),并且很可能引发额外的非预期问题。不过当前K8S及其社区本身也在不断发展和演进,随着K8S&容器技术的发展,在之后能够不断更好地支持这一类服务
- 任务型服务:非常驻运行的服务,比如定时触发的任务、提交的异步批量执行任务等等,比如AI模型训练、音视频转码处理、数据迁移同步、报表计算。这类服务的特点是启停频次较高,运行时间较短,相对来讲部署到K8S&容器环境中也没有无状态服务那么适合。主要是这类任务型服务本身的特性,使得K8S和容器技术本身带来的overhead被突显了出来,高频的启停让容器的拉起和销毁带来的额外时间成本被注意到、同时也为K8S本身带来性能瓶颈压力。因此,当前大多数主流的方案还是在K8S上再运行一层任务编排调度框架系统来规避各类问题,不过目前K8S及其社区也在不断去完善K8S支持任务型服务的能力和性能表现,期待在不远的将来,任务型服务也能较为完美地运行在K8S上。另外值得一提的是,这类服务由于其本身碎片化程度较高的特点,所以非常适合做调度来提升硬件资源的使用率,所谓的在离线混部就是类似的思路,如果有两类业务的高峰和低峰刚好在时间上相反,那么可调度复用资源的空间就非常大,也就意味着带来的潜在成本收益也会很高,这也是近几年大厂降本增效比较喜欢的一个思路方向
总结
总体来看,容器及K8S技术有着逐渐成为业务开发人员直面底层架构一等公民(取代传统意义上“机器”的概念)的趋势,并在各个方面对开发人员进行着或多或少的影响。其实站在2024年来看,这一系列技术从诞生至今已经10年了,某种程度上讲,也应当进入了成熟阶段,整体的普及和渗透率也相当高。但还是有相当数量的业务开发人员对其思想、作用和少部分关键细节的了解还是不够具体,仍在不自觉、透明地使用这项技术,乃至于很多时候因为对其缺乏了解导致各种各样问题的出现。从技术发展趋势来看,积极地拥抱已经出现的更先进的模式,是能乘着时代的浪潮前进的,而固执地待在舒适圈内,最终可能还是免不了落后于时代。
附:如何借助对K8S&容器的理解提升开发效率
开发人员在最初使用容器&K8S技术时,经常遇到的一个很不舒服的点是:因为要维持容器镜像的精简及安全性,容器内总是缺少很多调试、观测及系统工具,甚至是某些容器连shell都去掉了,这会让开发人员在排查程序问题、调试和理解线上程序行为时非常头痛。
而实际上,从另一个方面来看,容器&K8S环境其实是天然适合开发、测试验证的,为什么呢?因为实际上这类技术能够做到把开发、验证以及生产环境完全固化,只要没有强力的干涉破坏,借助K8S本身自动化运维能力以及容器本身带来的一致性、可复现的程序运行环境特质,你需要的环境能够长期保持非常高度的完整性和一致性。
那我们到底如何能够规避最先提到的劣势,充分地运用后面提到的优势呢?方式有很多,只要你对容器&K8S有了一定的理解和感知,你都能摸索出来,这里我为各位读者介绍几个:
- 使用K8S提供的临时容器机制
这是最简单的方法,临时容器机制也是近期(2023)的版本中才在K8S中成为一个稳定功能,通过kubectl来创建临时容器,你可以在任意Pod中创建出一个和已有容器共享大多数环境的临时的容器。在这个临时的容器中,你的网络空间、进程空间以及挂载到Pod上的Volume(存储卷,可以简单地理解为“硬盘”)等,都是和之前在运行的容器一致的。因此,你可以方便地在临时容器中排查一些主容器遇到的问题,并且临时容器的镜像你可以随意指定选择,比如选择一个安装了很多工具的超大镜像,也完全没有问题。
临时容器最适合用于调试和排查在线上环境遇到的问题,比如在测试环境很难复现的问题,在生产环境出现了,就可以通过临时容器来进入现场进行排查,并且一般不会对主进程造成影响。
- 空容器
所谓的“空容器”是我发明的一个词:指空跑、空转的容器,这个容器中可能就只存在一个休眠的进程(比如sleep 99999、tail -f /dev/null),而没有其他实际工作的进程。
那么这种空容器有什么用呢?它最大的用处就是方便我们的开发调试。
我们知道,一旦拥抱使用了容器&K8S技术,我们的业务程序在传统对数据库等基础软件依赖的基础上,很可能还会使用微服务的架构、以及依赖各种各样中间件和基础服务,服务之间的互相依赖和调用关系会非常复杂,因而我们很难在本地搞一个完整的环境来测试我们的程序。
那么根据我在开始提到的容器&K8S技术能够固化环境的优势,如果我们直接把开发调试放到容器&K8S环境中进行,不就很方便了吗?
所以,空容器的想法就从这样的背景下诞生了,假定我们已经有了一个比较稳定的容器&K8S集群环境,我们的业务程序都是通过Pod部署运行在其中了,那么当我们想开发测试时,在本地熟悉的开发环境中将修改的代码编译完成后,就可以将可执行文件直接传入到容器中。于此同时,将这个容器的主进程换成一个空跑进程,然后启动我们要测试的程序,这样就实现了在空容器中进行开发验证,这会极大地提升我们的开发和验证效率。
具体的实现方法,也是借助kubectl工具提供的能力,cp和exec,cp能够让我们把文件拷贝到指定的Pod容器中,exec可以让我们进入到容器执行我们的程序,另外,如果你需要gdb等调试工具,同样可以把工具拷贝进去使用。
空容器的这个思路更适合用于完成编写代码-编译-验证-调试-编写代码的Loop流程,其中的编写代码及编译还是放到本地进行会更加方便合适,验证、调试则可以利用容器&K8S本身提供的优势化能力来提高效率。
- 其他开源方案
还有一些方案,思路上和空容器有点类似,不过他们是通过一些特殊的网络技术,把本地环境和Pod容器环境做了打通,使得进出集群环境容器的流量完全对应到本地的程序,这样,从联通性的角度来说,本地和远程调试也就没什么区别了。这一类方案的开源项目很多,包括Telepresence、kt-connect、nocalhost等等,感兴趣的读者可以自行了解。这类方案为了达到极致简单的体验,会把实现方式搞得复杂一些,可能会遇到工具一直跑不起来,要花费大量时间去调试排查自己各类环境问题,因此笔者个人感觉不如前两种方案简洁,效率上也不会差很多。
