目录
一. k8s基础与组成
- k8s的优点:
- 传统部署模式下,在物理服务器上运行应用程序,不能方便的动态为应用程序定义资源边界,造成导致资源分配问题
- k8s提供了服务发现,负载均衡,及异常恢复功能,能够更好的个管理应用
- k8s的组成可以在架构组成与应用两个角度去看
- k8s从架构角度看提供了一下组件
- APIServer: 所有指令不管外部ui界面发起,还是命令行,还是k8s内部操作指令都是由apiServer接收
- ControllerManager: 控制管理器,用来解析请求,生成操作指令保存到etcd中
- Scheduler: 调度器在etcd键值数据库获取到controllerManager解析保存的操作指令,进行调度计算
- Kubelet: 获取指令并执行,以及负责当前节点上应用的启停,状态上报等功能,实际命令的执行者
- Kube-proxy: 网络代理器,基于ipvs,管理k8s内部网络关系
- Etcd: 键值数据库,用来存储数据,包括命令等信息
- 从k8s架构角度看执行过程,基于List-Watch机制执行业务,首先整个List-Watch中至少有三个watch监听,分别是 ControllerManager(运行在 Master), Scheduler(运行在 Master), kubelet(运行在 Node) 在它们进程启动时就会监听APIServer 发出来的事件。
- 用户通过 kubectl 或其他 API 客户端提交请求命令到 APIServer
- APIServer 接收到操作命令后将命令相关元信息存入 etcd 中
- 当 Etcd 接收操作指令后, 事件给 APIServer。
- 当中ControllerManager与Scheduler 会一直监听APIServer 中的事件,拿到执行事件
- 通过ControllerManager监听获取到事件后对事件命令进行二次解析,将解析后的命令存入etcd
- 通过Scheduler监听获取到事件后,进行调度计算,确认当前事件要在哪一个节点上执行
- 自此执行命令,与执行该命令的节点都已经获取到,
- kubelet 运行在node工作节点上,通过List-Watch 的方式监听APIServer 的事件,当发现该事件属于当前节点时,kubelet 执行事件命令进行实际工作,并将结果状态回送至 APIServer。
- kubelet 后续一直监听, 负责当前node节点上应用的启停,通过apiServer进行状态上报等工作
- 从应用角度k8s中重点关注一下组件
- container容器,服务在容器中运行
- Pod: k8s部署的最小单位,一个pod中可以安装多个容器,又称为容器组
- Service: 负载均衡网络,用来封装一组pod,基于Service可以实现服务发现,四层负载
- Ingress: 用来代理Service,基于ipvs实现七层负载,解决Service负载中的问题
二. 名称空间
- 在k8s中还存在一种资源,namespace名称空间, 默认提供了3个
- default: 在创建k8s对象时,不指定metadata.namespace时的默认值
- kube-system: k8s中系统创建的对象放在此名称空间下
- kube-public: 此名称空间自动在安装集群是自动创建,并且所有用户都是可以读取的(即使是那些未登录的用户)。主要是为集群预留的,例如,某些情况下,某些Kubernetes对象应该被所有集群用户看到
- 同一名称空间下资源可以共享, 不同名称空间下则不可,但是不同名称空间下网了是互通的(实际也可以隔离),通过该资源在k8s中可以实现隔离功能,例如使用名称空间实现
- 基于环境创建名称空间隔离
- 基于产品线创建名称空间隔离
- 基于团队创建名称空间隔离
- 默认情况下安装Kubernetes集群时会初始化一个default名称空间,用来承载那些未指定名称空间的Pod、Service、Deployment等对象,大多数kubernetes资源(例如Pod、Service、副本控制器等)都位于某些命名空间中。但是命名空间资源本身并不在命名空间中。而且底层资源,例如nodes和持久化卷不属于任何命名空间
三. 标签与选择器
- 为了对k8s资源进行归类方便管理,k8s支持针对资源打标签
//针对资源添加标签
kubectl label 资源类型 标签名=标签值
//针对资源下的指定资源添加标签
kubectl label 资源类型 资源名 标签名=标签值
- 添加标签后可以根据标签去选中指定资源
四. Pod
什么是Pod: k8s中最小的部署单元,一个pod中可以运行一个或多个容器,这些容器共享存储、网络、以及怎样运行这些容器的声明,一般不直接创建Pod,而是创建一些工作负载由工作负载来创建Pod(下方有工作负载的解释)
1. Pod中的paush是什么
- 通过pod运行一个容器,底层除了运行这个实际应用容器以外,还会多运行一个paush容器,与部署的实际应用容器是成对出现的,通过这个paush容器来给当前应用容器设置网,pause容器的最主要的作用:创建共享的网络名称空间,以便于其它容器以平等的关系加入此网络名称空间
- 面试题1: 为什么一个pod中的多个容器可以共享网络: 因为在pod在部署容器时,每个应用都会多部署一个对应的paush容器,通过这个paush容器设置来设置当前实际容器的网络
- 面试题2: 如果pod中的容器宕机重启,ip会变吗,不会,底层会通过对应的paush去设置网络
2. Pod 声明周期
- pod对象从创建到终止的这段时间范围称为pod的生命周期,主要高寒:
- pod创建
- 运行初始化容器(initContainer)
- 运行注容器(main container)过程
3.1 容器启动后钩子(post start),容器终止前钩子(pre stop)
3.2 容器的存活性探测(liveness probe),就绪性探测(readness probe)
- pod终止过程
- 在整个声明周期中,Pod会出现5种状态(相位)
- 挂起(Pending):挨批server已经创建了pod资源对象,但它尚未被调度完成或者仍处于下载镜像的过程中
- 运行中(Running):pod已经被调度至某节点,并且所有容器都已经被kubectl创建完成
- 成功(Succeed):pod中的所有容器都已经成功终止并且不会被重启
- 失败(Failed):所有容器都已经停止,但至少有一个容器终止失败,即容器返回了非0的退出状态
- 未知(Unknown):apiserver无法正常获取到pod对象的状态信息,通常由网络通信失败所致
- Pod的创建过程详解:
- 用户通过kubectl或其他api客户端提交需要创建的pod信息给apiServer
- apiServer开始生成pod对象的信息,并将信息存入etcd,然后返回确认信息至客户端
- apiServer开始反映etcd中的pod对象的变化,其他组件使用watch机制来跟踪检查apiServer上的变动
- scheduler发现有新的pod对象要创建,开始为pod分配足迹并将结果更新只apiServer
- node节点上的kubectl发现有pod调度过来,尝试调用docker启动容器,并将结果回送至apiServer
- apiServer将接收到的pod状态信息存入etcd中
- Pod的终止过程详解:
- 用户向apiServer发送删除pod对象的命令
- apiServer中的pod对象信息随着时间的退役而更新,在宽限期内(默认30秒),pod被视为dead
- 将pod标记为terminating状态
- kubelet在监控到pod对象转为terminating状态的同时启动pod关闭过程
- 端点控制器监控到pod对象的关闭行为时将其从所有匹配到此端点的service资源的端点列表中移除
- 如果当前pod对象定义了preStop钩子处理器,则在其标记为terminating后即会以同步的方式启动执行
- pod对象的容器进程收到停止信号
- 宽限期结束后,若pod中还存在仍在运行的进程,那么pod对象会收到吉利终止的信号
- kubelet请求apiServer将此pod资源的款限制设置为0从而完成删除操作,此时pod对于用户已不可见
3. Pod 重启策略
- 先了解一下pod的几种状态
- Pending挂起: 请求创建pod时条件不满足,调度没有完成,已经创建了但是没有节点运行当前pod则叫做挂起,这其中也包含集群为容器创建网络或者下载镜像的过程
- Running: Pod内所有的容器都已经被创建,且至少一个容器正在处于运行、启动或重启状态。
- Succeeded:Pod中所以容器都执行成功后退出,并且没有处于重启的容器
- Failed:Pod中所以容器都已退出,但是至少还有一个容器退出时为失败状态
- Unknown未知状态:所谓pod是什么状态是apiserver和运行在pod节点的kubelet进行通信获取状态信息的,如果节点之上的kubelet本身出故障,那么apiserver就连不上kubelet,得不到信息了,就会看Unknown
- Pod支持的重启策略:(如果pod的restartpolicy没有设置,那么默认值是Always)
- Always: 只要容器失效退出就重新启动容器。
- OnFailure: 当容器以非正常(异常)退出后才自动重新启动容器。
- Never: 无论容器状态如何,都不重新启动容器
- Pod状态流转的几种情况
五. Service 负载均衡网络
- Service负载均衡网络, 我理解的Service用来封装管理一组pod,基于Service将一组pod统一对外暴露,并且基于Service实现了基于四层的负载均衡,服务发现等功能, 并且通过探针使被代理的这组pod实现服务发现功能,即使Service中的无头服务类型,虽然需要使用ingress网络定义所有的访问逻辑才能被外部访问,但是Pod之间可以使用service名字作为域名访问Service中的pod
- Service中type属性解释:(基于这个type属性确定当前Service管理的一组pod对外暴露的方式)
- ClusterIP: 以集群ip方式暴露,多副本时带负载均衡.该方式下暴露的地址只允许集群内访问
- NodePort: 以节点端口方式暴露,每一台机器都会为这个Service随机分配一个指定的端口,.多副本时带负载均衡,该方式允许公网以"公网ip+暴露的端口"访问
- LoadBalancer:是可以实现集群外部访问服务的另外一种解决方案,并不是所有的k8s集群都会支持,大多是在公有云托管集群中会支持该类型, 负载均衡器是异步创建的,在 NodePort 的基础上,借助 cloud provider 创建一个外部的负载均衡器,并将请求转发到< NodeIP>:NodePort,此模式只能在云服务器上使用。
- ExternalName:将服务通过 DNS CNAME记录方式转发到指定的域名,通过service代理外部域名
- 封装Service时可以通过"spec.clusterIP"属性指定为"None"创建不分配ip的无头服务,由上层组件统一管理ip,配合StatefulSet有状态部署,外部通过ingress定义访问逻辑访问,内部Pod之间通过service名作为域名来访问,更方便内部地址的管理
六. k8s工作负载
- 在k8s部署应用时,如果部署pod,pod内部的容器具有异常恢复功能,但是整个pod如果异常则不会有异常恢复功能,k8s中提供了工作负载
- 工作负载中包含一下类型:
- Deployment: 通过Deployment部署的一个应用(内部可能存在多个pod),Deployment中可以根据设置的期望状态更改实际状态,实现pod的异常恢复功能,例如期望几个副本,实际几个副本,
- DaemonSet: 在使用Deployment部署时会根据调度规则将pod部署到节点上,使用DaemonSet时不需要设置副本数,默认除master外会给每个机器都部署一个,比如日志收集组件,在每个机器都运行一份
- StatefulSet: 有状态应用部署,比如redis,提供稳定的存储、网络等功能
- Job: 任务,实际也可以认为部署的是一个pod,是非阻塞的,有结束点
- CronJob: 定时执行Job,定时任务
Deployment相关问题
通过Deployment实现了pod的自愈,滚动更新,动态扩缩容,蓝绿部署金丝雀部署等功能
1. 自愈
- 部署Deployment时,可以设置spec期望状态,内部实际运行时存在一个实际状态,在运行时会基于探针探测服务状态,一直维护实际状态到期望状态,进而实现自愈功能,这块要跟随探针一块去理解
2. 滚动更新
- 首先k8s中存在一个ReplicaSets副本集控制器,用来操作部署的副本信息
- 部署时Deployment 中存在 Pod 模板,当模板修改时会触发更新,在Deployment中存在几个属性实现滚动更新
- revisionHistoryLimit: k8s升级时历史版本记录,Deployment revision history存储在它控制的ReplicaSets中。默认在etcd中保存记录10个,如果删除的旧的RepelicaSet,或将该值设置为0,Deployment就无法再回退到指定版本
- strategy: 升级策略,枚举值为Recreate重新创建或RollingUpdate滚动升级,重新创建会先杀死所有旧版本然后升级,推荐使用滚动升级
- minReadySeconds: 新pod等待就绪时间,默认情况下服务启动完成就认为升级生效对外提供服务,可以设置成30秒,防止pod启动了但是服务还没准备好导致系统不可用
- maxSurge: 升级过程中最多可以比原先设置多出的pod数量, 例如:maxSurage=1,replicas=5,则表示Kubernetes会先启动1一个新的Pod后才删掉一个旧的POD,整个升级过程中最多会有5+1个POD。
- maxUnavaible: 升级过程中最多有多少个POD处于无法提供服务的状态,当maxSurge不为0时,该值也不能为0,最好和maxSurge保持一致。例如:maxUnavaible=1,则表示Kubernetes整个升级过程中最多会有1个POD处于无法服务的状态,可以保证有足够的pod(或者认为是足够的性能)提供服务
- progressDeadlineSeconds: 升级版本时超时时间,默认600s,超过这个时间整个升级未完成,则认为失败
- paused: 暂停升级, 默认false
- Deployment 更新底层实现原理:
- 在上面已经知道Deployment 通过RS副本资源控制器控制副本数
- Deployment针对版本更新回退提供了: revisionHistoryLimit 历史版本记录, maxSurge级过程中允许多出的副本数,与maxUnavaible升级过程中不对外提供服务的副本数等设置属性
- 在我们使用Deployment滚动更新时,并不是所有pod全部升级直接替换,例如:maxSurage=1,replicas=5,则表示Kubernetes会先启动1一个新的Pod后才删掉一个旧的POD,整个升级过程中最多会有5+1个POD, 新升级的pod等待minReadySeconds时间后表示就绪才会下掉旧pod,然后依次替换其它pod,并且当maxSurge不为0时,maxUnavaible也不能为0,推荐与maxSurge保持一致。例如:maxUnavaible=1,整个升级过程中最多会有1个POD处于无法服务的状态,可以保证有足够的pod(或者认为是足够的性能)提供服务
- 每更新一次,就会创建一个RS副本资源,不同的RS控制不同版本的副本数,这些RS副本资源通过revisionHistoryLimit 进行记录,当我们进行版本回退,或者修改yaml中更新版本属性后拿到与某个旧版本的hash,则启用对应版本的RS副本提供服务,revisionHistoryLimit 默认10个, 注意当该值为0时表示不进行历史记录,则无法版本回退
- 并且通过maxSurge与maxUnavailable组合起来实现的功能就叫做比例缩放
3. HPA (动态扩缩容)
- 动态扩缩容需要配合安装Metrics-server实现, 编写HorizontalPodAutoscaler HPA的yaml,监控指定资源,可以根据cup等资源实现动态扩缩容
##hpa配置 hpa.yaml
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: php-apache
spec:
scaleTargetRef: #将要扩展的目标引用
apiVersion: apps/v1
kind: Deployment #目标类型
name: php-apache #目标名称
targetCPUUtilizationPercentage: 50 #限制根据cup占有率进行扩缩容,超过50%则扩容,低于50%则缩容
maxReplicas: 10 #最大副本数,最大可扩容到的副本数量
minReplicas: 1 #最小副本数,缩容时最小保存副本数
StatefulSet
- Deployment部署的应用一般称为无状态应用,StatefulSet部署的应用一般称为有状态应用
- 无状态应用:可以简单理解为无状态应用不会记录当前状态,例如网络,存储等信息,如果应用宕机重启,重启后网络,存储,顺序可能等可能会变,例如Deployment部署的业务代码
- 有状态应用:可以记录当前状态,即使重启,重启后根据状态记录信息可以做到网络,存储,顺序等不变,例如中间件MySQL、Redis、MQ等等,以mysql为例,部署为有状态的服务,即使宕机重启访问地址不会变,其它服务还是可以正常访问的
- StatefulSe通常与Service负载均衡网络配合使用,以访问地址不变为例,封装Service负载均衡网络时设置" clusterIP: None" 不要分配ClusterIP,headless service等,访问地址由Service内部代理的通过StatefulSe部署pod自己控制
- 该方式下k8s内部通过"podName.serviceName.namespace.svc.cluster.local" 访问指定pod
- 通过无头服务Service代理StatefulSet 部署的多个pod,也是可以直接访问service,通过Service负载均衡选择指定pod访问
- 此时外部无法访问这个无头服务Service的,如果想要外部访问需要使用ingress网络定义所有的访问逻辑
七. k8s探针与服务发现
- k8s中的服务发现是基于Service负载均衡网络与探针实现的,k8s中可以通过封装Service管理一组pod
探针
- k8s中提供了三种探针: startupProbe启动探针, livenessProbe存活探针, readinessProbe就绪探针,探针支持使用方式
- exec: 通过钩子程序执行命令
- httpGet: 通过钩子发送http get请求
- tcpSocket: 容器创建之后连接tcp端口进行指定操作
- 探针是用来做什么的:
探针是用来做什么的:
- startupProbe启动探针: 通过该探针来查看当前容器是否启动成功
- livenessProbe存活探针: 通过该探针判断当前容器是否存活,基于该探针如果探测到容器不再存活,则会重新拉起
- readinessProbe就绪探针: 通过该探针通知kubelet当前容器是否就绪,能否对外提供服务,以调用服务负载均衡为例,当接收到请求后如果通过该探针探测到某个服务节点不可用,则不会将该节点加入负载均衡
- 探针中的几个属性值
- initialDelaySeconds:容器启动后要等待多少秒后就探针开始工作,单位“秒”,默认是 0 秒,最小值是 0
- periodSeconds:执行探测的时间间隔(单位秒),默认为 10s,单位“秒”,最小值是 1
- timeoutSeconds:探针执行检测请求后,等待响应的超时时间,默认为 1s,单位“秒”,最小值是 1
- successThreshold:探针检测失败后认为成功的最小连接成功次数,默认为 1s,在 Liveness 探针中必须为 1s,最小值为 1s。
- failureThreshold:探测失败的重试次数,重试一定次数后将认为失败,在 readiness 探针中,Pod会被标记为未就绪,默认为 3s,最小值为 1s
- initialDelaySeconds在readinessProbe其实可以不用配置,不配置默认pod刚启动,开始进行readinessProbe探测,但那有怎么样,除了startupProbe,readinessProbe、livenessProbe运行在pod的整个生命周期,刚启动的时候readinessProbe检测失败了,只不过显示READY状态一直是0/1,readinessProbe失败并不会导致重启pod,只有startupProbe、livenessProbe失败才会重启pod。而等到多少后,真正服务启动后,检查success成功后,READY状态自然正常
- kubelet会主动按照Pod里面配置的探针,给对应的容器容器发送探测请求进行监听
- kubelet使用启动探针,来检测应用是否已经启动。启动探针启动成功后就不再使用,后续通过存活探针和就绪探针持续探测,慢容器一定指定启动探针。 一个服务如果前期启动需要很长时间,那么它后面死亡未被发现的时间就越长,如果只使用存活探针检查,探测间隔时间要超过这个启动时间否则会造成一直循环重启,所以添加启动探针,即使重启最开始也是有启动探针检测,当启动探针检测成功后才交由其它探针检测
- 当探测到容器启动成功后,后续kubelet通过livenessProbe存活探针探测,每隔periodSeconds时间探测一次获取容器状态,如果连续successThreshold次返回成功当前容器表示存活,如果连续failureThreshold次拿到的都是失败则认为容器为不存活状态,对容器进行重启
- 通过livenessProbe存活探针只探测容器的存活状态,至于容器能不能访问,能不能对外提供服务,k8s中通过readinessProbe就绪探针定时检查,当就绪探针发现对应容器返回失败,那么这个容器不会加入Service负载均衡网络,不接受流量,此时执行"kubectlexec-it"进不去,可以通过"Kubectl describe"查看异常情况,当前我们使用的就绪检测都是基于http的,例如定时发送http访问当前容器服务,如果返回不是0,那就是探测失败.注意就绪探针探测失败时不会对容器重启
- 当前生产上配置就绪探针,存活探针都是5秒执行一次,连续两次失败未失败,1次成功则认为是成功,存活探针基于tcp,就绪探针基于httpGet
tcp存活探针: 类似于telnet 80端口,如果连接失败则将杀死 Pod 重启容器
http就绪探针: 与tcp类似,给指定端口发送http请求,根据响应结果判断容器状态,当前访问的服务域名
readinessProbe:
httpGet:
path: /
port: <PORT>
scheme: HTTP #返回不是0,那就是探测失败
initialDelaySeconds: 30 #指定的这个秒以后才执行探测
periodSeconds: 5 #每隔几秒来运行这个
failureThreshold: 2 #失败阈值,连续几次失败才算真失败
successThreshold: 1 #成功阈值,连续几次成才算成功
timeoutSeconds: 5 #探测超时,到了超时时间探测还没返回结果说明失败
livenessProbe:
tcpSocket:
port: <PORT>
initialDelaySeconds: 30
periodSeconds: 5
failureThreshold: 2
successThreshold: 1
timeoutSeconds: 5
EndPoint 与 k8s服务发现原理
- endpoint是k8s集群中的一个资源对象,存储在etcd中,用来记录一个service对应的所有pod的访问地址,在封装service时配置selector选择器,endpointController会根据选择器选中的服务自动创建对应的endpoint对象.否则不会生成endpoint对象,endpoint中如果就绪探针探测就绪,会将该副本的地址加入endpoint中,如果后续存活探针探测异常,会将endpoint中对应的服务地址踢除
- endpointController: 是k8s集群控制器的其中一个组件功能如下:
- 负责生成和维护所有endpoint对象的控制器
- 负责监听service和对应pod的变化
- 监听到service被删除,则删除和该service同名的endpoint对象
- 监听到新的service被创建,则根据新建service信息获取相关pod列表,然后创建对应endpoint对象
- 监听到service被更新,则根据更新后的service信息获取相关pod列表,然后更新对应endpoint对象
- 监听到pod事件,则更新对应的service的endpoint对象,将podIp记录到endpoint中
八. 灰度
- 先说一下滚动发布的缺点:
- 不能直接控制新老版本的存活时间,新版本只要发布就绪成功,会直接下掉所有老版本
- 不能精确控制新老版本间的流量划分,只能通过新老版本pod比例来控制
- k8s中可以基于Service实现灰度,也可以基于Ingress实现灰度
- 基于基于封装Service的金丝雀灰度,使用选择器根据标签选中pod的实现的,详细流程:
- Deployment部署服务pod, 设置副本数,设置pod标签,标签可以后缀版本号例如"v1"
- 通过新建一个Deployment的方式进行版本更新,新建的Deployment拉取 新版本的镜像,设置副本数,设置pod标签,注意标签前缀与旧版本的前缀相同,标签后缀为"v2",此时旧版本,新版本共存
- 由于Service使用选择器,根据pod标签选中pod代理,那么原service就可以同时选中新旧两个版本的pod,新旧版本同时对外提供服务,可以通过新版Deployment中的副本数控制调整新旧版本流量划分
- 当通过第二个Deployment部署的新版本验证成功后,直接下掉旧版本的pod即可,或者Service选中pod的选择器中修改选中标签为"前缀+v版本号"选中指定版本
- 通过Ingress可以实现更强大的灰度,Ingress对应七层网络,可以对请求拆解包,针对请求进行指定负载,Ingress提供了基于注解的强大功能,以灰度举例提供了:
- “nginx.ingress.kubernetes.io/canary”: 为true表示开启金丝雀灰度功能
- 在获取路由标识时提供了基于:“canary-by-header”," canary-by-cookie"与"canary-weight"(权重)三种,并且可以就有优先级的共同使用
- 如果通过"canary-by-header"," canary-by-cookie"与"canary-weight"获取到了需要的请求参数,则将请求路由到rules中指定的service上
Ingress
- Ingress 在k8s中可以理解为对应七层网络的一个服务网关,底层通常采用nginx实现
- Ingress 使用 annotations 方式提供了多种功能,使用指定功能时配置上对应的注解即可
- Ingress 常用的功能
- 基于"nginx.ingress.kubernetes.io/canary"实现金丝雀灰度发布功能
- 基于"nginx.ingress.kubernetes.io/rewrite-target"实现路径重写,请求路由功能
- 基于"nginx.ingress.kubernetes.io/limit-rps"实现简单限流功能
- 配置SSL,基于Ingress实现HTTPS请求
九. k8s kube-proxy 与 负载均衡
kube-proxy
- kube-proxy中管理了整个集群中的网络相关操作,称为网络管理器,支持一下负载方案
- userspace:最早的负载均衡方案,它在用户空间监听一个端口,所有服务通过 iptables 转发到这个端口,然后在其内部负载均衡到实际的 Pod。该方式最主要的问题是效率低,有明显的性能瓶颈。
- iptables:目前推荐的方案,完全以 iptables 规则的方式来实现 service 负载均衡。该方式最主要的问题是在服务多的时候产生太多的 iptables 规则,非增量式更新会引入一定的时延,大规模情况下有明显的性能问题
- ipvs:为解决 iptables 模式的性能问题,v1.11 新增了 ipvs 模式(v1.8 开始支持测试版,并在 v1.11 GA),采用增量式更新,并可以保证 service 更新期间连接保持不断开
- winuserspace:同 userspace,但仅工作在 windows 节点上
- iptables 存在的问题
- iptables规则复杂零乱, 规则多了之后性能下降,这是因为iptables规则是基于链表实现,查找复杂度为O(n),当规模非常大时,查找和处理的开销就特别大。据官方说法,当节点到达5000个时,假设有2000个NodePort Service,每个Service有10个Pod,那么在每个Node节点中至少有20000条规则,内核根本支撑不住,iptables将成为最主要的性能瓶颈。
- iptables主要是专门用来做主机防火墙的,而不是专长做负载均衡的。虽然通过iptables的statistic模块以及DNAT能够实现最简单的只支持概率轮询的负载均衡
- 我们需要更多更灵活的算法,比如基于最少连接算法、源地址HASH算法等。而同样基于netfilter的ipvs却是专门做负载均衡的,配置简单,基于散列查找O(1)复杂度性能好,支持数十种调度算法。因此显然ipvs比iptables更适合做kube-proxy的后端,毕竟专业的人做专业的事,物尽其美
- kube-proxy ipvs 是基于 NAT 实现的,通过ipvs的NAT模式,对访问k8s service的请求进行虚IP到POD IP的转发。当创建一个 service 后,kubernetes 会在每个节点上创建一个网卡,同时帮你将 Service IP(VIP) 绑定上,此时相当于每个 Node 都是一个 ds,而其他任何 Node 上的 Pod,甚至是宿主机服务(比如 kube-apiserver 的 6443)都可能成为 rs参考文档
- kube-proxy ipvs下支持的负载均衡算法:
- rr: round-robin(默认),轮询
- lc: least connection: 最小连接,实时记录每台服务器的连接数,获取当前连接数最小的服务器
- dh: destination hashing: 目标地址散列调度
- sh: source hashing: 源地址散列调度,正好与目标地址散列调度算法相反
- sed: shortest expected delay: 最短的期望的延迟
- nq: never queue: 最少队列调度,如果有台realserver的连接数=0 就直接分配过去,不需要进行sed运算
- kube-proxy 工作原理:
- kube-proxy 监听 APIServer 中 service 和 endpoint 的变化情况,并通过 userspace、iptables、ipvs 或 winuserspace 等 proxier 来为服务配置负载均衡(仅支持 TCP 和 UDP)
负载均衡
- 可以先说一下service与探针实现的服务发现
- 在k8s中支持基于四层和七层的两种负载,四层负载时基于负载均衡器直接将请求打到指定的服务上,而七层负载,请求先打到负载均衡器,对请求拆解包,可以根据请求参数进行负载
- 了解k8s的负载均衡首先要了解kube-proxy,Service就是由kube-proxy实现的
- k8s中有一个专门的负载均衡组件Service,但对应的是一个四层负载均衡,有针对七层负载均衡Ingress,在Ingress中可以针对Service基于ipvs配置成外网能够访问的URL,并且Ingress可以对请求拆解包,根据请求参数将请求打到指定Service,例如根据"canary-by-header"," canary-by-cookie"与"canary-weight"判断请求是否下发到指定的service,
- 并且Ingress使用注解方式实现了路径重写,流量控制等功能可以看为一个服务网关
- 请求进来的执行流程:
- 封装Service,代理一组pod
- 针对指定Service封装Ingress,通过Ingress判断请求下发到哪个Service上
- Ingress上层安装针对Ingress的负载均衡
- 请求到达Ingress后,解析请求参数,根据请求参数选择指定Service
- Service基于kube-proxy 监听 APIServer 中 service 和 endpoint 的变化情况,并通过 userspace、iptables、ipvs 或 winuserspace 等 proxier 来为服务配置负载均衡,默认轮询,选择指定服务调用
十. k8s挂载与配置
挂载相关
- 使用Docker时就有数据卷的概念,方式容器删除数据也被删除的问题,想要持久化使用数据,需要把主机上的目录挂载到Docker中去,在K8S中默认情况下如果Pod删除,数据卷也会一起删除,k8s中提出了卷的概念,是docker数据卷的扩展
- k8s根据存储的数据可以分为临时存储相关的, 持久化存储相关,支持
- EmptyDir: 临时挂载,在pod中开辟空间
- hostPath:
- 持久化挂载要根据实际使用的持久化存储供应商进行配置
- 为了方便持久化存储的挂载设置提出了PV&PVC&StorageClass,开发人员只需要编写持久化申请即可,持久化存储卷pv由StorageClass根据存储供应商的存储制备器自动创建
- 挂载开辟空间的回收策略: 由 StorageClass 动态创建的 PersistentVolume 会在类的 reclaimPolicy 字段中指定回收策略,可以是 Delete 或者 Retain。如果 StorageClass 对象被创建时没有指定 reclaimPolicy,它将默认为 Delete。通过 StorageClass 手动创建并管理的 PersistentVolume 会使用它们被创建时指定的回收政策
配置相关
- k8s中的配置比较重要的就是两种:
- Secret: 通常用来保存敏感信息
- ConfigMap: 应用级别相关配置,非加密
十一. k8s调度原理
- 假设当前使用的是k8s集群,有多个节点, 在部署pod时底层会通过scheduler调度器计算调度,选择其中一个节点进行部署, 默认情况下这个计算调度过程是k8s决定的, 也可以通过设置一定的规则,根据规则选择指定的节点,例如部署pod时通过"spec.nodeName"属性设置选择指定节点部署
- 在调度策略中区分亲和与反亲和性,亲和性中又有硬亲和与软亲和的两种
- 例如通过nodeSelector选择器,根据标签选中指定节点进行安装,这是硬亲和,
- 其它还包括podAffinity对应pod的硬亲和,和podAntiAffinity对应pod的反亲合
- 软亲和性为节点选择机制提供了一种柔性控制逻辑,被调度的Pod对象不再是“必须”而是“应该”放置于某些特定节点之上,当条件不满足时它也能够接受调度到其它节点上
- 在设置亲和时当Pod资源基于节点亲和性规则调度至某节点之后,节点标签发生了改变而不再符合此节点亲和性规则时 ,调度器不会将Pod对象从此节点上移出,因为,它仅对新建的Pod对象生效
- 在k8s中还存在一个拓扑键与拓扑分区的概念,用来划分逻辑区域的,例如节点根据区域设置 topologyKey 拓扑键,后续如果使用亲和或反亲和时,就可以根据区域去选择安装的节点
- 在k8s中还存在污点与容忍的两个概念, 污点是设置在k8s节点上的, 容忍是设置在pod上的, 污点的类型有三种
- NoSchedule: 不调度,不给有该类型污点的节点部署pod
- PreferNoSchedule: 比NoSchedule宽泛,尽量不会在该类型的节点上调度
- NoExecute: 不能在节点上运行,如果已经运行将被驱逐(被赶走后基于k8s的恢复机制,可以在其它节点重新拉起)
- 以k8s集群master节点上不会安装应用服务为例, 因为在安装k8s的master节点时会给master节点添加一个NoSchedule污点,后续在执行pod安装时,由于pod上没有设置容忍NoSchedule污点,所以不会给master节点安装
- k8s调度时还会考虑一个资源限制问题,在部署pod时,containers容器中可以设置requests与limit, 在调度部署时,k8s底层会根据这个限制判断选择合适的节点安装
- 总结调度原理:
- 在调度部署时,k8s中会考虑是否设置亲和与反亲和,考虑污点与容忍的设置,并且可以使用 topologyKey 设置逻辑分区,
- 由于containers容器中可以设置requests与limit, 在调度部署时,k8s底层会根据这个限制判断选择合适的节点安装
十二. NetworkPolicy 网络隔离策略
- NetworkPolicy 网络策略(网络隔离策略): k8s中资源是通过命名空间隔离的,但是为了保证k8s的网络互通性,是没有做隔离的,防止调用错误,例如测试环境请求到了生产服务等问题,可以通过NetworkPolicy定义网络策略
- 通过NetworkPolicy可以精确到pod设置网络隔离,基于pod的标签,NameSpace, CIDR进行隔离(默认情况下Pod 都是非隔离的)
- 实际可以理解成编写NetworkPolicy的yaml,内部使用选择器选中指定pod,对选中的pod设置网络策略,限制被选中的pod允许哪些资源访问,限制被选中的pod可以访问哪些资源
- 一个NetworkPolicy的yaml示例与解释
- “kind: NetworkPolicy”: 当前资源时NetworkPolicy
- 通过podSelector设置选择器,设置选中哪些pod,针对选中的pod编写网络策略
- “policyTypes”: 有两个值"Ingress" 入站策略与"Egress"出站策略
- “ingress”: 在该字段下编写入站策略,也就是被选中的pod允许哪些资源访问
- “egress”: 在该字段下编写出站策略,也就是被选中的pod可以访问哪些资源
- to和from选择器的行为:NetworkPolicy 的 spec.ingress.from 和 spec.egress.to 字段中,都可以指定 4 种类型的标签选
择器
- podSelector 选择与 NetworkPolicy 同名称空间中的 Pod 作为入方向访问控制规则的源或者出方向访问控制规则的目标
- namespaceSelector 选择某个名称空间下所有的Pod作为入方向访问控制规则的源或者出方向访问控制规则的目标
- namespaceSelector 和 podSelector 在一个 to / from 条目中同时包含 namespaceSelector 和podSelector 将选中指定名称空间中的指定 Pod。此时请特别留意 YAML 的写法
- ipBlock 可选择 IP CIDR 范围作为入方向访问控制规则的源或者出方向访问控制规则的目标。这里应该指定的是集群外部的 IP,因为集群内部 Pod 的 IP 地址是临时分配的,且不可预测
十三. 配置
- k8s中的配置比较重要的就是两种:
- Secret: 通常用来保存敏感信息
- ConfigMap: 应用级别相关配置,非加密
- Secret配置也分为不同类型:(可以执行"kubectl create secret --help" 命令查看解释)
- Opaque: 用户自定义的任意kv数据(例如服务应用需要的数据)
- docker-register: 下载私有镜像,连接私库的秘钥
- generic:
- tls: 证书相关
- 其它:
- ConfigMap用来保存配置,将配置数据和应用程序代码分开,与Secret 不同的是,Secret 中保存的数据value是经过base64编码后的,读取Secret时会自动解码,而ConfigMap不会编码,读取时也不会解码
十四. 存储挂载
- 使用Docker时就有数据卷的概念,方式容器删除数据也被删除的问题,想要持久化使用数据,需要把主机上的目录挂载到Docker中去,在K8S中默认情况下如果Pod删除,数据卷也会一起删除,k8s中提出了卷的概念,是docker数据卷的扩展
- 编写部署服务yaml时,在pod中通过"volumes"设置当前pod用到的挂载卷,卷的详情,通过容器中的"containers.volumeMounts"配置当前容器的挂载点,当前容器使用volumes中的哪个卷
- 根据功能不同可以分为: 配置信息相关的, 临时存储相关的, 持久化存储相关
- 临时存储相关的:
- emptyDir: 相当于分配一个当前pod上的空目录
- hostPath: 挂载一个主机的目录
- 为什么这两种称为临时存储: 如果当前pod宕机,在其它节点拉起,由于其它节点上没有对应的目录,可能访问不到数据
- 持久化存储的 PV&PVC&StorageClass
- Pv (Persistent Volume)持久化存储卷是对底层共享存储的一种抽象,pv由管理员进行配置和创建,要提供存储能力,访问模式,存储类型,回收策略,后段存储类型等主要信息。它和具体的底层的存储技术实现有关,比如NFS, Hostpath等
- Pvc(PersistentVolumeClaim)持久化卷申请,是一种用户对存储的需求声明,声明中包括了存储大小、存储类型、以及k8s中选择器的属性等,另一个角度来看PVC和Pod类似,Pod是消耗节点node资源,PVC消耗的是PV资源,Pod可以请求CPU的内存,而PVC可以请求特定的存储空间和访问模式
- PersistentVolume 和 PersistentVolumeClaim,封装存储如何供应的细节, 是集群中的一块存储,可以由管理员事先设置好PV为静态供应, 或者通过StorageClass申请多少创建多少的动态供应,动态创建PV
- PV&PVC解决的问题: 对应不同的存储,挂载方式也不同,在编写挂载时比较繁琐,为了方便开发,提供了专门的挂载编写方式persistentVolumeClaim持久卷申请
使用持久卷申请后,设置挂载时可以挂载指定的存储挂载系统例如NFS,也可以编写pv持久卷申请,在持久卷申请中编写挂载设置,然后pod挂载时挂载pv,通过pv再进行实际挂载,这个pv由运维提供,简化了开发的工作
持久卷是集群资源,就像节点也是集群资源一样。PV 持久卷和普通的 Volume 一样,也是使用卷插件来实现的,只是它们拥有独立于使用他们的Pod的生命周期
十五. ResourceQuota 资源限制
- ResourceQuota 是k8s中对每个命名空间资源消耗总量提供的一种限制,可以限制指定命名空间中指定类型资源对象的总数目上限,也可以限制命名空间中Pod可以使用的资源总上限
- 可以为每个命名空间创建一个或多个ResourceQuota 对象,当用户在该命名空间下创建资源时例如Pod,Service…,k8s的配额系统会跟踪集群资源的使用情况,以确保使用资源不会超过ResourceQuota 中设置的硬性资源限额
- 如果启用指定命名空间的ResourceQuota ,限制计算资源配额(例如cpu,或memory),则必须为这些资源设置请求值request和约束值limit,否则系统将拒绝Pod的创建,提示使用LimitRanger准入控制器为没有设置资源请求的Pod设置默认值(也就是部署pod的yaml中设置resources资源限制属性,如果不设置这个resources则需要使用LimitRanger)
- ResourceQuota 中可以针对计算资源总量进行限制:
- ResourceQuota 中可以针对存储资源总量进行限制
- 通过ResourceQuota 限制计算资源配额(例如cpu,或memory)时必须为这些资源设置请求值request和约束值limit,否则系统将拒绝Pod的创建,提示使用LimitRanger准入控制器为没有设置资源请求的Pod设置默认值(也就是部署pod的yaml中设置resources资源限制属性,如果不设置这个resources则需要使用LimitRanger)
- 这个LimitRanger就是指定限制范围的,一个 LimitRange限制范围对象提供的限制能够做到:
- 可以在一个命名空间中对每个 Pod 或 Container 最小和最大的资源使用量的限制。
- 可以在一个命名空间中对每个 PersistentVolumeClaim 能申请的最小和最大的存储空间大小的限制。
- 可以在一个命名空间中实施对一种资源的申请值和限制值的比值的控制。
- 可以设置一个命名空间中对计算资源的默认申请/限制值,并且自动的在运行时注入到多个 Container 中。
十六. 安装调度策略
- k8s在安装资源时提供了安装调度策略
- 通过"spec.nodeName"属性设置选择指定节点
- 通过nodeSelector选择器,根据节点标签选中
- Affinity: 亲和
- anti-affinity: 反亲和
- nodeSelector 是硬亲和的一种,是PodSpec的一个字段,包含键值对的映射,为了使pod在某个节点上运行,该节点标签中必须包含这里的键值对
实际可以理解为给k8s节点设置对应的标签,在部署pod时设置节点选择器,通过节点选择器选中指定节点安装部署
- NodeAffinity意为Node节点亲和性的调度策略,是用于替换NodeSelector的全新调度策略,分为:硬亲和性required和软亲和性preferred两种
- node硬亲和支持:
- nodeAffinity: 节点亲和设置
- podAffinity: pod亲和设置
- podAntiAffinity: pod反亲和设置
- 节点软亲和性为节点选择机制提供了一种柔性控制逻辑,被调度的Pod对象不再是“必须”而是“应该”放置于某些特定节点之上,当条件不满足时它也能够接受被编排于其他不符合条件的节点之上。另外,它还为每种倾向性提供了weight属性以便用户定义其优先级,取值范围是1 ~ 100,数字越大优先级越高
- 通过preferredDuringSchedulingIgnoredDuringExecution属性设置软亲和
拓扑键
- 在设置亲和与反亲和时存在一个topologyKey拓扑键,用来划分逻辑区域,
- pod亲和性调度需要各个相关的pod对象运行于"同一位置", 而反亲和性调度则要求他们不能运行于"同一位置",这里指定“同一位置” 就是通过 topologyKey 来定义的,topologyKey 对应的值是 node 上的一个标签名称,是指一个范围的概念,比如一个 Node、一个机柜、一个机房或者是一个地区(如杭州、上海)等,实际上对应的还是 Node 上的标签, 比如各别节点zone=A标签,各别节点有zone=B标签,pod affinity topologyKey定义为zone,那么调度pod的时候就会围绕着A拓扑,B拓扑来调度,而相同拓扑下的node就为“同一位置”
- 执行命令给"k8s-node1"节点设置zone标签为sh-1
kubectl label node k8s-node1 zone=sh-1
- 如果基于各个节点"kubernetes.io/hostname"标签作为评判标准,那么“同一位置”意味着同一节点,不同节点既为不同位置
也就是拓扑键为"kubernetes.io/hostname"作为标准时,如果在亲和策略中,那么会加入到同一个节点上,如果在反亲和策略中,那么就不能在同一节点上
污点与容忍
- 以k8s调度部署pod为例,在部署时首先会按照拓扑键拓扑分区约束进行划分,然后通过亲和与反亲和策略选择指定节点安装部署,然后会通过污点与容忍度去判断选择
- 首先污点是设置到节点上的,容忍是设置在pod上的,默认情况下不会给有污点标签的节点调度,除非pod中设置了容忍,容忍跟污点是有对应关系的
- 进而解释在k8s集群中安装部署时为什么不回安装部署到master节点,因为master节点给打了污点标签,查看污点命令
执行命令后返回的数据中,前面"node-role.kubernetes.io/master"是污点名,冒号后面是污点的作用效果,例如 “NoSchedule”
资源限制与调度策略
- 在部署pod时,containers容器中可以设置requests与limit, 在调度部署时,k8s底层会根据这个限制判断选择合适的节点安装
十七. 其它问题
内部组成
- k8s组件:在整个k8s集群中分为master节点与工作节点
- master节点中内部包含以下组件:
- APIServer: api网关服务器
- ControllerManager: 控制管理器
- Scheduler:调度器
- etcd: 键值数据库(有点像redis)
- master节点下连接多个工作节点
- 工作节点中包含以下组件
- kubelet: 每一个节点必须安装,可以理解为一个监工,负责通过apiServer与master交互,获取指令并执行,以及当前node节点上应用的启停,状态上报等等
- kube-proxy: 网络代理器
- docker: 容器运行时环境
- pod: k8s的基本单位,相当于docker 中的 container容器,但是在docker环境时一个docker容器代表不了一个基本应用,而一个pod可以代表一个基本应用,内部可以运行多个docker容器
- fluentd: 可以理解为当前节点运行的日志信息收集系统,不是k8s默认自带的,不太重要
一次请求执行流程简介
- 以部署一个服务为例,先执行命令编写部署的yaml
- 部署yaml编写完成后,运行"kubectl create -f nginx.yml",
- 该命令首先会提交给 API Server ,然后解析 yml 文件,并对其以 API 对象的形式存到 etcd 里。
- k8s集群的master节点通过ControllerManager 会以控制循环的方式来做编排工作,创建应用所需的Pod。同时 Scheduler 会 watch etcd 中新 pod 的变化,如果他发现有一个新的 pod 的变化。
- 如果 Scheduler 发现有一个新的 pod 出现,它会运行调度算法,然后选择出最佳的 Node 节点,并将这个节点的名字写到 pod 对象的 NodeName 字段上,这一步就是所谓的 Bind Pod to Node,然后把 bind 的结果写到 etcd。
- 在构建 k8s 集群的时候,默认每个节点都会初始化创建一个 kubectl 进程,kubectl 进程会 watch etcd 中 pod 的变化,当 kubectl 进程监听到 pod 的 bind 的更新操作,并且 bind 的节点是本节点时,它会接管接下来的所有事情,如镜像下载,创建容器等
- 再次简述
- 所有指令都会请求到master节点的apiServer网关服务器,apiServer网关服务器是master的唯一入口
- apiServer只负责接收请求,接收到的请求由apiServer交给master节点的controllerManager
- controllerManager先解析请求,生成操作指令保存到etcd中,假设当前是一个部署tomcat应用的请求,解析请求拿到操作指令例如"tomcat–image:tomcat6–8080"(指令描述"拉取tomcat6版本镜像,指定端口号为8080"注意此处只是简单解释,不等于实际生成的指令,)
- 是不是可以理解controllerManager只会解析请求,将解析后的操作指令存储到etcd上(此处都是异步执行的?),并不会真实执行操作指令
- scheduler调度器在etcd键值数据库获取到controllerManager解析保存的操作指令,进行调度计算
- scheduler 会将调度计算拿到的结果再次存到etcd中,假设当前是一个部署指令,计算调度可以简单理解为计算获取当前部署到那台node节点上,将这个节点地址与操作指令等信息存储到etcd中
- 每个node节点上都会存在一个kubelet,相当于一个监工,所有kubelet也是通过apiServer与master进行交互, kubelet调用apiServer不断获取etcd最新数据
- 当某个node节点上的kubelet发现由scheduler存储到etcd上的操作指令属于当前节点,获取并执行该指令,假设是部署命令,kubelet会run一个应用在当前节点上,后续会定时上报当前节点容器运行状态给master
- 上面node节点上通过kubelet运行了一个应用,会对该应用分配ip地址,
- 当不同node节点上运行的应用相互调用时,通过kube-proxy网络代理进行请求转发,kube-proxy可以获取到集群中所有节点服务的地址
- 以上node与master之间的相互通信都是通过apiServer完成的,大项目集群情况下apiServer压力实际是挺大的
- 以上运行的整个流程都发送请求,解析缓存指令,拿到指令调度获取节点信息再次缓存,都监听etcd上保存的指令完成的,这种方式在Kubernetes中称为 List-Watch 的机制
List-Watch 的机制
- 什么是List-Watch 的机制, 有哪些List-Watch
- Kubernetes 是通过 List-Watch 的机制进行每个组件的协作,保持数据同步的
- 用户是通过 ui页面或kubectl 根据配置文件,向 APIServer 发送命令,在 Node 节点上面建立 Pod 和 Container。
- APIServer 经过 API 调用,权限控制,调用资源和存储资源的过程,实际上还没有真正开始部署应用。这里需要 Controller Manager、Scheduler 和 kubelet 的协助才能完成整个部署过程。
- 在 Kubernetes 中,所有部署的信息都会写到 etcd 中保存。实际上 etcd 在存储部署信息的时候,会发送 Create事件给 APIServer,而 APIServer 会通过监听(Watch)etcd发过来的事件。其他组件也会监听(Watch)APIServer 发出来的事件
- 详细过程
- 首先整个 List-Watch中至少有三个watch监听,分别是 ControllerManager(运行在 Master), Scheduler(运行在 Master), kubelet(运行在 Node) 在它们进程启动时就会监听APIServer 发出来的事件。
- 用户通过 kubectl 或其他 API 客户端提交请求给 APIServer 来建立一个 Pod 对象副本。
- APIServer 尝试着将 Pod 对象的相关元信息存入 etcd 中,待写入操作执行完成,APIServer 即会返回确认信息至客户端。
- 当 etcd 接受创建 Pod 信息以后,会发送一个 Create 事件给 APIServer。
- 由于 Controller Manager 一直在监听(Watch,通过http的8080端口)APIServer 中的事件。此时 APIServer 接受到了 Create 事件,又会发送给 Controller Manager。
- Controller Manager 在接到 Create 事件以后,调用其中的 Replication Controller 来保证 Node 上面需要创建的副本数量。一旦副本数量少于 RC 中定义的数量,RC 会自动创建副本。总之它是保证副本数量的 Controller(PS:扩容缩容的担当)
- 在 Controller Manager 创建 Pod 副本以后,APIServer 会在 etcd 中记录这个 Pod 的详细信息。例如 Pod 的副本数,Container 的内容是什么。
- 同样的 etcd 会将创建 Pod 的信息通过事件发送给 APIServer。
- 由于 Scheduler 在监听(Watch)APIServer,并且它在系统中起到了“承上启下”的作用,“承上”是指它负责接收创建的 Pod 事件,为其安排 Node;“启下”是指安置工作完成后,Node 上的 kubelet 进程会接管后继工作,负责 Pod 生命周期中的“下半生”。 换句话说,Scheduler 的作用是将待调度的 Pod 按照调度算法和策略绑定到集群中 Node 上。
- Scheduler 调度完毕以后会更新 Pod 的信息,此时的信息更加丰富了。除了知道 Pod 的副本数量,副本内容。还知道部署到哪个 Node 上面了。并将上面的 Pod 信息更新至 API Server,由 APIServer 更新至 etcd 中,保存起来。
- etcd 将更新成功的事件发送给 APIServer,APIServer 也开始反映此 Pod 对象的调度结果。
- kubelet 运行在node工作节点上,,也是通过 List-Watch 的方式监听(Watch,通过https的6443端口)APIServer 发送的 Pod 更新的事件。如果监听到apiServer更新pod指令属于当前keubelet所在节点的,kubelet 会尝试在当前节点上调用 Docker 启动容器,并将 Pod 以及容器的结果状态回送至 APIServer。
- APIServer 将 Pod 状态信息存入 etcd 中。在 etcd 确认写入操作成功完成后,APIServer将确认信息发送至相关的 kubelet,事件将通过它被接受。
- kubelet 后续一直监听, 负责通过apiServer与master交互,获取指令并执行,以及当前node节点上应用的启停,状态上报等等
scheducer调度器详解
- 什么是scheducer,或者说一下scheducer执行过程,或者一个pod创建的过程
- Scheduler 是 kubernetes 的调度器,在Kubernetes的master节点上作为单独的程序运行的,启动之后会一直监听APIServer等待命令
- 假设发送一个安装启动应用指令,通过apiServer被ControllerManager接收并解析,将解析后的指令保存到etcd,此时需要确认当前要在Kubernetes的哪个工作节点上安装启动这个应用,这里就是通过scheducer调度器选择出来的指定工作节点
- ,获取spec.nodeName为空的pod,对每个pod都会创建一个binding,表明该pod应该放到哪个节点上, 主要的任务是决定把定义的 pod分配到集群的哪个节点上, 主要会进行如下考虑:
- 公平:如何保证每个节点都能被分配资源
- 资源高效利用:集群所有资源最大化被使用
- 效率:调度的性能要好,能够尽快地对大批量的pod完成调度工作
- 灵活:允许用户根据自己的需求控制调度的逻辑
- 调度分为如下三个部分:
- 首先是过滤掉不满足条件的节点,这个过程称为预算策略(predicate);
- 然后对通过的节点按照优先级排序,这个是优选策略(priorities);
- 最后从中选择优先级最高的节点。如果中间任何一步骤有错误,就直接返回错误。
- Predicate预算策略, 有一系列的常见的算法可以使用:
- PodFitsResources:节点上剩余的资源是否大于 pod 请求的资源。
- PodFitsHost:如果 pod 指定了 NodeName,检查节点名称是否和 NodeName 匹配。
- PodFitsHostPorts:节点上已经使用的 port 是否和 pod 申请的 port 冲突。
- PodSelectorMatches:过滤掉和 pod 指定的 label 不匹配的节点。
- NoDiskConflict:已经 mount 的 volume 和 pod 指定的 volume 不冲突,除非它们都是只读。
- 如果在 predicate 预算策略过程中没有合适的节点,pod 会一直在 pending状态,不断重试调度,直到有节点满足条件。经过这个步骤,如果有多个节点满足条件,就继续 priorities 过程:按照优先级大小对节点排序。
- 优选策略, 优先级由一系列键值对组成,键是该优先级项的名称,值是它的权重(该项的重要性)有一系列的常见的优先级选项包括:
- LeastRequestedPriority:通过计算CPU和Memory的使用率来决定权重,使用率越低权重越高。也就是说,这个优先级指标倾向于资源使用比例更低的节点。
- BalancedResourceAllocation:节点上CPU和Memory使用率越接近,权重越高。这个一般和上面的一起使用,不单独使用。比如node01的CPU和Memory使用率20:60,node02的CPU和Memory使用率50:50,虽然node01的总使用率比node02低,但node02的CPU和Memory使用率更接近,从而调度时会优选node02。
- ImageLocalityPriority:倾向于已经有要使用镜像的节点,镜像总大小值越大,权重越高。
- 通过算法对所有的优先级项目和权重进行计算,得出最终的结果。
CRI层面简述一个pod的创建流程
- 在创建一个pod命令执行时,k8s内部会通过 CRI (Container Runtime Interface) 接口调用 dockershim,请求创建一个容器。这一步中,Kubectl 可以视作一个简单的 CRI Client,而 dockershim 就是接收的 Server。
- dockershim 收到请求后,通过适配的方式,适配成 Docker Daemon 的请求格式,发到 Docker Daemon 上请求创建一个容器。在 docker 1.12 后的版本,docker daemon 被拆分成了 dockerd 和 containerd,其中,containerd 负责操作容器。
- dockerd 收到请求后,会调用 containerd 进程去创建一个容器
- containerd 收到请求后,并不会自己直接去操作容器,而是创建一个叫做 containerd-shim 的进程,让 containerd-shim 去操作容器,创建 containerd-shim 的目的主要有以下几个
- 让 containerd-shim 做诸如收集状态,维持 stdin 等 fd 打开等工作。
- 允许容器运行时( runC ) 启动容器后退出,不必为每个容器一直运行一个容器运行时的 runC
- 即使在 containerd 和 dockerd 都挂掉的情况下,容器的标准 IO 和其它的文件描述符也是可以用的
- 向 containerd 报告容器的退出状态
- 在不中断容器运行时的情况下,升级或重启 dockerd
- containerd-shim会调用 runC 命令行工具,来启动容器,runC 是 OCI(Open Container Initiative, 开放标准协议)的一个参考实现。主要用来设置 namespaces 和 cgroups,挂载 root filesystem等操作。
- runC 启动完容器后,本身会直接退出。containerd-shim 则会成为容器进程的父进程,负责收集容器进程的状态,上报给 containerd,并在容器中的 pid 为 1 的进程退出后接管容器中的子进程进行清理,确保不会出现僵尸进程 (关闭进程描述符)。
什么是CRI接口
- CRI (容器运行时接口)基于 gRPC 定义了 RuntimeService 和 ImageService 等两个 gRPC 服务,分别用于容器运行时和镜像的管理
- ImageService: 提供了从仓库中拉取镜像、查看和移除镜像的功能。
- RuntimeService: 负责Pod 和容器的生命周期管理,以及与容器的交互 (exec/attach/port-forward) ,rkt和Docker这样的容器运行时可以使用一个 Socket同时提供两个服务,在 kubelet 中可以用container-runtime-endpoint 和 image-service-endpoint 参数设置这个 Socket。
service RuntimeService {
// Version returns the runtime name, runtime version, and runtime API version.
rpc Version(VersionRequest) returns (VersionResponse) {}
// RunPodSandbox creates and starts a pod-level sandbox. Runtimes must ensure
// the sandbox is in the ready state on success.
rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {}
// StopPodSandbox stops any running process that is part of the sandbox and
// reclaims network resources (e.g., IP addresses) allocated to the sandbox.
// If there are any running containers in the sandbox, they must be forcibly
// terminated.
// This call is idempotent, and must not return an error if all relevant
// resources have already been reclaimed. kubelet will call StopPodSandbox
// at least once before calling RemovePodSandbox. It will also attempt to
// reclaim resources eagerly, as soon as a sandbox is not needed. Hence,
// multiple StopPodSandbox calls are expected.
rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {}
// RemovePodSandbox removes the sandbox. If there are any running containers
// in the sandbox, they must be forcibly terminated and removed.
// This call is idempotent, and must not return an error if the sandbox has
// already been removed.
rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {}
// PodSandboxStatus returns the status of the PodSandbox. If the PodSandbox is not
// present, returns an error.
rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse) {}
// ListPodSandbox returns a list of PodSandboxes.
rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {}
// CreateContainer creates a new container in specified PodSandbox
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {}
// StartContainer starts the container.
rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {}
// StopContainer stops a running container with a grace period (i.e., timeout).
// This call is idempotent, and must not return an error if the container has
// already been stopped.
// TODO: what must the runtime do after the grace period is reached?
rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {}
// RemoveContainer removes the container. If the container is running, the
// container must be forcibly removed.
// This call is idempotent, and must not return an error if the container has
// already been removed.
rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse) {}
// ListContainers lists all containers by filters.
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {}
// ContainerStatus returns status of the container. If the container is not
// present, returns an error.
rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse) {}
// UpdateContainerResources updates ContainerConfig of the container.
rpc UpdateContainerResources(UpdateContainerResourcesRequest) returns (UpdateContainerResourcesResponse) {}
// ReopenContainerLog asks runtime to reopen the stdout/stderr log file
// for the container. This is often called after the log file has been
// rotated. If the container is not running, container runtime can choose
// to either create a new log file and return nil, or return an error.
// Once it returns error, new container log file MUST NOT be created.
rpc ReopenContainerLog(ReopenContainerLogRequest) returns (ReopenContainerLogResponse) {}
// ExecSync runs a command in a container synchronously.
rpc ExecSync(ExecSyncRequest) returns (ExecSyncResponse) {}
// Exec prepares a streaming endpoint to execute a command in the container.
rpc Exec(ExecRequest) returns (ExecResponse) {}
// Attach prepares a streaming endpoint to attach to a running container.
rpc Attach(AttachRequest) returns (AttachResponse) {}
// PortForward prepares a streaming endpoint to forward ports from a PodSandbox.
rpc PortForward(PortForwardRequest) returns (PortForwardResponse) {}
// ContainerStats returns stats of the container. If the container does not
// exist, the call returns an error.
rpc ContainerStats(ContainerStatsRequest) returns (ContainerStatsResponse) {}
// ListContainerStats returns stats of all running containers.
rpc ListContainerStats(ListContainerStatsRequest) returns (ListContainerStatsResponse) {}
// UpdateRuntimeConfig updates the runtime configuration based on the given request.
rpc UpdateRuntimeConfig(UpdateRuntimeConfigRequest) returns (UpdateRuntimeConfigResponse) {}
// Status returns the status of the runtime.
rpc Status(StatusRequest) returns (StatusResponse) {}
}
// ImageService defines the public APIs for managing images.
service ImageService {
// ListImages lists existing images.
rpc ListImages(ListImagesRequest) returns (ListImagesResponse) {}
// ImageStatus returns the status of the image. If the image is not
// present, returns a response with ImageStatusResponse.Image set to
// nil.
rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse) {}
// PullImage pulls an image with authentication config.
rpc PullImage(PullImageRequest) returns (PullImageResponse) {}
// RemoveImage removes the image.
// This call is idempotent, and must not return an error if the image has
// already been removed.
rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse) {}
// ImageFSInfo returns information of the filesystem that is used to store images.
rpc ImageFsInfo(ImageFsInfoRequest) returns (ImageFsInfoResponse) {}
}
CRD
- CRD 是 Kubernetes 内置的一种资源类型,也就是前面编写Yaml时对应 Kind 的内容,比如Service、Deployment 等也有对应CRD类型,用于描述用户定义的资源是什么样子
- CRD出现的原因: 随意k8s使用越来越多, 用户自定义资源的需求越来越多,为了提供聚合各个资源的能力,提出一种用户自定义的资源,满足定制化需求,提高k8s扩展能力,这就是CRD
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
creationTimestamp: null
labels:
controller-tools.k8s.io: "1.0"
# 名称必须与下面的spec字段匹配,格式为: <plural>.<group>
name: apps.app.o0w0o.cn
spec:
# 用于REST API的组名称: /apis/<group>/<version>
group: app.o0w0o.cn
names:
# kind字段使用驼峰命名规则. 资源清单使用如此
kind: App
# URL中使用的复数名称: /apis/<group>/<version>/<plural>
plural: apps
# 指定crd资源作用范围在命名空间或集群
scope: Namespaced
# 自定义资源的子资源的描述
subresources:
# 启用状态子资源
status: {}
# 验证机制
validation:
# openAPIV3Schema is the schema for validating custom objects.
openAPIV3Schema:
properties:
...
- 结论
- 从功能角度分析: 用户可以把 Kubernetes 已有的资源和能力通过CRD自由堆砌起来,从而拓展了 Kubernetes 原生不具备的能力。
- 从产品角度分析: CRD 允许用户基于自己产品的概念,让 Kubernetes 已有的资源为用户服务,而不是思考如何将场景应用到 Kubernetes。基于 Kubernetes 开发产品,无法避免如何将产品概念想 Kubernetes 靠拢,例如一个服务就是一个 Deployment,一个实例就是一个 Pod 等。
什么是 Informer 机制
- Informer: 是k8s中整合的一个本地缓存和索引机制,可以注册 EventHandler 的 client,本地缓存被称为 Store,索引被称为 Index,使用 informer 的目的是为了减轻 apiserver 数据交互的压力而抽象出来的一个 cache 层, 客户端对 apiserver 数据的 “读取” 和 “监听” 操作都通过本地 informer 进行
其它相关问题
- kubelet创建一个pod的流程(后续答完想起来面试官应该是想让我说CRI的)
- Pod内容器共享了哪些namespace?(IPC,NET,UTS,MNT 网络资源? 存储资源?)
- 介绍一个 Service 有什么类型 作用?
- Ingress 和 Service 是如何搭配使用 从 网络的角度上来说 分别工作于哪一层: Service对应四层, ingress对应七层
- 说下K8s网络隔离的原理: k8s中针对资源可以打标签, 并且提供了选择器选中资源,可以理解成编写NetworkPolicy的yaml,内部使用选择器选中指定pod,对选中的pod设置网络策略,限制被选中的pod允许哪些资源访问,限制被选中的pod可以访问哪些资源
- 说下常见的Namespace,是不是下面这些?
- default: 在创建k8s对象时,不指定metadata.namespace时的默认值
- kube-system: k8s中系统创建的对象放在此名称空间下
- kube-public: 此名称空间自动在安装集群是自动创建,并且所有用户都是可以读取的(即使是那些未登录的用户)。主要是为集群预留的,例如,某些情况下,某些Kubernetes对象应该被所有集群用户看到
- 说下informer的原理
- K8s的NFS有了解过吗: NFS 网络数据卷(持久化卷), 安装NFS插件,通过NFS支持网络访问挂载的持久卷
- etcd在里面是做什么的: 在k8s中最重要的就是存储中间命令,
- etcd脑裂问题
- CRD是怎么设计的
- CRD的一个问题,和ReplicaSet相关,涉及滚动更新
- 对于kubernetes生命周期管理的优化点在哪里
- kubernetes多集群管理有什么设计?有什么优缺点吗?你觉得哪种方式更好
- kubernetes有什么设计局限性吗?
- kubernetes调度器有什 么问题?
- docker或者containerd的局限性?
- 说下Docker进程间隔离的技术(Namespace)
- Docker image的实质是什么?
- docker镜像的优化有哪些方式?
- 介绍 k8s 基础组件 及其之间的协作 功能
- prometheus的exporter怎么做的,会怎么考虑优化
- 分布式定时任务为什么需要分布式?直接执行脚本不行吗
- 你觉得应该怎么保证高实时性调度还有安全调度