Docker与K8s笔记

https://www.bilibili.com/video/BV13GmDY9EtV

Docker

问题

在容器之类的技术出现前,用户直接将程序部署到物理机上运行。主要用ystemd类的守护程序管理业务程序。

这种方式可以很方便的查看每个业务程序的运行效率,并进行手动自动化操作。但是很多时候程序可能伴随着各种依赖。

比如

  • 动态链接库
  • 某个命令行工具
  • 第三方依赖
  • 特殊配置文件等

在多台机器规模化的情况下,通过下发部署脚本来实现。然而业务程序的依赖是复杂容易变动的。同一个程序的多个版本的依赖可能是不同的。我们还要管理多个程序在同一个服务器上运行的情况,程序之间的依赖可能产生冲突。

最简单的方式就是每个程序独占一个机器。当时虚拟机技术发展已经较为成熟了。这样做的本质就是把程序放在虚拟机中,但虚拟机过于庞大,启停速度过慢,虚拟化性能损耗大。

解决方案

镜像大小

虚拟机之所以那么大,是因为存储了操作系统运行所需要的所有文件。对于同一系列操作系统的不同版本来说,往往存在大量相同的文件,比如动态链接库,内核等等。如果相同的文件只存一份就可以大大减少镜像大小。如果多个虚拟机同时修改文件怎么办,那么就拷贝一个文件副本给新的镜像。

其实就是copy-on-write的思想。

同样,物理机和虚拟机上也存在大量重复的文件。因此这个优化思路不仅限于虚拟机,也适用于物理机和部署在其上的镜像。

该思想实现的具体技术就是UnionFS或UFS,在容器出现之前的2004年,这个技术就已经出现了。它的初衷是希望在多个文件系统上提供一个统一的文件系统视图。

启动速度

虚拟机启动慢因为把珍格格操作系统的流程都包含了进去。理论上如果我们只关心程序运行本身的化 ,启停是不需要的。因此就可以去掉操作系统的启停。因此必须要实现一个轻量化的虚拟机,把操作系统相关的启停开销去掉。Linux就是在这样的背景下,逐渐发展出了实现上述需求的技术。namespace和cgroup。

要实现一个程序的隔离,主要是以下方面。

  • 文件系统
  • 进程,线程管理
  • 进程线程通信
  • 网络
  • 用户权限
  • 硬件资源使用

如果一个运行的程序在以上几方面都有自己的转述隔离空间和配额。那么实际上就实现了我们所说的轻量化隔离目标。这也就可以称为容器了。

我们把程序放在容器中运行,保持隔离的前提下,只运行程序本身。那么其启动速度显然会比虚拟机快很多,这就满足了我们对隔离后启停速度的要求。namespace和cgroup。在2008年基本发展成型。这早于docker的出现。

Namespace和Cgroup是Linux内核提供的两项关键技术,它们共同构成了容器技术的基石。简单来说,Namespace主要负责提供隔离性,为进程创建一个独立的运行环境;而Cgroup主要负责提供限制性,对进程组可以使用的资源进行约束和管理

这样我们解耦了操作系统本身冗余的部分,而只关注进程本身的启停。这样容器的速度就和进程本身没什么不同了。

性能损耗

虚拟机是一个完整的操作系统,例如linux有很多内核进程来协助内核做系统管理工作。如果有多个内核,每个内核都会有自己的一套内核管理进程运行。从整体的角度来看,这些重复工作的进程就是额外损耗。

容器隔离相关的技术就解决了这个问题。因为本质上所有容器内部的进程都是直接使用共用内核的,不需要维护独立内核。

此外,硬件损耗也是一部分,早期虚拟化技术对于硬件的损耗较大,不过随着虚拟化技术的发展,CPU,内存已经被降到很低了。

但是GPU io设备的损耗依然存在。

最后

docker就是把上述方案都打包到一起,并提供了一个用户友好的使用界面的系统。Docker依赖容器基础技术,UFS,和轻量化隔离能力。在此之上,docker设计了容器镜像和运行的标准,提供了完整的易用工具。让程序和依赖进行绑定,作为独立单元来进行管理。

docker的命名是集装箱,很符合其实现的功能。即软件的标准化交付,将程序本身和依赖打包为一个标准单元。外部的使用者不需要关心docker镜像中到底有什么。

K8s

简介

在docker诞生和流行后,带来了相当大的技术红利。随着docker被大规模用于生产实践。

