文章目录
kubernetes发展历程
- 基于云计算和OpenStack的时代
2013年以前,虚拟机和云计算是比较普遍的技术和服务,那时主流用户的普遍用法,就是租一批 AWS 或者 OpenStack 的虚拟机,然后像以前管理物理服务器那样,用脚本或者手工的方式在这些机器上部署应用,而且在部署过程难免会碰到云端虚拟机和本地环境不一致的问题
- Cloud Foundry的诞生
2013年Cloud Foundry为代表的开源PaaS项目的诞生,成为了当时云计算技术中的一股清流;
PaaS提供了一种名叫“应用托管”的能力,最核心的组件就是一套应用的打包和分发机制
对比当时的云计算服务,竞争的就是谁能更好地模拟本地服务器环境,能带来更好的“上云”体验。而 PaaS 开源项目的出现,就是当时解决这个问题的一个最佳方案。
- docker的兴起
源于PaaS的缺点:
- 用户在用PaaS时就必须为每种语言、每种框架,甚至每个版本的应用维护一个打好的包
- 打包过程,没有任何章法可循
- 在本地运行得好好的应用,却需要做很多修改和配置工作才能在 PaaS 里运行起来
- 修改和配置并没有什么经验可以借鉴,基本上得靠不断试错,直到你摸清楚了本地应用和远端 PaaS 匹配的大部分情况才能够搞定
- 为了最终的一键部署费尽心机
Docker 项目确实与 Cloud Foundry 的容器在大部分功能和实现原理上都是一样的,可偏偏就是这剩下的一小部分不一样的Docker 镜像功能,成了 Docker 项目成功的优势。
docker解决的问题:
- 它解决了应用打包和发布这一困扰运维人员多年的技术难题
- 它第一次把一个纯后端的技术概念,通过非常友好的设计和封装,交到了最广大的开发者群体手里。在这种独特的氛围烘托下,你不需要精通 TCP/IP,也无需深谙 Linux 内核原理,哪怕只是一个前端工程师,都会对如何把自己的代码打包成一个随处可以运行的Docker镜像
- 容器编排的出现
虽然 Docker 项目备受追捧,但用户们最终要部署的还是网站、服务、数据库,甚至是云计算业务。Docker本身是个只能用来创建和启停容器的小工具;“编排”就是对 Docker 容器的一系列定义、配置和创建动作的管理。
编排的理解:假如现在用户需要部署的是应用容器 A、数据库容器 B、负载均衡容器 C,那么 Fig 就允许用户把 A、B、C 三个容器定义在一个配置文件中,并且可以指定它们之间的关联关系,比如容器 A 需要访问数据库容器 B。
Swarm:docker native的内置, 与Docker 生态的无缝集成
Mesos:社区却拥有一个独特的竞争力:超大规模集群的管理经验。
2014 年注定是一个神奇的年份。就在这一年的 6 月,基础设施领域的翘楚 Google 公司突然发力,正式宣告了一个名叫 Kubernetes 项目的诞生.
Google、RedHat 等开源基础设施领域玩家们,因不满docker公司的极端商业化行为,所以共同牵头发起了一个名为 CNCF(Cloud Native Computing Foundation)的基金会。
CNCF希望以 Kubernetes 项目为基础,建立一个由开源基础设施领域厂商主导的、按照独立基金会方式运营的平台级社区,来对抗以 Docker 公司为核心的容器商业生态
与内置到docker项目中极大增加了复杂度的Swarm相比,Kubernetes 的应对策略则是反其道而行之,开始在整个社区推进民主化架构:从 API 到容器运行时的每一层,Kubernetes 项目都为开发者暴露出了可以扩展的插件机制,鼓励用户通过代码的方式介入 Kubernetes 项目的每一个阶段。
Mesos和Kubernetes的区别和选型:
- 如果拿搭积木来类比的话,Mesos是零散的积木,你需要自己组装实现自己的业务模型,Kubernetes就是组装好的积木模型,直接拿来用就好
- 两者发展的侧重点不同,Mesos更侧重底层资源的管理,Kubernetes侧重业务层的调度,容器服务编排,服务发现等
- 容器技术圈最终定格
面对 Kubernetes 社区的崛起和壮大,无退路的docker公司将 Docker 项目的容器运行时部分 Containerd 捐赠给 CNCF 社区,标志着 Docker 项目已经全面升级成为一个 PaaS 平台;容器编排纷争的始作俑者,Docker 公司的 CTO Solomon Hykes 宣布辞职,至此容器技术圈基本尘埃落定。
容器概念理解
容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”。
对于 Docker 等大多数 Linux 容器来说,Cgroups 技术是用来制造约束的主要手段,而 Namespace 技术则是用来修改进程视图的主要方法。
- 进程的隔离-Namespace机制
Linux 系统中创建进程的系统调用是clone(), 而 Namespace 的使用方式只是 Linux 创建新进程的一个可选参数:
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
每个 PID Namespace 里的应用进程,都会认为自己是当前容器里的第 1 号进程,它们既看不到宿主机里真正的进程空间,也看不到其他 PID Namespace 里的具体情况。
Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 这些 Namespace。
比如,Mount Namespace,用于让被隔离进程只看到当前 Namespace 里的挂载点信息;Network Namespace,用于让被隔离进程看到当前 Namespace 里的网络设备和配置。这,就是 Linux 容器最基本的实现原理了
总结来说虚拟机和容器的区别:
1、虚拟机是硬件隔离,因为hypervisor 虚拟一系列硬件资源
2、容器是进程级隔离,依靠NameSpace 机制实现进程间的隔离(障眼法)
Namespace隔离的缺点:
- 容器只是运行在宿主机上的一种特殊的进程,多个容器之间使用的就还是同一个宿主机的操作系统内核
- 在 Linux 内核中,有很多资源和对象是不能被 Namespace化的,最典型的例子就是:时间。
- 资源的限制-Cgroups机制
Linux Cgroups 的全称是 Linux Control Group。作用就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。
Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下的。
Cgroups限制的缺点:
1.内核/proc文件系统的问题,如果生产环境不进行订正,应用程序在容器里读取到的 CPU 核数、可用内存等信息都是宿主机上的数据,例如:执行 top 指令显示的信息居然是宿主机的 CPU 和内存数据,而不是当前容器的数据
- 分层镜像-rootfs
UnionFs:一种分层、轻量级并且高性能的文件系统,支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。
Union文件系统是Docker镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。
UnionFs的特性:一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录
容器镜像 : 实际上是由一层一层的统UnionFS文件系统组成,最终通过联合加载将各层文件挂载在容器根目录上, 用来为容器进程提供隔离后执行环境的文件系统。另外它还有一个更为专业的名字,叫作:rootfs(根文件系统)
rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核;在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像
- rootfs 只包括了操作系统的“躯壳”,并没有包括操作系统的“灵魂”
- 同一台机器上的所有容器,都共享宿主机操作系统的内核
- 通过结合Mount Namespace 和 rootfs,容器就能够为进程构建出一个完善的文件系统隔离环境
镜像都是只读的,当容器启动时,一个新的可写层加载到镜像的顶部。这一层就是我们通常说的容器层,容器之下的都叫镜像层。
kubernetes架构理解
全局架构
由 Master 和 Node 两种节点组成,而这两种角色分别对应着控制节点和计算节点
- Master节点
由三个紧密协作的独立组件组合而成:
- 负责 API 服务的 kube-apiserver
- 负责调度的 kube-scheduler
- 负责容器编排的 kube-controller-manager
-
Etcd
负责整个集群的持久化数据,由 kube-apiserver 处理后保存在 Etcd 中 -
Node节点
核心组件: kubelet
CRI(Container Runtime Interface)的远程调用接口:接口定义了容器运行时的各项核心操作,比如:启动一个容器需要的所有参数;Kubernetes不关心部署的是什么容器运行时、使用的什么技术实现,只要容器运行时能够运行标准的容器镜像,它就可以通过实现 CRI 接入到 Kubernetes 项目当中(同时也是Kubernetes抛弃docker引擎的原因,docker是不符合CRI规范的)
OCI(Open Container Initiative):容器运行时规范同底层的 Linux 操作系统进行交互接口,把 CRI 请求操作转化成对 Linux 操作系统的调用(操作 Linux Namespace 和 Cgroups 等)
Device Plugin:kubelet通过 gRPC 协议与Device Plugin交互,用来管理 GPU 等宿主机物理设备
CNI(Container Networking Interface):kubelet通过CNI调用网络插件,可以为容器配置网络
CSI(Container Storage Interface):kubelet通过CSI调用存储插件, 实现持久化存储
从Kubernetes的架构中可以看出,项目并没有把 Docker 作为整个架构的核心,而仅仅把它作为最底层的容器运行时的一种引擎实现而已。
核心功能图
图中各种资源类型的关系说明:
Pod 是 Kubernetes 项目中最基础的一个对象,实际上是在扮演传统基础设施里“虚拟机”的角色;而容器,则是这个虚拟机里运行的用户程序。
有了 Pod 之后,一般都希望能一次启动多个应用的实例,于是就需要 Deployment 这个 Pod 的多实例管理器;而有了这样一组相同的 Pod 后,我们又需要通过一个固定的 IP 地址和端口以负载均衡的方式访问它,于是就有了 Service,而ingress和Service、Deployment一样,也是kubernetes的一种资源类型,用于实现用域名的方式访问k8s内部应用。
Ingress为Kubernetes集群中的服务提供了入口,可以提供负载均衡、SSL终止和基于名称的虚拟主机,在生产环境中常用的Ingress有Treafik、Nginx、HAProxy、Istio等
kubernete的安装
对于开发来说,学习 Kubernetes 本身知识点是核心,我们不用太关注如何手工部署 Kubernetes 集群,可以直接使用 MiniKube 或者 Kind,来在本地启动简单的 Kubernetes 集群进行后面的学习即可
MiniKube:https://minikube.sigs.k8s.io/docs/start
Kind:https://github.com/kubernetes-sigs/kind
容器编排管理
Pod对象
pod中容器的相互关系
在 Kubernetes 里,Pod 的实现需要使用一个中间容器,叫作 Infra 容器。在 Pod 中,Infra 容器永远都是第一个被创建的容器,而其他用户定义的容器,则通过 Join Network Namespace 的方式,与 Infra 容器关联在一起。组织关系示意图:
pod容器的sidecar设计模式
apiVersion: v1
kind: Pod
metadata:
name: javaweb-2
spec:
initContainers:
- image: geektime/sample:v2
name: war
command: ["cp", "/sample.war", "/app"]
volumeMounts:
- mountPath: /app
name: app-volume
containers:
- image: geektime/tomcat:7.0
name: tomcat
command: ["sh","-c","/root/apache-tomcat-7.0.42-v2/bin/start.sh"]
volumeMounts:
- mountPath: /root/apache-tomcat-7.0.42-v2/webapps
name: app-volume
ports:
- containerPort: 8080
hostPort: 8001
volumes:
- name: app-volume
emptyDir: {}
说明:
- 在 Pod 中,所有 Init Container 定义的容器,都会比 spec.containers 定义的用户容器先启动;
- Init Container 容器会按顺序逐一启动,直到它们都启动并且退出了,用户容器才会启动
在 Init Container 类型的 WAR 包容器启动后,执行了一句"cp /sample.war /app",把应用的 WAR 包拷贝到 /app 目录下,然后退出。而后这个 /app 目录,就挂载了一个名叫 app-volume 的 Volume。
而Tomcat 容器,同样声明了挂载 app-volume 到自己的 webapps 目录下。所以,等 Tomcat 容器启动时,它的 webapps 目录下就一定会存在 sample.war 文件:这个文件正是 WAR 包容器启动时拷贝到这个 Volume 里面的,而这个 Volume 是被这两个容器共享的。
像这样用一种“组合”方式,解决了 WAR 包与 Tomcat 容器之间耦合关系的问题。这种“组合”操作,正是容器设计模式里最常用的sidecar模式。
在上述的Pod例子中,Tomcat 容器是我们要使用的主容器,而 WAR 包容器的存在,只是为了给它提供一个 WAR 包而已。所以用 Init Container 的方式优先运行 WAR 包容器,扮演了一个 sidecar 的角色。
pod的生命周期
Pod 生命周期的变化,主要体现在 Pod API 对象的 Status 部分,这是它除了 Metadata 和 Spec 之外的第三个重要字段。其中,pod.status.phase,就是 Pod 的当前状态。Pod 对象在 Kubernetes 中的生命周期有如下几种可能的情况:
Pending:这个状态意味着,Pod 的 YAML 文件已经提交给了 Kubernetes,API 对象已经被创建并保存在 Etcd 当中。但是,这个 Pod 里有些容器因为某种原因而不能被顺利创建。比如,调度不成功。
Running:这个状态下,Pod 已经调度成功,跟一个具体的节点绑定。它包含的容器都已经创建成功,并且至少有一个正在运行中。
Succeeded:这个状态意味着,Pod 里的所有容器都正常运行完毕,并且已经退出了。这种情况在运行一次性任务时最为常见。
Failed:这个状态下,Pod 里至少有一个容器以不正常的状态(非 0 的返回码)退出。这个状态的出现,意味着你得想办法 Debug 这个容器的应用,比如查看 Pod 的 Events 和日志。
Unknown:这是一个异常状态,意味着 Pod 的状态不能持续地被 kubelet 汇报给 kube-apiserver,这很有可能是主从节点(Master 和 Kubelet)间的通信出现了问题。
pod探针livenessProbe
在Kubernetes中,可以为 Pod 里的容器定义一个健康检查“探针”(Probe)。kubelet 就会根据这个 Probe 的返回值决定这个容器的状态,而不是直接以容器镜像是否运行(来自 Docker 返回的信息)作为依据。这种机制,是生产环境中保证应用健康存活的重要手段。
-
只要 Pod 的 restartPolicy 指定的策略允许重启异常的容器(比如:Always),那么这个 Pod 就会保持 Running 状态,并进行容器重启。否则,Pod 就会进入 Failed 状态
-
对于包含多个容器的 Pod,只有它里面所有的容器都进入异常状态后,Pod 才会进入 Failed 状态。在此之前,Pod 都是 Running 状态。此时,Pod 的 READY 字段会显示正常容器的个数
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
shareProcessNamespace: true
containers:
- name: shell
image: busybox
stdin: true
tty: true
- name: nginx
image: nginx:1.18.0
imagePullPolicy: Always
tty: true
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: localhost
port: 80
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3