admin管理员组

文章数量:1546852

【带你上手云原生体系】第一部分:文章简介 及 云原生思维概览
【带你上手云原生体系】第二部分:Go语言从入门到精通
【带你上手云原生体系】第三部分:Docker从入门到精通
【带你上手云原生体系】第四部分:Kubernetes从入门到精通


持续更新中:2022年8月16日 17:00



文章目录

  • 持续更新中:2022年8月16日 17:00
  • 1、云计算
    • 1.1、什么是云计算
    • 1.2、云计算平台的分类
      • Openstack简介
      • Google Borg简介
        • 特性
        • 优势
        • 基本概念
        • Borg架构
        • Borg 系统在应用高可用的层面做了什么
        • Borg 系统的自身高可用
        • Borg 系统的资源利用率
        • Borg 调度原理
        • Borg 隔离性
  • 2、Kubernetes安装
    • 2.1、使用kubeadm安装工具安装K8S
      • 让 iptables 看到桥接的流量
      • 安装 kubeadm、kubelet、kubectl
      • kubeadm 初始化
        • 报错“runtime.v1alpha2.RuntimeService”
      • 复制 kubeconfig
      • Untaint master
    • 2.2、安装 calico cni 插件
    • 2.3、安装metrics-server
    • 2.4、如果要在启动期间启用 containerd,请在 kubeadm init 期间设置 cri-socket 参数
  • 3、初识 Kubernetes
    • 3.1、什么是Kubernetes(k8s)
    • 3.2、命令式( Imperative)vs 声明式( Declarative)
      • 命令式系统关注 “如何做”
      • 声明式系统关注“做什么”
        • 声明式(Declaritive)系统规范
    • 3.3、Kubernetes:声明式系统
    • 3.4、Kubernetes 的架构
    • 3.5、Kubernetes 的主要组件
    • 3.6、Kubernetes 的主节点(Master Node)
    • 3.7、Kubernetes 的工作节点(Worker Node)
    • 3.8、etcd
      • etcd简介
      • 如何直接访问 etcd 的数据
    • 3.9、APIServer
      • 简介
      • APIServer 展开
    • 3.10、Controller Manager
      • 控制器的工作流程
      • Informer 的内部机制
      • 控制器的协同工作原理
        • 原理
        • 流程(创建Deployment+service=高可用)
        • deployment的rollingUpdate strategy 滚动升级
    • 3.11、Scheduler
    • 3.12、Kubelet
    • 3.13、Kube-Proxy
    • 3.14、推荐的 Add-ons
  • 4、Kubectl 命令和 kubeconfig
    • 4.1、Kubectl 命令
      • kubectl命令到底做了什么
    • 4.2、kubeconfig
      • yaml文件的基本语法
        • \> 语法
        • \| 语法
        • \- 语法
      • ~/.kube/config文件说明
  • 5、常用命令
    • 5.1、常用对象全名及缩写
    • 5.2、Kubernetes系统相关命令
    • 5.3、kubectl get命令
      • all
      • componentstatuses
      • node
      • pod
      • namespace
      • replicasets
      • services
      • deployments
      • replication controller
      • serviceAccount
    • 5.4、kubectl describe命令
    • 5.5、kubectl run命令
    • 5.6、kubectl create命令
      • pod
      • deployment
      • namespace
    • 5.7、kubectl edit命令
      • pod
      • svc
      • deployment
    • 5.8、kubectl delete命令
      • Pod
      • svc
      • Label
    • 5.9、kubectl expose命令
      • pod
      • deployment
      • replicationcontrollers
    • 5.10、kubectl exec命令
    • 5.11、kubectl logs命令
    • 5.12、kubectl label命令
      • pod
      • namespace
  • 6、深入理解Kubernetes
    • 6.1、云计算的传统分类
    • 6.2、Kubernetes 生态系统
    • 6.3、Kubernetes 设计理念
    • 6.4、Kubernetes Master
    • 6.5、分层架构
    • 6.6、API 设计原则
    • 6.7、Kubernetes 如何通过对象的组合完成业务描述
    • 6.8、架构设计原则
    • 6.9、引导(Bootstrapping)原则
  • 7、API 对象的四大属性
    • 7.1、TypeMeta
    • 7.2、Metadata
      • Label
      • Annotations
    • 7.3、Spec 和 Status
  • 8、核心对象概览
    • 8.1、常用 Kubernetes 对象及其分组
    • 8.2、Node
    • 8.3、Namespace
    • 8.4、ConfigMap
      • 创建ConfigMap的4种方式:
        • 1. 通过命令行参数 --from-literal 创建
        • 2. 指定文件创建
        • 3. 指定目录创建
        • 4. 通过事先写好configmap的标准yaml文件创建
      • ConfigMap的两种使用方式
        • 1. 通过环境变量的方式,直接传递给Pod
        • 2. 作为volume的方式挂载到Pod内
    • 8.5、密钥对象(Secret)
      • Secret有三种类型
      • Opaque
      • Service Account
      • kubernetes.io/dockerconfigjson
      • Secret对象和ConfigMap对象的比较
    • 8.6、Pod
      • 什么是 Pod
      • 如何通过 Pod 对象定义支撑应用运行
        • 方式一:环境变量
        • 方式二:外挂存储卷
      • Pod 网络
      • 资源限制
        • kubectl set resources命令,限制CPU和Memory
        • kubectl set resources命令 的底层原理
      • 健康检查
        • readinessProbe实验
    • 8.7、Service
      • Service Spec
    • 8.8、无状态副本集(Replica Set)
    • 8.9、部署(Deployment)
      • Try it
    • 8.10、有状态服务集(StatefulSet)
      • Statefulset 与 Deployment 的差异
    • 8.11、任务(Job)
    • 8.12、后台支撑服务集(DaemonSet)
    • 8.13、存储 (PV 和 PVC)
    • 8.14、CRD
  • 9、Kubernetes 控制平面组件的深入理解
    • 9.1、etcd
      • 简介
      • 主要功能
      • 使用场景
      • 键值对存储
      • 服务注册与发现
      • 消息发布与订阅
      • 概念术语
      • Etcd的安装
      • 第三方库和客户端工具
      • etcdctl命令的使用
        • 理解Kubernetes如何使用etcd机制
        • 使用etcdctl命令访问在Kubernetes里面的etcd
          • 方式一:通过kubectl exec命令进入etcd容器中执行etcdctl
          • 方式二:通过指定endpoints、CA证书文件、签名密钥对文件 的方式访问K8S的etcd
      • 核心:TTL & CAS
      • CAP原则


1、云计算

1.1、什么是云计算

​ 现在的趋势是,从架构层面看,任何的应用都从单体架构开始往微服务架构迁移了,那么一家公司就可能会把整个网站或者所有业务的上下游从一个单体应用被分散到几百个微服务,几千几万的实例上面去。面向的是上千上万台服务器,面向数千数万的应用实例,这个时候人工操作会出现大量的纰漏,花大量的人力,而且从效能提升的角度来看,需要自动化,利用自动化替换掉原来的人工操作,因为重复性劳动一般被认为是低效没有创新的,所以优化的时候都选择此入手,为了满足这种需求,就有了云计算的概念。

​ 云计算是对所有计算资源、网络资源、存储资源的一种抽象。

例如:我有五千台服务器:

  • 从云平台角度看:
    1. 我可以把它们从网络上打通,形成一个集群。
    2. 有一个控制平面,把这些计算资源抽象出来,即五千个节点上有多少CPU、Memory我是知道的,那么对整个集群来说,哪些节点是健康的、不健康的、谁能参与计算、谁不能,这些我都是知道的。那么我就拥有了一个大的计算池,并且我把计算资源抽象出来了。
  • 从业务角度看:
    1. 不用管我们的业务部署到哪里,只需要知道业务要跑多少个实例,每个实例需要多少CPU、Memory、多少存储即可,交由云平台,云平台自己会帮我们找适合我们的计算节点。


1.2、云计算平台的分类

云计算平台主要分为两类:虚拟化平台和基于进程的作业调度平台

  • 虚拟化平台:把一台物理机切成多个虚拟机,在虚拟机的基础之上去部署应用,比如VMware、VirtualBox、Openstack(云的方案)等
  • 基于进程的作业调度平台:比如谷歌borg
  • 以Openstack为典型的虚拟化平台
    • 虚拟机构建和业务代码部署分离
    • 可变的基础架构使后续维护风险变大
  • 以谷歌borg(Google内部的集群管理系统)为典型的基于进程的作业调度平台
    • 技术的迭代引发borg的换代需求
      • 早期的隔离依靠chroot jail实现
      • 一些不合理的设计需要在新产品中改进
        • 对象之间的强依赖job和task是强包含关系,不利于重组
        • 所有容器共享IP,会导致端口冲突,隔离困难等问题
        • 为超级用户添加复杂逻辑导致系统过于复杂

Openstack简介

  • 2002年,美国著名的电商公司亚马逊(Amazon)向客户推出了一项全新的业务——包括存储空间、计算能力等资源服务的Web Service。这就是大名鼎鼎的AWS(Amazon Web Service)

    说白了,这个Web Service服务,就是为大家提供“远程电脑”。你可以远程控制它,有硬盘,有CPU,有内存啥的。你在上面配置你的各种服务,然后给你的用户使用,例如网站、FTP等。

    这个就是云计算的一种早期形式。

  • 2006年,亚马逊又推出了弹性计算云(Elastic Compute Cloud),也称 EC2 。EC2配置界面更简单,使用起来更方便,关键一点,它开始有了“弹性”!(可以根据用户的需要,动态增加和删减资源,不用中断用户的使用,更无需全新申请)

  • 同样是2006年,8月9日,Google首席执行官埃里克·施密特在搜索引擎大会上首次提出**“云计算”(Cloud Computing)**的概念。从此,云计算进入了高速发展阶段。

  • 到了2010年,当时有一家名叫Rackspace的公司,他们一直在做和亚马逊一样的云主机和云储存服务,但是始终都干不过亚马逊,排名第二。他们一气之下,干脆就把它们的云储存服务给开源了。

    开源就是开放源代码,把程序的代码公开了,给所有人免费查看和使用。

    和他们一起开放源代码的,还有一个家伙,就是——NASA

    Rackspace和NASA并不是简单地代码一丢完事,而是联手共同成立了一个开源项目。这个项目,就是OpenStack

  • 为了保证项目能规范、有序地推进下去,还是需要有人“牵头”和“打杂”的。

    OpenStack作为一个开源项目,它是由开源社区来负责推进和维护的。

    目前仅次于LINUX的全球第二大开源社区。

    首先,有一个OpenStack基金会,基金会成员有三种形式。

    • 独立个体:也就是以个人名义为OpenStack做出贡献。
    • 铂金会员:主要由对OpenStack作出重要承诺的公司组成,他们提供资金与资源。例如:华为、Intel、redhat、suse等。
    • 金牌会员:同样由公司组成,他们赞助的资金与资源比铂金会员稍微少一些。例如:Ubuntu、中国移动、中国电信、中国联通、中兴、腾讯云等。

    其次,从2010年项目诞生之日起,OpenStack开源社区每年都会开两次设计峰会(Design Summit)

  • Openstack的版本不是按照“数字序号命名”,而是从当次设计峰会所在城市中选一个地名,作为该版本的名字,但是也是有规律的即版本号的第一个字母,版本号就从A开始,然后B、C、D…

  • OpenStack从一开始,就是为了云计算服务的。简单来说,它就是一个操作系统,一套软件,一套IaaS软件。

  • 云计算的三种服务模式:IaaS、PaaS、SaaS

    Infrastructure as a Service,基础设施即服务

    基础设施资源,主要包括三个方面:计算、存储、网络。说通俗点,就是CPU,硬盘,网卡。

  • OpenStack对资源进行管理,并且以服务的形式提供给上层应用或者用户去使用。

  • Openstack核心组件

    OpenStack的组件都有自己的功能定位。其实,每个组件都可以算是独立的一个程序(Software)。

    Open为开放之意,Stack则是堆砌,也就是许多Open的Softwares进行集合和堆砌。

  • Openstack的管理页面

  • OpenStack之所以这么受欢迎,主要原因有三个方面

    • 首先是快速。OpenStack安装部署所需要的时间很少,而时间就是价值。
    • 其次是灵活。OpenStack获得了各大领导厂商的广泛支持,兼容性和适用性极强,使用起来非常方便可靠。
    • 最后是便宜。作为开源项目,OpenStack的使用成本相对低廉,还能获得源源不断的更新,因为开源社区在为项目贡献活力。
  • 目前国内公有云基于OpenStack的是华为云和金山云、腾讯云、京东。阿里云是基于自研底层的飞天,并不是基于OpenStack的;

  • Openstack所有代码云采用Python开发

  • Openstack没有被淘汰,只不过被K8S抢了一些风头


Google Borg简介

Google内部的集群管理系统,是典型的基于进程的作业调度平台、调度系统。

Borg支持两类应用:

  • long running service(long production service):我们接触到的一般是在线服务、在线任务,比如Gmail、Google Docs、Web Search等,这些应用的特点是都需要高可用(在任何时候都是可以提供稳定的服务的状态)
  • non-production service:一般是短时作业、临时作业、离线任务。


特性
  • 物理资源利用率高。(没有虚拟化、支持混部)
    • 混部:针对需求的波峰、波谷做资源的调整;例如一个网站白天访问量大,晚上大家都睡着了,访问量低,那么晚上这些机器的资源就浪费了,我们就可以在晚上的时候跑一些作业。即把prod应用和non-prod应用部署在一台机器上面。
  • 服务器共享,在进程级别做隔离。
  • 应用高可用,故障恢复时间短。
  • 调度策略灵活。
  • 应用接入和使用方便,提供了完备的Job描述语言(点出了K8S的一个方向),服务发现,实时状态监控和诊断工具。


优势
  • 对外隐藏底层资源管理和调度、故障处理等(这就是云平台的优势)。
  • 实现应用的高可靠和高可用。
  • 足够弹性,支持应用跑在成千上万的机器上。

所以,Borg也就变成了Kubernetes的指导原则。



基本概念

  • Cell:就是一个集群。一般来讲我们会去探讨一个集群应该有多大,这就要从多维度去考虑了。一个集群的控制面到底能管理多大的集群,Kubernetes号称可以管理5000台机器,有些公司希望把它放大那就做各种优化变它变成1万了;有些觉得集群太大不好,比如故障域控制:一个集群如果过大,那么控制面坏掉了影响就会更大。如果过小的话就会面临过多的集群,面临过多的集群意味着你的维护成本就上去了。
  • Job和Task:在Borg里面,一个Job是一个基本的调度单位。Task是Job的子单位,类似于作业。以K8S为例的话大概就是一个pod和container这样的关系。
  • Naming:任何的微服务平台,最终要提供的是一个微服务和微服务之间的调用关系,那么就需要提供一套服务注册和服务发现的机制,Naming就是这样的一套机制,通过Borg Name Service来提供域名服务,即为运行在Borg系统上的每一个微服务应用提供域名,那么应用和应用之间就可以通过域名去访问了。


Borg架构

了解Borg架构也就了解了Kubernetes前身,再回头看Kubernetes就会发现很多共通性,如果每天我们需要做一套自己的云平台,那么架构也是八九不离十的。

架构图讲解:

  • 一个Cell就是一个集群

  • 每一个蓝色立方体:真实的物理机器,是参与计算的节点。

    在集群初始化的时候,所有的这些蓝色节点上都是空的,没有任何的作业,在上面安装了Borglet组件(是在每台物理机上的一个代理)。

  • BorgMaster组件就是大脑,用来接收用户的指令,然后去做调度,然后把作业发到每个计算节点上面(每台物理机),我们可以从集群中选出5台来作为整个集群的管理节点,剩下的节点就作为计算节点。

    在管理节点中,我们要安装额外的管理组件:

    • persistent store:是一个是基于Paxos协议的分布式键值存储数据库;
    • UI:去接收用户的请求;
    • scheduler调度器:因为我们是作业调度系统,就必然要有一个调度器。
  • 这样从Borg架构层面,不复杂,有一个数据库用来存数据、UI去接收请求、调度器用来做调度,然后调度完了以后把请求下发。作业调度到某个节点后,节点的Borglet负责把进程启动起来、然后同时汇报应用的状态、节点的状态。

  • Borgmaster主进程:

    • 处理客户端RPC请求,比如创建Job,查询Job等。
    • 维护系统组件和服务的状态,比如服务器、Task等。
    • 负责与Borglet通信。
  • Scheduler进程:

    • 调度策略

      • Worst Fit:与Best Fit不同,即看哪个节点空闲的资源最多,就把作业放在哪。即作业的执行永远找最空闲的机器。那最终整个集群的所有节点的资源利用率差不多,不会出现一部分机器特别忙、一部分机器特别闲的情况。

      • Best Fit:一个作业假如需要1个CPU、2G的内存,那么就试图在集群中找一个节点是刚刚满足需求的、即尽量填满某个节点,然后把作业放到这个节点上去执行。这样的好处就是让这个集群出现碎片的可能变得最小了,即刚好装箱装满。

        那么最终整个集群在理想状态下是一部分特别忙、一部分特别闲,那么我们就可以通过收缩把闲着的节点先踢出去。那么在公有云上就是把集群规模缩小、那么费用就降低了,节省了成本。那么在私有云上就可以把这些节点关机,关机省电,也节省了成本。

        一般调度就是一个装箱问题,我有一堆作业,如果要把这堆作业塞到不同的节点上面去,Best Fit就是把这些作业刚刚好放进去或者尽量的填满某个节点。

      • Hybrid

    • 调度优化

      • Score caching:当服务器或者任务的状态未发生变更或者变更很少时,直接采用缓存数据,避免重复计算。
      • Equivalence classes:调度同一Job下多个相同Task只需计算一次。
      • Relaxed randomization:引入一些随机性,即每次随机选择一些机器,只要符合需求的服务器数量达到一定值时,就可以停止计算,无需每次对Cell中所有服务器进行feasibility checking。
  • Borglet:

    • Borglet是部署在所有服务器上的Agent,负责接收Borgmaster进程的指令。

所以,调度器里面可玩的花样是非常多的,这里之所以将很多理论是因为一个系统的出现并不是偶然的,一定是很多以前的经验、知识积累把这些思想带到新系统上来,再用新的思维、技术手段去实现,这就是一个系统的变革、迭代。

这也就是为什么非常看好Kubernetes的原因,等到后面我们介绍面向对象设计的时候再详谈,其实谈的就是API的定义、一个module的定义,不涉及到实现。



Borg 系统在应用高可用的层面做了什么
  • 被抢占的 non-prod 任务放回 pending queue,等待重新调度。

    • 之前介绍了Borg资源利用率高有一个原因是因为混部(即把prod应用和non-prod应用部署在一台机器上面)。如果节点本身资源利用率低,那么prod和non-prod任务皆大欢喜,大家和平相处。如果突然在线请求业务多了(即prod应用),那么更多的在线业务要跑起来,但是资源被non-prod应用占掉了,那么怎么办呢?这时候就有一个抢占的概念,在线应用prod要把non-prod应用占用的资源抢回来,non-prod应用kill掉,回到任务的队列里面,等待重新调度。
    • 这样就尽可能保证在线业务(prod应用)的资源。
  • 多副本应用跨故障域部署。所谓故障域有大有小:比如相同城市、相同机器、相同机架或相同电源插座等,一挂全挂。

  • 对于类似服务器或操作系统升级的维护操作,避免大量服务器同时进行。

  • 支持幂等性,支持客户端重复操作。

    • 幂等性就是对于离线系统、异步系统,我们经常会一个作业发出去,然后告诉客户端我们接受了这个作业,但是客户端有时候会重复提交的,可能会再提交一次。所谓的幂等性就是不管这个作业的请求提交多少次,那么对提供服务的这一端即服务端会去重,不管你提交多少次请求过来,服务端给客户端的返回都是一样的,那么这样就容忍了重复提交的场景。
  • 当服务器状态变为不可用时,要控制重新调度任务的速率。因为 Borg 无法区分是节点故障还是出现了短暂的网络分区,如果是后者,静静地等待网络恢复更利于保障服务可用性。

  • 当某种 “任务 @ 服务器” 的组合出现故障时,下次重新调度时需避免这种组合再次出现,因为极大可能会再次出现相同故障。

  • 记录详细的内部信息,便于故障排查和分析

  • 保障应用高可用的关键性设计原则:无论何种原因,即使 Borgmaster 或者 Borglet 挂掉、失联,都不能杀掉正在运行的服务(Task)。



Borg 系统的自身高可用
  • Borgmaster 组件多副本设计。

  • 采用一些简单的和底层(low-level)的工具来部署 Borg 系统实例,避免引入过多的外部依赖。

  • 每个 Cell 的 Borg 均独立部署,避免不同 Borg 系统相互影响。



Borg 系统的资源利用率
  • 通过将在线任务(prod)和离线任务(non-prod,Batch)混合部署,空闲时,离线任务可以充分利用计 算资源;繁忙时,在线任务通过抢占的方式保证优先得到执行,合理地利用资源。

  • 98% 的服务器实现了混部。

  • 90% 的服务器中跑了超过 25 个 Task 和 4500 个线程。

  • 在一个中等规模的 Cell 里,在线任务和离线任务独立部署比混合部署所需的服务器数量多出约 20%-30%。 可以简单算一笔账,Google 的服务器数量在千万级别,按 20% 算也是百万级别,大概能省下的服务器采 购费用就是百亿级别了,这还不包括省下的机房等基础设施和电费等费用。



Borg 调度原理

当用户在跑一个作业的时候,用户需要设置资源的limit,但很多时候,用户并不知道自己资源的limit是多少,就比如我们开发了一个应用,应该跑多少CPU、多少Memory其实我们自己都不知道,很难去评估。

那么,Borg是可以让用户自己去设置资源的limit的,你可以设的很高,但是Borg本身会做动态的评估计算,会监控你的任务的资源利用率。即Borg会在你的任务启动300秒后,Borg会自己做评估,评估出来的结果:比如你的真实使用率如下图绿色部分,但是你申请的资源limit是黄色这么多,那么意味着蓝色+黄色部分都是可以进行资源回收的,但是Borg会保守的为你保留蓝色区域的资源limit,回收黄色区域的资源limit,所以Borg会在task任务启动300秒以后,通过对你task任务的资源使用率做评估,进而根据评估的结果来降低此task分配资源的limit。



Borg 隔离性
  • 安全性隔离:

    • 早期采用 Chroot jail,后期版本基于 Namespace。
  • 性能隔离:

    • 采用基于 Cgroup 的容器技术实现。

    • 在线任务(prod)是延时敏感(latency-sensitive)型的,优先级高,而离线任务(non-prod, Batch)优先级低。

    • Borg 通过不同优先级之间的抢占式调度来优先保障在线任务的性能,牺牲离线任务。

    • Borg 将资源类型分成两类:

      • 可压榨的(compressible),CPU 是可压榨资源,资源耗尽不会终止进程;

      • 不可压榨的(non-compressible),内存是不可压榨资源,资源耗尽进程会被终止。


2、Kubernetes安装

master节点至少要2个cpu,否则报错;硬盘推荐30G大小。内存最少4G。