单机上的容器可以使用docker很轻松的进行管理,但有一定规模的企业往往会有很多个应用,很多台机器。一旦应用机器的数量急剧增长。管理这些容器就变得非常麻烦了。如果没有任何手段,那么就只能编写运维脚本,然后批量下发到每台机器进行管理了。这种方案

  1. 操作效率低:大规模重复的劳动,人为容易出错
  2. 缺失全局记录信息:部署后无法感知多个机器的运行情况,需要自己建设一个系统来进行监视
  3. 精细化控制成本高:比如实现一组应用程序受控分批逐步升降级、根据资源使用针对应用程序所申请的各类计算资源进行扩缩容等调整,一台机器宕机后对上面运行的应用程序进行恢复故障转移等。
  4. 难以进行高效的资源管理使用,分配不高效。
  5. 服务难以高效管理和治理:应用服务如何在大规模之间高效的互相感知协作。

我们更多的将k8s叫做容器编排系统而不是管控系统。不管理实现细节。K8s是一个面向终态的系统。

例如空调冰箱关注对象的温度达到指定值,否则会一直改变温度来尝试达到指定值。

面向中台的关键就在于让控制逻辑感知到某种状态(现状),然后根据状态调整现状,达到目标状态。而目标状态也不是一成不变的。

问题解决

K8s要真正解决的问题是:大规容器化程序应用在大规模机器集群内的高效部署管理问题。

其中部署挂历可以分为操作目标状态和最终目的三部分

通过操作来改变目标状态实现目的。K8s将达成目标状态的操作自动化。

声明式API是一种现代编程和系统设计范式,其核心思想是用户只需定义期望的系统目标状态,而不必关心实现该状态的具体步骤。系统会自动负责将当前状态调整到期望状态。这种模式与强调逐步执行命令的命令式API形成鲜明对比

这种面向终态的思想有一个行业内的专业术语叫做 声明式API。

k8s中使用的yml就是我们能直接看到的体现,我们把对yml写入终态,k8s就能帮我们自动实现,所以这里写yaml就好像变成了调API一样。我们只需要声明一下就可以达成效果。业内k8s工程师也经常被调侃为yml工程师。

有了面向终态的思想带来了以下好处

  1. 自动化、标准化操作流程:如重启升级等操作,基本操作流程都是不变的,会变的只是外部参数版本号。消除了人工介入的成本
  2. 天然重试机制:面向终态带来了一个结果就是重试,比如进行某个操作时出现了偶然的出错,程序就可以重试来最终达成目标状态。
  3. 全局状态记录:面向终态要时刻比较当下和目标状态的差异,因此对当下目标状态信息的记录成为了必要条件。所有的机器的信息都在管理机构中统一保存(ETCD)
  4. 降低门槛:由于所有的操作都被自动化标准化了,运维人员的关注点从关注过程变成了关注结果。

接下来就是剩下的俩个问题

  1. 难以进行高效的资源管理使用,分配不高效。
  2. 服务难以高效管理和治理:应用服务如何在大规模之间高效的互相感知协作。

K8s解决资源分配调度问题

进程线程调度,CDN网络流量,物流中多地仓库,内存管理或GC都可以试做调度问题:资源如何分配能达成某些条件下的最优效果。

这个最优可能是全局最优或者局部最优。

k8s面对的调度场景就是:如何将多个具备不同特点和规格的应用程序,安置到由多个不同特点和规格的机器所组成的集群内才能达成最高的资源利用率.

一般来说资源录用率往往和服务质量(如实时性)是不可兼得的。一般都是在俩者之间做取舍。

如果大部分调度对象对执行时间有较为严格的要求,调度侧重点就会变成平均时间开销,而不是资源录用率。

k8s提供了一个通用的调度框架,帮使用者实现不同的调度需求目标。这里先介绍默认的调度策略。

调度最重要的过程是过滤和打分

过滤阶段调度器会将所有满足pod调度需求的节点筛选出来,过滤函数会检查所有候选节点的可用资源是否满足pod要求。在过滤之后得出一个节点列表,里面包含所有可调度的节点。通常情况下这个列表不止一个节点,列表为空则不可调度。在打分阶段调度器会根据当前启用的打分规则对调度列表中的每个节点进行打分。最终得到一个得分最高最合适的节点,如果存在多个得分一样的节点,调度器会随机选择一个。因此我们可以将调度理解为打分。

