k8s从容器到编排
一,K8s 是啥?
Production-Grade Container Orchestration
Kubernetes, also known as K8s, is an open-source system for automating deployment, scaling, and management of containerized applications.
K8s maser-slave架构
所以说K8s是啥呢?Kubernetes的缩写,最早的原型来自google的Borg,
很多产品的原型有google论文的影子吧,tidb,mapreduce等。
k8s是一个开源软件,用golang写的,自动发布,扩展,管理容器化应用。
所谓自动化发布,扩展,管理,用一个词概括了,就是编排。
是企业级,产品级,生产级,反正是好用可用大规模用,事实标准的容器编排
总之,k8s是做容器编排的。那么我们就从这两个方面入手,容器和编排
二,容器与编排
容器大家不陌生,docker嘛,经常跑啊,docker pull,docker run ,docker ps,docker exec,那这些命令背后都发生了什么呢?那容器本质是啥呢,为啥还需要编排呢?我们接下来一一讨论。
docker pull,docker run背后都发生了啥?
虽然docker pull,docker run ,docker ps,docker exec。经常用。测试环境,跑一个起来,免去安装依赖的麻烦,要知道在一个不确定的os上安装一个不确定的版本软件,没有安装脚本的情况下,安装一个软件是及其繁琐耗时的,因为要做各种适配,依赖,调整:
比如,前段时间我们云查杀在测试环境安装一个redis:
wget http://download.redis.io/redis-stable.tar.gz
tar xvzf redis-stable.tar.gz
cd redis-stable
make USE_SYSTEMD=yes PREFIX=/usr/local/redis/ install
就需要这俩:
yum install gcc -y
yum install systemd-devel
然后安装好了,结束了吗?
make test 时还需要这个:
wget http://downloads.sourceforge.net/tcl/tcl8.6.1-src.tar.gz
tar xzvf tcl8.6.1-src.tar.gz -C /usr/local/
cd /usr/local/tcl8.6.1/unix/
./configure
make
make isntall
那在容器里多好,docker pull,docker run一把起来,利用容器镜像的移植性和一致性,一切都打包好的。
App,bin lib全部通过镜像封装在一起。
但现在是方便了,redis也用容器跑起来了,但现在只有一个redis容器,那它挂了怎么办,手动拉起来吗,如果想做容器化了的redis集群怎么办,不然没有可用性,不能上生产环境,那就白搭啊。实际上,非容器化的,也是一样的,物理机上虚机上,redis有什么主从,有哨兵,有redis cluster,有各种方案,做集群,这个做集群,可用简单粗暴的理解在k8s里就是做容器的编排。
这种需求是普遍的,因为我们在程序世界,有架构,设计,这些东西,因为往往不是一个单一的程序,或者中间件就能完成一个事情,比如一个想承载大流量的博客系统,可能除了博客程序本身,还需要nginx,redis,MySQL,等不同中间件的配合,博客程序自己,也有注册登录模块,发布模块,帖子模块,评论模块这样的拆解,松耦合,高内聚,是程序世界的准则。现实生活中也一样无论公司还是各种结构,都有不同的职能部门与分工,而多个不同的实体之间的联系与协作,这是普遍的。有redis集群,就有nginx集群,就有mysql集群,mongodb集群,他们也都需要,上下线,保持可用,健康检查,扩缩容等等。所以说不同实体间,做架构,做联系,做编排是普遍的抽象需求。这是技术之上的抽象,不管什么技术,都需要。
那既然是普遍的抽象需求,那容器或者说docker被大规模使用之前,这些问题肯定就已经在生产环境存在且被解决了。
传统paas平台,在技术架构中的位置
以前怎么做的?
无非是安装标准化,自动化,抽象出运维标准化,自动化,做paas运维平台。
用中间协调程序比如zookeeper,MHA等工具做集群,做管理。
传统的部署办法:把程序包(包括可执行二进制文件、配置文件等)放到服务器上,接着运行启动脚本把程序跑起来,
同时启动守护脚本定期检查程序运行状态、必要的话重新拉起程序。
如果服务的请求量上来,已部署的服务响应不过来怎么办?
传统的做法往往是,如果请求量、内存、CPU 超过阈值做了告警,运维马上再加几台服务器,问题出现了:从监控告警到部署服务,中间需要人力介入!
如果不用人力介入,也需要开发自己应用场景的管理程序,而虚拟化技术之前,物理机的上下线一般都是要有人来介入,
这种管理程序也都是粗粒度的报警,没有那么智能的运维。
而这其实也是容器出现以前,后端一直在做的工作,做到极致就是做成了paas平台,所谓平台即服务,
比如自动化运维平台,自动化数据库平台等等。但这些做到极致,大多数情况下也没有k8s那么好用。
那有没有办法自动完成服务的部署、更新、卸载和扩容、缩容呢?
这,就是 K8S 要做的事情:自动化运维管理 Docker(容器化)程序。
传统的paas平台,运维平台,为了自动化操作物理机虚机而生。k8s为了自动化操作容器管理容器而生。不过k8s创新性的引入了编排,控制器模式等等这些想法。
对容器,和编排,现在有了初步介绍。接下来,在展开一点说容器和k8s编排
总结一下,docker容器通过镜像把依赖打包,做的可移植的一致性,并且还有隔离性。
而编排,这个概念是一个普遍需求,因为我们的计算机业务系统大多数就是这么设计的,各个组件之间互相协调,并发生调用关系,一起完成业务。
三,展开的说说,容器和编排
操作系统与硬件
那展开容器和编排之前呢,我们得先说一下操作系统,这一下就把高度拔起来了哈。计算机,咱们天天用,一个确定的输入,经过计算,给一个确定的输出,由CPU,内存,磁盘,输入输出设备构成,如果联网还有网络,当然大部分都联网,我们都默认有网络。虽然这些东西我们天天打交道,在学校也学过什么逻辑门,触发器,加法器,累加器,L1L2缓存,寄存器,但工作后,除了极少部分工种,实际上在软件和互联网这个层面,我们比较少的用到。因为计算机软件系统,是一个抽象封装系统,我们在某一个点,使用下层已经给我们封装好的东西。这个点,可以认为是操作系统,操作系统给我们提供了一个环境,屏蔽了刚才说的那些所有东西,我们大部分时候面向操作系统就可以。好比我们调用一个函数,使用某种语言的库,我们安装参数传递,拿return就可以了。(当然想深究,那才去看他们的源码,大部分时间不关心)
操作系统定义
操作系统是一个系统软件,管理计算机软硬件资源,为程序提供公共服务。
说白了向下管理软硬件资源,向上为应用程序提供服务。
我们一般在上层,从上层的视角看:
无论是Windows的图形界面,什么开始,控制面板,还是Linux shell,什么ls,mkdir,都是用户接口而已,包括我们自己写的程序,ls,mkdir也是程序,这些最终都会调用操作系统提供的库编程接口,最终调用系统调用来实现。所以我们就把这条基准线,拉到系统调用。
像系统调用之下这些东西:
什么内存怎么管理,段还是页,什么VFS怎么抽象文件系统,什么网卡怎么硬中断,我们全不关心,拿来直接用。就是系统调用通过操作系统已经给我们搞好了。
Linux系统调用,64位的,大约有300多个,我们要关心的是和namespace及进程相关的系统调用,比如,
#define __NR_clone 56 和创建容器进程指定namaspace参数相关,
#define __NR_chroot 161 和容器镜像相关,
#define __NR_setns 308 和docker exec 这样的命令相关。
咱们总说容器是一个沙箱,在里边跑的东西,就好像只有它自己,看不见外边的东西,可用在里面随便搞,容器和容器之间,容器和外部宿主机大多数情况下都互相不影响。可以在宿主机上docker run多个容器,那这一切都是怎么实现的?
实际上,主要靠三个东西,Linux namespace cgroup 和 rootfs。
可以简单理解,namespace做隔离,cgroup做资源限制,rootfs就是做容器镜像。
当用docker pull下载下来一个镜像,并使用docker run命令启动一个容器时,实际上会调用clone这个系统调用,Namespace其实只是 Linux 创建新进程的一个可选参数,当我们用 clone() 系统调用创建一个新进程时,就可以在参数中指定 CLONE_NEWPID 参数,比如:
int pid = clone(main_function, CLONE_NEWPID , NULL);
除了我们刚刚用到的 PID Namespace,Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 这些 Namespace,比如,Mount Namespace,用于让被隔离进程只看到当前 Namespace 里的挂载点信息;Network Namespace,用于让被隔离进程看到当前 Namespace 里的网络设备和配置。
所以,Docker实际上是在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。
说白了就是call系统调用创建容器进程时,指定上namespace参数,并没有一个真正的“Docker 容器”运行在宿主机里面。所以说,容器,其实是一种特殊的进程而已。docker只是一套管理API罢了。
Docker 帮助用户启动的,还是原来的应用进程,只不过在创建这些进程时,Docker 为它们加上了各种各样的 Namespace 参数。
这时,这些进程就会觉得自己是各自 PID Namespace 里的第 1 号进程,只能看到各自 Mount Namespace 里挂载的目录和文件,只能访问到各自 Network Namespace 里的网络设备。
镜像只是提供了一套镜像文件系统中的各种文件,比如Alpine镜像, Busybox镜像,centos镜像,Ubuntu镜像,nginx镜像,
只是rootfs文件系统目录不同,有的可能没有/opt目录,有的可能没有netstat这个命令,但这些都不包括操作系统内核。
而各种内核相关的模块或者特性支持,完全依赖于宿主机。所以说,所有运行在一个宿主机上的容器,实际上是共享一个操作系统内核的。
因为容器实际上就是一个通过namaspace参数,并设置相应的cgroup调起的一个进程而已。所谓把nginx放在docker里跑,就是在启动nginx这个进程的时候给它加上namespace参数,并把这个加了namespace启动参数的进程放cgroup控制组里,并chroot切换这个进程的跟文件系统rootfs为下载的这个nginx的镜像image,这样这个nginx就隔离起来了,看到它自己的进程空间,文件空间,只能使用被限制了的CPU资源。docker就只是一个call系统调用的管理程序而已。
另外要注意,所有的容器共用一个操作系统内核,bootfs和rootfs是分开的,不同的东西。Bootfs操作系统引导加载内核那个过程的东西。见下图:
rootfs,这一部分我们称为“容器镜像”(Container Image),是容器的静态视图;
一个由 Namespace+Cgroups 构成的隔离环境,这一部分我们称为“容器运行时”(Container Runtime),是容器的动态视图。
那现在容器介绍完了。编排是啥也清楚了。正式进入k8s
四,k8s到底是啥,如何做容器编排
这里我们发现,中间有一个标红的,没错,就是pod,这是k8s最原子的单位。
为什么pod这么重要,需要pod这么一个东西?
我们经常说程序是磁盘上的源码文件,或者编译后的二进制文件,进程是动态的程序运行,一个程序可以有多个进程,一个进程有多个线程,还可能有协程。就是实际上,在计算机世界,或者上工作中 我们仅仅单一跑一个进程得出一个结果,比如算π圆周率小数点后面的多少位是比较少的。而更多的是一个协作,比如什么master进程,work线程这样的概念,非常普遍,是多个进程线程协程等一起协调配合完成程序目标的。这就是为什么要设计pod这样一个东西,因为关联紧密,不单个出现,一个协作完成一个原子目标。而容器被设计成了单进程模型,所以就需要把多个紧密的容器拉拢在一起协调的抽象,就是pod。
我们可以先直观看一下pod的yaml定义,然后展开讲一个为什么需要抽象出一个pod,以及pod是如何实现的
Pod的定义文件:
apiVersion: v1
5 kind: Pod
6 metadata:
7 name: nginx-demo
8 namespace: default
9 labels:
10 k8s-app: nginx
11 environment: dev
12 annotations:
13 name: nginx-demo
14 spec:
15 containers:
16 - name: nginx
17 image: registry.cn-beijing.aliyuncs.com/google_registry/nginx:1.17
18 imagePullPolicy: IfNotPresent
19 ports:
20 - name: httpd
21 containerPort: 80
22
25 hostPort: 8090
26 protocol: TCP
27 volumeMounts:
29 mountPath: /usr/share/nginx/html
30 readOnly: false
31 - name: nginx-log
32 mountPath: /var/log/nginx/
33 readOnly: false
34 volumes:
35 - name: nginx-site
36 hostPath:
37 path: /data/volumes/nginx/html/
38 type: DirectoryOrCreate
39 - name: nginx-log
40 hostPath:
41 path: /data/volumes/nginx/log/
42 type: DirectoryOrCreate
这里面最主要的是14 spec: 字段,也就是pod的描述。
上面的yaml也可以大致的当成这个图看:
Pod里面是容器
pod是一个逻辑概念(从kubelet-cri-infra容器这条线往下套),最终容器还是namespace和cgroup它们两个的系统调用。比如我们yaml文件里request和limit,最终还是宿主机操作系统的cgroup起作用。而启动pod,只是先启动了一个基础容器pause容器,也叫infra容器,然后pod里的其他容器加入这个infra容器而已,都加入到一起了,不就在一个pod里了吗
所以说一组共享了资源的容器组成pod,可以简单理解Infra容器先有,先创建出来,然后其他容器加入它的namespace就可以了。那这些容器不就有了联系且与其他隔离了吗。在 Kubernetes 中,pause 容器被当作 Pod 中所有容器的“父容器”。Pod 的生命周期只跟 Infra 容器一致,和其他应用容器无关
所以说pod内的容器共享一个IP地址就是pause容器network namespace的IP地址,可以回环通信,可以挂载同一个volume
这样pod就提供了一个环境,一个可以共享IP,共享volume等的环境,至于如何在这个环境里设计容器,是用户自己的事。这里也体现编排思想
那现在我们有了nginx容器,有了存放nginx容器的pod,接下来就用编排的idea做一个nginx集群,看看和没用k8s时有哪些优势?
编排和集群的思考不同出发点,我们一般是先玩单机nginx然后才是nginx集群,nginx自己不能成为集群要么我们用一个外部组件keeplive,但这个外部组件又是问题点,总之我们是拼接拼凑式的。要么我们手动,或者用脚本部署,和最开始讲编排一样。存在各种不方便和问题。
MySQL或其他组件情况也一样,先安装一个单机MySQL(或者用容器跑一个MySQL)先用着,然后才是MySQL集群,mongodb也一样。
但编排更抽象于集群,也就是资源间的抽象关系,及资源的定义,比如MySQL,mongodb,es都是有状态的集群,都可以分片,而无论他们底层的结构是b树,还是倒排列表,还是什么
我们看一下deployment的定义:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
就是pod模板和期望spec描述,接下来引入最后一个概念,也是最重要的控制器模型,声明式API和调谐。
比如说实际中部署nginx,无状态,多副本,水平扩展,负载均衡,那么对于这样的抽象,kubernetes用deployment这个资源定义,但它部署的就不只是nginx了,而是所有无状态,多副本,水平扩展,负载均衡的单位。这里的单位是pod,kubernetes最原子的单位,而什么是pod。暂时可以这样理解,容器跑在pod里,那么nginx就是跑在docker里,docker跑在kubernetes的pod里,pod被deployment部署。接上LB类型的service,就可以上线了
曾经提到过一个叫作 kube-controller-manager 的组件。
实际上,这个组件,就是一系列控制器的集合。我们可以查看一下 Kubernetes 项目的 pkg/controller 目录:
$ cd kubernetes/pkg/controller/
$ ls -d */
deployment/ job/ podautoscaler/
cloud/ disruption/ namespace/
replicaset/
serviceaccount/ volume/
cronjob/
garbagecollector/
nodelifecycle/
replication/ statefulset/ daemon/
实际状态往往来自于 Kubernetes 集群本身。
比如,kubelet 通过心跳汇报的容器状态和节点状态,或者监控系统中保存的应用监控数据,或者控制器主动收集的它自己感兴趣的信息,这些都是常见的实际状态的来源。
而期望状态,一般来自于用户提交的 YAML 文件。
比如,Deployment 对象中 Replicas 字段的值。很明显,这些信息往往都保存在 Etcd 中。
在 Kubernetes 项目中,
首先,通过一个“编排对象”,比如 Pod、Job、CronJob 等,来描述你试图管理的应用;
然后,再为它定义一些“服务对象”,比如 Service、Secret、Horizontal Pod Autoscaler(自动水平扩展器)等。这些对象,会负责具体的平台级功能。
这种使用方法,就是所谓的“声明式 API”。这种 API 对应的“编排对象”和“服务对象”,都是 Kubernetes 项目中的 API 对象(API Object)。
这就是 Kubernetes 最核心的设计理念,
声明式api,资源对象,控制器,期望,调谐
但如果具体的实现一个个功能,就是放弃抽象,面向命令过程。如果反过来思考,设计抽象,囊括功能,就可以使用面向声明范氏
我们用ansible 发布应用,虽然不是k8s。但却用了声明式的控制器模式。我们把ansible的 hosts 当成k8s的yaml用。。。
最后,捋一遍。
K8s是怎么为我们提供自动化且自动扩缩容且编排思想的?
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了