k8s的两种常见部署方案:

  • 传统的部署方式:让k8s自己的相关组件统统运行为系统级的守护进程,这包括master节点上的四个组件,以及每个node节点上的三个组件,都运行为系统级守护进程,但是这种安装方式每一组都需要我们手动解决,包括做证书等等,而安装过程也需要我们手动解决,编辑配置文件也需要手动解决,他的配置过程繁琐而复杂,一般第一次部署需要一天左右的时间能调通。

    kubernetes官方GitHub

  • 使用kubeadm工具:这个工具是k8s官方的集群部署管理工具,是一个快捷搭建kubernetes(k8s)的安装工具,它提供了kubeadm init 以及 kubeadm join这两个命令来快速创建kubernetes集群。

    • kubeadm工具会把Kubernetes四个核心组件(etcd、apiserver、controller manager、scheduler)运行为容器,并通过StaticPod方式运行,而非传统部署模式的守护进程(systemd),只有kubelet是需要单独装在主机上的,其他统统运行为容器,通过kubectl -n kube-system get po命令就可以看到这些核心组件被容器化,好处就是让Kubernetes自己管自己,让Kubernetes的控制平面也是高可用的。
    • 但是kubelet本身还是systemd去管的(因为kubelet本身是用来起Pod的。所以需要kubelet配置成systemd的服务,由systemd拉起来,剩下的由kubelet拉,可以通过cat /var/lib/kubelet/config.yaml查看,可以看到有个"staticPodPath: /etc/kubernetes/manifests",kubelet会一直去扫描staticPodPath路径下的所有文件,只要我们把任何Pod的yaml文件放到staticPodPath路径下,kubelet就认为这个Pod是需要在当前节点起的Pod,接下来Kubernetes会在apiserver那边把这个Pod补出来,然后存到etcd里,这种Pod就叫StaticPod(由特定节点的kubelet守护进程直接管理),同时kubelet会自动为每一个StaticPod在kubernetes的apiserver上创建一个镜像pod(mirror Pod),因为我们可以在apiserver中查询到该pod,但是不能通过apiserver进行控制(例如不能删除)。kubelet就是一个普通的进程。

    kubeadm官方GitHub

    kubernetes kubeadm官方安装文档


  • 总结一下使用kubeadm安装k8s(不管是不是单节点):

    • 在master、nodes节点安装docker、kubeadm、kubelet

    • 在master节点上,执行 kubeadm init命令去初始化master

    • 各nodes节点上,执行kubeadm join命令加入到集群中

2.1、使用kubeadm安装工具安装K8S


让 iptables 看到桥接的流量

  • br_netfilter内核模块:即透明防火墙(Transparent Firewall)又称桥接模式防火墙(Bridge Firewall)。简单来说,就是在网桥设备上加入防火墙功能。透明防火墙具有部署能力强、隐蔽性好、安全性高的优点。

    • 是Linux系统自带的模块,用于将桥接流量转发至iptables链。
    • 更多的系统自带模块可以在/sys/module/目录下找到。
    • 使用lsmod命令列出目前系统中已加载的模块的名称及大小等。
    • 查看Linux系统是否加载了br_netfilter内核模块lsmod | grep br_netfilter
  • Kubernetes 环境中,很多时候都要求节点内核参数设置 bridge-nf-call-iptables = 1

    Kubernetes Network Plugin Requirements

    如果不开启或中途因某些操作导致参数被关闭了,就可能造成一些奇奇怪怪的网络问题,排查起来非常麻烦。[这里有详细讲解](#Kubernetes为什么“要求节点内核参数开启 bridge-nf-call-iptables=1”)

  • /etc/modules-load.d目录详解:

    • /etc/modules-load.d/目录是用来配置系统启动时加载哪些内核模块。
    • 每个配置文件都以 /etc/modules-load.d/xxxxx.conf的样式命名
    • 配置文件仅包含要加载的内核模块名称列表,以换行符分隔。 空行和第一个非空白字符为#或;的行被忽略。
  • /etc/sysctl.d/目录详解:

    • sysctl命令:被用于在内核运行时动态地修改内核的运行参数。可用的内核参数在目录/proc/sys中,即我们可以用sysctl命令修改/proc/sys下所有的参数信息
    • 如果不想每次都执行sysctl命令去修改参数,那么就要此目录中创建.conf的文件,在文件内写上内核参数的配置
  • 综上,这里建议使用下面命令进行配置

# 配置Linux系统启动时需要加载br_netfilter内核模块
$ sudo echo "br_netfilter" >> /etc/modules-load.d/k8s.conf

# 修改br_netfilter内核模块的参数,将桥接的流量传递到iptables的链上,并开启转发
$ cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF

# 从所有系统配置文件中加载内核参数(使br_netfilter内核模块的参数生效)
$ sudo sysctl --system

有一些ipv4的流量不能走iptables链,会导致流量丢失,iptables链为linux内核的一个过滤器,每个流量都会经过他,然后再匹配是否可进入当前应用进程去处理。


安装 kubeadm、kubelet、kubectl

  • Kubeadm:是一个快捷搭建kubernetes(k8s)的安装工具,它提供了kubeadm init 以及 kubeadm join这两个命令来快速创建kubernetes集群。

    • kubeadm init:引导初始 Kubernetes 控制平面节点,创建一个Master节点。
    • kubeadm join:引导一个 Kubernetes Node工作节点或一个额外的控制平面节点,并将其加入集群。
    • kubeadm upgrade:将 Kubernetes 集群升级到更新的版本。
    • kubeadm reset:以恢复 kubeadm init 或 kubeadm join 对此主机所做的任何更改。
  • Kubelet:是 kubernetes 工作节点上的一个代理组件,运行在每个节点上,Kubelet会通过docker本身的interface接口去起一个个的容器。

  • kubectl:是 Kubernetes 的命令行工具(CLI),是 Kubernetes 用户和管理员必备的管理工具。我们使用Kubectl 命令行工具管理 Kubernetes 集群。

  • apt-key:用于管理Debian Linux系统(Ubuntu基于Debian发行版和Gnome桌面环境,而从11.04版起,Ubuntu发行版放弃了Gnome桌面环境,改为Unity)中的软件包密钥。每个发布的deb包,都是通过密钥认证的,apt-key用来管理密钥。

    • apt-key list:列出已保存在系统中key。包括/etc/apt/trusted.gpg/etc/apt/trusted.gpg.d/目录下的密钥
    • apt-key add <keyname >:把下载的key添加到本地trusted数据库中,使用描述性名称,以gpg或asc作为文件扩展名
# 更新apt包索引
$ sudo apt-get update
# 安装 使用Kubernetes apt存储库 所需的包
$ sudo apt-get install -y apt-transport-https ca-certificates curl

# 添加阿里云源的Kubernetes的GPG密钥,-s:静音模式
$ sudo curl -s https://mirrors.aliyun/kubernetes/apt/doc/apt-key.gpg | sudo apt-key add -

# 设置阿里云Kubernetes apt存储库
$ sudo tee /etc/apt/sources.list.d/kubernetes.list <<-'EOF'
deb https://mirrors.aliyun/kubernetes/apt kubernetes-xenial main
EOF

# 更新apt包索引,安装最新版本的 kubelet、kubeadm、kubectl
$ sudo apt-get update
$ sudo apt-get install -y kubelet kubeadm kubectl

# 保持版本,取消自动更新
$ sudo apt-mark hold kubelet kubeadm kubectl

kubeadm 初始化

$ echo "192.168.56.56 jenrey" >> /etc/hosts
$ kubeadm init \
 --image-repository registry.aliyuncs/google_containers \
 --pod-network-cidr=192.168.0.0/16 \
 --apiserver-advertise-address=192.168.56.56

报错“runtime.v1alpha2.RuntimeService”
$ rm -rf /etc/containerd/config.toml
$ systemctl restart containerd
# 再次执行上面kubeadm init命令

最后会得到一个token

kubeadm join 192.168.56.56:6443 --token ioplma.uribvo8z3mb7twfu \
	--discovery-token-ca-cert-hash sha256:4f08fd1fc1d841305051455a409646ebe0f30be456f726f625ad996a10828c0c

复制 kubeconfig

$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config

Untaint master

因为这里是单节点的集群,默认情况master不接受本机pod的,调度是disable的。

$ kubectl taint nodes --all node-role.kubernetes.io/master-

2.2、安装 calico cni 插件

为了让pod可以有网络

https://docs.projectcalico/getting-started/kubernetes/quickstart

$ kubectl create -f https://docs.projectcalico/manifests/tigera-operator.yaml
$ kubectl create -f https://docs.projectcalico/manifests/custom-resources.yaml

2.3、安装metrics-server

Metrics-Server是集群核心监控数据的聚合器,从 Kubernetes1.8 开始,它作为一个 Deployment对象默认部署在由kube-up.sh脚本创建的集群中。

metrics-server是通过Aggregator的方式嵌入到标准的APIServer里面来,最终通过kubectl top命令可以拿到资源的使用率数据(通过top命令最终转化成了一个扩展的一个对象路径,这个对象路径被APIServer接收到以后,APIServer就知道这个路径应该转到另外一个扩展的APIServer里面去),这就是Aggregator存在的场景。即Aggregator是对组件的一个扩展。

# 官方安装方式:

# 单机版安装
$ kubectl apply -f https://github/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

# 高可用安装,请注意:此配置要求集群至少有2个可以调度 Metrics Server 的节点。
$ kubectl apply -f https://github/kubernetes-sigs/metrics-server/releases/latest/download/high-availability.yaml
# 本文使用这种方式安装:

# 下载官方最新yaml文件
$ wget https://github/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

# 查看镜像地址
$ grep -rn image components.yaml 

140:        image: k8s.gcr.io/metrics-server/metrics-server:v0.6.1
141:        imagePullPolicy: IfNotPresent

# 设置镜像地址为阿里云
$ sed -i "s#k8s.gcr.io/metrics-server#registry-hangzhou.aliyuncs/chenby#g" components.yaml

# 查看镜像地址,已更新
$ grep -rn image components.yaml

140:        image: registry-hangzhou.aliyuncs/chenby/metrics-server:v0.6.1
141:        imagePullPolicy: IfNotPresent

# 在spec.template.spec.containers.args中添加tls证书配置选项
$ vim components.yaml
# - --kubelet-insecure-tls
template:
    metadata:
      labels:
        k8s-app: metrics-server
    spec:
      containers:
      - args:
        - --cert-dir=/tmp
        - --secure-port=4443
        - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
        - --kubelet-use-node-status-port
        - --metric-resolution=15s
        - --kubelet-insecure-tls  # 添加此行
        image: registry-hangzhou.aliyuncs/chenby/metrics-server:v0.6.1
        imagePullPolicy: IfNotPresent
# 创建
$ kubectl apply -f components.yaml

# 验证
$ kubectl get pod -n kube-system | grep metrics

metrics-server-5d78c4b4f5-lxhgs   1/1     Running   0              1m

# 查看node资源情况
$ kubectl top node

NAME     CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
ubuntu   278m         13%    1883Mi          49%

# 查看pod资源情况
$ kubectl top pod

NAME                                CPU(cores)   MEMORY(bytes)
nginx-app                           0m           3Mi
nginx-deployment-55649fd747-n42rh   0m           11Mi
nginx-deployment-55649fd747-npn8f   0m           3Mi
nginx-deployment-55649fd747-z6r29   0m           3Mi
nginx-pod                           0m           3Mi
test-secret-myregistry              1m           0Mi

2.4、如果要在启动期间启用 containerd,请在 kubeadm init 期间设置 cri-socket 参数

kubeadm init \
 --image-repository registry.aliyuncs/google_containers \
 --kubernetes-version v1.22.2 \
 --pod-network-cidr=192.168.0.0/16 \
 --cri-socket /run/containerd/containerd.sock \
 --apiserver-advertise-address=192.168.34.2

3、初识 Kubernetes

3.1、什么是Kubernetes(k8s)

​ Kubernetes 是谷歌开源的容器集群管理系统,是 Google 多年大规模容器管理技术 Borg 的开源版本,Kubernetes的功能大部分沿袭了Borg的功能、能力。

主要功能包括:

  • 基于容器的应用部署、维护和滚动升级;

  • 负载均衡和服务发现;

  • 跨机器和跨地区的集群调度;

  • 自动伸缩;

  • 无状态服务和有状态服务;

    • 无状态服务:比如一个Web应用里面只有一个war包和tomcat,它本地不保存任何的数据,这就是无状态的。无状态的特性就是比如这个实例坏了,那么我们直接把这个实例换掉,再起一个新的实例就行了,因为它没有状态,所以新实例和老实例是完全一样的。
    • 有状态服务:比如说一个应用,它会把一些数据存到本地磁盘,如果它坏掉了,这些数据就需要通过一些方式抢救出来,很难说这个实例坏掉了,我就把它换掉就行。有状态复杂性就在于本地的每一个实例都是不一样的,有可能是配置不一样、命令不一样、保存的数据不一样。所以坏掉能直接用新实例替换的就叫无状态,不能就是有状态服务。
  • 插件机制保证扩展性。


上图是Kubernetes早期的图,但是现在本身的架构基本也没变过。

图中得知:在每一个计算节点上都会运行一个Kubelet,Kubelet会通过docker本身的interface接口去起一个个的容器,容器和容器之间依然是通过Namespace和Cgroup去做安全隔离和性能隔离。简单来看在每个节点上的架构就是这样的。


3.2、命令式( Imperative)vs 声明式( Declarative)

命令式系统关注 “如何做”

​ 在软件工程领域,命令式系统是写出解决某个问题、 完成某个任务或者达到某个目标的明确步骤。此方法明确写出系统应该执行某指令,并且期待系统返回期望结果。

​ 一般是交互式系统,比如ls告诉我们当前目录有什么,pwd告诉我们当前在哪等等。人是需要全程参与的,人要根据上一条命令的结果决定下一条命令怎么敲。

​ 一般来说命令式系统(交互式系统)是响应比较快的,或简单的系统,用户给一个命令,此系统会立马返回。



声明式系统关注“做什么”

​ 在软件工程领域,声明式系统指程序代码描述系统应该做什么而不是怎么做。仅限于描述要达到什么目的,如何达到目的交给系统。

​ 比如我们配置了一个yaml文件,通过此文件定义了(跑什么容器镜像、运行什么命令、指定多少CPU多少资源等等)我们要在k8s平台中运行什么样的程序,然后执行kubectl create -f centos-readiness.yaml命令即可。k8s所做的事就是先接收请求并把请求保存下来,然后经过调度把这个请求和某个计算节点绑定起来,然后在计算节点上通过容器的接口,把yaml文件中所定义的镜像容器在此计算节点中跑起来。

​ 即我们只需要约定做什么(例如人只需要配置yaml文件),但是系统怎么做,我是不关心的。

​ K8S就是声明式系统,我告诉你我要什么,你做好了告诉我。

# centos-readiness.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    run: centos
  name: centos
spec:
  replicas: 1
  selector:
    matchLabels:
      run: centos
  template:
    metadata:
      labels:
        run: centos
    spec:
      containers:
      - command:
        - tail
        - -f
        - /dev/null
        image: centos
        name: centos
        readinessProbe:
          exec:
            command:
            - cat
            - /tmp/healthy
          initialDelaySeconds: 5
          periodSeconds: 5
# 通过yaml文件来创建一个centos的Pod
$ kubectl create -f centos-readiness.yaml

# 查看所有Pod
$ kubectl get po
# 可以看到名为centos-c6d568cb9-vkmsl的Pod

# 查看Pod的yaml配置,也可以连着写-oyaml
$ kubectl get po centos-c6d568cb9-vkmsl -o yaml
# 在centos Pod上可以看到Name:centos-c6d568cb9-vkmsl、namespace:default等属性信息
# namespace是用来做隔离的,用来把不同的对象装到不同的namespace,然后我们可以为这个namespace设置权限,来做访问的控制,这样实现的目的是我建的对象,不让别人来动、甚至不让别人来看;从整个集群的角度,即使集群里面有几十万的pod,因为它们部署在不同的namespace,我们每次查询只查特定的namespace,这样查询速度快,而且方便管理。
# 按照特定namespace名来查询所有Pod
$ kubectl -n default get po

声明式(Declaritive)系统规范


3.3、Kubernetes:声明式系统

​ Kubernetes就是一个以声明式系统为核心的平台,这也就是为什么Kubernetes能变成业内标准的核心原因。

  • Kubernetes定义了一堆api,这些api是厂商中立的、通用的api。
  • Kubernetes提供了一套框架来支撑这些api,给其提供了最核心的功能,这些框架代码也完全是厂商中立的,Kubernetes的目标是通过这种复用的、核心的框架代码来解决业内的一些通用问题,例如:高可用怎么做、滚动升级怎么做、故障转移怎么做、扩缩容怎么做等等。所有这些问题的解都跟任何的厂商实现、任何的技术实现没有太大关系,因为这些逻辑都是通用逻辑。

总结:Kubernetes的核心是抽象一些对象,通过通用框架来解决业内的通用问题。任何人任何方案都可以和Kubernetes做对接。所以Kubernetes的生命周期一定会是非常长的。

Kubernetes 的所有管理能力构建在对象抽象的基础上,核心对象包括:

  • Node:计算节点的抽象,用来描述计算节点的资源抽象、健康状态等。kubectl get node命令获取集群中所有的计算节点,NAME就是主机名。

  • Namespace:资源隔离的基本单位,可以简单理解为文件系统中的目录结构。

  • Pod:用来描述应用实例,包括镜像地址、资源需求等。Kubernetes 中最核心的对象,也是打通应用和基础架构的秘密武器。

  • Service:服务如何将应用发布成服务,本质上是负载均衡和域名服务的声明。

3.4、Kubernetes 的架构

采用与 Borg 类似的架构。所以说Borg系统是Kubernetes前身是不为过的。

  • 一个Kubernetes的集群就是Borg的一个Cell。

  • Node:每一个蓝色长方体就是真实的计算节点(Worker Node)、黄色长方体是真实的主节点(Master Node)。

  • Kubelet组件:是 kubernetes 工作节点上的一个代理组件,运行在每个计算节点(Worker Node)上。

    • 和Borg的Borglet是一样的。
    • Kubelet负责:
      • 把每一个计算节点(Worker node)的状态(健康状况、可用资源等信息)上报给API Server,API Server会把这些信息存到etcd里面。
      • Kubelet会通过docker本身的interface接口去起一个个的容器(下载容器镜像、启动容器实例、配置新建Namespace、通过Cgroup控制资源、挂载网络)。
  • API Server组件:是在K8S Master也就是K8S管理节点上的一个组件,API Server组件也是整个集群的API网关;

    • 图中得知组件和组件之间是不通信的,所有的组件都是和API Server进行通信的,API Server组件相当于是整个集群中所有请求的路由器。
    • 但API Server组件本身的逻辑又比较简单,所有的请求给到API Server组件,API Server组件只做认证、鉴权、校验。
    • 只要校验通过了,API Server组件就会把请求的数据存到etcd里。
  • etcd组件:基于Raft协议开发的分布式key-value存储,可用于服务发现、共享配置以及一致性保障(如数据库选主、分布式锁等)。

  • scheduler组件:用来做调度的。

    • 即用户创建一个Pod,这个Pod就会被scheduler组件去做调度。
    • scheduler组件怎么知道当前集群中有哪些节点、谁是健康的、谁是不健康的呢?当然是Kubelet组件上报上来的。
    • 当用户把一个Pod创建出来后,这个请求会发送到API Server组件,scheduler组件就会去找当前集群有哪些节点是最适合当前Pod作业的节点,找到最适合工作的节点后,就会把这个Pod的Node Name更新并写回到API Server,API Server就会保存到etcd里,API Server一旦保存了这个数据,Kubelet就会看到这个Pod作业调度到我这里的计算节点Node上了,那么Kubelet就会去读取当前作业它的镜像是什么,它需要的资源是什么,那么Kubelet就会去调用容器接口,在运行时下载容器镜像,最后会去起容器镜像的容器实例,在起容器实例的时候就会通过Namespace技术把其放到新的Namespace里面、通过Cgroup给容器实例控制资源,然后帮容器挂载网络,这样这个进程就跑起来了。
  • Controllers组件:是一个复杂的系统,里面有很多很多的控制器。不同的控制器,是为了完成不同的业务目的。

    • 上面我们讲过Kubernetes把所有管理能力都抽象成一个个api、一个个对象了。这些对象就是由Controllers组件内部很多复杂的控制器去监控,如果某个感兴趣的对象发生了一些变化,那么Controllers组件的控制器就会去做一些配置的操作,用来确保整个集群计算节点是健康的、里面跑的应用也是健康的。

    • 例如Node Controller:用来处理Node节点坏了(磁盘坏了、断电了、死机了等等)后,里面所有运行的Pod应用都不能提供正常服务了,此时Node Controller就会把坏了的Node节点里面的Pod杀掉,如果是高可用的实例,那么Controllers组件里面还有别的控制器负责在别的Node节点上创建新的实例,这个新的实例重新经过调度就会到新的健康的Node节点上面继续运行。


3.5、Kubernetes 的主要组件

上一章我们看的架构是很简单的,但是我们把架构扩展开来,其实每一个组件都是很复杂的。


上图左边1个是Master节点(Master Node)、右边2个是计算节点(Worker Node)

  • Master Node
    • API Server组件:API网关、被动接收所有请求、做认证授权、准入的动作、把接收的请求下入etcd分布式数据库中
    • controller manager组件:用来观察整个集群、监听不同的K8S对象。里面有很多控制器角色,不同的控制器角色观察不同的K8S对象。一旦某些K8S对象发生变化,controller manager就会发起一个重新配置的动作,用来确保所有的应用都是健康的、活着的。
    • Scheduler组件:是一种特殊的控制器,只去关注创建出来的并且没有绑定Node的Pod,说明这些Pod是需要被调度的,所以Scheduler组件就会把这些Pod过滤出来,然后一个个去调度。Scheduler组件会从API Server获取所有节点的状况,然后按照一定的调度算法,为这些Pod选择一个合适的节点,并且把这个节点的名字更新回这个Pod的属性里面,然后一旦这个Pod和某个节点发生了绑定关系(Scheduler组件绑的),那么对应节点上的Kubelet就会观测到这个变化(Kubelet就会知道这个Pod帮的Node name和本级的Hostname是一致的),说明这个应用是要起到我的本地的,Kubelet就会先去看看这个应用是不是本地已经跑起来了,如果没跑说明需要Kubelet去创建Pod,所以Kubelet组件就会去调用容器各种接口(通过运行时把这个进程拉起来、挂载网络、挂载存储,其实K8S把这些都抽象出来了叫容器运行时接口 (CRI)容器网络接口 (CNI)容器存储接口 (CSI) ),完成这个Pod的启动。
  • Worker Node
    • Kubelet组件:是 kubernetes 工作节点上的一个代理组件,运行在每个计算节点(Worker Node)上。两个作用:1、把每一个计算节点(Worker node)的状态(健康状况、可用资源等信息)上报给API Server,API Server会把这些信息存到etcd里面。2、观测并调用容器接口完成Pod启动。
    • Poxy进程(Kube-proxy):每个计算节点(Worker Node)都会有个Poxy进程。Pod的应用进程跑起来以后怎么提供服务呢?有一个Service对象,用来将一组Pod应用发布成Web服务,别人就可以通过某域名进行访问了,那么对于这样的Service对象,它的负载均衡和域名服务都是由Poxy去配置的。一旦Poxy配置完成以后,客户端就可以通过Service提供的一个访问入口,去访问对应的服务了。

3.6、Kubernetes 的主节点(Master Node)

注意:API Server组件没有逻辑,只是做认证、鉴权、简单的转发(把这个请求发到etcd里面保存下载);etcd里面也没有逻辑;是Controller Manager和Scheduler让整个集群动起来的,Controller Manager和Scheduler会去监控API Server。


3.7、Kubernetes 的工作节点(Worker Node)


3.8、etcd

etcd是一个有状态的应用。

如果一个系统是单节点存储的,那么效率一定是很高的,带来的风险就是数据可能会丢,万一磁盘坏了,数据就没了,那么要保证数据安全就需要备份。

分布式存储让数据安全性提高了一个等级,分布式存储就是用2个、3个…服务器去存同一份数据,多拷几分、冗余的,这样一个节点坏了还有多份拷贝呢。带来的问题就是数据如何保证一致呢?某个节点坏掉了如何确保整个应用是正常的呢?这都是Raft协议去解决的。

etcd简介

etcd是 CoreOS 基于 Raft协议 开发的分布式 key-value 存储的一个组件,可用于服务发现、共享配置以及一致性保障(如 数据库选主、分布式锁等)。

  • 基本的 key-value 存储;

  • 监听机制;即客户端访问etcd数据库不是通过get的方式,而是通过watch的方式,现在什么样你告诉我,等会发生变化了你再继续告诉我,客户端是长连接的方式连上去的,而且有点像消息中间件了,很多分布式系统里面需要的Kafka、RabbitMQ这些都不需要了,因为etcd本身就提供了这些能力,巧妙的解决了组件和组件之间的协调关系。

  • key 的过期及续约机制,用于监控和服务发现;

  • 原子 CAS 和 CAD,用于分布式锁和 leader 选举。

如何直接访问 etcd 的数据

kubectl run --image=nginx nginx
# 查看kube-system这个namespace下所有的Pod,找到“etcd-主机名”的Pod
# 名为kube-system的namespace就是:通过kubeadm安装K8S以后所有控制面组件会放在kube-system这个namespace下。
$ kubectl get po -n kube-system 
# 类似于docker exec,进入etcd pod的sh
$ kubectl exec -it etcd-ubuntu -n kube-system -- sh 

# 上面也可以临时设置一下别名,方便后续使用(永久设置别名请在~/.bashrc文件中添加)
$ alias ks='kubectl -n kube-system'
$ ks get po
$ ks exec -it etcd-ubuntu -- sh

# 进入etcd pod后有一个etcdctl的命令,etcdctl相当于是etcd的客户端
$ export ETCDCTL_API=3
# 通过etcdctl的get的方式来获取etcd数据库里面以/为开头的keys的数据
$ etcdctl --endpoints https://localhost:2379 --cert /etc/kubernetes/pki/etcd/server.crt --key /etc/kubernetes/pki/etcd/server.key --cacert /etc/kubernetes/pki/etcd/ca.crt get --keys-only --prefix /
# 例如这里我们拿到/registry/pods/default/nginx这个key
# 通过etcdctl的watch的方式来观测/registry/pods/default/nginx这个pod的状态变化情况,default是这个nginx pod的namespace的名字;而且执行完命令发现是长连接
etcdctl --endpoints https://localhost:2379 --cert /etc/kubernetes/pki/etcd/server.crt --key /etc/kubernetes/pki/etcd/server.key --cacert /etc/kubernetes/pki/etcd/ca.crt watch --prefix /registry/pods/default/nginx

新开一个命令行窗口

$ alias k='kubectl'
$ k get po
# 编辑名为nginx Pod的yaml文件,给这个Pod在"metadata"下"annotations"下增加"a: b"的属性,保存
$ k edit po nginx
# 保存后,发现watch的窗口发生了一些变化,最上面输出了PUT,代表刚才nginx这个Pod发生了PUT操作,即观测到了这个变化,这就是etcd watch的机制

通过上面的实验操作,我们了解了etcd 的watch机制,通过这种方式,etcd同时提供了一个安全的数据存储,并且提供了异步消息机制,使得整个集群能够通过etcd的event联动起来。这样每个控制器就不会一直的轮训一直的查数据库。

通过之前的架构图我们得知除了API Server组件,其他的组件都是没有和etcd连接的,所以API Server组件是唯一一个和etcd通信的组件。所以API Server组件在启动的时候会构建一个etcd的watch cache对etcd里面所有数据做监听,然后把监听到的数据放在API Server组件里面进行缓存。API Server组件同样也提供了watch的机制,所有的Kubernetes组件连到API Server也一样首先通过list把当前的状态查询出来,第二使用watch的方式和API Server保持长连接,API Server如果发生任何的变更,都通过event的方式通知每个组件,例如Kubelet组件关注了Pod对象,这个Pod发生了一次PUT有了什么样的变化,那么Kubelet组件应该对此变化去做什么这就是Kubelet组件要去实现的业务逻辑了。


3.9、APIServer

简介

Kube-APIServer 是 Kubernetes 最重要的核心组件之一,主要提供以下功能:

  • 提供集群管理的 REST API 接口,包括:

    • 认证 Authentication

    • 授权 Authorization

    • 准入 Admission(Mutating & Valiating)

  • 提供其他模块之间的数据交互和通信的枢纽(其他模块通过 APIServer 查询或 修改数据,只有 APIServer 才直接操作 etcd)。

  • APIServer 提供 etcd 数据缓存以减少集群对 etcd 的访问。


APIServer是唯一一个和etcd通信的组件,因为APIServer这边有一个watch缓存,这个缓存相当于对etcd做了一定的保护(分布式存储一定是慢的,K8S有那么多的组件都在不停的访问这些数据,所以所有的读操作都从APIServer里读),除非请求参数指定直接读etcd,否则是不会击穿APIServer去直接访问etcd数据库的,大部分读操作在APIServer这里就处理掉了,写操作会回到etcd里面去。

etcd的版本变化有v2、v3版本,v2当时是有非常多的问题,但是现在v3已经没什么问题了。


APIServer 展开

  • APIHandler:处理请求,即把请求交由对应的function去处理。
  • AuthN:全名叫Authentication,即认证、身份验证,与身份有关,即某人是谁。
  • Rate Limit:用来做限流,如果你发很多request过来,可能我就死掉了,所以在做后面复杂逻辑的时候,要限流,看看现在还能不能处理这个请求。如果能处理这个请求就做Auditing。
  • Auditing:记录一个访问日志;这个主要用途就是我们要做一些安全审计,比如别人删除了一些东西怪我们,我们可以查看审计日志,发现不是我们人删除的,是他们自己删除的,这样就保护了我们平台的声誉。很多时候有些破坏性的动作我们要找出是谁做的,也是要通过访问日志来做。
  • AuthZ:全名叫Authorization,即鉴权、授权,与权限有关,即某人被允许做什么。
    • K8S RBAC:基于角色的访问控制,后面再详细谈。
  • Aggregator:聚合器。如果是K8S内置对象应该横着走、走K8S原生流程(即粉色部分);如果不是K8S内置对象,但是我需要发布到APIServer上,Aggregator就会让请求走下面的路线(浅蓝色部分就需要我们自己仿造了,仿造成K8S对应的逻辑,这就是K8S的扩展性,我们可以开发我们自己的APIServer,然后内嵌到原生的APIServer里面去,这样就可以在K8S基础之上实现无限的扩展能力)。
  • Mutating:变形。即APIServer可以把接收到的请求加一个属性或改一个属性。
  • Validation:校验器,可以用内置校验器或咱们附加的自定义校验器(可以放在外边跑成一个服务,然后用APIServer直接访问这个自定义校验器的URL地址)。
  • 上面这些流程都过了才会存到etcd。

3.10、Controller Manager

Controller Manager是一个集合,这个集合里面有很多的控制器,但是这些控制器里面的逻辑是一样的,都是一个个的生产者消费者模型,生产者监控APIServer的变化放到中心的一个队列里面去,消费者从队列中取数据,取出来以后控制器就去做配置,所以任何的控制器就是生产者消费者模型,如果配置失败,那就要把数据放回到队列里去,然后不断的重试。

Controller Manager由 kube-controller-manager 和 cloud-controller-manager 组成,是 Kubernetes 的大脑,它通过 apiserver 监控整个集群的状态,并确保集群处于预期的工作状态。

kube-controller-manager 由一系列的控制器组成

  • Certificate Controller
  • ClusterRoleAggregation Controller
  • Node Controller
  • CronJob Controller
  • Daemon Controller
  • Deployment Controller
  • StatefulSet Controller
  • Endpoint Controller
  • Endpointslice Controller
  • Garbage Collector
  • Namespace Controller
  • Job Controller
  • Pod AutoScaler
  • PodGC Controller
  • ReplicaSet Controller
  • Service Controller
  • ServiceAccount Controller
  • Volume Controller
  • Resource quota Controller
  • Disruption Controller

cloud-controller-manager 在 Kubernetes 启用 Cloud Provider 的时候才需要,用来配合云服务提供商的控制,也包括一系列的控制器,如

  • Node Controller
  • Node Lifecycle Controller
  • Route Controller
  • Service Controller
  • Controller Manager 是集群的大脑,是让整个集群动起来的关键;

  • 作用是确保 Kubernetes 遵循声明式系统规范,确保系统的真实状态(Actual State)与用户定义的期望状态(Desired State)一致;

  • Controller Manager 是多个控制器的组合,每个 Controller 事实上都是一个 control loop(控制回路),负责侦听其管控的对象,当对象发生变更时完成配置;

  • Controller 配置失败通常会触发自动重试,整个集群会在控制器不断重试的机 制下确保最终一致性( Eventual Consistency )


控制器的工作流程

​ 下面就是生产者消费者模型,其中一部分的代码是由K8S代码生成框架自己生成的,例如任何的K8S对象都会生成自己的Informer(event 观察的代码框架)以及Lister(获取全量数据的代码框架),这些代码都是K8S自己封装好的。

​ 所以当我们自己生成控制器的时候,基本上第一步就是通过Informer、Lister去观察、监听某一个对象,监听的对象就会有一些event比如Add event、Delete event、Update event,不同的event可以注册Handler,通过Handler把对象的Key取出来(Namespace和Name)放到RateLimiting queue里面,与此同时起新的goroutine可以配一个或多个worker不停的从RateLimiting queue里面取数据,只要这个队列里还有数据就说明这个集群有对象变化还没有被处理,最终这些worker就会去完成配置,一直完成到这个RateLimiting queue空了为止,如果出错就通再放回到RateLimiting queue里,这就是控制器基本的逻辑。简单来讲就是生产者消费者模型,不用急、慢慢再深入了解。



Informer 的内部机制

建议对应K8S代码去读。

做Kubernetes的人都是对Linux非常熟悉的一波人。

  • informer是list-watch实现中的核心模块,通过它可以注册资源变化的回调函数
  • 一个服务里面可能会起多个controller去消费同一个Resource CRUD事件,如果每个controller都起一个informer,则很浪费资源。通过SharedInformer,不同controller往其中注册Listener就能实现类似观察者模式的处理方式了。

  • Informer会提供一个 List & Watch 机制,List & Watch 机制就是Informer在启动后会发起一个长连接到APIServer,并在第一时间List一下,比如是Pod的List & Watch,那么就会把当前所有Pod List下来。然后接下来就会Watch,Watch APIServer这边有什么对象的变化,就会告诉这个Informer。

  • Reflector:APIserver是一个标准的REST API,当你查询的时候APIServer会返回一个JSON string或者protoBuff的序列化数据,任何程序想要消费这个序列化的数据就需要反序列化(把字串转成Go对象),这个反序列化就是Reflector,Reflector是通过反射机制去做的,反射机制会去解析这些对象的Key(Key里面有json tag),之前在讲Go语言的时候说过json tag,通过Key的json tag就知道对应Go语言的什么属性,通过这种反射机制就把APIServer返回的序列化对象转化成Go语言的struct并放在Memory里面。

  • Delta、Fifo:是一个环状内存结构,好处就是任何时候数据只要一直写就可以了,如果Buffer满了就会覆盖最老的数据。当有数据写入之后,就会去通知Informer去处理。

  • Informer

    • 会把序列化好的对象(例如Pod、Namespace等)放在Thread Safe Store里面,好处就是未来当访问K8S对象的时候就可以用Indexer来访问了,就不用找APIServer去访问了,只需要来自己的Local Store里访问即可、进而减少对APIServer的访问。
    • 同时把对象的变化通过Event发给到EventHandler,如果我们注册了EventHandler就能发现此次事件,然后在放到WorkQueue里面。
  • Worker:从WorkQueue里面取,再进行处理。

需要强调的是:任何的控制器都应该用ShareInformer,假如我们自己写一个控制器,那么只要实现了ShareInformer就有APIServer对象的本地缓存了。由ShareInformer去保证本地缓存和APIServer之间的版本一致性。
所以,我们要写控制器代码的时候要避免访问APIServer,第一,因为有新事件的话ShareInformer会通知我们。第二,对象的信息会存在本地缓存里,读取任何完整对象应该去Local的Thread Safe Store里去读。第三,一般来讲只有要更新一个对象的时候才要去往APIServer去更新。
这样的话,如果每一个组件能够遵循这样的模式,那么APIServer本身的压力是不大的,但是,如果哪一个人写的代码不好,频繁的去读取APIServer,那么很可能就把APIServer压死了。

// informer framework Generated code
secretInformer := kubecoreinformers.NewSecretInformer()-->
	NewFilteredSecretInformer()-->
		NewSharedIndexInformer(&cache.ListWatch{}, &corev1.Secret{}, resyncPeriod, indexers)-->
			sharedIndexInformer := &sharedIndexInformer{
				processor:                       &sharedProcessor{clock: realClock},
				indexer:                         NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, indexers),
				listerWatcher:                   lw,
				objectType:                      exampleObject,
				resyncCheckPeriod:               defaultEventHandlerResyncPeriod,
				defaultEventHandlerResyncPeriod: defaultEventHandlerResyncPeriod,
				cacheMutationDetector:           NewCacheMutationDetector(fmt.Sprintf("%T", exampleObject)),
				clock:                           realClock,
			}

secretInformer.AddEventHandler()-->
	AddEventHandlerWithResyncPeriod()-->
		listener := newProcessListener(handler, resyncPeriod, determineResyncPeriod(resyncPeriod, s.resyncCheckPeriod), s.clock.Now(), initialBufferSize)-->
			ret := &processorListener{
				nextCh:                make(chan interface{}),
				addCh:                 make(chan interface{}),
				handler:               handler,
				pendingNotifications:  *buffer.NewRingGrowing(bufferSize),
				requestedResyncPeriod: requestedResyncPeriod,
				resyncPeriod:          resyncPeriod,
			}
		s.processor.addListener(listener)
			listener.run()-->
				for next := range p.nextCh {
					p.handler.OnUpdate(notification.oldObj, notification.newObj)
					p.handler.OnAdd(notification.newObj)
					p.handler.OnDelete(notification.oldObj)
				}
			listener.pop()-->
				for {
					select {
					case nextCh <- notification:
						notification, ok = p.pendingNotifications.ReadOne()
					case notificationToAdd, ok := <-p.addCh:
							p.pendingNotifications.WriteOne(notificationToAdd)
				}
		for _, item := range s.indexer.List() {
			listener.add(addNotification{newObj: item})-->
				p.addCh <- notification
		}

go secretInformer.Run(ctx.Stop)
	fifo := NewDeltaFIFOWithOptions()
	cfg := &Config{
			Queue:            fifo,
			ListerWatcher:    s.listerWatcher,
			ObjectType:       s.objectType,
			FullResyncPeriod: s.resyncCheckPeriod,
			RetryOnError:     false,
			ShouldResync:     s.processor.shouldResync,
	
			Process: s.HandleDeltas,
		}
	wg.StartWithChannel(processorStopCh, s.cacheMutationDetector.Run)
	wg.StartWithChannel(processorStopCh, s.processor.run)
	s.controller = New(cfg)
	s.controller.Run(stopCh)-->
		r := NewReflector(
			c.config.ListerWatcher,
			c.config.ObjectType,
			c.config.Queue,
			c.config.FullResyncPeriod,
		)
		wg.StartWithChannel(stopCh, r.Run)-->
			r.ListAndWatch(stopCh)-->
				list := pager.List(context.Background(), options) (1)
				items, err := meta.ExtractList(list)
				r.syncWith(items, resourceVersion)-->
					r.store.Replace(found, resourceVersion) (2)
				r.watchHandler(start, w, &resourceVersion, resyncerrc, stopCh)-->
					r.store.Update(event.Object)
		c.processLoop-->
			c.config.Queue.Pop(PopProcessFunc(c.config.Process))//HandleDeltas
				for _, d := range obj.(Deltas) {
					s.processor.distribute(updateNotification)
					s.processor.distribute(addNotification)
					s.processor.distribute(deleteNotification)
				}

控制器的协同工作原理

建议先看下面的流程,再回过头来看原理。

原理

  • Controller Manager是整个集群的大脑,会监控APIServer,Controller Manager里面有很多很多的控制器,不同的控制器会监控不同的对象变化。这就涉及到Deployment Controller控制器。

  • 我们人为的创建了Deployment,在这个Deployment里定义了我要跑1个实例、跑nginx的image。那么我创建的Deployment对象就发给APIServer了。

  • APIServer会去做认证即某人是谁,APIServer会读一个本地的配置文件,这样APIServer就知道我是谁了;然后鉴权,即某人被允许做什么;然后这个Deployment又是合法的,那么这个Deployment就被APIServer接受了并且存到etcd里面了。

  • 上述事件发生以后,Controller Manager就要去干活了,因为是大脑,负责让整个集群动起来。

    # 查看kube-system这个namespace下所有的Pod
    # 名为kube-system的namespace就是:通过kubeadm安装K8S以后所有控制面组件会放在kube-system这个namespace下。
    $ kubectl -n kube-system get po
    
    # 下面就是Controller Manager的Pod了
    kube-controller-manager-ubuntu   1/1     Running   5 (18h ago)   18h
    

    如果我们现在就看Log就能看到Controller Manager在做什么事情。

  • Controller Manager里面有一个Deployment Controller,Deployment Controller顾名思义感兴趣的对象是Deployment。Deployment Controller会干什么完全是看你创建的Deployment代码是怎么写的了

    • Deployment Controller首先会做一个代码部署、要1个副本、运行nginx镜像。所以就会创建ReplicaSet对象,在ReplicaSet对象里面说需要1个replicas、需要什么样的Pod、Pod的模版是什么,并且把这个Deployment对象创建出来并且发送给APIServer

      # 查看ReplicaSet对象细节
      # replicationcontrollers (rc)
      # ReplicaSet (rs)
      # 在新版的Kubernetes中建议使用ReplicaSet (RS)来取代ReplicationController。ReplicaSet跟ReplicationController没有本质的不同,只是名字不一样,但ReplicaSet支持集合式selector。
      $ kubectl get rs 
      NAME                          DESIRED   CURRENT   READY   AGE
      nginx-deployment-6799fc88d8   1         1         1       147m
      
      $ kubectl get rs nginx-deployment-6799fc88d8 -oyaml
      # 重点就是spec-replicas:1、spec-template:整个Pod的模版
      
  • 发送完以后,Controller Manager里面还有一个ReplicaSet Controller,ReplicaSet Controller在监听APIServer,发现有一个ReplicaSet被创建了,就会去分析ReplicaSet语意,原来ReplicaSet要建一个Pod,且这个Pod不存在,那么ReplicaSet Controller就会新建一个Pod,再由ReplicaSet发到APIServer去。所有的控制器都是需要和APIServer通信的。

  • APIServer只需把其固化下来存到etcd里。

  • 而后,查看Pod,里面有一个属性叫nodeName,这个属性在创建之初是没有被写值的,nodeName是空,这个时候调度器就会发现有一个Pod它的nodeName属性是空,说明这个Pod没有经过调度,那么调度器就会给其做调度。调度器就看第一这个Pod长什么样、第二调度器会主要watch 没有调度过的Pod对象,以及当前集群的所有节点来找到哪个节点是最适合这个Pod的。最后通过调度器的bind方法把最适合的节点名字和Pod的nodeName属性产生关系(绑定)。绑定后将结果写会到APIServer。

    $ kubectl get po nginx-deployment-6799fc88d8-gkvw7 -oyaml
    
  • 接下来,节点上的kubelet就开始干活了,kubelet会关注当前APIServer里面和本节点相关的Pod有哪些,因为之前发生了调度,所以kubelet就发现了这个Pod,然后kubelet就会去本地docker ps看一下这个Pod有没有在运行,发现没有运行,就说明这是一个新Pod,那么就会进入一个create Pod的流程。起Pod有如下几个动作:

    • 通过docker run把这个容器启动起来,docker有自己的network manager,CNM,但是很厚重。K8S不用docker的CNM,而是用自己的CNI,因为K8S在起docker的时候,只起runtime,不会去挂载网络的。
    • 接下来K8S会调用网络插件,为这个Pod setup网络。
    • 最后使用CSI(Container Storage Interface)为这个Pod挂载磁盘。
  • 如果我们把Pod删除,就会产生一个event,就是Pod delete事件,这个事件会被ReplicaSet Controller监听到,ReplicaSet Controller监听这个事件就是为了比对,因为它负责副本集,ReplicaSet Controller的职责是确保所有的副本集里面的这些运行Pod的数量和用户期望的数量是一样的,因为我们设置的replicas:1,所以当对应Pod被删除以后,数量是不相等的,用户期望的是1,但是现在是0,ReplicaSet Controller就发现了不等的情况,那么ReplicaSet Controller就会去创建新的Pod,到此ReplicaSet Controller的指责又完成了,后面就是经过调度器调度、调度完kubelet又去起。


流程(创建Deployment+service=高可用)
# 运行一个nginx的Pod
$ kubectl run --image=nginx nginx

# 查看所有Pod信息
$ kubectl get po -owide
nginx   1/1     Running   0          3h5m   192.168.243.214

# 查看nginx Pod的yaml信息,kind: Pod,为Pod对象
$ kubectl get po nginx -oyaml

# 访问nginx
$ curl 192.168.243.214

# 删除Pod
$ kubectl delete po nginx

如果我们是服务提供者,如果有人误删nginx Pod,那么这个服务就会出现问题了。

我们说过K8S最重要的功能是确保应用高可用,所以K8S里面提供了更高阶的对象叫deployment,用来描述应用的部署。

# nginx-deploy.yaml
apiVersion: apps/v1
kind: Deployment  # 看到kind为Deployment对象
metadata:
  name: nginx-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx
# 根据yaml文件创建deployment对象
$ kubectl create -f nginx-deploy.yaml
deployment.apps/nginx-deployment created

# 查看
$ kubectl get deployment.apps/nginx-deployment
NAME                          READY     UP-TO-DATE   AVAILABLE   AGE
nginx-deployment-6799fc88d8   1         1            1           10m
# 或
$ kubectl get po
NAME                                READY   STATUS    RESTARTS   AGE
nginx-deployment-6799fc88d8-gkvw7   1/1     Running   0          10m

# 查看细节
$ kubectl describe deployment.apps/nginx-deployment
Name:                   nginx-deployment
Namespace:              default
CreationTimestamp:      Mon, 25 Jul 2022 11:14:19 +0800
Labels:                 <none>
Annotations:            deployment.kubernetes.io/revision: 1
Selector:               app=nginx
Replicas:               1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
  Labels:  app=nginx
  Containers:
   nginx:
    Image:        nginx
    Port:         <none>
    Host Port:    <none>
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    True    NewReplicaSetAvailable
OldReplicaSets:  <none>
NewReplicaSet:   nginx-deployment-6799fc88d8 (1/1 replicas created)
Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  6m9s  deployment-controller  Scaled up replica set nginx-deployment-6799fc88d8 to 1
  • Events:最近发生的一些事件。Events也是K8S对象,但是不孤立存在,会附着在某一个主对象上面,用来表述这个主对象最近发生过什么。

    • 从上面可知,有一个事件发生了,由deployment-controller发出来的,做了Scaled up replica set nginx-deployment-6799fc88d8 to 1这个动作,创建了replica set

      $ kubectl get replicaset nginx-deployment-6799fc88d8
      NAME                          DESIRED   CURRENT   READY   AGE
      nginx-deployment-6799fc88d8   1         1         1       11m
      
      • 再查看replica set的细节,发现由replicaset-controller发生了一个Created pod: nginx-deployment-6799fc88d8-gkvw7事件

        $ kubectl describe replicaset nginx-deployment-6799fc88d8
        Events:
          Type    Reason            Age   From                   Message
          ----    ------            ----  ----                   -------
          Normal  SuccessfulCreate  21m   replicaset-controller  Created pod: nginx-deployment-6799fc88d8-gkvw7
          
        $ kubectl get po nginx-deployment-6799fc88d8-gkvw7
        NAME                                READY   STATUS    RESTARTS   AGE
        nginx-deployment-6799fc88d8-gkvw7   1/1     Running   0          24m
        
  • 综上:

    1. 用Go写一些代码
    2. 把Go的代码交叉编译成可执行二进制文件
    3. 把可执行二进制文件打包成docker镜像
    4. 写K8S的yaml文件
      1. 指定image为上面的docker镜像
      2. 配置replicas副本数

    这样就能把我们自己的应用运行在K8S上,并且访问了。

  • 冗余部署

# 如果我们delete这个nginx deployment pod会发生什么呢?
$ kubectl delete po nginx-deployment-6799fc88d8-gkvw7
# 发现马上有起了一个新的Pod、新的IP

中间会断几秒钟(老Pod消失,但是新Pod还没起来),这就是很难实现的故障转移,但是在K8S里就如此的简单。但是这个故障转移虽然做了,但是删除老Pod和新Pod启动中间断了几秒,那么对于很重要的应用它是不可被接受的,虽然有一定程度的故障转移,但是没有真正的高可用,真正的高可用就不应该这样做,高可用最重要的是冗余部署。

# deployment可以简写成deploy
$ kubectl edit deployment nginx-deployment
# 修改spec下的replicas: 3

$ kubectl describe deployment.apps/nginx-deployment
# 发现Scaled up replica set nginx-deployment-6799fc88d8 to 3

$ kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-6799fc88d8   3         3         3       3h53m

$ kubectl describe rs nginx-deployment-6799fc88d8
Name:           nginx-deployment-6799fc88d8
Namespace:      default
Selector:       app=nginx,pod-template-hash=6799fc88d8
Labels:         app=nginx
                pod-template-hash=6799fc88d8
Annotations:    deployment.kubernetes.io/desired-replicas: 3
                deployment.kubernetes.io/max-replicas: 4
                deployment.kubernetes.io/revision: 1
Controlled By:  Deployment/nginx-deployment
Replicas:       3 current / 3 desired
Pods Status:    3 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
  Labels:  app=nginx
           pod-template-hash=6799fc88d8
  Containers:
   nginx:
    Image:        nginx
    Port:         <none>
    Host Port:    <none>
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Events:
  Type    Reason            Age   From                   Message
  ----    ------            ----  ----                   -------
  Normal  SuccessfulCreate  48m   replicaset-controller  Created pod: nginx-deployment-6799fc88d8-fqj92
  Normal  SuccessfulCreate  6m3s  replicaset-controller  Created pod: nginx-deployment-6799fc88d8-p5jr2
  Normal  SuccessfulCreate  6m3s  replicaset-controller  Created pod: nginx-deployment-6799fc88d8-hcfcp
  
$ kubectl get po -owide
NAME                                READY   STATUS    RESTARTS AGE     IP                NODE     NOMINATED NODE   READINESS GATES
nginx-deployment-6799fc88d8-fqj92   1/1     Running   0        49m     192.168.243.228   ubuntu   <none>           <none>
nginx-deployment-6799fc88d8-hcfcp   1/1     Running   0        7m29s   192.168.243.229   ubuntu   <none>           <none>
nginx-deployment-6799fc88d8-p5jr2   1/1     Running   0        7m29s   192.168.243.230   ubuntu   <none>           <none>

这样我们现在就有3个nginx在跑了,实现了冗余部署。

  • 负载均衡实现高可用

冗余部署了以后,对于高可用应用我们都是需要做负载均衡的,因为冗余部署后有多个ip,那你让用户访问哪个ip呢?所以我们要提供一个相对稳定的、静态的、统一入口。那么K8S就提供了另外一个对象叫service

$ kubectl get po --show-labels
NAME                               READY  STATUS   RESTARTS      AGE   LABELS
nginx-deployment-6799fc88d8-fqj92  1/1    Running  0  55m   app=nginx,pod-template-hash=6799fc88d8
nginx-deployment-6799fc88d8-hcfcp  1/1    Running  0  13m   app=nginx,pod-template-hash=6799fc88d8
nginx-deployment-6799fc88d8-p5jr2  1/1    Running  0  13m   app=nginx,pod-template-hash=6799fc88d8
# 注意命令中的app要和LABELS中一致
$ kubectl expose deploy nginx-deployment --selector app=nginx --port=80 --type=NodePort
$ kubectl get svc
NAME               TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
nginx-deployment   NodePort    10.99.183.87     <none>        80:32516/TCP   2m46s
$ curl 10.99.183.87
$ kubectl get po
# 删除任意Pod
$ kubectl delete po nginx-deployment-6799fc88d8-hcfcp
# 服务是不受影响的
$ curl 10.99.183.87

这样,我们实现了冗余部署,负载均衡,就实现了高可用!可以放心的部署成生产系统啦~

总结:通过deployment来控制一次部署确保我需要几个实例,K8S就一定会运行几个实例,这样就可以指定多副本+负载均衡service来做真正的高可用的应用。


deployment的rollingUpdate strategy 滚动升级
$ kubectl edit deploy nginx-deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
  creationTimestamp: "2022-07-25T03:14:19Z"
  generation: 2
  name: nginx-deployment
  namespace: default
  resourceVersion: "67032"
  uid: ea058104-60fb-4619-bf40-bc0b3f4897ac
spec:
  progressDeadlineSeconds: 600
  replicas: 3
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app: nginx
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        imagePullPolicy: Always
        name: nginx
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30
status:
  availableReplicas: 3
  conditions:
  - lastTransitionTime: "2022-07-25T03:14:19Z"
    lastUpdateTime: "2022-07-25T03:17:08Z"
    message: ReplicaSet "nginx-deployment-6799fc88d8" has successfully progressed.
    reason: NewReplicaSetAvailable
    status: "True"
    type: Progressing
  - lastTransitionTime: "2022-07-25T07:27:45Z"
    lastUpdateTime: "2022-07-25T07:27:45Z"
    message: Deployment has minimum availability.
    reason: MinimumReplicasAvailable
    status: "True"
    type: Available
  observedGeneration: 2
  readyReplicas: 3
  replicas: 3
  updatedReplicas: 3
  • deployment通过语义得知是“部署”,什么时候我们谈“部署”,就是版本更新的时候,K8S不是一个一次性的工具,是一个云平台的工具,要确保持续运维,一个公司是常年跑着应用的,研发要不停的更新代码,会造成版本变更,docker的image是可以打tag的,这样就可以很好的做版本管控,K8S正是利用了这一点,我们的K8S yaml文件的spec-containers-“image”字段,我们之前写的是image:nginx,
    docker的image是有三部分的:镜像仓库在哪,没指定的话默认就是docker hub;镜像名;版本没写就是latest;

  • 在deployment里面有一个重要的属性叫strategy,里面有rollingUpdate,rollingUpdate就是滚动升级,我们之前已经完成了冗余部署,那么意味着有多个副本,那么在做版本升级的时候只要不把这些副本全部杀死,而是通过一种滚动的方式一部分一部分去更新,那么任何时候都有正常的实例提供当前服务。所以只要实现了滚动升级,应用就是高可用,可以在任何时间做发布新版本,持续服务的,对用户来说是不会有任何感觉的。

    • maxSurge:默认25%;maxSurge指出了我们在滚动更新时,可以有多少个额外的 Pod。假如我们一个应用有2个实例,如果我们先杀掉一个Pod,然后再更新(替换)新版本,那么意味着当前能正常提供服务的实例会减少,万一这个时候请求很高,容易把服务打死。maxSurge就是可以允许可以先多出来25%的新版本Pod实例。
    • maxUnavailable:默认25%;MaxUnavailable 则代表在滚动更新时,我们可以忍受多少个 Pod 无法提供服务。maxUnavailable就是如果有25%的Pod实例出现问题就不要继续往新版本滚了。

    这两个参数可以是 Pod 数量,也可以是 Deployment 的实例数量百分比;两个参数都可以设置为 0(但是不能同时为 0)。

    MaxUnavailable 设置为 0 意味着:“在新 Pod 启动并就绪之前,不要关闭任何旧 Pod”。
    MaxSurge 设置为 100% 的意思是:“立即启动所有新 Pod”,也就是说我们有足够的资源,我们希望尽快完成更新。

    这两个参数的却升值都是 25%,如果我们更新一个 100 Pod 的 Deployment,会立刻创建 25 个新 Pod,同时会关闭 25 个旧 Pod。每次有 Pod 启动就绪,就可以关闭旧 Pod。每次有旧 Pod 完成关闭过程(释放资源),就可以创建另一个新 Pod 了。

  • 演示如何滚动升级

    $ kubectl get deploy
    $ kubectl describe deployment nginx-deployment
    Events:          <none>
    $ kubectl edit deploy nginx-deployment # 把image改成: nginx:latest
    # 其实nginx和nginx:latest是一个镜像,但是template里面的字符串变了
    $ kubectl describe deployment nginx-deployment
    

    如果我们改完立即看Pod,会发现老的Pod被杀,新的在起。

    上图黄色是新建的ReplicaSet。

    新的ReplicaSet变成1、老的变成2、新的变成2、老的变成1、新的变成3、老的变成0,这就是deployment-controller做的事情:它建了一个新的ReplicaSet,逐渐把老版本从3变成0,把新版本从1变成3。这样从老版本到新版本就替换完成了。

    deployment-controller之所以知道要建新版本就是因为我们改了template里面的image值,

    template:
        metadata:
          creationTimestamp: null
          labels:
            app: nginx
        spec:
          containers:
          - image: nginx:latest
            imagePullPolicy: Always
            name: nginx
            resources: {}
            terminationMessagePath: /dev/termination-log
            terminationMessagePolicy: File
          dnsPolicy: ClusterFirst
          restartPolicy: Always
          schedulerName: default-scheduler
          securityContext: {}
          terminationGracePeriodSeconds: 30
    

    即上面的template在deployment-controller里面有一段逻辑:它会把template模版这一段字串作为输入计算字串的哈希值,所以当我们对template模版这一段字串做了变更以后,它的哈希值发生变化,只要哈希值发生变化deployment-controller就认为你要做新版本发布的,deployment-controller就会去算新版本的哈希值,通过kubectl get rs命令可以看到新老版本的ReplicaSet信息,ReplicaSet NAME字段后半部分的字串就是版本的哈希值。

    $ kubectl get rs
    NAME                          DESIRED   CURRENT   READY   AGE
    nginx-deployment-55649fd747   3         3         3       16m
    nginx-deployment-6799fc88d8   0         0         0       23h
    

3.11、Scheduler

特殊的 Controller,工作原理与其他控制器无差别。 Scheduler 的特殊职责在于监控当前集群所有未调度的 Pod,并且获取当前集群所有节点的健康状况和资源 使用情况,为待调度 Pod 选择最佳计算节点,完成调度。

调度阶段分为:

  • Predict:过滤不能满足业务需求的节点,如资源不足、端口冲突等。

  • Priority:按既定要素将满足调度需求的节点评分,选择最佳节点。

  • Bind:将计算节点与 Pod 绑定,完成调度。


3.12、Kubelet

Kubelet是每一个节点上都有的组件,Kubelet是整个Kubernetes的初始化系统(init system),所有其他的Pod都是Kubelet拉起来的。

  • 从不同源获取 Pod 清单,并按需求启停 Pod 的核心组件:

    • Pod 清单可从本地文件目录,给定的 HTTPServer 或 Kube-APIServer 等源头获取;

    • Kubelet 将运行时,网络和存储抽象成了 CRI,CNI,CSI。

      • CRI(Container Runtime Interface):容器运行时接口,提供计算资源
      • CNI(Container Network Interface):容器网络接口,提供网络资源
      • CSI(Container Storage Interface):容器存储接口,提供存储资源
  • 负责汇报当前节点的资源信息和健康状态;

  • 负责 Pod 的健康检查和状态汇报。


3.13、Kube-Proxy

Kube-Proxy的作用是用来去搭负载均衡的。

通过执行kubectl expose命令后就可以通过Service IP去访问了,这个背后就是Kube-Proxy去做的。

  • 监控集群中用户发布的服务,并完成负载均衡配置。

  • 每个节点的 Kube-Proxy 都会配置相同的负载均衡策略,使得整个集群的服务发现建立在分布式负载均衡器之上,服务调用无需经过额外的网络跳转(Network Hop)。

  • 负载均衡配置基于不同插件实现:

    • userspace。

    • 操作系统网络协议栈不同的 Hooks 点和插件:

      • iptables;

      • ipvs。



3.14、推荐的 Add-ons

  • kube-dns:负责为整个集群提供 DNS 服务;

  • Ingress Controller:为服务提供外网入口;

  • MetricsServer:提供资源监控;

  • Dashboard:提供 GUI;

  • Fluentd-Elasticsearch:提供集群日志采集、存储与查询。


4、Kubectl 命令和 kubeconfig

4.1、Kubectl 命令

  • kubectl 是一个 Kubernetes 的命令行工具,它允许 Kubernetes 用户以命令行的方式与 Kubernetes 交互,其默认读取配置文件 ~/.kube/config

  • kubectl 会将接收到的用户请求转化为 rest 调用以 rest client 的形式与 apiserver 通讯。

  • apiserver 的地址,用户信息等配置在 kubeconfig。

kubectl命令到底做了什么

kubectl本身是对REST API的封装,所以最终所有的请求都是通过REST调用发到APIServer的。

  • 随便执行一个kubectl命令,使用 -v 9 参数来查看debug log
# 查看名为“default”的Namespace的debug log且查看的日志等级为9(最详细日志)
$ kubectl get ns default -v 9
  • 查看日志,kubectl命令主要做了2件事
# 1、加载/root/.kube/config配置文件
Config loaded from file:  /root/.kube/config
# 2、发送curl https://192.168.56.56:6443/api/v1/namespaces/default
curl -v -XGET  -H "Accept: application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json" -H "User-Agent: kubectl/v1.22.2 (linux/amd64) kubernetes/8b5a191" 'https://192.168.56.56:6443/api/v1/namespaces/default'

4.2、kubeconfig

kubeconfig文件其实也是标准的APIServer对象,里面也有自己的版本信息

使用kubectl config view命令也可以查看kubeconfig文件内容


yaml文件的基本语法

  • 大小写敏感
  • 对象键值对使用冒号结构表示 key: value,冒号后面要加一个空格。
  • 使用缩进表示层级关系
  • 缩进不允许使用tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • '#'表示注释

> 语法

​ 解决长字符串一行写不完的情况,为了可读性我们要使用>语法分行写,除最后一行外,其余行的换行符均用空格替代,而最后一行的换行符用\n替代

singleLineString: >
  this
  is a
  very long
  string

# 上面会解析成{"singleLineString": "this is a very long string\n"}

如果不需要最后一行的换行符\n,就使用>- 语法即可

singleLineString: >-
  this
  is a
  very long
  string

# 上面会解析成{"singleLineString": "this is a very long string"}
| 语法

用来写文本段落,会保留每一行结尾的换行符

multiLineString: |
  this
  is a
  very long
  string
  
# 上面会解析成{"multiLineString": "this\nis a\nvery long\nstring\n"}
- 语法

辅助理解在线YAML和JSON转化网站

  • key的首字母缩进相同时,代表这些key是在同一个{}内的
ports:
- 6379
- 6380

# 上面会解析成{"ports": [6379,6380]}

ports:
  - 6379
  - 6380 # 注意要和上一行对其
  
# 可以和其父亲在同级,也可以有缩进,结果都是一样的,上面会解析成{"ports": [6379,6380]}

ports: [6379,6380]

# 是等价的写法,称为“行内写法”,上面会解析成{"ports": [6379,6380]}

综合案例:

apiVersion: v1 
kind: Pod 
metadata:
  name: hello 
spec:
  containers:
  - image: nginx:1.15 
    name: nginx
{
  "apiVersion": "v1",
  "kind": "Pod",
  "metadata": {
    "name": "hello"
  },
  "spec": {
    "containers": [
      {
        "image": "nginx:1.15",
        "name": "nginx"
      }
    ]
  }
}
  • 以 - 开头的行的上一行的key为数组;
  • key的首字母缩进相同时,代表这些key是在同一个{}内的

~/.kube/config文件说明

apiVersion: v1
# 集群列表,多集群需再此配置
clusters: 
- cluster: # 可以有多个
    certificate-authority-data: REDACTED # 集群证书
    server: https://192.168.56.56:6443 # 集群地址
  name: kubernetes # 集群名字
# 上下文。即用哪个用户连上面哪个集群
contexts: 
- context: # 可以有多个。在不同的上下文中,资源是相互隔离的,互不干扰。
    cluster: kubernetes
    user: kubernetes-admin
  name: kubernetes-admin@kubernetes
# 当前默认context。如果不指定任何context,默认用哪个context
current-context: kubernetes-admin@kubernetes

kind: Config
preferences: {}
# 用户配置,包含用户的认证信息
users:
- name: kubernetes-admin
  user:
    client-certificate-data: REDACTED
    client-key-data: REDACTED

当我们执行kubectl命令的时候,kubectl会先来读此文件,读当前默认current-context,根据current-context的值分析找到对应集群的server地址(APIServer地址),然后用kubernetes-admin用户去访问,这样kubectl命令就能组装成REST调用发给APIServer了。


5、常用命令

5.1、常用对象全名及缩写

namespaces: ns
nodes: no
pods: po
replicationcontrollers: rc
replicasets: rs
services: svc
deployments: deploy
events: ev
componentstatuses: cs
serviceAccount: sa

5.2、Kubernetes系统相关命令

# 查看k8s版本
$ kubectl version
$ kubectl version --short
# 查看kubeconfig文件内容
$ kubectl config view

# context切换,切换为ctx-dev context
$ kubectl config use-context ctx-dev
# 显示该服务器上支持的K8S API资源(对象),也可用来查看对象的全名和简写
$ kubectl api-resources
# 查询kubectl命令的所有参数及使用方法
$ kubectl options

5.3、kubectl get命令

获取列出一个或多个资源的信息。

常用参数:使用kubectl options命令可以查询所有参数

  • -oyaml:输出详细信息为 yaml 格式。
  • -w:watch 该对象的后续变化。
  • -owide:以详细列表的格式查看对象,不同对象会输出不同的详细信息。
  • -v 9, --v=9:查看debug log,日志级别为0~9,9 为查看最全日志。官方文档
  • -n, --namespace='':指定namespace名进行名空间范围查询
  • --show-labels:展示标签字段

all

# 列出所有不同的资源对象
$ kubectl get all

componentstatuses

# 查看集群状态
$ kubectl get cs
$ kubectl get componentstatuses
$ kubectl get cs -owide
$ kubectl get cs --show-labels

node

# 获取集群中所有的计算节点
$ kubectl get node
$ kubectl get no
$ kubectl get node -owide
$ kubectl get node --show-labels

pod

# 列出default namespace下运行的Pod信息
$ kubectl get po
$ kubectl get pod
$ kubectl get pods
$ kubectl get po -owide
$ kubectl get po -owide --show-labels
$ kubectl get po -w

# 列出所有namespace下的Pod信息
$ kubectl get po --all-namespaces

# 查看名为centos-c6d568cb9-vkmsl的Pod的yaml配置,也可以连着写-oyaml
$ kubectl get po centos-c6d568cb9-vkmsl -o yaml

# 以JSON格式输出一个pod信息
$ kubectl get -o json pod nginx-deployment-55649fd747-pr95n

# 根据"app=nginx"的Label标签查找对应Pod
$ kubectl get po -l app=nginx

namespace

# 获取所有Namespace
$ kubectl get namespace
$ kubectl get ns

# 查看名为"default"的Namespace的debug log
$ kubectl get ns default -v 9

# 查看名为"kube-system"的namespace下所有的Pod.
# 通过kubeadm安装K8S以后所有控制面组件会放在kube-system这个namespace下。
$ kubectl -n kube-system get po

replicasets

# 查看所有replicasets
$ kubectl get rs
$ kubectl get replicasets
$ kubectl get rs -owide
$ kubectl get rs -owide --show-labels

# 查看名为"nginx-deployment-55649fd747"的replicasets信息
$ kubectl get rs nginx-deployment-55649fd747

services

# 查看所有services
$ kubectl get svc
$ kubectl get services
$ kubectl get svc -owide
$ kubectl get svc -owide --show-labels

deployments

# 查看所有deployments
$ kubectl get deploy
$ kubectl get deployments
$ kubectl get deploy -owide
$ kubectl get deploy -owide --show-labels

# 查看名为"nginx-deployment"的deployment信息
$ kubectl get deployment nginx-deployment
$ kubectl get deployment.apps/nginx-deployment

replication controller

# 列出名为"web"的 replication controller信息
$ kubectl get replicationcontroller web

serviceAccount

$ kubectl get serviceAccount
$ kubectl get sa -n default

5.4、kubectl describe命令

kubectl describe 展示资源的详细信息和相关 Event。

describe会展示两部分:

  • 详细信息:但是不如-oyaml看的准确。
  • 相关Event:这是最重要的用途。一般我们出现问题去查找问题的时候都是通过describe命令去做。
$ kubectl describe node
$ kubectl describe node ubuntu
$ kubectl describe po
# 查看名为"nginx-deployment-55649fd747"的replicaSet的详细信息
$ kubectl describe replicaset nginx-deployment-55649fd747

5.5、kubectl run命令

  • 创建并运行一个或多个容器镜像。
  • 创建一个deployment 或job 来管理容器。
# 启动nginx实例
$ kubectl run --image=nginx nginx
$ kubectl run --image=nginx nginx --restart='Always'

# 启动nginx实例,设置副本数5
kubectl run nginx --image=nginx --replicas=5

# 启动hazelcast实例,暴露容器端口 5701
$ kubectl run hazelcast --image=hazelcast --port=5701

# 启动hazelcast实例,并在容器中设置环境变量
$ kubectl run hazelcast --image=hazelcast --env="DNS_DOMAIN=cluster" --env="POD_NAMESPACE=default"

5.6、kubectl create命令

通过配置文件名或stdin创建一个集群资源对象。

支持JSON和YAML格式的文件。

pod

# 通过pod.json文件创建一个pod。
$ kubectl create -f ./pod.json

# 通过stdin的JSON创建一个pod。
$ cat pod.json | kubectl create -f -

deployment

# 创建一个名为my-dep的deployment,运行busybox镜像。
$ kubectl create deployment my-dep --image=busybox

# 通过nginx-deploy.yaml文件创建deployment对象
$ kubectl create -f nginx-deploy.yaml
# API版本为v1的JSON格式的docker-registry.yaml文件创建资源
$ kubectl create -f docker-registry.yaml --edit --output-version=v1 -o json

namespace

# 创建一个名为my-namespace的namespace
$ kubectl create namespace my-namespace

5.7、kubectl edit命令

使用默认编辑器 编辑服务器上定义的资源。

文件默认输出格式为YAML。要以JSON格式编辑,请指定“-o json”选项。

pod

# 编辑名为nginx Pod的yaml文件,就可以编辑一些这个Pod的属性
$ kubectl edit po nginx

svc

# 编辑名为'docker-registry'的service
$ kubectl edit svc/docker-registry
# 使用替代的编辑器
KUBE_EDITOR="nano" kubectl edit svc/docker-registry

# 编辑名为“myjob”的service,输出JSON格式 V1 API版本
kubectl edit job.v1.batch/myjob -o json

deployment

# 以YAML格式输出编辑deployment“mydeployment”,并将修改的配置保存在annotation中
$ kubectl edit deployment/mydeployment -o yaml --save-config

5.8、kubectl delete命令

通过配置文件名、stdin、资源名称或label选择器来删除资源。

支持JSON和YAML格式文件。可以只指定一种类型的参数:文件名、资源名称或label选择器。

Pod

# 删除pod
$ kubectl delete po nginx

# 使用 pod.json中指定的资源类型和名称删除pod
$ kubectl delete -f ./pod.json

# 根据传入stdin的JSON所指定的类型和名称删除pod
$ cat pod.json | kubectl delete -f -

# 删除所有pod
$ kubectl delete pods --all

svc

# 删除名为“nginx”的Service
$ kubectl delete svc nginx

# 删除名为“baz”和“foo”的Pod和Service
$ kubectl delete pod,service baz foo

Label

# 删除 Label name = myLabel的pod和Service
$ kubectl delete pods,services -l name=myLabel

5.9、kubectl expose命令

将资源暴露为新的Kubernetes Service。

指定deployment、service、replica set、replication controller或pod,并使用该资源的选择器作为指定端口上新服务的选择器。deployment 或 replica set只有当其选择器可转换为service支持的选择器时,即当选择器仅包含matchLabels组件时才会作为暴露新的Service。

资源包括(不区分大小写):pod(po),service(svc),replication controller(rc),deployment(deploy),replica set(rs)

$ kubectl get svc

NAME             TYPE       CLUSTER-IP     EXTERNAL-IP  PORT(S)       AGE
kubernetes       ClusterIP  10.96.0.1      <none>       443/TCP       7d20h
nginx            NodePort   10.109.114.119 <none>       80:32603/TCP  7d19h
nginx-deployment NodePort   10.99.183.87   <none>       80:32516/TCP  6d23h

pod

# 给名为"nginx"的pod暴露80端口
$ kubectl expose pod nginx --selector run=nginx --port=80 --type=NodePort

deployment

# 给名为"nginx-deployment"的deploy暴露80端口,注意命令中的app要和LABELS中一致
$ kubectl expose deploy nginx-deployment --selector app=nginx --port=80 --type=NodePort

replicationcontrollers

# 为RC的nginx创建service,并通过Service的80端口转发至容器的8000端口上。
$ kubectl expose rc nginx --port=80 --target-port=8000

# 由“nginx-controller.yaml”中指定的type和name标识的RC创建Service,并通过Service的80端口转发至容器的8000端口上。
$ kubectl expose -f nginx-controller.yaml --port=80 --target-port=8000

5.10、kubectl exec命令

kubectl exec 提供进入运行容器的通道,可以进入容器进行 debug 操作。

和docker的exec是一样的。

# 进入到名为"nginx-deployment-55649fd747-pr95n"的pod中,好习惯是写-- bash,也可以省略--
$ kubectl exec -it nginx-deployment-55649fd747-pr95n -- bash

5.11、kubectl logs命令

Kubectl logs 可查看 pod 的标准输入(stdout, stderr),与 tail 用法类似。

# 查看名为"nginx-deployment-55649fd747-pr95n"的pod的日志
$ kubectl logs -f nginx-deployment-55649fd747-pr95n
# 仅查看最后10行日志
$ kubectl logs -f --tail=10 nginx-deployment-55649fd747-pr95n
# 如果应用的日志是写到某个文件里的,那么就要加exec去查看pod应用里某个日志文件的日志信息
$ kubectl logs exec -it nginx-deployment-55649fd747-pr95n -- tail -f /xxx.log 

5.12、kubectl label命令

更新(增加、修改或删除)资源上的 label(标签)

  • label 必须以字母或数字开头,可以使用字母、数字、连字符、点和下划线,最长63个字符。
  • 如果–overwrite 为 true,则可以覆盖已有的 label,否则尝试覆盖 label 将会报错。
  • 如果指定了–resource-version,则更新将使用此资源版本,否则将使用现有的资源版本。

pod

# 给名为foo的Pod添加label unhealthy=true
$ kubectl label pods foo unhealthy=true

# 给名为foo的Pod修改label 为 'status' / value 'unhealthy',且覆盖现有的value
$ kubectl label --overwrite pods foo status=unhealthy

# 仅当resource-version=1时才更新 名为foo的Pod上的label
$ kubectl label pods foo status=unhealthy --resource-version=1

# 删除名为“bar”的label 。(使用“ - ”减号相连)
$ kubectl label pods foo bar-

# 给 namespace 中的所有 pod 添加 label
$ kubectl label pods --all status=unhealthy

namespace

# 给名为"default"的namespace增加新的label
$ kubectl label ns default a=b

6、深入理解Kubernetes

6.1、云计算的传统分类

在Kubernetes的世界中,模糊了下图这些边界,提供了统一的API,即由基础架构的运维来面对又由应用接入来面对,容器提供了dockerfile、容器镜像,意味着任何的容器镜像都是面向应用的,K8S提供了所有自动化的东西。


6.2、Kubernetes 生态系统


控制平面:K8S的那些管控组件。集群管理员来控制。

数据平面:用户跑在K8S上面的这些应用。这些应用是由用户去创建一堆K8S对象,例如Pod、PVC、Service等。应用开发人员控制。


6.3、Kubernetes 设计理念

任何云平台最重要的特性就是高可用。对于在线应用要想尽一切办法保证应用高可用。所以K8S提供了一系列的方式来保证应用高可用:

  • 副本集的方式:replicaset、statefulset分别来保证无状态应用和有状态应用的高可用。
  • K8S组件本身也是高可用的。

简单来说,Kubernetes的高可用原则有两点:

  • 要保证在Kubernetes平台上运行的应用是高可用的。
  • Kubernetes自己的控制平面的组件是高可用的。

6.4、Kubernetes Master


6.5、分层架构

  • 核心层:Kubernetes 最核心的功能,对外提供 API 构建高层的应用,对内提供插件式应用执行环境。

  • 应用层:部署(无状态应用、有状态应用、批处理任务、集群应用等)和路由(服务发现、DNS 解析 等)。

  • 管理层:系统度量(如基础设施、容器和网络的度量)、自动化(如自动扩展、动态 Provision 等)、 策略管理(RBAC、Quota、PSP、NetworkPolicy 等)。

  • 接口层:Kubectl 命令行工具、客户端 SDK 以及集群联邦。

  • 生态系统:在接口层之上的庞大容器集群管理调度的生态系统,可以划分为两个范畴:

    • Kubernetes 外部:日志、监控、配置管理、CI、CD、Workflow、FaaS、OTS 应用、 ChatOps 等;

    • Kubernetes 内部:CRI、CNI、CVI、镜像仓库、Cloud Provider、集群自身的配置和管理等。





6.6、API 设计原则

  • 所有 API 都应是声明式的

    • 相对于命令式操作,声明式操作对于重复操作的效果是稳定的,这对于容易出现数据丢失或重复的分布式环境来说是很重要的。

    • 声明式操作更易被用户使用,可以使系统向用户隐藏实现的细节,同时也保留了系统未来持续优化的可能性。

    • 此外,声明式的 API 还隐含了所有的 API 对象都是名词性质的,例如 Service、Volume 这些 API 都是名词,这些名词描述了用户所 期望得到的一个目标对象。

  • API 对象是彼此互补而且可组合的

    • 这实际上鼓励 API 对象尽量实现面向对象设计时的要求,即“高内聚,松耦合”,对业务相关的概念有一个合适的分解,提高分解出 来的对象的可重用性。
  • 高层 API 以操作意图为基础设计

    • 如何能够设计好 API,跟如何能用面向对象的方法设计好应用系统有相通的地方,高层设计一定是从业务出发,而不是过早的从技术 实现出发。

    • 因此,针对 Kubernetes 的高层 API 设计,一定是以 Kubernetes 的业务为基础出发,也就是以系统调度管理容器的操作意图为基础 设计。

  • 低层 API 根据高层 API 的控制需要设计

    • 设计实现低层 API 的目的,是为了被高层 API 使用,考虑减少冗余、提高重用性的目的,低层 API 的设计也要以需求为基础,要尽量抵抗受技术实现影响的诱惑。
  • 尽量避免简单封装,不要有在外部 API 无法显式知道的内部隐藏的机制

    • 简单的封装,实际没有提供新的功能,反而增加了对所封装 API 的依赖性。

    • 例如 StatefulSet 和 ReplicaSet,本来就是两种 Pod 集合,那么 Kubernetes 就用不同 API 对象 来定义它们,而不会说只用同一个 ReplicaSet,内部通过特殊的算法再来区分这个 ReplicaSet 是 有状态的还是无状态。

  • API 操作复杂度与对象数量成正比

    • API 的操作复杂度不能超过 O(N),否则系统就不具备水平伸缩性了。
  • API 对象状态不能依赖于网络连接状态

    • 由于众所周知,在分布式环境下,网络连接断开是经常发生的事情,因此要保证 API 对象状态能应 对网络的不稳定,API 对象的状态就不能依赖于网络连接状态。
  • 尽量避免让操作机制依赖于全局状态

    • 因为在分布式系统中要保证全局状态的同步是非常困难的。

6.7、Kubernetes 如何通过对象的组合完成业务描述

  • 引用依赖:例如Pod对象里面有一个属性的名字指向Node对象,那么这种情况我们定义为引用依赖。例如Pod里面有NameNode指向了某个Node。例如Ingress里面有ServiceName指向Service。
  • 基于命名规范:Deployment和Replicaset之间就是基于命名规范的。
    Deployment是如何创建Replicaset呢?Deployment是根据其里面Pod Template字符串做了一个Hash值,以这个Hash值作为Replicaset名字的一部分。即它们两个之间的关系是写在程序里面的,所以基于命名规范的业务逻辑要特别小心,容易出现致命问题,即万一发生代码变更,很可能原来的命名规范全都被打破了,很有可能所有功能废掉,进而引发事故。
  • 基于标签:Replicaset怎么知道Pod满不满足副本数需求呢?在kubectl get rs nginx-deployment-55649fd747 -oyaml里面有一个叫“matchLabels”的selector,所以Replicaset会去看当前Namespace里面打着这些Label的Pod。所以这种就是基于标签的过滤条件的。
    Service和Pod的关系也是一样:kubectl get svc -owide就可以看到又个“SELECTOR”,在K8S的世界里,只要看到“SELECTOR”就把它和Label产生关联关系就行了,基本上“SELECTOR”就是用来过滤标签的。例如SELECTOR为app=nginx,那么我们通过kubectl get po -l app=nginx就能查到对应的这些Pod。

单独说一下Ingress:在微服务领域,一个网站的背后是由很多微服务的。之前都是通过Nginx去转,根据不同的URL路径转到不同的微服务里进行处理;在K8S世界里提供了Ingress对象,Ingress对象里面可以让咱们应用的发布者去定义不同的URL路径转到不同的微服务上去。


6.8、架构设计原则

  • 只有 APIServer 可以直接访问 etcd 存储,其他服务必须通过 Kubernetes API 来访问集群状态;

  • 单节点故障不应该影响集群的状态;

  • 在没有新请求的情况下,所有组件应该在故障恢复后继续执行上次最后收到的请求 (比如网络分区或服务重启等);

  • 所有组件都应该在内存中保持所需要的状态,APIServer 将状态写入 etcd 存储,而其他组件则通过 APIServer 更新并监听所有的变化;

  • 优先使用事件监听而不是轮询。


6.9、引导(Bootstrapping)原则

  • Self-hosting 是目标。

  • 减少依赖,特别是稳态运行的依赖。

  • 通过分层的原则管理依赖。

  • 循环依赖问题的原则:

    • 同时还接受其他方式的数据输入(比如本地文件等),这样在其他服务不可用时还可以手动配置引导服务;

    • 状态应该是可恢复或可重新发现的;

    • 支持简单的启动临时实例来创建稳态运行所需要的状态,使用分布式锁或文件锁等来协调不同状态的切换(通常称为 pivoting 技术);

    • 自动重启异常退出的服务,比如副本或者进程管理器等。



7、API 对象的四大属性

Kubernetes对象是Kubernetes最厉害的地方。我们可以想一下,如果去推一个项目,怎么样才能和Openstack和Docker去打呢?即使是Google也是很困难的。所以Kubernetes就走了一个比较巧妙的路径,就是定规范,把我们的项目定成业内的规范。
这个定义规范就是定义API,如果所有云计算的API都是我们这个项目所掌控的,无论你用什么技术,最终还是要符合这些API规范的。这个API定义什么呢?我们就可以把现有的云计算世界的所有领域的对象全纳入到管控范围,比如你的计算节点应该是怎么样的、你的作业应该是怎么样的、你的服务是怎么样的、你的入站流量应该怎么管、你的安全策略防火墙规则应该怎么管 等等,如果我们把这所有的东西都定成一个标准一个规范,那是不是就把这个世界固化了?你不同公司有不同技术手段,那么是OK的,我们提供的是API层,你实现的这些控制器由你每家公司依据不同技术去实现,然后你来和我们的API配合,那么我们的整个体系就会是一个非常非常长周期的产品。

例如阿里以前就站错队了,之前和docker是战略合作,现在也转投Kubernetes了。现在几乎所有的云计算公司都认可Kubernetes这套规范。

API 对象是 Kubernetes 集群中的管理操作单元。

Kubernetes 集群系统每支持一项新功能,引入一项新技术,一定会新引入对应的 API 对象,支持对该功能的管理操作。

每个 API 对象都有四大类属性:

  • TypeMeta

  • MetaData

  • Spec

  • Status

用下面的yaml文件举例:

# TypeMeta部分:用来定义这个对象是啥
apiVersion: apps/v1
kind: Deployment
# MetaData部分:用来定义这个对象的基本属性,例如名字、标签等
metadata:
  name: nginx-deployment
# Spec部分:用来定义这个对象的用户期望值是什么
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx
# Status部分:不是由用户来指定的,通常是由这个系统的控制器来指定的,会体现当前建出来的这个对象的状态信息。

# 下面为执行完kubectl create -f nginx-deploy.yaml命令创建deployment对象后才有Status部分。
# 使用kubectl get deploy nginx-deployment -oyaml命令可以查看Status部分如下
status:
  availableReplicas: 3
  conditions:
  - lastTransitionTime: "2022-07-25T03:14:19Z"
    lastUpdateTime: "2022-07-26T02:08:45Z"
    message: ReplicaSet "nginx-deployment-55649fd747" has successfully progressed.
    reason: NewReplicaSetAvailable
    status: "True"
    type: Progressing
  - lastTransitionTime: "2022-08-01T05:56:31Z"
    lastUpdateTime: "2022-08-01T05:56:31Z"
    message: Deployment has minimum availability.
    reason: MinimumReplicasAvailable
    status: "True"
    type: Available
  observedGeneration: 3
  readyReplicas: 3
  replicas: 3
  updatedReplicas: 3

7.1、TypeMeta

Kubernetes对象的最基本定义,它通过引入GKV(Group,Kind,Version)模型定义了一个对象的类型。我们一般叫TypeMeta为GKV。

  1. Group

    Kubernetes 定义了非常多的对象,如何将这些对象进行归类是一门学问,将对象依据其功能范围归入不同的分组, 比如把支撑最基本功能的对象归入 core 组,把与应用部署有关的对象归入 apps 组,会使这些对象的可维护性和可理解性更高。

  2. Kind

    定义一个对象的基本类型,比如 Node、Pod、Deployment 等。

  3. Version

    社区每个季度会推出一个 Kubernetes 版本,随着 Kubernetes 版本的演进,对象从创建之初到能够完全生产化就绪的版本是不断变化的。与软件版本类似,通常社区提出一个模型定义以后,随着该对象不断成熟,其版本可能会从 v1alpha1 到 v1alpha2,或者到 v1beta1,最终变成生产就绪版本 v1。


7.2、Metadata

Metadata 中有两个最重要的属性:NamespaceName,分别定义了对象的 Namespace 归属及名字,这两个属性唯一定义了某个对象实例

  1. Label

    顾名思义就是给对象打标签,一个对象可以有任意对标签,其存在形式是键值对。 Label 定义了对象的可识别属性,Kubernetes API 支持以 Label 作为过滤条件查询对象。

  2. Annotation

    Annotation 与 Label 一样用键值对来定义,但 Annotation 是作为属性扩展, 更多面向于系统管理员和开发人员,因此需要像其他属性一样做合理归类。

  3. Finalizer

    Finalizer 本质上是一个资源锁,Kubernetes 在接收某对象的删除请求时,会检查 Finalizer 是否为空,如果不为空则只对其做逻辑删除,即只会更新对象中的metadata.deletionTimestamp 字段。

    Finalizer 实际上就是把这个对象锁住,让其不能被物理删除,一般来说应用的场景为:一个Kubernetes对象要配一些外部资源的时候,比如要分一个ip或者控制器要配一个外部的DNS,假如我们要删除这个对象的时候,控制器没有正常的工作,那么外部的DNS那些记录都留在那了,那么我们要做的一件事就是在配外部资源的时候先给这个对象加上Finalizer,然后再去操作外部的配置,当我们要删除这个对象的时候,先去清除外部的配置,清除完以后由控制器去清除Finalizer,清除完Finalizer后这个对象就直接消失了。这样就确保外部资源不被泄露。

    举例:我们使用kubectl run --image=nginx nginx --restart="Always"起一个名为nginx的Pod,然后删除kubectl delete po nginx,那么这个Pod就消失了。比如我们有一个控制器要来watch这个Pod,那么在nginx Pod被删除之前,假如控制器因为一些问题死掉了比如OM了,那么在nginx Pod删除之后,控制器恢复正常了,那么这个event事件就丢了,这是不行的。那么有没有办法让这个Pod是逻辑删除呢?那么我们就可以在run命令创建出来nginx pod后,执行kubectl edit po nginx命令在metadata里面加一个finalizers属性

    apiVersion: v1
    kind: Pod
    metadata:
      labels:
        run: nginx
      name: nginx
      finalizers: # finalizers是一个数组
      - kubernetes
      namespace: default
    

    上面加了一个- kubernetes的值,这个时候再去执行kubectl delete po nginx删除nginx命令(记得执行完要control+c中断一下,否则会一直等待),发现nginx Pod不会消失了,这个Pod会一直处于“Terminating”的状态。
    如果想彻底删除就要把- kubernetes删掉,删掉后会发现nginx Pod立刻就被删除了。

  4. ResourceVersion

    ResourceVersion 可以被看作一种乐观锁,每个对象在任意时刻都有其 ResourceVersion,当 Kubernetes 对象被客户端读取以后,ResourceVersion 信息也被一并读取。此机制确保了分布式系统中任意多线程能够无锁并发访问对 象,极大提升了系统的整体效率。
    任何对象都有ResourceVersion,ResourceVersion是用来做版本控制的;在分布式系统里面,一般一个对象会被多个控制器管,那么怎么样避免多个线程去读写一个对象锁造成的资源冲突呢?在Go语言中我们用channel,channel底层是一个锁。那么在Kubernetes中使用版本控制来解决此问题,即用到了ResourceVersion。ResourceVersion其实是一个乐观锁,即任何的Kubernetes对象在APIServer这边都有一个版本信息,这个版本信息其实是从etcd来的,这个版本信息就代表着当前对象的版本,如果两个控制器去操作同一个对象的时候,第一时间这两个控制器读到的是同一个版本,接下来A控制器去改了这个对象同时会把这个版本变掉,那么B控制器也去修改同一个对象的时候(这个时候APIServer就会去判断当前这个对象的ResourceVersion是啥,B控制器带的ResourceVersion是啥,如果这两个ResourceVersion不一样,那说明B控制器是基于一个比较旧的对象数据去做修改的,那么这个B控制器对这个对象的修改操作会被拒绝掉,就会返回409给B控制器的进程),B进程就收到409版本冲突,B控制器就知道应该重新获取一次最新的对象信息,然后再此基础上做修改。


Label

  • Label 是识别 Kubernetes 对象的标签,以 key/value 的方式附加到对象上。

  • key 最长不能超过 63 字节,value 可以为空,也可以是不超过 253 字节的字符串。

  • Label 不提供唯一性,并且实际上经常是很多对象(如 Pods)都使用相同的 label 来标志具 体的应用。

  • Label 定义好后其他对象可以使用 Label Selector 来选择一组相同 label 的对象

  • Label Selector 支持以下几种方式:

    • 等式,如 app=nginx 和 env!=production;
    • 集合,如 env in (production, qa);
    • 多个 label(它们之间是 AND 关系),如 app=nginx,env=test。


Annotations

  • Annotations 是 key/value 形式附加于对象的注解。

  • 不同于 Labels 用于标志和选择对象,Annotations 则是用来记录一些附加信息,用来辅助应用部署、安全策略以及调度策略等。

  • 比如 deployment 使用 annotations 来记录 rolling update 的状态。


7.3、Spec 和 Status

  • Spec 和 Status 才是对象的核心。每一个对象的Spec和Status基本都是不一致的。

  • Spec 是用户的期望状态,由创建对象的用户端来定义。

  • Status 是对象的实际状态,由对应的控制器收集实际状态并更新。

  • 与 TypeMeta 和 Metadata 等通用属性不同,Spec 和 Status 是每个对象独有的。

kubectl get ns default -oyaml可以看到如下yaml信息

apiVersion: v1
kind: Namespace
metadata:
  creationTimestamp: "2022-07-24T10:30:55Z"
  labels:
    a: b
    kubernetes.io/metadata.name: default
  name: default
  resourceVersion: "116378"
  uid: 31a2483c-34c9-4c96-98c8-02fee9932ea1
spec:
  finalizers:
  - kubernetes
status:
  phase: Active

可以看到Namespace的spec是空的,因为Namespace不需要定义什么东西,只是一个目录,目录能有什么呢。Namespace的status就会说一下这个Namespace当前的状态是Active还是Terminating。

Namespace的spec的finalizers和之前我们说的Pod Metadata的finalizers是不一样的,但是它们的作用是一样的,这属于历史遗留问题,我们可以看到上面yaml的finalizers是在Namespace的spec里面的,当我们删除一个NS的时候,不能让这个ns直接被删除,需要先清除这个ns里面所有的对象,然后这个ns才能被删掉。所以在早期版本的Kubernetes的Namespace是在spec里面加入finalizers,后面应该会把finalizers放在Metadata里面。


8、核心对象概览

8.1、常用 Kubernetes 对象及其分组

建议先从最下面核心对象开始看起。

只要我们了解:Node、Pod、ConfigMap、Secret、Namespace、Service这6个对象,基本上就能满足部署业务时,所需要掌握最小的知识点集合。


8.2、Node

  • Node 是 Pod 真正运行的主机,可以物理机,也可以是虚拟机。是用来代表Kubernetes计算节点的对象。

  • 为了管理 Pod,每个 Node 节点上至少要运行 container runtime (比如 Docker 或者 Rkt)、Kubelet 和 Kube-proxy 服务。


8.3、Namespace

Namespace 是对一组资源和对象的抽象集合,比如可以用来将系统内部的对象划分为不同的项目组或用户组。是用来做隔离的。

常见的 pods、services、replication controllers 和 deployments 等都是属于某一个 Namespace 的(默认是 default),而 Node、persistentVolumes 等则不属于任何 Namespace。

可以看到我们之前一直在用的nginx-deployment.yaml文件是没有加namespace属性的:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx
# 默认用default Namespace
$ kubectl create -f nginx-deploy.yaml

# 手动指定namespace为“kube-system”
$ kubectl create -f nginx-deploy.yaml --namespace kube-system
# 或
$ kubectl create -f nginx-deploy.yaml -n kube-system

8.4、ConfigMap

​ 任何在Kubernetes上运行的应用,我们始终在讲 “应用和应用的配置分离” ,应用的代码从容器镜像来,应用的配置信息从 环境变量(可以把ConfigMap对象文件传递到Pod的yaml文件的环境变量参数中) 来 或者 从 外挂存储卷来(把ConfigMap对象通过volume的方式挂载到Pod内) 来。

​ ConfigMap对象主要用于 容器内应用程序 的配置管理,ConfigMap对象可以通过“环境变量”或“外挂存储卷”两种方式在容器中应用配置。

​ configMap:就是一个文件;有自己的kind、apiVersion、metadata、没有spec(其实就是data,data就是keyvalue);其作用就是让开发人员保存一些应用程序的配置,然后通过环境变量的方式或者挂载存储卷的方式 灌到一个Pod里面去。Secret也是类似的。

  • ConfigMap 用来将非机密性的数据保存到键值对中。

  • 使用时, Pods 可以将其用作环境变量、命令行参数或者存储卷中的配置文件。

  • ConfigMap 将环境配置信息和 容器镜像解耦,便于应用配置的修改。


创建ConfigMap的4种方式:

  • 通过直接在命令行中指定configmap参数创建,即--from-literal
  • 通过指定文件创建,即将一个配置文件创建为一个ConfigMap --from-file=<文件>
  • 通过指定目录创建,即将一个目录下的所有配置文件创建为一个ConfigMap,--from-file=<目录>
  • 事先写好标准的configmap的yaml文件,然后kubectl create -f 命令创建
1. 通过命令行参数 --from-literal 创建
# 查看所有的configmap文件
$ kubectl get configmap
NAME               DATA   AGE
kube-root-ca.crt   1      11d

# 通过命令行参数 --from-literal 创建
$ kubectl create configmap test-config1 --from-literal=db.host=10.5.10.116 --from-literal=db.port='3306'

# 查看所有的configmap文件
$ kubectl get configmap
NAME               DATA   AGE
kube-root-ca.crt   1      11d
test-config1       2      8s

# 查看名为“test-config1”的configmap文件信息
$ kubectl get configmap test-config1 -oyaml
apiVersion: v1
data:
  db.host: 10.5.10.116
  db.port: "3306"
kind: ConfigMap
metadata:
  creationTimestamp: "2022-08-04T10:46:10Z"
  name: test-config1
  namespace: default
  resourceVersion: "234466"
  uid: 2b0ce4d2-5251-48ae-b243-8e8f8332b534

2. 指定文件创建
# app.properties
property.1 = value-1
property.2 = value-2
property.3 = value-3
property.4 = value-4

[mysqld]
!include /home/jenrey/mysql/etc/mysqldf

port
socket
pid-file
basedir
datadir
# 通过指定文件创建configmap(可以有多个--from-file)
$ kubectl create configmap test-config2 --from-file=./app.properties

# 查看所有的configmap文件
$ kubectl get configmap
NAME               DATA   AGE
kube-root-ca.crt   1      11d
test-config1       2      6m43s
test-config2       1      5s

# 查看名为“test-config2”的configmap文件信息
$ kubectl get configmap test-config2 -oyaml
apiVersion: v1
data:
  app.properties: |
    property.1 = value-1
    property.2 = value-2
    property.3 = value-3
    property.4 = value-4

    [mysqld]
    !include /home/jenrey/mysql/etc/mysqldf

    port
    socket
    pid-file
    basedir
    datadir
kind: ConfigMap
metadata:
  creationTimestamp: "2022-08-04T10:52:48Z"
  name: test-config2
  namespace: default
  resourceVersion: "235342"
  uid: aeddf5d2-94ef-40f4-9ecd-3e6ed854f01b

可以看到指定文件创建时configmap会创建一个key/value对,key是文件名,value是文件内容。

假如不想configmap中的key为默认的文件名,还可以在创建时指定key名字:

$ kubectl create configmap test-config2 --from-file=<my-key-name>=<path-to-file>

3. 指定目录创建

configs 目录下的config-1和config-2内容如下所示:

# 通过指定目录创建configmap文件
$ kubectl create configmap test-config3 --from-file=./configs

# 查看所有的configmap文件
$ kubectl get configmap
NAME               DATA   AGE
kube-root-ca.crt   1      11d
test-config1       2      13m
test-config2       1      7m14s
test-config3       2      9s

# 查看名为“test-config3”的configmap文件信息
$ kubectl get configmap test-config3 -oyaml
apiVersion: v1
data:
  config-1: |
    aaaaaaaa
    bbbbbbbb
    c=d
  config-2: |
    eeeeeeee
    ffffffff
    g=h
kind: ConfigMap
metadata:
  creationTimestamp: "2022-08-04T10:59:53Z"
  name: test-config3
  namespace: default
  resourceVersion: "236292"
  uid: 3104cc05-b7b0-409c-b804-44ad715d6ee9

可以看到指定目录创建时configmap内容中的各个文件会创建一个key/value对,key是文件名,value是文件内容。

如果configs目录中有子目录,在子目录下还有配置文件,指定目录时只会识别指定目录的文件,忽略子目录


4. 通过事先写好configmap的标准yaml文件创建
# test-config4.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: test-config4
  namespace: default
data:
  cache_host: memcached-gcxt
  cache_port: "11211"
  cache_prefix: gcxt
  myf: |
    [mysqld]
    log-bin = mysql-bin
    hahaha = hehehe
# 通过指定yaml文件创建configmap文件
$ kubectl create -f ./test-config4.yaml

# 查看所有的configmap文件
$ kubectl get configmap
NAME               DATA   AGE
kube-root-ca.crt   1      11d
test-config1       2      13m
test-config2       1      7m14s
test-config3       2      9s

# 查看名为“test-config3”的configmap文件信息
$ kubectl get configmap test-config4 -oyaml
apiVersion: v1
data:
  cache_host: memcached-gcxt
  cache_port: "11211"
  cache_prefix: gcxt
  myf: |
    [mysqld]
    log-bin = mysql-bin
    hahaha = hehehe
kind: ConfigMap
metadata:
  creationTimestamp: "2022-08-06T04:10:50Z"
  name: test-config4
  namespace: default
  resourceVersion: "253724"
  uid: 3a2735ab-e3cb-466c-bc4a-20c880427598

ConfigMap的两种使用方式

  • 环境变量
    • 使用configmap中 指定 的key
    • 使用configmap中 所有 的key
    • 在容器启动命令中 引用configmap的环境变量
  • 外挂存储卷

查看环境变量:

# 1、进入容器
$ kubectl exec -it nginx-deployment-55649fd747-pr95n -- bash

# 2、执行env查看环境变量,可以看到好多环境变量是Kubernetes内置的环境变量
$ env
# HOSTNAME=nginx-deployment-55649fd747-pr95n
1. 通过环境变量的方式,直接传递给Pod
  • 使用valueFromconfigMapKeyRefnamekey指定要用的key:

    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx-pod
    spec:
      containers:
      - image: nginx
        name: nginx
        env:
            - name: DB.HOST # 容器内环境变量的名字
              valueFrom:
                configMapKeyRef:
                  name: test-config1 # configmap文件名
                  key: db.host # configmap文件内的data字段下的key
            - name: DB.PORT
              valueFrom:
                configMapKeyRef:
                  name: test-config1
                  key: db.port
    
  • 通过envFromconfigMapRefname使得configmap中的所有key/value对都自动变成环境变量:

    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx-pod
    spec:
      containers:
      - image: nginx
        name: nginx
        envFrom:
        - configMapRef:
            name: test-config1
    
  • 在容器启动命令中 引用configmap的环境变量

    apiVersion: v1
    kind: Pod
    metadata:
      name: dapi-test-pod
    spec:
      containers:
        - name: test-container
          image: k8s.gcr.io/busybox
          command: [ "/bin/sh", "-c", "echo $(SPECIAL_LEVEL_KEY) $(SPECIAL_TYPE_KEY)" ]
          env:
            - name: SPECIAL_LEVEL_KEY
              valueFrom:
                configMapKeyRef:
                  name: special-config
                  key: SPECIAL_LEVEL
            - name: SPECIAL_TYPE_KEY
              valueFrom:
                configMapKeyRef:
                  name: special-config
                  key: SPECIAL_TYPE
      restartPolicy: Never
    

2. 作为volume的方式挂载到Pod内

注意:

  1. 删除configmap后原pod不受影响;然后再删除pod后,重启的pod的events会报找不到cofigmap的volume;
  2. Pod起来后再通过kubectl edit configmap …修改configmap,过一会(实测大概10秒)Pod内部的配置也会刷新。
  3. 在容器内部修改挂进去的配置文件后,过一会内容会再次被刷新为原始configmap内容。
  4. 使用“通过环境变量的方式,直接传递给Pod”方式,更新configmap是不会刷新容器内env的。只有“作为volume的方式挂载到Pod内”才会自动更新。

  • 通过存储卷可以将外挂存储挂载到 Pod 内部使用。
  • 存储卷定义包括两个部分: Volume 和 VolumeMounts。
    • Volume:定义 Pod 可以使用的存储卷来源;
    • VolumeMounts:定义存储卷如何 Mount 到容器内部。
  • 把名为test-config4的configmap文件所有key/value挂载进来

    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx-pod
    spec:
      containers:
      - image: nginx
        name: nginx
        volumeMounts:
        - name: config-volume4
          mountPath: /tmp/config4
      volumes:
      - name: config-volume4
        configMap:
          name: test-config4
    

    使用kubectl exec -it nginx-pod -- bash命令进入容器,cd /tmp/config4,可以看到在config4文件夹下以每一个key为文件名value为值创建了多个文件。

    root@nginx-pod:/tmp/config4# ls
    cache_host  cache_port	cache_prefix  myf
    
  • 如不想以key名作为配置文件名可以引入items 字段,在其中逐个指定要用相对路径path替换的key:

    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx-pod
    spec:
      containers:
      - image: nginx
        name: nginx
        volumeMounts:
        - name: config-volume4
          mountPath: /tmp/config4
      volumes:
      - name: config-volume4
        configMap:
          name: test-config4
          items:
          - key: myf
            path: mysql-key # 配置文件的新名
          - key: cache_host
            path: cache-host # 配置文件的新名
    
    root@nginx-pod:/tmp/config4# ls
    cache-host  mysql-key
    

8.5、密钥对象(Secret)

  • Secret 是用来保存和传递密码、密钥、认证凭证这些敏感信息的对象。

  • 使用 Secret 的好处是可以避免把敏感信息明文写在配置文件里。

  • Kubernetes 集群中配置和使用服务不可避免的要用到各种敏感信息实现登录、认证等功能,例如访问 AWS 存储的用户名密码。

  • 为了避免将类似的敏感信息明文写在所有需要使用的配置文件中,可以将这些信息存入一个 Secret 对象,而在配置文件中通过 Secret 对象引用这些敏感信息。

  • 这种方式的好处包括:意图明确,避免重复,减少暴漏机会。


Secret有三种类型

  • Opaque:base64 编码格式的 Secret,用来存储密码、密钥等;但数据也可以通过base64 –decode解码得到原始数据,所以加密性很弱。

  • Service Account:用来访问Kubernetes API,由Kubernetes自动创建,并且会自动挂载到Pod的 /run/secrets/kubernetes.io/serviceaccount 目录中。

  • kubernetes.io/dockerconfigjson : 用来存储私有docker registry(docker 镜像仓库)的认证信息。


Opaque

使用base64加密后,用来存储密码、密钥。

需要注意的是:base64加密时我们手动加密的。K8S不会自动加密,但是会自动解密。

Opaque 类型的数据是一个 map 类型,要求 value 是 base64 编码格式:

  1. 手动加密用户名和密码

    $ echo "admin" | base64
    
    YWRtaW4K
    
    $ echo -n "1f2d1e2e67df" | base64 # -n 打印信息之后不换行,如果不写会多出Cg==在结尾,Cg==是换行是换行
    
    MWYyZDFlMmU2N2Rm
    

    这里需要注意的是,像这样创建的 Secret 对象,它里面的内容仅仅是经过了转码,而并没有被加密。在真正的生产环境中,你需要在 Kubernetes 中开启 Secret 的加密插件,增强数据的安全性。

  2. 生成secret

    # test-secret.yaml
    apiVersion: v1
    kind: Secret
    metadata:
      name: mysecret
    type: Opaque # Opaque类型
    data:
      password: MWYyZDFlMmU2N2Rm  # 加密的密码
      username: YWRtaW4K   # 加密的用户名称,所有的value值都是要加密的
    
    # 根据test-secret.yaml创建secret对象
    $ kubectl create -f test-secret.yaml
    
    # 查看secret对象
    $ kubectl get secret
    
    NAME                  TYPE                                  DATA   AGE
    default-token-pgfh4   kubernetes.io/service-account-token   3      15d
    mysecret              Opaque                                2      2s
    
    # edit命令可以查看到和yaml文件一样的username、password的值
    $ kubectl edit secret mysecret
    
  3. 将 Secret 挂载到 Volume 中

    # test-secret-pod-nginx.yaml
    apiVersion: v1
    kind: Pod
    metadata:
      labels:
        name: test-secret-pod
      name: test-secret-pod
    spec:
      volumes:
      - name: test-volumes
        secret:  # 挂载secret
          secretName: mysecret  # 挂载那个secret
      containers:
      - image: nginx
        name: test-secret-pod-nginx
        volumeMounts:
        - name: test-volumes
          mountPath: "/data"  # 挂载到容器的路径
    
    # 创建Pod
    $ kubectl create -f test-secret-pod-nginx.yaml
    
    # 查看Pod
    $ kubectl get po
    
    NAME                READY   STATUS    RESTARTS     AGE
    test-secret-pod     1/1     Running   0            40s
    
    # 进入容器查看挂载文件
    $ kubectl exec -it test-secret-pod -- bash
    $ cd /data
    $ cat username # admin
    $ cat password # 1f2d1e2e67df
    
  4. 除了作为volume的方式挂载到Pod内,还可以将Secret导入到Pod环境变量中

    • 部分键引入

      # test02-secret-pod-nginx.yaml
      apiVersion: v1
      kind: Pod
      metadata:
        name: test-secret-pod-02
      spec:
        containers:
        - image: nginx
          name: test-secret-pod-02
          env:
          - name: user # 变量名,其值来自于名为“mysecret”的Secret对象上的指定键的值
            valueFrom:
              secretKeyRef:
                name: mysecret # 引用的Secret对象的名称,需要与该Pod位于同一名称空间
                key: username # 引用的Secret对象上的键,其值将传递给环境变量
          - name: pass
            valueFrom:
              secretKeyRef:
                name: mysecret
                key: password
      
      $ kubectl create -f test02-secret-pod-nginx.yaml
      $ kubectl exec -it test-secret-pod-02 -- bash
      $ env # user=admin pass=1f2d1e2e67df
      
    • 全量引入

      # test03-secret-pod-nginx.yaml
      apiVersion: v1
      kind: Pod
      metadata:
        name: test-secret-pod-03
      spec:
        containers:
        - image: nginx
          name: test-secret-pod-03
          envFrom:
          - secretRef:
              name: mysecret
      
      $ kubectl create -f test03-secret-pod-nginx.yaml
      $ kubectl exec -it test-secret-pod-03 -- bash
      $ env # username=admin password=1f2d1e2e67df
      

通过Volume挂载到容器内部时,当该Secret的值发生变化时,容器内部具备自动更新的能力,但是通过环境变量设置到容器内部该值不具备自动更新的能力。所以一般推荐使用Volume挂载的方式使用Secret。

注意:

  • 通过yaml创建Opaque类型的Secret值需要base64编码后才行。

  • 而直接通过kubectl create secret generic子命令也可以创建Opaque类型的Secret对象,从而简化上面创建Secret对象的过程。
    注意通过命令的创建Secret对象,其base64编码中均有换行符哦。

    • 通过文件方式创建Secret对象

      $ cat ./username.txt
      admin
      $ cat ./password.txt
      1f2d1e2e67df
      $ kubectl create secret generic user --from-file=./username.txt
      $ kubectl create secret generic pass --from-file=./password.txt
      
    • 直接通过键值对创建

      $ kubectl create secret generic user --from-literal=username=admin
      $ kubectl create secret generic pass --from-literal=password=1f2d1e2e67df
      

Service Account

系统默认使用的类型,k8s集群为了确认pod是否属于这个集群,所以在每个pod里都会有证明pod身份的信息。

$ kubectl exec nginx-deployment-55649fd747-n42rh -- ls /run/secrets/kubernetes.io/serviceaccount

# 这三个文件就是证明pod身份的东西
ca.crt   namespace  token  

kubernetes.io/dockerconfigjson

  • 用处:用来创建用户docker registry认证的Secret,直接使用kubectl create命令创建即可

  • 好处:使用 Kuberctl 创建 docker registry(镜像仓库) 认证的 secret,将仓库的认证写入secret认证,这样的话,我们创建pod的时候将创建的secret引入创建Pod的yaml文件,就不需要手动在node节点手动登录仓库了。这样使用更加的灵活方便,如果你的node节点过多,在node节点手动登录仓库的弊端就暴露出来了。

# 一、使用htpasswd命令生成用户名和密码的文本文件
# htpasswd命令是Apache附带的程序,用于生成包含用户名和密码的文本文件,每行内容格式为“用户名:密码”,
$ apt install apache2-utils
$ mkdir -p /registry/auth
$ htpasswd -Bbn jenrey 123456 > /registry/auth/htpasswd

# 二、快速搭建一下docker registry私有镜像仓库,目前Docker Registry已经升级到了v2,使用Go语言编写,最新版的Docker已不再支持v1
# 拉取docker官方提供的用来搭建私有仓库的镜像,名为registry
$ docker pull registry
# 把registry镜像创建为带用户验证的私有镜像仓库容器,容器名为myRegistry,注意一定要挂载htpasswd命令生成的文本文件到容器内的/auth目录
$ docker run -d -v /registry/auth:/auth/ -v /registry:/var/lib/registry -p 5000:5000 --name myRegistry --restart=always -e "REGISTRY_AUTH=htpasswd" -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" -e  "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" registry
# 访问进行测试,看到{}代表成功
$ curl -XGET --user jenrey:123456 localhost:5000/v2/
# 随便下载一个镜像
$ docker pull alpine
# 把刚才下载的alpine image打tag为后续推送到私有仓库做准备
$ docker image tag alpine localhost:5000/alpine:1.0
# 登陆私有仓库,登陆成功后Docker会将token存储在 ~/.docker/config.json 文件中,从而作为拉取私有镜像的凭证。
$ docker login localhost:5000 -u jenrey -p 123456
# 把镜像推送到私有仓库
$ docker push localhost:5000/alpine:1.0
# 登出私有仓库,登出成功后私有仓库的token会在本地~/.docker/config.json中删除。
$ docker logout localhost:5000
# 删除本地docker缓存的alpine镜像和localhost:5000/alpine:1.0镜像,避免后面K8S起Pod的时候使用本地缓存就不去私有镜像仓库拉去了。
$ docker rmi alpine:latest localhost:5000/alpine:1.0
# 查看私有镜像仓库目前存储的镜像,到此就看到咱们自己搭建的私有仓库中有一个名为“apline”的image了
$ curl -XGET --user jenrey:123456 localhost:5000/v2/_catalog # {"repositories":["alpine"]}
# 查看alpine image的tag信息
$ curl -XGET --user jenrey:123456 localhost:5000/v2/alpine/tags/list # {"name":"alpine","tags":["1.0"]}
# 创建的secret的名称叫mysecretbyregistry
$ kubectl create secret docker-registry mysecretbyregistry --docker-server=localhost:5000 --docker-username=jenrey --docker-password=123456 --docker-email=jenrey

# 查看
$ kubectl get secret
$ k get secret mysecretbyregistry -oyaml

因为我们配置了镜像从私有仓库拉取,且私有仓库开启了权限验证,所以在创建 Pod 的时候,通过 imagePullSecrets 来引用刚创建的mysecretbyregistry

# test-secret-myregistry.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-secret-myregistry
spec:
  containers:
  - name: test-secret-myregistry
    image: localhost:5000/alpine:1.0 # docker registry私有镜像仓库
    command:
      - tail
      - -f
      - /dev/null
  imagePullSecrets:
    - name: mysecretbyregistry
$ kubectl create -f test-secret-myregistry.yaml
$ kubectl get po
$ kubectl exec -it test-secret-myregistry -- sh

Secret对象和ConfigMap对象的比较

  • 相同点:

    • key/value的形式

    • 属于某个特定的namespace

    • 可以导出到环境变量

    • 可以通过目录/文件形式挂载

    • 通过 volume 挂载的配置信息均可热更新

  • 不同点:

    • Secret 可以被 ServerAccount 关联

    • Secret 可以存储 docker register 的鉴权信息,用在 ImagePullSecret 参数中,用于拉取私有仓库的镜像

    • Secret 支持 Base64 加密

    • Secret 分为 kubernetes.io/service-account-token、kubernetes.io/dockerconfigjson、Opaque 三种类型,而 Configmap 不区分类型


8.6、Pod

什么是 Pod

  • Pod 是一组紧密关联的容器集合,它们共享 PID、IPC、Network 和 UTS namespace,是 Kubernetes 调度的基本单位。

  • Pod 的设计理念是支持多个容器在一个 Pod 中共享网络和文件系统,可以通过进程间通信和文件共享这种简单高效的方式组合完成服务。

  • 同一个 Pod 中的不同容器可共享资源:

    • 共享网络 Namespace;

    • 可通过挂载存储卷共享存储;

    • 共享 Security Context。

apiVersion: v1 
kind: Pod 
metadata:
  name: hello 
spec:
  containers:
  - image: nginx:1.15 
    name: nginx

Pod是Kubernetes最核心的对象,是把一组紧密相关容器定义到一个Pod里,可以看到在Pod的spec里面有一个containers的属性,是支持数组的,如果一个应用有多个容器,就可以将其组织到一起;
Pod本身是一个调度单元,即Kubernetes在做调度的时候是以Pod为单位去调度的,你可以将多个容器镜像组合成一个Pod放在一起,那么这么做的好处就是在一个Pod中的多个容器是共享Pid、IPC、Network和UTS namespace的。
所以一个Pod里面每一个容器发布的服务,在另外容器中可以通过localhost去访问的。


如何通过 Pod 对象定义支撑应用运行

​ 任何在Kubernetes上运行的应用,我们始终在讲 “应用和应用的配置分离” ,应用的代码从容器镜像来,应用的配置信息可以从环境变量来、也可以从外挂存储卷(类似传统的应用程序的配置文件)来。

方式一:环境变量

从环境变量来设置应用的配置信息又有几种方式:

  • 直接设置值;在Pod的yaml的spec里的env字段中设置,那么你的应用所在的系统中就会有一个环境变量HELLO=world如下图 中间部分 所示。

  • 读取 Pod Spec 的某些属性;如下图 上部分 所示。

  • 从 ConfigMap对象 读取某个值;如下图 左部分 所示。

  • 从 Secret对象 读取某个值。如下图 右部分 所示。



方式二:外挂存储卷
  • 通过存储卷可以将外挂存储挂载到 Pod 内部使用。

  • 存储卷定义包括两个部分: Volume 和 VolumeMounts。

    • Volume:定义 Pod 可以使用的存储卷来源;

    • VolumeMounts:定义存储卷如何 Mount 到容器内部。

apiVersion: v1 
kind: Pod 
metadata:
  name: hello-volume 
spec:
  containers:
  - image: nginx:1.15 
    name: nginx 
    volumeMounts:
    - name: data 
      mountPath: /data 
  volumes:
  - name: data 
    emptyDir: {}

Pod 网络

Pod的多个容器是共享网络 Namespace 的,这意味着:

  • 同一个 Pod 中的不同容器可以彼此通过 Loopback 地址访问:

    • 在第一个容器中起了一个服务 http://127.0.0.1 。

    • 在第二个容器内,是可以通过 httpGet http://172.0.0.1 访问到该地址的。

  • 这种方法常用于不同容器的互相协作。


资源限制

Kubernetes 通过 Cgroups 提供容器资源管理的功能,可以限制每个容器的 CPU 和内存使用。

kubectl set resources命令,限制CPU和Memory

比如对于之前创建的 deployment,可以通过下面的命令限制 nginx 容器最多只用 50% 的 CPU 和 128MB 的内存:
这里说一下,–limits=cpu=500m指的是Cgroup CPU子系统的cpu.cfs_quota_us参数的值,CPU 的计量单位叫做毫核(m),cpu.cfs_period_us默认是1000微秒,则一个节点的 CPU 核心数量乘以 1000,得到的就是节点总的 CPU 数量;如果一个节点有两个核心,那么该节点的 CPU 总量为 2000m

# 查看所有deployment
$ kubectl get deploy -owide

NAME              READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES  SELECTOR
nginx-deployment  3/3   3          3         13d nginx      nginx:latest  app=nginx

# 修改名为"nginx-deployment"的deployment,为其nginx容器修改CPU和Memory资源开销
$ kubectl set resources deployment nginx-deployment -c=nginx --limits=cpu=500m,memory=128Mi

deployment.apps/nginx-deployment resource requirements updated
# 原来
spec:
  containers:
    resources: {}
# 现在
spec:
  containers:
    resources:
      limits:
        cpu: 500m
        memory: 128Mi

等同于在每个 Pod 中设置 resources limits

apiVersion: v1 
kind: Pod 
metadata: 
  labels:
    app: nginx 
  name: nginx 
spec: 
  containers:
    - image: nginx
      name: nginx
      resources:
        limits:
          cpu: "500m"
          memory: "128Mi"

同时发现所有的deployment的Pod都滚动升级了。


kubectl set resources命令 的底层原理

下面为kubectl set resources命令设置CPU、Memory底层原理(Linux Cgroup技术)

# 查看所有deployment
$ kubectl get deploy

NAME               READY   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3/3     3            3           14d

# 查看所有ReplicaSet
$ kubectl get rs

NAME                         DESIRED   CURRENT   READY   AGE
nginx-deployment-754bf85c9   3         3         3       26h

# 查看所有Pod
$ kubectl get po

NAME                               READY   STATUS    RESTARTS        AGE
nginx-deployment-754bf85c9-49ns4   1/1     Running   0               17m
nginx-deployment-754bf85c9-5rxrk   1/1     Running   1 (8m49s ago)   12m
nginx-deployment-754bf85c9-lvj49   1/1     Running   3 (8m49s ago)   26h

# 查看此deployment的所有docker container,拿到3个Pod的docker container ID
$ docker ps |grep nginx-deployment-754bf85c9

195328818fcf
badcb27ef1c3
bae9808897ed

$ docker inspect 195328818fcf |grep -i pid
	"Pid": 7483,
	"PidMode": "",
	"PidsLimit": null,
$ docker inspect badcb27ef1c3 |grep -i pid
	"Pid": 7064,
	"PidMode": "",
	"PidsLimit": null,
$ docker inspect bae9808897ed |grep -i pid
	"Pid": 6642,
	"PidMode": "",
	"PidsLimit": null,
	
# 列出Pid=7483的所有Cgroups路径,得到下列Cgroups路径
$ cat /proc/7483/cgroup

8:cpu,cpuacct:/kubepods.slice/kubepods-pod089f877c_815d_4f1d_8a18_c8d4dc528932.slice/docker-195328818fcf8fb487f12939cc883d9bc9b1dd9369bf4ed22dc3c64a79cb91ef.scope
5:memory:/kubepods.slice/kubepods-pod089f877c_815d_4f1d_8a18_c8d4dc528932.slice/docker-195328818fcf8fb487f12939cc883d9bc9b1dd9369bf4ed22dc3c64a79cb91ef.scope

# 进入Pid=7483的Cgroup Memory路径
$ cd /sys/fs/cgroup/memory/kubepods.slice/kubepods-pod089f877c_815d_4f1d_8a18_c8d4dc528932.slice/docker-195328818fcf8fb487f12939cc883d9bc9b1dd9369bf4ed22dc3c64a79cb91ef.scope

# 使用pstree命令查看进程关系树
$ pstree -up 7483

nginx(7483)-+-nginx(7537,systemd-resolve)
            |-nginx(7538,systemd-resolve)
            `-nginx(7539,systemd-resolve)
            
$ cat cgroup.procs
7483 # 这个是本进程,下面三个Pid都是子进程号忽略即可
7537
7538
7539

$ cat memory.limit_in_bytes 

134217728 bytes也就是128MB

# 下面是Cgroup CPU系统的示例
$ cd /sys/fs/cgroup/cpu/kubepods.slice/kubepods-pod089f877c_815d_4f1d_8a18_c8d4dc528932.slice/docker-195328818fcf8fb487f12939cc883d9bc9b1dd9369bf4ed22dc3c64a79cb91ef.scope
$ cat cpu.cfs_quota_us # 50000

# 最后,查看Pid为7064、6642也是一样的,这里就不展示了。

健康检查

​ 我们之前介绍deloyment的时候说过maxUnavailable属性(默认是25%),是用来做滚动升级时,判断当前管控的Pod有多少是不能提供服务的,如果达到了25%则本次滚动升级时要暂停的,那么如何判断Pod是否能正常提供服务呢?其实我们执行kubectl get po命令就能看到有一个READY字段,会显示0/1或者1/1。
0/1就代表这个Pod有1个容器,当前READY的容器有0个,所以这个Pod本身就是不READY,即滚动升级时有一些Pod处于这种状态超过25%,那么滚动升级就不继续往下滚动了。那么READY还是不READY是由谁来控制的呢?我们一起来看一下吧。

Kubernetes 作为一个面向应用的集群管理工具,需要确保容器在部署后确实处在正常的运行状态。

  • Kubernetes提供了3种探针:

    • LivenessProbe

      • 存活性探针。
      • 探测应用是否处于健康状态,如果不健康则删除并重新创建容器。
    • ReadinessProbe

      • 就绪性探针。
      • 探测应用是否就绪并且处于正常服务状态,如果不正常则不会接收来自 Kubernetes Service 的流量。
      • 一般来说Pod在初始化的时候还没结束,ReadinessProbe探针还没过,那么此时就不应该接收流量接收请求。
      • 如果ReadinessProbe失败,应用进程不会被重启,K8S不会动这个Pod,只是把其标记为还没有READY。到底是READY还是没有READY的作用其实就是在建service的时候,service做负载均衡流量转发的时候到底要不要把这个Pod加进去,如果没READY默认的行为是不会转发流量过来的。
    • StartupProbe

      • 在K8S 1.16版本后增加的探针。主要解决在复杂的程序中readinessProbe、livenessProbe探针无法更好的判断程序是否启动、是否存活。进而引入startupProbe探针为readinessProbe、livenessProbe探针服务。
      • 有些应用在初始化阶段是不能很频繁的去做Probe的,例如一些应用在启动的时候需要很长的时间,可能此时正在拉取数据库,有可能此时端口都还没起呢,端口都没起呢,要做频繁的Probe那么就会有很多的pending(挂起)的Probe process,反而会给这个应用带来很大的压力,所以StartupProbe类似于ReadinessProbe,只不过在StartupProbe里面定义了在应用初始化阶段以更低的频度去做Probe健康检查。
      • 探测应用是否启动完成,如果在 failureThreshold*periodSeconds 周期内未就绪,则会应用进程会被重启。
  • 探活方式:

    • Exec:去容器里面执行一个进程,执行一个脚本。

    • TCP socket:相当于做一个端口的检查,类似于netcat操作,也就是去看这个ip+这个端口是否是通的,是由kubelet触发器去做一个网络连通性检查。

    • HTTP:去做一个真正的Http Call。这就是为什么在Go语言那部分要做HttpServer的时候需要专门做一个用来做健康检查的端口了,因为一般来说,除了主业务的API,还要开放一个健康检查的API,用来给Kubernetes做探活的。


readinessProbe实验

首先我们创建centos-readiness.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    run: centos
  name: centos
spec:
  replicas: 1
  selector:
    matchLabels:
      run: centos
  template:
    metadata:
      labels:
        run: centos
    spec:
      containers:
      - command:
        - tail
        - -f
        - /dev/null
        image: centos
        name: centos
        readinessProbe:
          exec:
            command:
            - cat
            - /tmp/healthy
          initialDelaySeconds: 5
          periodSeconds: 5
# 创建
$ kubectl create -f centos-readiness.yaml

# 查看所有Pod,注意到此Pod永远是READY 0/1
$ kubectl get po

NAME                               READY   STATUS    RESTARTS       AGE
centos-5fdd4bb694-6jzx5            0/1     Running   0              3m50s

# 查看名为"centos-5fdd4bb694-6jzx5"的Pod详细信息
$ kubectl get po centos-5fdd4bb694-6jzx5 -oyaml
apiVersion: v1
kind: Pod
metadata:
  annotations:
    cni.projectcalico/containerID: 578066f828f86d2ccb77f7bff8a9221b3398966fe3a28c12a3b6782718a8625a
    cni.projectcalico/podIP: 192.168.243.196/32
    cni.projectcalico/podIPs: 192.168.243.196/32
  creationTimestamp: "2022-08-08T08:31:58Z"
  generateName: centos-5fdd4bb694-
  labels:
    pod-template-hash: 5fdd4bb694
    run: centos
  name: centos-5fdd4bb694-6jzx5
  namespace: default
  ownerReferences:
  - apiVersion: apps/v1
    blockOwnerDeletion: true
    controller: true
    kind: ReplicaSet
    name: centos-5fdd4bb694
    uid: f322a16d-e96a-45c7-af97-d7297ee20b65
  resourceVersion: "328149"
  uid: 9d79b1ee-8c26-4047-86d5-3902d04259a9
spec:
  containers:
  - command:
    - tail
    - -f
    - /dev/null
    image: centos
    imagePullPolicy: Always
    name: centos
    readinessProbe: # 这个Pod比普通Pod多了readinessProbe属性,这就是健康探针
      exec: # 定义了探活方式为exec
        command:
        - cat # 执行cat命令去cat /tmp/healthy文件
        - /tmp/healthy
      failureThreshold: 3 # 出了多少次错误才认为是Failure
      initialDelaySeconds: 5 # 等5秒才开始做探活
      periodSeconds: 5 # 每次探活间隔5秒,一般这里要有一个平衡,不能太长也不能太短。太短对业务有压力,太长可能业务出错好久了才发现。
      successThreshold: 1 
      timeoutSeconds: 1 # 1秒超时
    resources: {}
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-n2vlx
      readOnly: true
  dnsPolicy: ClusterFirst
  enableServiceLinks: true
  nodeName: ubuntu
  preemptionPolicy: PreemptLowerPriority
  priority: 0
  restartPolicy: Always
  schedulerName: default-scheduler
  securityContext: {}
  serviceAccount: default
  serviceAccountName: default
  terminationGracePeriodSeconds: 30
  tolerations:
  - effect: NoExecute
    key: node.kubernetes.io/not-ready
    operator: Exists
    tolerationSeconds: 300
  - effect: NoExecute
    key: node.kubernetes.io/unreachable
    operator: Exists
    tolerationSeconds: 300
  volumes:
  - name: kube-api-access-n2vlx
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          expirationSeconds: 3607
          path: token
      - configMap:
          items:
          - key: ca.crt
            path: ca.crt
          name: kube-root-ca.crt
      - downwardAPI:
          items:
          - fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace
            path: namespace
status:
  conditions:
  - lastProbeTime: null
    lastTransitionTime: "2022-08-08T08:31:58Z"
    status: "True"
    type: Initialized
  - lastProbeTime: null
    lastTransitionTime: "2022-08-08T08:31:58Z"
    message: 'containers with unready status: [centos]'
    reason: ContainersNotReady
    status: "False"
    type: Ready
  - lastProbeTime: null
    lastTransitionTime: "2022-08-08T08:31:58Z"
    message: 'containers with unready status: [centos]'
    reason: ContainersNotReady
    status: "False"
    type: ContainersReady
  - lastProbeTime: null
    lastTransitionTime: "2022-08-08T08:31:58Z"
    status: "True"
    type: PodScheduled
  containerStatuses:
  - containerID: docker://676c1752e205239265817583f780710772e8aefecef84527df4448f999efb961
    image: centos:latest
    imageID: docker-pullable://centos@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177
    lastState: {}
    name: centos
    ready: false
    restartCount: 0
    started: true
    state:
      running:
        startedAt: "2022-08-08T08:32:34Z"
  hostIP: 10.0.2.15
  phase: Running
  podIP: 192.168.243.196
  podIPs:
  - ip: 192.168.243.196
  qosClass: BestEffort
  startTime: "2022-08-08T08:31:58Z"

解读上述信息:Pod启动5秒后开始做Probe健康检查(initialDelaySeconds),然后每5秒做一次(periodSeconds),做什么?就是做cat /tmp/healthy文件,如果命令返回非0(文件不存在或不可读),那么本次Probe健康检查就失败,否则就成功。

综上,我们可以收到exec到容器内,创建一个/tmp/healthy文件

$ kubectl exec -it centos-5fdd4bb694-6jzx5 -- bash
$ touch /tmp/healthy
$ exit
$ kubectl get po
NAME                                READY   STATUS    RESTARTS      AGE
centos-5fdd4bb694-6jzx5             1/1     Running   3 (17h ago)   19h

做下一次探活的时候,发现文件存在了,那么就READY了。

$ kubectl exec -it centos-5fdd4bb694-6jzx5 -- bash
$ rm -rf /tmp/healthy
$ exit
$ kubectl get po
NAME                                READY   STATUS    RESTARTS      AGE
centos-5fdd4bb694-hc48g             1/1     Running   0             19h

当我们删除文件后,下一次探活后,发现文件不存在,又不READY了。


8.7、Service

Service 是应用服务的抽象,通过 labels 为应用提供负载均衡和服务发现。匹配 labels 的 Pod IP 和端口列表组成 endpoints,由 Kube-proxy 负责将服务 IP 负载均衡到这些 endpoints 上。

每个 Service 都会自动分配一个 cluster IP(仅在集群内部可访问的虚拟地址)和 DNS 名,其他容器可以通过该地址或 DNS 来访问服务,而不需要了解后端容器的运行。


我们之前通过expose的方式定义过service,定义完之后会给我们分配一个ClusterIP,然后通过ipFamilies告诉我们是IPv4还是IPv6的ip,然后还有selector用来根据Pod的LABELS信息选择对应Pod,然后通过

# 查看default namespace的所有Pod并显示labels信息
$ kubectl get po --show-labels
NAME                                READY  STATUS   RESTARTS     AGE   LABELS
nginx-deployment-55649fd747-n42rh   1/1    Running  2 (21h ago)  45h   app=nginx,pod-template-hash=55649fd747
nginx-deployment-55649fd747-npn8f   1/1    Running  2 (21h ago)  45h   app=nginx,pod-template-hash=55649fd747
nginx-deployment-55649fd747-z6r29   1/1    Running  2 (21h ago)  45h   app=nginx,pod-template-hash=55649fd747

# 查看default namespace的所有svc
$ kubectl get svc
NAME               TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes         ClusterIP   10.96.0.1      <none>        443/TCP        16d
nginx-deployment   NodePort    10.99.183.87   <none>        80:32516/TCP   15d

# 查看名为"nginx-deployment"的service详情信息
$ kubectl get svc nginx-deployment -oyaml
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: "2022-07-25T07:20:03Z"
  name: nginx-deployment
  namespace: default
  resourceVersion: "65970"
  uid: a8abccc1-2db8-4f33-bc56-9f88e60883be
spec:
  clusterIP: 10.99.183.87
  clusterIPs:
  - 10.99.183.87
  externalTrafficPolicy: Cluster
  internalTrafficPolicy: Cluster
  ipFamilies: # 用来标识clusterIP是IPv4还是IPv6
  - IPv4
  ipFamilyPolicy: SingleStack
  ports:
  - nodePort: 32516
    port: 80 # Service对外提供服务的端口
    protocol: TCP
    targetPort: 80 # 目标端口,即匹配到的Pod上的端口
  selector: # 用来匹配 LABELS 为app=nginx的Pod IP和端口
    app: nginx
  sessionAffinity: None
  type: NodePort
status:
  loadBalancer: {}
# 通过clusterIP,就可以自动负载均衡的访问到这些Pod了
$ curl 10.99.183.87

Service Spec

apiVersion: v1 
kind: Service 
metadata:
  name: nginx 
spec:
  ports:
  - port: 8078 # the port that this service should serve on
    name: http
    # the container on each pod to connect to, can be a name
    # (e.g. 'www') or a number (e.g. 80)
    targetPort: 80
    protocol: TCP 
selector:
  app: nginx

8.8、无状态副本集(Replica Set)

  • Pod 只是单个应用实例的抽象,要构建高可用应用,通常需要构建多个同样的副本,提供同一个服务。

  • Kubernetes 为此抽象出副本集 ReplicaSet,其允许用户定义 Pod 的副本数,每一个 Pod 都会被当作一个无状态的成员进行管理,Kubernetes 保证总是有用户期望的数量的 Pod 正常运行。

  • 当某个副本宕机以后,控制器将会创建一个新的副本。

  • 当因业务负载发生变更而需要调整扩缩容时,可以方便地调整副本数量。



8.9、部署(Deployment)

  • 部署表示用户对 Kubernetes 集群的一次更新操作。

  • 部署是一个比 RS 应用模式更广的 API 对象,可以是创建一个新的服务,更新一个新的服务,也可以是滚动升级一个服务。

  • 滚动升级一个服务,实际是创建一个新的 RS,然后逐渐将新 RS 中副本数增加到理想状态,将旧 RS 中的副本数减小到 0 的复合操作。

  • 这样一个复合操作用一个 RS 是不太好描述的,所以用一个更通用的 Deployment 来描述。

  • 以 Kubernetes 的发展方向,未来对所有长期伺服型的的业务的管理,都会通过 Deployment 来管理。

Try it

# 通过类似 Docker run 的命令在 Kubernetes 运行容器,即创建Pod
$ kubectl run --image=nginx:alpine nginx-app --port=80
kubectl get po

# 创建svc并给名为"nginx-app"的Pod暴露80端口
kubectl expose po nginx-app --port=80 --target-port=80

# 查看services
kubectl describe svc

# 查看endpoints
kubectl describe ep

8.10、有状态服务集(StatefulSet)

一般是面对比较复杂的应用。

副本集的方式:replicaset、statefulset分别来保证无状态应用和有状态应用的高可用。

  • 对于 StatefulSet 中的 Pod,每个 Pod 挂载自己独立的存储,如果一个 Pod 出现故障,从其他节点启动一个同样名字的 Pod,要挂载上原来 Pod 的存储继续以它的状态提供服务。

  • 适合于 StatefulSet 的业务包括数据库服务 MySQL 和 PostgreSQL,集群化管理服务 ZooKeeper、etcd 等有状态服务。

  • 使用 StatefulSet,Pod 仍然可以通过漂移到不同节点提供高可用,而存储也可以通过外挂的存储来提供高可靠性, StatefulSet 做的只是将确定的 Pod 与确定的存储关联起来保证状态的连续性。



Statefulset 与 Deployment 的差异

  • 身份标识

    • StatefulSet Controller 为每个 Pod 编号,序号从0开始递增。创建的时候是从小编号到大编号的,缩容的时候是从大编号开始到小编号去删的。
    • Deployment创建出来的Pod名字的构成由Deployment Name + hash值 + 随机串,因为对Deployment来说描述的是无状态的应用,K8S对无状态应用的名字是无所谓的,叫什么都行。
  • 数据存储

    • StatefulSet 允许用户定义 volumeClaimTemplates,每一个Pod 被创建的同时,Kubernetes 会以 volumeClaimTemplates 中定义的模板创建存储卷,这样每一个Pod有自己独占的存储,用来保存其有状态的数据,并挂载给 Pod。
  • StatefulSet 的升级策略不同

    • onDelete:即新版本Pod出来后,老版本是不会自动替换的,需要删除一个老版本才会替成新版本。

    • 滚动升级

    • 分片升级

示例

apiVersion: v1
kind: Service
metadata:
  name: nginx-ss
  labels:
    app: nginx-ss
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx-ss
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: statefulset-with-pvc
spec:
  serviceName: "nginx-ss"
  replicas: 2
  selector:
    matchLabels:
      app: nginx-ss
  template:
    metadata:
      labels:
        app: nginx-ss
    spec:
      containers:
      - name: nginx-ss
        image: nginx
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

8.11、任务(Job)

除了long running service,还有一次性任务(以Job为首)。

long running就是前台忙的程序,比如centos本身是没有任何的ENTRYPOINT让其前台保持一直忙碌的状态的,那么怎么样把centos操作系统的空壳容器镜像建成long running service呢?我们可以使用tail -f /dev/null命令的方式去follow一个文件的末尾,那么这样就可以确保前台是busy,前台忙也就是long running。

一次性任务或者叫短时任务,就是执行完就结束了,例如ls命令。Job就是来支持短时任务的。

Job完成后,Pod是completed的状态;Job建出来的Pod的restart Policy是不能为Always的,所以结束了就结束了,kubelet不会再把它拉起来。

  • Job 是 Kubernetes 用来控制批处理型任务的 API 对象。

  • Job 管理的 Pod 根据用户的设置把任务成功完成后就自动退出。

  • 成功完成的标志根据不同的 specpletions 策略而不同:

    • 单 Pod 型任务有一个 Pod 成功就标志完成;

    • 定数成功型任务保证有 N 个任务全部成功;

    • 工作队列型任务根据应用确认的全局成功而标志成功。



8.12、后台支撑服务集(DaemonSet)

为每一个节点建立一个Pod,更多的是给系统管理员用的。

  • 长期伺服型和批处理型服务的核心在业务应用,可能有些节点运行多个同类业务的 Pod,有些节点上又没有这类 Pod 运行;

  • 而后台支撑型服务的核心关注点在 Kubernetes 集群中的节点(物理机或虚拟机),要保证每个节点上都有一个此类 Pod 运行。

  • 节点可能是所有集群节点也可能是通过 nodeSelector 选定的一些特定节点。

  • 典型的后台支撑型服务包括存储、日志和监控等在每个节点上支撑 Kubernetes 集群运行的服务。



8.13、存储 (PV 和 PVC)

  • PersistentVolume(PV)是集群中的一块存储卷,可以由管理员手动设置, 或当用户创建 PersistentVolumeClaim(PVC)时根据 StorageClass 动态设置。

  • PV 和 PVC 与 Pod 生命周期无关。也就是说,当 Pod 中的容器重新启动、 Pod 重新调度或者删除时,PV 和 PVC 不会受到影响,Pod 存储于 PV 里的数据得以保留。

  • 对于不同的使用场景,用户通常需要不同属性(例如性能、访问模式等)的 PV。


8.14、CRD

CRD全称为:CustomResourceDefinition

Kubernetes有很多的扩展方式:例如Aggregator、CRD等

  • Aggregator是对APIServer的扩展,APIServer本身是一个API网关,就像一个普通的网站一样,APIServer会把不同的请求转给多个后端,所以Kubernetes也是一样的,K8S源社区的APIServer是提供最核心功能的,但是比如我们要做自动扩缩容k8s-HPA(即水平自动更新(Horizontal Pod Autoscales),会基于Pods当前的CPU使用率和Memory,让其更新pods 数量以对抗增加的请求负载)的需求,那么就需要一些指标数据,这些指标数据要从APIServer里面取的,但是在默认的APIServer里面是不支持这种指标数据的,所以我们要安装metrics-server组件,metrics-server用来获取使用率数据。而metrics-server是通过Aggregator的方式嵌入到标准的APIServer里面来,最终通过kubectl top命令可以拿到资源的使用率数据(通过top命令最终转化成了一个扩展的一个对象路径,这个对象路径被APIServer接收到以后,APIServer就知道这个路径应该转到另外一个扩展的APIServer里面去),这就是Aggregator存在的场景。即Aggregator是对组件的一个扩展。
  • CRD是对Kubernetes对象的一个扩展。

基本对象没有办法支撑你的需求的时候,可以通过CRD对象用来扩展当前业务。

其实calico依赖与一堆CRD,它就定义了一些自己的对象,使用命令kubectl get crd ipamblocks.crd.projectcalico -oyaml可以看到其spec里面是在定义对象,定义这个对象是哪个组、名字是什么、schema长啥样等。

一旦定义好了这个CRD以后,就可以通过kubectl get ipamblocks.crd.projectcalico -oyaml的命令直接get,这样就能看到这个扩展对象了,可以看到这就是calico维护的一个对象,其实calico每分配一个ip,就会来修改这样的ipBlock,所谓kind.IPAMBlock就是某网段的管理对象,原理就是每分配一个ip出去就回把spec.allocations对应的位给占掉。

CRD主要用来做扩展项目的,比如说Istio、calico、Knative等,所有的这些大的、扩展的、围绕着K8S这种生态项目,都会有自己的CRD,有自己的控制器。所以有了CRD后,我们就有能力为自己的业务去定义一些K8S对象,然后写上自己的控制器,那么我们就成为了K8S的一个扩展。无论运维还是开发最后都会涉及到CRD开发的。

  • CRD 就像数据库的开放式表结构,允许用户自定义 Schema。

  • 有了这种开放式设计,用户可以基于 CRD 定义一切需要的模型,满足不同业务的需求。

  • 社区鼓励基于 CRD 的业务抽象,众多主流的扩展应用都是基于 CRD 构建 的,比如 Istio、Knative。

  • 甚至基于 CRD 推出了 Operator Mode 和 Operator SDK,可以以极低的 开发成本定义新对象,并构建新对象的控制器。



本章节就到此结束啦,我们可以做个小练习。

  • 启动一个 Envoy Deployment。

  • 要求 Envoy 的启动配置从外部的配置文件 Mount 进 Pod。

  • 进入 Pod 查看 Envoy 进程和配置。

  • 更改配置的监听端口并测试访问入口的变化。

  • 通过非级联删除的方法逐个删除对象。

Envoy类似于nginx,是一个反向代理的软件,可以去读外部的配置文件,我们可以通过ConfigMap的方式Mount到Pod里面去的。

级联删除:就是我们有一个deployment,如果我们kubectl delete deployment nginx-deployment,那么其deployment下所有的Pod就都被删除了,其实这里面是有父子关系的,级联删除就是把父亲删除,儿子也会被删除的。

# envoy.yaml
admin:
  address:
    socket_address: { address: 127.0.0.1, port_value: 9901 }

static_resources:
  listeners:
    - name: listener_0
      address:
        socket_address: { address: 0.0.0.0, port_value: 10000 }
      filter_chains:
        - filters:
            - name: envoy.filterswork.http_connection_manager
              typed_config:
                "@type": type.googleapis/envoy.extensions.filterswork.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http
                codec_type: AUTO
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains: ["*"]
                      routes:
                        - match: { prefix: "/" }
                          route: { cluster: some_service }
                http_filters:
                  - name: envoy.filters.http.router
  clusters:
    - name: some_service
      connect_timeout: 0.25s
      type: LOGICAL_DNS
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: some_service
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: nginx
                      port_value: 80
# envoy-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    run: envoy
  name: envoy
spec:
  replicas: 1
  selector:
    matchLabels:
      run: envoy
  template:
    metadata:
      labels:
        run: envoy
    spec:
      containers:
      - image: envoyproxy/envoy-dev
        name: envoy
        volumeMounts:
        - name: envoy-config
          mountPath: "/etc/envoy"
          readOnly: true
      volumes:
      - name: envoy-config
        configMap:
          name: envoy-config
# Run envoy
$ kubectl create configmap envoy-config --from-file=envoy.yaml
$ kubectl create -f envoy-deploy.yaml
$ kubectl expose deploy envoy --selector run=envoy --port=10000 --type=NodePort

# Access service,Notices: Node IP Address base on your kubernetes cluster.
$ curl <NODE IP Address>:<NodePort>

# Scale up/down/failover
$ kubectl scale deploy <deployment-name> --replicas=<n>

9、Kubernetes 控制平面组件的深入理解

9.1、etcd

etcd是整个Kubernetes里面唯一的一个数据存储的数据库。

"etcd"这个名字源于两个想法,即 unix系统的 “/etc” 文件夹和分布式系统“distibuted”。 “/etc” 文件夹为单个系统存储配置文件数据的地方,而 etcd 存储大规模分布式系统的配置信息。因此,"d"istibuted 的 “/etc” ,是为 “etcd”。


简介

Etcd是名为CoreOS的公司基于Raft开发的分布式key-value存储数据库,可用于服务发现、共享配置以及一致性保障(如数据库选主、分布式锁等)。

在分布式系统中,如何管理节点间的状态一直是一个难题,etcd像是专门为集群环境的服务发现和注册而设计,它提供了数据TTL失效、数据改变监视、多值、目录监听、分布式锁原子操作等功能,可以方便的跟踪并管理集群节点的状态。

  • 键值对存储:将数据存储在分层组织的目录中,如同在标准文件系统中

  • 监测变更:监测特定的键或目录以进行更改,并对值的更改做出反应

  • 简单:curl可访问的用户的API(HTTP+JSON)

  • 安全:可选的SSL客户端证书认证

  • 快速:单实例每秒1000次写操作,2000+次读操作

  • 可靠:使用Raft算法保证一致性


主要功能

  • 基本的key-value存储

  • 监听机制

  • key的过期及续约机制,用于监控和服务发现

  • 原子Compare And Swap和Compare And Delete,用于分布式锁和leader选举


使用场景

  • 也可以用于键值对存储,应用程序可以读取和写入 etcd 中的数据

  • etcd 比较多的应用场景是用于服务注册与发现

  • 基于监听机制的分布式异步系统


键值对存储

是etcd最重要的一个功能,也是Kubernetes用的最多的功能。

etcd 是一个键值存储的组件,其他的应用都是基于其键值存储的功能展开。

  • 采用kv型数据存储,一般情况下比关系型数据库快。

  • 支持动态存储(内存)以及静态存储(磁盘)。

  • 分布式存储,可集成为多节点集群。

  • 存储方式,采用类似目录结构。(B+tree)

    • 只有叶子节点才能真正存储数据,相当于文件。
    • 叶子节点的父节点一定是目录,目录不能存储数据。

服务注册与发现

  • 强一致性、高可用的服务存储目录。

    • 基于 Raft 算法的 etcd 天生就是这样一个强一致性、高可用的服务存储目录。
  • 一种注册服务和服务健康状况的机制。

    • 用户可以在 etcd 中注册服务,并且对注册的服务配置 key TTL( Time To Live,生存时间值),定时保持服务的心跳以达到监控健康状态的效果。


消息发布与订阅

  • 在分布式系统中,最适用的一种组件间通信方式就是消息发布与订阅。

  • 即构建一个配置共享中心,数据提供者在这个配置中心发布消息,而消息使用者则订阅他们 关心的主题,一旦主题有消息发布,就会实时通知订阅者。

  • 通过这种方式可以做到分布式系统配置的集中式管理与动态更新。

  • 应用中用到的一些配置信息放到etcd上进行集中管理。

  • 应用在启动的时候主动从etcd获取一次配置信息,同时,在etcd节点上注册一个Watcher并 等待,以后每次配置有更新的时候,etcd都会实时通知订阅者,以此达到获取最新配置信息 的目的。


概念术语

  • Raft:etcd所采用的保证分布式系统强一致性的算法。
  • Node:一个Raft状态机实例。
  • Member:一个etcd实例。它管理着一个Node,并且可以为客户端请求提供服务。
  • Cluster:由多个Member构成可以协同工作的etcd集群。
  • Peer:对同一个etcd集群中另外一个Member的称呼。
  • Client:向etcd集群发送HTTP请求的客户端。
  • WAL:预写式日志,etcd用于持久化存储的日志格式。
  • snapshot:etcd防止WAL文件过多而设置的快照,存储etcd数据状态。
  • Proxy:etcd的一种模式,为etcd集群提供反向代理服务。
  • Leader:Raft算法中通过竞选而产生的处理所有数据提交的节点。
  • Follower:竞选失败的节点作为Raft中的从属节点,为算法提供强一致性保证。
  • Candidate:当Follower超过一定时间接收不到Leader的心跳时转变为Candidate开始竞选。
  • Term:某个节点成为Leader到下一次竞选时间,称为一个Term。
  • Index:数据项编号。Raft中通过Term和Index来定位数据。

Etcd的安装

下载安装包, 参考 https://github/etcd-io/etcd/releases

  1. 下载和安装

    # 设置版本及环境变量
    $ ETCD_VER=v3.4.20
    $ DOWNLOAD_URL=https://github/etcd-io/etcd/releases/download
    
    # 安装前准备
    $ rm -f /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz
    $ rm -rf /tmp/etcd-download-test && mkdir -p /tmp/etcd-download-test
    
    # 在线下载安装包
    $ curl -L ${DOWNLOAD_URL}/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz -o /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz
    
    # 解压到/tmp/etcd-download-test目录下
    $ tar xzvf /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz -C /tmp/etcd-download-test --strip-components=1
    
    # 删除安装包
    $ rm -f /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz
    
    # 把可执行二进制文件etcd、etcdctl复制到/usr/local/bin目录下,这样就可全局使用命令
    $ cd /tmp/etcd-download-test && cp etcd etcdctl /usr/local/bin
    
    # 查看etcd版本
    $ etcd --version
    $ etcdctl version
    
  2. 启动etcd

    因为我们虚机上跑了Kubernetes,Kubernetes自己跑了一个etcd,而Kubernetes本身跑在了hostNetwork下面,所以为避免跟本地的 hostNetwork 的 etcd 容器冲突,这里我们需要修改 etcd 的监听端口。

    官方默认:2379 用于客户端请求,2380 用于对等通信。

    etcd在通信有两个面向:

    • 面向客户端的URL。主要体现在参数中有“client”关键字的参数。端口默认2379
    • etcd本身又是一个集群,集群里有多个member(etcd实例),多个member彼此之间有通讯的需要,所以就有面向member的URL。主要体现在参数中有“peer”关键字的参数。端口默认2380

    etcd官方配置参数文档说明

    # 如果没有启动Kubernetes集群那么直接使用下面命令就起来了
    $ etcd
    
    # 但是,因为我们现在启动了Kubernetes集群,就通过指定参数的方式规避默认端口。
    # 前台启动
    $ etcd --listen-client-urls 'http://localhost:12379' \
     --advertise-client-urls 'http://localhost:12379' \
     --listen-peer-urls 'http://localhost:12380' \
     --initial-advertise-peer-urls 'http://localhost:12380' \
     --initial-cluster 'default=http://localhost:12380'
     
    # 后台启动
    $ nohup etcd --listen-client-urls 'http://localhost:12379' \
     --advertise-client-urls 'http://localhost:12379' \
     --listen-peer-urls 'http://localhost:12380' \
     --initial-advertise-peer-urls 'http://localhost:12380' \
     --initial-cluster 'default=http://localhost:12380' >/tmp/etcd.log 2>&1 &
    
    • --listen-client-urls

      • Etcd端监听客户端的url。即哪些Nodes可以访问Etcd,可以多个并用逗号隔开,如果配置是http://0.0.0.0:2379,将不限制node访问地址。
      • default: http://localhost:2379
      • env variable: ETCD_LISTEN_CLIENT_URLS
    • --advertise-client-urls

      • 用于通知其他ETCD节点,有哪些客户端接入本ETCD节点的监听地址,一般来说advertise-client-urls是listen-client-urls子集,这些URL可以包含域名。
      • default: http://localhost:2379
      • env variable: ETCD_ADVERTISE_CLIENT_URLS
    • --listen-peer-urls

      • 本etcd节点与其他etcd节点进行数据交换(选举,数据同步)的监听地址
      • default: http://localhost:2380
      • env variable: ETCD_LISTEN_PEER_URLS
      • 可以多个并用逗号隔开,如果配置是http://0.0.0.0:2380,将不限制node访问地址
    • --initial-advertise-peer-urls

      • 通知其他节点与本节点进行数据交换(选举,同步)的地址,URL可以使用domain地址
      • default: http://localhost:2380
      • env variable: ETCD_INITIAL_ADVERTISE_PEER_URLS
      • 与–listener-peer-urls不同在于listener-peer-urls用于请求客户端的接入控制,initial-advertise-peer-urls是告知其他集群节点访问哪个URL,一般来说,initial-advertise-peer-urlsl将是istener-peer-urls的子集
    • --initial-cluster

      • 初始化集群,需要列所有成员地址
      • default: “default”
      • env variable: ETCD_NAME
      • 这个值和–initial-cluster flag (e.g., default=http://localhost:2380)中的key值一一对应,如果在集群环境中,name必须是唯一的,建议用主机名称或者机器ID。
    • --name

    • 节点名称

    • 默认为default

    • --data-dir

      • 数据目录的路径。
      • 如果不指定,会在当前执行命令的目录下生成一个${name}.etcd的目录

第三方库和客户端工具

etcd是集群,会有不同的角色,当客户端和etcd集群通信的时候,一般会涉及到一个问题“怎么样把这个请求按照一定的规则均分到不同的etcd实例上去。比如有一个etcd集群,有3个member,如果所有的读请求都集中于访问其中1个member,那么此etcd实例可能就会过载,压力很大。而别的etcd实例都很闲。这就涉及到负载均衡”,如何把请求分散到不同的member上去?这就需要 etcd客户端工具来支持。

目前有很多支持etcd的库和客户端工具

  • 命令行客户端工具etcdctl

  • Go客户端go-etcd

  • Java客户端jetcd

  • Python客户端python-etcd

所以,我们看Kubernetes代码时,当我们看到etcd-client的时候就会转到go-etcd里面去。


etcdctl命令的使用

etcdctl就是命令行客户端,本身就承载了负载均衡的功能,当我们执行指令的时候帮我们转成REST API发给etcd。

建议Key的名字采用类似目录命名的方式去规划,这样的好处就是达到类似分级的效果,这样最终会把整个系统的Key组织成目录层次结构的形式。

自然而然我们联想一下,我们知道K8S每一对象都有自己对应的API,API都是对应一个URL,例如使用kubectl get ns default -v 9命令可以查看到这个对象的curl URL为https://192.168.56.56:6443/api/v1/namespaces/default,这样的方式就能确定唯一性、确定了路径,这样的话如果我们设计系统就可以自然而然的把/api/v1/namespaces/default这段字符串作为它的key存到etcd里面。

那么,如果所有的对象都遵循这种命名规则存储key的话,其实就可以把所有的K8S对象按照分组、版本、类型、什么对象 组织起来了,好处就是我们在get对象的时候可以用–prefix来查找key的前缀了。

# 注意:因为我们改了端口,所以如果我们不指定endpoints参数,则会报错,因为会连默认的端口

# 查看集群成员状态
$ etcdctl member list --write-out=table --endpoints=localhost:12379

# 写入数据, key为/a , value为b
$ etcdctl --endpoints=localhost:12379 put /a b

# 读取数据
$ etcdctl --endpoints=localhost:12379 get /a

# 查看数据细节,可以看到kvs里面的key和value是base64加密的
# 使用echo L2E= |base64 -d解密就得到原始key /a
$ etcdctl --endpoints=localhost:12379 get /a -wjson

# 按key的前缀查询数据
$ etcdctl --endpoints=localhost:12379 get --prefix /

# 只显示键值
$ etcdctl --endpoints=localhost:12379 get --prefix / --keys-only

# 查看当前命令的debug信息和其环境变量
$ etcdctl --endpoints=localhost:12379 get --prefix / --keys-only --debug

# 使用watch来监听某个key的变化
$ etcdctl --endpoints=localhost:12379 watch /a

# 使用watch来监听当前etcd里面所有以/开头的key
$ etcdctl --endpoints=localhost:12379 watch --prefix /

可以看到一个名为“default”的member(etcd实例),状态是started,


理解Kubernetes如何使用etcd机制
# 使用watch来监听所有的namespaces
$ etcdctl --endpoints=localhost:12379 watch --prefix /api/v1/namespaces

# 使用watch来监听名为default的namespaces
$ etcdctl --endpoints=localhost:12379 watch --prefix /api/v1/namespaces/default

使用etcdctl命令访问在Kubernetes里面的etcd

两种方式:

  • 通过kubectl exec命令进入到K8S的etcd里执行etcdctl命令(不推荐,因为etcd 容器的sh窗口十分不友好)
  • 通过指定endpoints、CA证书文件、签名密钥对文件 的方式访问K8S的etcd(推荐)
方式一:通过kubectl exec命令进入etcd容器中执行etcdctl
# 查看名为“kube-system”的namespace下的所有Pod,可以看到有一个名为"etcd-xxx"的Pod
$ kubectl get po -n kube-system
NAME                              READY   STATUS    RESTARTS      AGE
etcd-ubuntu                       1/1     Running   3 (17h ago)   20h

# 进入etcd Pod内部,内部里面是有etcdctl的,所以是可以这样操作的
$ kubectl exec -it etcd-ubuntu -n kube-system -- sh
$ etcdctl version
方式二:通过指定endpoints、CA证书文件、签名密钥对文件 的方式访问K8S的etcd

或者我们也可以在Kubernetes宿主系统(这里是Ubuntu)上通过etcdctl指定endpoints的方式来访问Kubernetes里的etcd,因为开启了安全的访问协议,所以我们要把CA证书(ca.crt)和签名密钥对(server.crtserver.key)指定一下(Kubeadm在安装的时候会把key、crt这些对象放在指定路径下)才能访问到Kubernetes里的etcd。

# 查看名为“kube-system”的namespace下所有的Pod
$ kubectl get po -n kube-system
NAME                              READY   STATUS    RESTARTS      AGE
kube-proxy-xdmbq                  1/1     Running   1 (21h ago)   21h
...

# 找到Kubernetes的etcd容器进程详细信息(主要是要找到endpoints、CA证书路径、签名密钥对路径)
$ ps -ef | grep etcd

# 查看集群成员状态
$ etcdctl --endpoints https://127.0.0.1:2379 --cert /etc/kubernetes/pki/etcd/server.crt --key /etc/kubernetes/pki/etcd/server.key --cacert /etc/kubernetes/pki/etcd/ca.crt member list --write-out=table

# 查看所有以/开头的key,可以看到有一个名为“/registry/pods/kube-system/kube-proxy-xdmbq”的key,对应上面我们get到的Pod
$ etcdctl --endpoints https://127.0.0.1:2379 --cert /etc/kubernetes/pki/etcd/server.crt --key /etc/kubernetes/pki/etcd/server.key --cacert /etc/kubernetes/pki/etcd/ca.crt get --prefix / --keys-only

# 查看名为“/registry/pods/kube-system/kube-proxy-xdmbq”Key的Value
$ etcdctl --endpoints https://127.0.0.1:2379 --cert /etc/kubernetes/pki/etcd/server.crt --key /etc/kubernetes/pki/etcd/server.key --cacert /etc/kubernetes/pki/etcd/ca.crt get /registry/pods/kube-system/kube-proxy-xdmbq

这里可以看到etcd是双向认证TLS证书。

  • 单向TLS:比如我们访问一个网站,网站是Https的,就需要持有一张证书,这个证书要和其网站域名匹配并且由一个授信的机构去颁发的。由此证明这个网站是真正的、授信过的网站,是没有别人劫持其域名的网站。
  • 双向TLS:指的是不仅客户端要验证服务端(即网站是可信的,没有人修改其域名),而且服务端也要验证客户端(你是谁?你有没有权限访问我?)。这种就是双向TLS,即首先客户端要以Https协议去服务端(也就是endpoints是https,此时就用到了–cacert参数,即携带了ca.crt);与此同时客户端要携带自己的–key和–cert去访问服务端,这样就会把自己的key 丢给服务端,这样服务端也可以认证一下服务端是谁。

核心:TTL & CAS

在Kubernetes里对TTL的使用场景很少,所以在整个云原生技术栈中没有那么重要。

  • TTL(time to live)指的是给一个key设置一个有效期,到期后这个key就会被自动删掉,这在 很多分布式锁的实现上都会用到,可以保证锁的实时有效性。

  • **Atomic Compare-and-Swap(CAS)**指的是在对key进行赋值的时候,客户端需要提供一些条件,当这些条件满足后,才能赋值成功。这些条件包括:

    • prevExist:key当前赋值前是否存在,一般是新建一个key的时候使用。

    • prevValue:key当前赋值前的值,写代码的时候可以写if keyValue = 1,则把其值置为2。

    • prevIndex:key当前赋值前的Index

这样的话,key的设置是有前提的,需要知道这个key当前的具体情况才可以对其设置。


CAP原则

CAP原则:又称CAP定理,指的是在一个分布式系统中,三个要素不可同时具有、只能选择其中两个。CAP 原则是分布式系统的基石,在构建分布式系统时要根据实际需求进行取舍,而不是试图搭建一个完美的分布式系统。

  • 一致性(Consistency):在分布式系统中,所有节点在同一时刻的数据都是一致的。
    • 强一致性:保证系统改变提交以后立即改变集群的状态。
      • Paxos
      • Raft(muti-paxos):是对Paxos算法的简化和改进:整个系统只有一个Proposer,称之为Leader。
        一个Node只会处于下述3种状态中的一种:
        • Leader总统节点,负责发出提案
        • Follower追随者节点,负责同意Leader发出的提案
        • Candidate候选人,负责争夺Leader
      • ZAB(muti-paxos)
    • 弱一致性:也叫最终一致性,系统不保证改变提交以后立即改变集群的状态,但是随着时间的推移最终状态是一致的。
      • DNS系统
      • Gossip协议
  • 可用性(Availability):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。即每个请求不管成功与否都能得到响应。
  • 分区容错(Partition-tolerance):保证系统中任意信息的丢失都不会影响系统的运行。

CAP各种使用场景:

  • CA:几乎不存在适用场景。在分布式系统中,P必然存在(因为分区问题总是会出现在分布式系统中),除非适用单机,要提升分区可靠性,需要通过提升基础设施的可靠性实现。即放弃系统扩展性,不部署子节点,比如单节点的关系型数据库。
  • CP:在分布式服务器中保持强一致性,强一致就是所有人都一致,其带来的问题就是系统的可用性降低、很慢、效率低的,因为如果有1个人不在,就达不成一致,所以可能会导致同步时间长,比如分布式数据库 redis、HBase、zk、etcd。
  • AP:放弃一致性,保持高可用,每个节点使用本地数据提供服务,使得全局数据不一致。比如使用手机网购,可能浏览商品时还有货,但下单时系统告诉你下单失败。
    例如:12306购票、淘宝购物。保证服务可用,购票下单后,一段时候后系统提示余票不足。

本文标签: 上手带你一文圣经第四部分