打分结果是由多种策略加权求和得来的。K8s1.30默认策略包括(括号内是权重,以下分数为**加权求和后计算)

Kubernetes调度器中“加权”的“权”主要来源于三个层面:Kubernetes项目内置的默认权重、管理员通过配置文件显式设置的权重,以及Pod配置中声明的权重。

在 Kubernetes 中,污点是应用于节点(Node)上的一种特殊标记,它的核心作用是 “排斥” Pod。你可以把它想象成节点设立的一个“门禁”规则,明确告知调度器:“只有携带特定通行证(即容忍度)的 Pod,才能被我接纳。

  1. 污点容忍情况(3):节点打上了污点后,除非容器显式声明容忍,否则都不会调度到污点节点上。
  2. 节点亲和性情况(2):容器组声明的更倾向于希望所处的机器节点的条件
  3. 可用资源情况(1):节点上可供使用的计算资源的情况
  4. 容器组拓扑分布情况(2):相同的容器组希望或不希望出现在同一类机器节点上。比如希望在不同机房上分散开
  5. 容器组亲和性情况(2)某些容器组希望或不希望处于同一或不同的节点
  6. 节点资源分配情况(1):节点当前已经分配出自愿的情况
  7. 节点上存在镜像的情况(1):机器是否有需要的容器镜像了(不需要重新下载拉取镜像)

其中1,2,4,5可以分为一类,它们都是用户主动指定的调度偏好。即我想让这些应用运行在哪些地方,这里都给了相对较高的权重。因此K8s的默认策略是更照顾用户的主观需求的。

3,6,7这三个条件则是客观资源条件。即吧应用放在哪里可以达成最好的资源录用率。这些权重是比前一种主观性要求低的。

这些是默认策略,如果有具体需求,这些调度策略,权重,甚至是计算方法都是可以用户完全自定义的。例如一般的调度策略仅会将CPU和内存作为计算资源考量,事实上,除此之外的磁盘,网卡,GPU,各类设备硬件都可能成为调度对象。

服务管理治理问题

微服务在k8s火的时间也非常流行。俩者的理念模式存在非常多的相同点。

微服务核心是把庞大的单体服务拆分成多个独立灵活小巧的微型服务。在拆分完服务之后,多个微服务的部署管理治理都变得更加复杂和棘手。

而容器和k8s技术恰恰能够帮助解决这些问题。以下是微服务相关的技术栈

配置中心

k8s提供了俩类存在于集群中的资源ConfigMap和secret来实现配置的管理,前者用于保存配置数据,后者用于保存敏感数据。所有需要使用配置的容器化应用都可以通过挂在的方式,讲一个或多个configmap、secret放到与文件中,实现应用对配置的感知,同时多个实例也可以挂在同一个configmap、secret。用户对于configmap以及secret的修改也会实时的同步到挂载这些对象的应用实例上。这样k8s就实现了对服务配置的统一管理。

注册中心、服务发现

K8s可以将某个服务的一组实例抽象成一类集群中的资源。通过service来进行管理,service作为集群资源,会被保存到k8s的全局记录中,这样可以通过K8s API轻易获取集群中存在哪些可访问的应用。不同的服务间可以通过某个特定的Service,其具备固定的域名和虚拟IP,称为ClusterIP来实现互访,而无需感知实际要访问哪一个地址。

负载均衡

在上一层service上 k8s有一个名为kube-proxy的组件,通过os内核提供的(iptable和ipvs)实现了应用在发起端的负载均衡。这样当访问某个Service,流量基本能够均匀按照要求分布在服务的多个实例副本之间。实现负载均衡

网关

K8s抽象了Ingress ,getway等资源来一声明的方式提供网关实例。K8s自身没有强制绑定某一个网关的实现,而是定义il网管需要支持的基本语义规范(协议、接口)来提供给用户使用 。可以根据具体需求选择网管来进行使用。

RPC

由于RPC是一个和语言密切相关的操作,K8s没有提供具体的Rpc框架,K8s不强制绑定某个实现

限流和熔断也是在网管和RPC层进行实现。

总结

K8s对应springcloud提供的能力,K8s均做了对应的实现。无法实现的提供了接口进行嵌入适配。k8s因为语言中立带来了普适性,以及面向协议,接口兼容的扩展性。

应用程序应当且仅需管理应用程序自身。应用程序开发者应该更专注于业务程序本身 。

Last modification:October 28, 2025
如果觉得我的文章对你有用,请随意赞赏