腾讯云原生

腾讯云原生 查看完整档案

深圳编辑  |  填写毕业院校腾讯云  |  云原生 编辑 cloud.tencent.com 编辑
编辑

云原生技术交流阵地,汇聚云原生最新技术资讯、文章、活动,以及云原生产品及用户最佳实践内容。

个人动态

腾讯云原生 发布了文章 · 3月4日

TKE 容器网络中的 ARP Overflow 问题探究及其解决之道

作者朱瑜坚,腾讯云后台开发工程师,熟悉 CNI 容器网络相关技术,负责腾讯云 TKE 的容器网络的构建和相关网络组件的开发维护工作,作为主力开发实现了 TKE 下一代容器网络方案。

1. 问题背景

1.1 问题描述

最近,某内部客户的 TKE VPC-CNI 模式的独立网卡集群上出现了 pod 间访问不通的情况,问题 pod ping 不通任何其他 pod 和节点。

查看 dmesg 内核日志,有如下报错信息:neighbour: arp_cache: neighbor table overflow!(下图为后续复现的日志截图)

并且,这个集群规模较大,约有 1000 个节点,30000 个 pod,基本可以怀疑是由于集群规模较大,导致 ARP 表项过多,从而引起 ARP Overflow 的问题。

1.2 名词解释

名词说明
TKE全称 Tencent Kubernetes Engine, 腾讯云容器服务,是基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务
VPC-CNI 模式是容器服务 TKE 基于 CNI 和 VPC 弹性网卡实现的容器网络能力
PodPod 是 kubernetes 的基本资源管理单位,拥有独立的网络命名空间,1个 Pod 可包含多个容器

2. 问题初步分析

从如上报错信息可知,这个问题的基本原因在于 ARP 缓存表打满了。这里涉及到内核的 ARP 缓存垃圾回收机制。当 ARP 表项太多且又没有可回收的表项的时候,新表项就会无法插入。

这就导致网络包发送时无法找到对应的硬件地址(MAC)。使得网络包不能发送。

那么具体什么情况会导致新表项无法插入呢?回答这个问题,我们需要先深入了解一下 ARP 缓存老化及垃圾回收机制。

3. ARP 缓存老化回收机制

3.1 ARP 缓存表项状态机

如上图,是整个 ARP 表项的生命周期及其状态机。

我们知道,对于 TCP/IP 网络包发送时,网络栈需要对端的 MAC 地址才能让网络包转换成二层的数据结构——帧,从而在网络中传输。而对于不同广播域的 IP 地址,其对端 MAC 地址为网关,发送端会将网络包发给网关让其转发,而对于同广播域中的 IP 地址,其对端 MAC 地址即与 IP 地址对应。

而通过 IP 地址找到 MAC 地址就是 ARP 协议的主要工作内容。ARP 协议的工作过程此处不再赘述,而通过 ARP 协议找到 IP 地址对应的 MAC 地址后,会将该对应关系存储在本机上一段时间,以减少 ARP 协议的通信频率,加快网络包的发送。该对应关系,即 ARP 缓存表项,其状态机或整个生命周期可描述如下:

  1. 初始时,对于任何网络包发送时,内核协议栈需要找到目的 IP 地址对应的对端 MAC 地址,如果这时 ARP 缓存中没有命中,则会新插入一条状态为 Incomplete 的表项。Incomplete 状态会尝试发送 ARP 包,请求某 IP 地址对应的 MAC 地址。
  2. 若收到 ARP 回应的,表项状态就变为 Reachable。
  3. 若尝试了一定次数后没收到响应,表项即变为 Failed。
  4. Reachable 表项在到达超时时间后,会变成 Stale 状态,Stale 状态的表项已不可再使用。
  5. Stale 的表项若有被引用来发包,则表项会变为 Delay 状态。
  6. Delay 状态的表项也不可使用来发包,但在 Delay 状态到期前,收到 ARP 的本机确认,则重新转为 Reachable 状态。
  7. Delay 状态到期,表项变为 Probe 状态,该状态与 Incomplete 状态类似。
  8. Stale 状态到期后,会被启动的垃圾回收起回收删除。

通过以下命令可查看当前网络命名空间(network namespace) 中 arp 表项及其状态:

ip neigh

如:

本机确认:这是代指本机收到了一个源 mac 地址匹配的网络包,这个网络包表示此次网络通信的“上一跳”即是该 mac 地址的机器,能收到这个网络包即说明该 mac 地址可达。因此即可把该表项转为 Reachable 状态。通过这一机制,内核可减少 ARP 的通信需求。

3.2 涉及到的内核参数

以下列出了该机制中主要涉及的内核参数:

参数含义默认值
/proc/sys/net/ipv4/neigh/default/base_reachable_timeReachable 状态基础过期时间,每个表项过期时间是在[1/2base_reachable_time,3/2base_reachable_time]之间30秒
/proc/sys/net/ipv4/neigh/default/base_reachable_time_msReachable 状态基础过期时间,毫秒表示30秒
/proc/sys/net/ipv4/neigh/default/gc_stale_timeStale 状态过期时间60秒
/proc/sys/net/ipv4/neigh/default/delay_first_probe_timedelay 状态过期到 Probe 的时间5秒
/proc/sys/net/ipv4/neigh/default/gc_intervalgc 启动的周期时间30秒
/proc/sys/net/ipv4/neigh/default/gc_thresh1少于这个值,gc 不会启动2048
/proc/sys/net/ipv4/neigh/default/gc_thresh2ARP表的最多纪录的软限制,允许超过该数字5秒4096
/proc/sys/net/ipv4/neigh/default/gc_thresh3ARP表的最多纪录的硬限制,大于该数目,gc立即启动,并强制回收8192

其中,gc 相关的内核参数是对所有网卡(interface)生效的。但是各种到期时间的设置是仅对单独网卡(interface)生效的,default 值仅对新增接口设备生效。

3.3 ARP 缓存垃圾回收机制

由其缓存表项的状态机我们知道,不是所有的表项都会被回收,只有 Stale 状态过期后,Failed 的表项可能会被回收。另外,ARP 缓存表项的垃圾回收是触发式的,需要回收的表项不一定立刻会被回收,ARP 缓存表项的垃圾回收有四种启动逻辑:

  1. arp 表项数量 < gc_thresh1,不启动。
  2. gc_thresh1 =< arp 表项数量 <= gc_thresh2,按照 gc_interval 定期启动
  3. gc_thresh2 < arp 表项数量 <= gc_thresh3,5秒后启动
  4. arp 表项数量 > gc_thresh3,立即启动

对于不可回收的表项,垃圾回收即便启动了也不会对其进行回收。因此当不可回收的表项数量大于 gc_thresh3 的时候,垃圾回收也无能为力了。

4. 进一步探究

4.1 垃圾回收阈值是按命名空间级别生效还是子机级别生效

我们知道,每个独立的网络命名空间是有完整的网络协议栈的。那么,ARP 缓存的垃圾回收也是每个命名空间单独处理的吗?

从涉及的内核参数可以看出,gc 相关的内核参数是对所有接口设备生效的,因此,这里可以推测垃圾回收的阈值也是子机级别生效的,而不是按网络命名空间。

这里做了一个简单的实验来验证:

  1. 在节点 default ns 上的 gc_thresh1, gc_thresh2 和 gc_thresh3 设置成60 。
  2. 在节点上创建了 19 个独立网卡模式的 Pod
  3. 任意选择一个 pod ping 其他的 pod,以此产生 arp 缓存
  4. 用 shell 脚本扫描节点上的所有 pod,计算 arp 表项的和,可以得到:

可以发现, 各命名空间的累计 arp 表项的数目在每次达到 60 之后就会快速下降,也就是达到 60 之后就产生了垃圾回收。重复几次都是类似的结果,因此,这说明了垃圾回收在计算 ARP 表项是否触发阈值时,是计算各命名空间的累计值,也就是按子机级别生效,而非命名空间级别。

4.2 不可回收的 ARP 表项达到 gc_thresh3 时,会发生什么

由前面的介绍我们知道,垃圾回收机制并非回收任意 ARP 缓存,因此,当所有可达状态的 ARP 表项打满 ARP 缓存表时,也即达到 gc_thresh3 时,会发生什么行为?可以推测,此时旧的无法回收,新的 ARP 表项也无法插入,新的网络包会无法发送,也即发生了本次文章所描述的问题。

为了验证这一点,继续在以上环境中实验:

  1. 将任意两个 Pod 的基础老化时间 base_reachable_time 调长到 1800秒,以此产生不可回收的 ARP 表项。
  2. 设置 gc_thresh3 为 40,以此更容易触发问题
  3. 选择调整了老化时间的 pod ping 其他的 pod,以此产生 arp 缓存。
  4. 可以发现,当到达阈值的时候,ping 会产生丢包或不通:

查看内核日志 dmesg -T,可以看到文章开头描述的信息:neighbour: arp_cache: neighbor table overflow!

以上实验说明了,不可回收的 ARP 表项打满 ARP 表会让新的表项无法插入,从而网络不通。

4.3 为什么相比 TKE 的全局路由模式和单网卡多 IP 模式,独立网卡模式更容易产生这个问题

要回答这个问题,我们先简单看一下 TKE 各网络模式的原理介绍

全局路由模式

该网络模式下,每个节点上的容器 IP 是预先分配到节点上的,这些 IP 同属一个子网,且每个节点就是一个小子网。我们知道,ARP 协议是为二层通信服务的,因此,该网络方案中,每个 Pod 的网络命名空间内的 ARP 表最大可能保存了节点上所有其他 Pod 的 ARP 表项,最后节点的 ARP 表项的数量最大即为 节点子网 IP 数的平方,如节点的子网大小是128,则其 ARP 表项最大可能为 127 的平方,约 16000。

共享网卡模式

该网络模式下,每个节点会绑定辅助弹性网卡,节点上的 Pod 共享使用该辅助网卡,每个 Pod 内不会做网络包的路由,只会有一条 ARP 表项,实际的路由控制在节点的 default 命名空间内完成。因此,该网络模式下,ARP 缓存表几乎是共享的,又因为网卡只能属于 1 个子网,因此每个节点的 Pod ARP 缓存表只能存储一个子网的 IP-MAC 映射关系,至多数量为各网卡所在子网内 IP 的数量和,如子网是 /22,即含有约 1000 个 ip, 那么 arp 表项也大概有 1000,由于节点网卡配额一般不超过 10,因此该节点的最大 ARP 表项一般不超过 10000。

下一代网络方案——独立网卡模式

独立网卡模式是 TKE 团队推出的下一代“零损耗”容器网络方案,其基本原理如下图所示:

即母机虚拟出的弹性网卡,直接置于容器中,使容器获得与 CVM 子机一样的网络通信能力和网络管理能力,大大提升了容器网络的数据面能力,真正做到“零损耗”。

目前,独立网卡网络方案已在 TKE 产品中开放白名单测试,欢迎内外部客户体验试用。

以上网络方案中,每个 Pod 都会独占一个网卡,也会拥有独立的命名空间和独立的 ARP 缓存表。而每个网卡都可以属于不同的子网。因此,在独立网卡模式里,ARP 缓存表项数量至多为同可用区的子网 IP 数量之和。这一数量量级是可以很轻易上万的,很容易就突破了默认的 ARP 缓存设置。也就触发了这个问题。

5. 解决方案

从以上的分析可以看出,这个问题,调大垃圾回收的阈值,可以比较好的解决问题。因此,临时的解决方案,就是调大 ARP 缓存表的垃圾回收阈值:

echo 8192 > /proc/sys/net/ipv4/neigh/default/gc_thresh1echo 16384 > /proc/sys/net/ipv4/neigh/default/gc_thresh2echo 32768 > /proc/sys/net/ipv4/neigh/default/gc_thresh3

6. 总结

ARP 缓存打满之后,Pod 就会网络不通。初看起来很简单,但是其背后的 ARP 缓存老化和垃圾回收机制也是比较复杂的。查询了很多资料,但是都对“垃圾回收阈值是对各命名空间的 ARP 表项累积值生效还是单独生效”,“垃圾回收会回收哪些表项”,“表项打满后的具体行为如何”等问题说不清、道不明。因此,笔者尝试通过几个小实验验证了具体的行为模式。相比直接阅读晦涩的内核源码,实验法也许也是一个研究问题和理解机制的捷径了。希望能够帮助到各位读者。

【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!
查看原文

赞 0 收藏 0 评论 0

腾讯云原生 发布了文章 · 3月4日

腾讯云容器服务 TKE 拿下新加坡 MTCS 最高级别安全认证

近日,腾讯云容器服务 TKE 荣获新加坡 MTCS 最高级安全认证,标志着腾讯云 TKE 在为用户提供可靠、易部署、灵活扩展等基础服务上,已经全面满足了新加坡监管机构以及多个行业客户对服务安全的要求。

科普一下:

可能很多人对新加坡 MTCS 认证还不熟悉,这里小编来给大家科普一下吧。😊

新加坡的多层云安全(MTCS)标准,是在新加坡资讯通信发展管理局(IDA)信息技术标准委员会(ITSC)的指导下拟定的,而新加坡曾被誉为最严苛网络安全合规要求之一的地区。

MTCS 是首个具有不同安全级别的云安全标准,总共包括 535 种控制措施,共有3个级别:第 1 级别,关注基本安全性;第 2 级别,关注更严格的管理和租赁控制;第 3 级别,关注高效信息系统的可靠性和可复原性。

而此次腾讯云容器服务 TKE 直接一举拿下新加坡 MTCS 安全认证的最高级别。

难道这就是传说中的学霸!!!获新加坡 MTCS 最高级安全认证页面截图如下:

介绍一下:

腾讯云容器服务 TKE (Tencent Kubernetes Engine,简称 TKE) ,是基于原生 Kubernetes,结合腾讯云的基础设施打造的企业级容器平台,涵盖发布管理、容器编排、急速弹性、日志、监控、告警、服务治理、流量接入、存储管理等完善的能力,帮助开发及运维人员快速迭代及上线,降低成本,提高效率。目前腾讯云容器服务广泛应用于腾讯内外部多个客户,覆盖电商、游戏、教育、金融、政府、企业等多个领域,总计部署容器超过 320+ 万个。

【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!
查看原文

赞 0 收藏 0 评论 0

腾讯云原生 发布了文章 · 2月25日

ImageApparate(幻影)镜像加速服务让镜像分发效率提升 5-10 倍

作者介绍

李昂,腾讯高级开发工程师,主要关注容器存储和镜像存储相关领域,目前主要负责腾讯容器镜像服务和镜像存储加速系统的研发和设计工作。

李志宇,腾讯云后台开发工程师。负责腾讯云 TKE 集群节点和运行时相关的工作,包括 containerd、docker 等容器运行时组件的定制开发和问题排查。

洪志国,腾讯云架构师,负责 TKE 产品容器运行时,K8s,容器网络,mesh 数据面等基础组件研发。

背景

在业务普遍已经完成容器化的大环境下,不同的业务场景对于容器启动需求也是不同的,在离线计算和一些需要快速增加计算资源(伸缩组)的在线服务场景下,往往对于容器的启动速度有较高的要求。

在容器启动的整个周期中镜像拉取的时间往往占据 70% 甚至更多。据统计,某离线计算业务因容器镜像较大,每次扩容上千 Pod 耗时高达 40 分钟。镜像分发成为容器快速弹性伸缩的主要障碍。

ImageApparate(幻影)

为了解决这个问题,腾讯云容器服务 TKE 团队开发了下一代镜像分发方案 ImageApparate(幻影), 将大规模大镜像分发的速度提升 5-10倍
benchmark.png

应对既有 Docker 下载镜像模式带来的问题,社区新方案的讨论主要在镜像数据的延迟加载(Lazy-Pull)和新镜像格式的设计不再以层为最小单位,而是 chuck 或者镜像内文件本身。

不过,目前看OCI V2离我们依然还很远,当前我们通过何种方式来应对这类场景呢?

回到问题本身,当前OCI V1和容器运行时交互逻辑需要先下载完整镜像才能运行容器,但是容器启动和运行时到底会使用镜像内的多少内容,这篇论文 FAST '16 统计了 DockerHub 中一些常见的官方镜像在其使用启动后需要读取的数据量,得出的结论是仅有平均 6.4% 的内容需要读取。也就是说镜像中的大部分内容可能在容器的整个生命周期内根本不需要,那么如果我们只加载 6% 的数据就可以大幅减少镜像拉取时间,从而加速容器启动速度,这也就为后续的优化提供了理论前提。

因此减少容器启动时间的重点就在容器的 rootfs 即容器镜像的获取上。

基于此前提,在兼容OCI V1的框架下,TCR 推出了 ImageApparate(幻影) 容器镜像加速服务。首先直接放结论,在 200 节点且镜像内容占镜像总大小的 5% 到 10%。如上所述,相比于传统的下载全部镜像的方式,ImageApparate 在容器全部启动时间上都有 5-10倍 的提升。而且本测试主要并不只是关注容器创建时间,而是继续测试了从容器启动到业务进程可以提供服务后的总体时间:

  • 顺序读取 500MB 大文件测试了包括从容器启动后到顺序读取 500MB 文件完成后的时间
  • 随机读取 1000 小文件测试了包括从容器启动后到随即读取 1000个 4k-16k 完成后的时间
  • 执行 python 程序测试了包括从容器启动后加载 Python 解释器执行一段简单的 python 代码完成后的时间
  • 执行 gcc 编译测试了包括从容器启动后执行 gcc 编译一段简单 C 代码并运行完成后的时间

ImageApparate 方案设计

传统模式的问题

自 Docker 发布以来云计算领域发生了巨大的变革,传统虚拟机逐步被容器替代。Docker 秉持 Build, Ship And Run 的理念出色的完成了容器运行时和容器镜像的设计,引领整个容器行业。但是随着时间的推移容器的 Ship And Run 在面对广泛的用户需求场景中也逐渐暴露出一些问题。

传统容器启动和镜像下载方式为:

  1. 访问镜像仓库服务获取权限认证以及获取镜像存储地址
  2. 通过网络访问镜像存储地址下载全部镜像层并解压
  3. 根据镜像的层信息使用联合文件系统挂载全部层作为rootfs,在此文件系统上创建并启动容器

traditional1.png

  1. 容器镜像的设计从 Docker 发布至今一直沿用下来,并已经成为事实标准也就是我们现在使用的OCI V1,使用分层的设计大大减少空间占用,利用各类联合文件系统(Aufs、Overlayfs)将每层联合挂载起来形成一个完整的RootFS只读根文件系统,容器运行时的写入操作会在联合文件系统的最上层的读写层,非常精巧的设计。

    但是,开发者和用户对于速度追求是永无止境的,随着业务上云的广泛普及,为了充分发挥云上资源的弹性能力,用户往往需要新扩出来的计算节点可以用最快的速度使用容器化的计算能力(容器启动服务可以接受流量),而此时这个全新节点就需要下载容器镜像全部的层,大大拖慢容器启动速度,在这个场景下容器镜像的分层设计没有得到充分的利用,完全失效了。

    针对OCI V1容器镜像格式的一些问题社区也开始有集中的讨论,当前tar包作为OCI V1的镜像层分发格式主要有以下问题:

    1. 不同层之间的内容冗余
    2. 没有基于文件的寻址访问能力,需要全部解包后才能访问
    3. 没有并发解包能力
    4. 使用 whiteout 处理文件删除在不同存储类型中转换导致解压效率低下

TCR-Apparate OCI制品

我们设计的目标是面向生产级别,在节点上同时支持镜像加速模式和普通模式,为了和正常OCI V1镜像存储解耦,我们开发了镜像附加存储IAS(ImageAttachStorage)结合镜像Manifest中的外部层类型(Foreign Layer),可以在契合OCI V1语义下完成加速镜像的制作、上传和下载,继承原有镜像权限的同时,加速后的镜像Manifest索引以 OCI 制品形式存储在镜像仓库本身的存储中。

在镜像格式方面为了支持按需加载和克服tar格式之前的一些缺点,ImageApparate 使用了只读文件系统代替了 tar 格式。只读文件系统解决了镜像层内文件寻址能力同时又具备成为Rootfs可靠的性能。ImageApparate 仍然使用分层的设计在Manifest外部层中直接指定附件存储地址,附加存储层IAS在下载镜像时就可以按需挂载。

用户开启镜像加速功能并设置相关规则后,push 镜像后 ImageApparate 会在后台运行如下流程:

  1. 用户以任意符合OCI V1接口标准的客户端(包括 Docker)Push 镜像到 TCR 仓库
  2. TCR 的镜像服务会将用户数据写入到镜像仓库本身的后端存储中,一般为 COS 对象存储。
  3. TCR 的镜像服务会检查镜像加速规则,如果符合规则会给 Apparate-client 组件发出 Webhook 通知,请求转换镜像格式。
  4. Apparate-client 组件收到通知后会把 COS 数据写入到IAS中,使用特定算法把此镜像的每个 Layer 逐个转换为支持 ImageApparate 挂载的 Layer 格式。

因此,对于 TCR 用户来说只需要定义规则标记哪些镜像需要加速,而 CI/CD 的使用方式上没有任何变化,原来的开发模式顺理成章地继承下来。

appa-flow.png

镜像附加存储 IAS(ImageAttachStorage)

顾名思义,狭义的镜像附加存储IAS是除了本身的镜像后端存储之外的数据存储地址,IAS既可以和镜像仓库的使用相同的对象存储,也可以使用 NFS 或者 Lustre。Apparate 中的镜像附加存储除了存储地址外,还包含一套插件化的接口(兼容Posix)和镜像层IAS中的布局(Layout)。IAS中每个目录代表一个 Layer,这里依然会使用基于内容寻址(Content Addressable)复用内容相同层, 只读文件系统文件包含了这个原始层中的全部内容,随时可以通过加载元数据索引获取整个目录树。目前 Apparate 使用了腾讯云 CFS 高性能版作为IAS的一种实现,高吞吐低延迟 CFS 目前和镜像下载场景非常契合。

镜像本地缓存由不同的IAS附加存储插件自身实现,目前 CFS 实现使用了 FScache 框架作为本地缓存可以自动按页缓存访问过的在远端存储上的部分数据,根据当前磁盘通过本地缓存能力,有效提升镜像数据重复访问的性能和稳定性。

IAS.png

运行时实现

当前 ImageApparate 在节点上使用的IAS附加存储插件被称之为 Apparate-snapshotter,是通过 containerd 的 proxy-snapshotter 能力实现的。

Apparate-snapshotter 主要负责解析记录在镜像层中的IAS信息,从而拿到另外数据存储地址,接下来 Apparate-snapshotter 会去数据存储服务中加载远程数据,并在本地提供访问的 Posix 入口。

比如在 CFS 场景下,会把远端数据 mount 到本地,并把挂载点作为接下来本地访问的入口。当需要使用远端数据时便由 snapshotter 或内核来提供按需加载的能力。

只读镜像格式

对于支持 Lazy-Pull 的镜像文件系统来说,只读是非常关键的属性,因为只读文件系统不需要考虑数据写入和删除造成的碎片和垃圾回收,可以提前在制作文件系统的时候优化数据块和索引的分布,这样可以大幅提高文件系统的读取性能。

当前 IAS 支持的只读文件系统还增加了基于字母顺序排序的目录项索引(directory index),可以大大加速目录项的Lookup操作。

rofs.png

ImageApparate在TCR中使用方式

创建加速组件

当前 ImageApparate 在 TCR 中为 alpha 功能需要白名单开启。开启加速组件需要选择对应 CFS 的高性能版,请确认所在地域有此版本 CFS。
use1.png
use2.png

创建加速规则

创建加速规则,只有规则中匹配的镜像或者 Tag 才会自动加速。之后再向 TCR 推送镜像后可以看到匹配加速规则的镜像会生成后缀为-apparateOCI制品。
use3.png
use5.png

TKE 集群侧开启加速功能

在 TKE 集群中创建 TCR 插件时开启镜像加速配置,之后可以给需要加速的集群中节点打标签kubectl label node xxx cloud.tencent.com/apparate=true,集群中 Pod 的镜像可以仍然使用原镜像名字(例如上述test/nginx:1.9),加速插件支持自动选取已加速的镜像来进行挂载。如果镜像已被加速,那么观察 TKE 集群中 Pod 的 image 字段可以看到已被替换为 test/nginx:1.9-apparate。
use4.png

后续工作

当容器镜像是按需加载后,Layer(层)可能已经不再是复用的最小单位了, ImageApparate 后续也会探索基于文件或者块镜像格式以及转换工具以获得更高的性能和效率。在接口侧镜像附加存储IAS也会支持更多数据源,包括和 TKE P2P 组件的集成,按需加载与 P2P 结合可以更好的应对超大规模镜像加载场景,大大减轻源站压力。

内测邀请

ImageApparate(幻影)镜像加速服务现已开启内测,我们诚挚邀请您参与内测申请 ~ 名额有限,快快 点击这里 直达内测申请页面进行信息提交。

参考

查看原文

赞 0 收藏 0 评论 0

腾讯云原生 发布了文章 · 2月23日

在 TKE 中使用 Velero 迁移复制集群资源

概述

Velero(以前称为Heptio Ark)是一个开源工具,可以安全地备份和还原,执行灾难恢复以及迁移 Kubernetes 群集资源和持久卷,可以在 TKE 集群或自建 Kubernetes 集群中部署 Velero 用于:

  • 备份集群并在丢失的情况下进行还原。
  • 将集群资源迁移到其他集群。
  • 将生产集群复制到开发和测试集群。

更多关于 Velero 介绍,请参阅 Velero 官网,本文将介绍使用 Velero 实现 TKE 集群间的无缝迁移复制集群资源的操作步骤。

迁移原理

在需要被迁移的集群和目标集群上都安装 Velero 实例,并且两个集群的 Velero 实例指向相同的腾讯云 COS 对象存储位置,使用 Velero 在需要被迁移的集群执行备份操作生成备份数据存储到腾讯云 COS ,然后在目标集群上使用 Velero 执行数据的还原操作实现迁移,迁移原理如下:

img

前提条件

  • 注册腾讯云账户
  • 已开通腾讯云 COS 服务。
  • 已有需要被迁移的 TKE 集群(以下称作集群 A),已创建迁移目标的 TKE 集群(以下称作集群 B),创建 TKE 集群请参阅 创建集群
  • 集群 A 和 集群 B 都需要安装 Velero 实例(1.5版本以上),并且共用同一个腾讯云 COS 存储桶作为 Velero 后端存储,安装步骤请参阅 配置存储和安装 Velero

注意事项

  1. 从 1.5 版本开始,Velero 可以使用 Restic 备份所有pod卷,而不必单独注释每个 pod。默认情况下,此功能允许用户使用 restic 备份所有 pod 卷,但以下卷情况除外:

    • 挂载默认 Service Account Secret 的卷
    • 挂载的 hostPath 类型卷
    • 挂载 Kubernetes secretsconfigmaps 的卷

    本示例需要 Velero 1.5 以上版本且启用 restic 来备份持久卷数据,请确保在安装 Velero 阶段开启 --use-restic--default-volumes-to-restic 参数,安装步骤请参阅 配置存储和安装 Velero

  2. 在执行迁移过程中,请不要对两边集群资源做任何 CRUD 操作,以免在迁移过程中造成数据差异,最终导致迁移后的数据不一致。
  3. 尽量保证集群 B 和集群 A 工作节点的CPU、内存等规格配置相同或不要相差太大,以免出现迁移后的 Pods 因资源原因无法调度导致 Pending 的情况。

操作步骤

在集群 A 创建备份

可以手动执行备份操作,也可以给 velero 设置定期自动备份,设置方法可以使用 velero schedule -h 查看。本示例将以 default 、default2 命名空间的资源情况作比较验证,下图可以看到集群 A 中两个命名空间下的 Pods 和 PVC 资源情况:

提示:可以指定在备份期间执行一些自定义 Hook 操作。比如,需要在备份之前将运行应用程序的内存中的数据持久化到磁盘。 有关备份 Hook 的更多信息请参阅 备份 Hook

img

其中,集群中的 minio 对象存储服务使用了持久卷,并且已经上传了一些图片数据,如下图所示:

img

执行下面命令来备份集群中不包含 velero 命名空间(velero 安装的默认命名空间)资源的其他所有资源,如果想自定义需要备份的集群资源范围,可使用 velero create backup -h 查看支持的资源筛选参数。

velero backup create <BACKUP-NAME> --exclude-namespaces <NAMESPACE>

本示例我们创建一个 “default-all” 的集群备份,备份过程如下图所示:

img

备份任务状态显示是 “Completed” 时,说明备份任务完成,可以通过 velero backup logs | grep error 命令检查是否有备份操作发生错误,没有输出则说明备份过程无错误发生,如下图所示:

注意:请确保备份过程未发生任何错误,假如 velero 在执行备份过程中发生错误,请排查解决后重新执行备份。

img

备份完成后,临时将备份存储位置更新为只读模式(非必须,这可以防止在还原过程中 Velero 在备份存储位置中创建或删除备份对象):

kubectl patch backupstoragelocation default --namespace velero \
    --type merge \
    --patch '{"spec":{"accessMode":"ReadOnly"}}'

在集群 B 执行还原

在执行还原操作前集群 B 中 default 、default2 命名空间下没有任何工作负载资源,查看结果如下图:

img

临时将集群 B 中 Velero 备份存储位置也更新为只读模式(非必须,这可以防止在还原过程中 Velero 在备份存储位置中创建或删除备份对象):

kubectl patch backupstoragelocation default --namespace velero \
    --type merge \
    --patch '{"spec":{"accessMode":"ReadOnly"}}'
提示:可以选择指定在还原期间或还原资源后执行自定义 Hook 操作。例如,可能需要在数据库应用程序容器启动之前执行自定义数据库还原操作。 有关还原 Hook 的更多信息请参阅 还原 Hook

在还原操作之前,需确保集群 B 中 的 Velero 资源与云存储中的备份文件同步。默认同步间隔是1分钟,可以使用--backup-sync-period 来配置同步间隔。可以使用下面命令查看集群 A 的备份是否已同步:

velero backup get <BACKUP-NAME>

获取备份成功检查无误后,执行下面命令还原所有内容到集群 B 中:

velero restore create --from-backup <BACKUP-NAME>

本示例执行还原过程如下图:

img

等待还原任务完成后查看还原日志, 可以使用下面命令查看还原是否有报错和跳过信息:

# 查看迁移时是否有错误的还原信息
velero restore logs <BACKUP-NAME> | grep error 

# 查看迁移时跳过的还原操作
velero restore logs <BACKUP-NAME> | grep skip

从下图可以看出没有发生错误的还原步骤,但是有很多 “skipped” 步骤,是因为我们在备份集群资源时备份了不包含 velero 命名空间的所有集群资源,有一些同类型同名的集群资源已经存在了,如 kube-system下的集群资源,当还原过程中有资源冲突时,velero 会跳过还原的操作步骤。所以实际上还原过程是正常的,可以忽略这些 “skipped” 日志,假如有特殊情况可以分析下日志看看。

img

迁移结果核验

查看校验集群 B 执行迁移操作后的集群资源,可以看到 default 、default2 命名空间下的 pods 和 PVC 资源已按预期迁移成功:

img

再通过 Web 管理页面登录集群 B 中的 monio 服务,可以看到 minio 服务中的图片数据没有丢失,说明持久卷数据也已按预期迁移成功。

img

至此,我们完成了 TKE 集群间资源的迁移,迁移操作完成后,请不要忘记把备份存储位置恢复为读写模式(集群 A 和 集群B),以便下次备份任务可以成功使用:

kubectl patch backupstoragelocation default --namespace velero \
   --type merge \
   --patch '{"spec":{"accessMode":"ReadWrite"}}'

总结

本文主要介绍了在 TKE 集群间使用 Velero 迁移集群资源的原理、注意事项和操作方法,成功的将示例集群 A 中的集群资源无缝迁移到集群 B 中,整个迁移过程非常简单方便,是一种非常友好的集群资源迁移方案。

【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!
查看原文

赞 0 收藏 0 评论 0

腾讯云原生 发布了文章 · 2月4日

手把手教你在容器服务 TKE 中使用动态准入控制器

原理概述

动态准入控制器 Webhook 在访问鉴权过程中可以更改请求对象或完全拒绝该请求,其调用 Webhook 服务的方式使其独立于集群组件,具有非常大的灵活性,可以方便的做很多自定义准入控制,下图为动态准入控制在 API 请求调用链的位置(来源于 Kubernetes 官网):

img

从上图可以看出,动态准入控制过程分为两个阶段:首先执行 Mutating 阶段,可以对到达请求进行修改,然后执行 Validating 阶段来验证到达的请求是否被允许,两个阶段可以单独使用也可以组合使用,本文将在 TKE 中实现一个简单的动态准入控制调用示例。

查看验证插件

在 TKE 现有集群版本中(1.10.5 及以上)已经默认开启了 validating admission webhookmutating admission webhook API,如果是更低版本的集群,可以在 Apiserver Pod 中执行 kube-apiserver -h | grep enable-admission-plugins 验证当前集群是否开启,输出插件列表中如果有 MutatingAdmissionWebhookValidatingAdmissionWebhook 就说明当前集群开启了动态准入的控制器插件,如下图所示:

img

签发证书

为了确保动态准入控制器调用的是可信任的 Webhook 服务端,必须通过 HTTPS 来调用 Webhook 服务(TLS认证), 所以需要为 Webhook 服务端颁发证书,并且在注册动态准入控制 Webhook 时为 caBundle 字段( ValidatingWebhookConfigurationMutatingAdmissionWebhook 资源清单中的 caBundle 字段)绑定受信任的颁发机构证书(CA)来核验 Webhook 服务端的证书是否可信任, 这里分别介绍两种推荐的颁发证书方法:

注意:当ValidatingWebhookConfigurationMutatingAdmissionWebhook 使用 clientConfig.service 配置时(Webhook 服务在集群内),为服务器端颁发的证书域名必须为 <svc_name>.<svc_namespace>.svc

方法一: 制作自签证书

制作自签证书的方法比较独立,不依赖于 K8s 集群,类似于为一个网站做一个自签证书,有很多工具可以制作自签证书,本示例使用 Openssl 制作自签证书,操作步骤如下所示:

  1. 生成密钥位数为 2048 的 ca.key:

    openssl genrsa -out ca.key 2048
  2. 依据 ca.key 生成 ca.crt,"webserver.default.svc" 为 Webhook 服务端在集群中的域名,使用 -days 参数来设置证书有效时间:

    openssl req -x509 -new -nodes -key ca.key -subj "/CN=webserver.default.svc" -days 10000 -out ca.crt
  3. 生成密钥位数为 2048 的 server.key:

    openssl genrsa -out server.key 2048

    i. 创建用于生成证书签名请求(CSR)的配置文件 csr.conf 示例如下:

    [ req ]
    default_bits = 2048
    prompt = no
    default_md = sha256
    distinguished_name = dn
    [ dn ]
    C = cn
    ST = shaanxi
    L = xi'an
    O = default
    OU = websever
    CN = webserver.default.svc
    [ v3_ext ]
    authorityKeyIdentifier=keyid,issuer:always
    basicConstraints=CA:FALSE
    keyUsage=keyEncipherment,dataEncipherment
    extendedKeyUsage=serverAuth,clientAuth
  4. 基于配置文件 csr.conf 生成证书签名请求:

    openssl req -new -key server.key -out server.csr -config csr.conf
  5. 使用 ca.key、ca.crt 和 server.csr 颁发生成服务器证书(x509签名):

    openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
      -CAcreateserial -out server.crt -days 10000 \
      -extensions v3_ext -extfile csr.conf
  6. 查看 Webhook server 端证书:

    openssl x509  -noout -text -in ./server.crt

其中,生成的证书、密钥文件说明如下:

ca.crt 为颁发机构证书,ca.key 为颁发机构证书密钥,用于服务端证书颁发。

server.crt 为 颁发的服务端证书,server.key 为颁发的服务端证书密钥.

方法二:使用 K8S CSR API 签发

除了使用方案一加密工具制作自签证书,还可以使用 k8s 的证书颁发机构系统来下发证书,执行下面脚本可使用 K8s 集群根证书和根密钥签发一个可信任的证书用户,需要注意的是用户名应该为 Webhook 服务在集群中的域名:

USERNAME='webserver.default.svc' # 设置需要创建的用户名为 Webhook 服务在集群中的域名
# 使用 Openssl 生成自签证书 key
openssl genrsa -out ${USERNAME}.key 2048
# 使用 Openssl 生成自签证书 CSR 文件, CN 代表用户名,O 代表组名
openssl req -new -key ${USERNAME}.key -out ${USERNAME}.csr -subj "/CN=${USERNAME}/O=${USERNAME}" 
# 创建 Kubernetes 证书签名请求(CSR)
cat <<EOF | kubectl apply -f -
apiVersion: certificates.k8s.io/v1beta1
kind: CertificateSigningRequest
metadata:
  name: ${USERNAME}
spec:
  request: $(cat ${USERNAME}.csr | base64 | tr -d '\n')
  usages:
  - digital signature
  - key encipherment
  - server auth
EOF
# 证书审批允许信任
kubectl certificate approve ${USERNAME}
# 获取自签证书 CRT
kubectl get csr ${USERNAME} -o jsonpath={.status.certificate} > ${USERNAME}.crt

其中, ${USERNAME}.crt 为服务端证书, ${USERNAME}.key 为 Webhook 服务端证书密钥。

操作示例

下面将使用 ValidatingWebhookConfiguration 资源在 TKE 中实现一个动态准入 Webhook 调用示例,本示例代码可在 示例代码 中获取(为了确保可访问性,示例代码 Fork 自 原代码库,作者实现了一个简单的动态准入 Webhook 请求和响应的接口,具体接口格式请参考 Webhook 请求和响应 。为了方便,我将使用它作为我们的 Webhook 服务端代码。

  1. 准备 caBundle 内容

    • 若颁发证书方法是方案一, 使用 base64 编码 ca.crt 生成 caBundle 字段内容:

       cat ca.crt | base64 --wrap=0
      
    • 若颁发证书方法是方案二,集群的根证书即为 caBundle 字段内容,可以通过 TKE 集群控制台【基本信息】-> 【集群APIServer信息】Kubeconfig 内容中的clusters.cluster[].certificate-authority-data 字段获取,该字段已经 base64 编码过了,无需再做处理。
  2. 复制生成的 ca.crt (颁发机构证书),server.crt(HTTPS 证书)), server.key(HTTPS 密钥) 到项目主目录:

    img

  3. 修改项目中的 Dockerfile ,添加三个证书文件到容器工作目录:
    img

    然后使用 docker 命令构建 Webhook 服务端镜像:

    docker build -t webserver .
    
  4. 部署一个域名为 "weserver.default.svc" 的 Webhook 后端服务,修改适配后的 controller.yaml 如下:

    img

  5. 注册创建类型为 ValidatingWebhookConfiguration 的资源,本示例配置的 Webhook 触发规则是当创建 pods类型,API 版本 "v1" 时触发调用,clientConfig 配置对应上述在集群中创建的的 Webhook 后端服务, caBundle 字段内容为证书颁发方法一获取的ca.crt 内容,修改适配项目中的 admission.yaml 文件如下图:

    img

  6. 注册好后创建一个 Pod 类型, API 版本为 "v1" 的测试资源如下:

    img

  7. 测试代码有打印请求日志, 查看 Webhook 服务端日志可以看到动态准入控制器触发了 webhook 调用,如下图:

    img

  8. 此时查看创建的测试pod 是成功创建的,是因为测试 Webhook 服务端代码写死的 allowed: true,所以是可以创建成功的,如下图:
    img
  9. 为了进一步验证,我们把 "allowed" 改成 "false" ,然后重复上述步骤重新打 Webserver 服务端镜像,并重新部署 controller.yaml 和 admission.yaml 资源,当再次尝试创建 "pods" 资源时请求被动态准入拦截,说明配置的动态准入策略是生效的,如下图所示:

    img

总结

本文主要介绍了动态准入控制器 Webhook 的概念和作用、如何在 TKE 集群中签发动态准入控制器所需的证书,并使用简单示例演示如何配置和使用动态准入 Webhook 功能。

参考

Kubernetes Dynamic Admission Control by Example

Dynamic Admission Control(官网)

【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!
查看原文

赞 0 收藏 0 评论 0

腾讯云原生 发布了文章 · 2月3日

边缘计算场景下云边端一体化的挑战与实践

本文整理自腾讯云专家工程师王继罗在 2020年12月深圳 Qcon 大会上的分享内容——边缘计算场景下云边端一体化的挑战与实践

边缘计算想必大家都已经听过了,但是如何将业务扩展到边缘,从而实现更大的业务价值呢?

关于这个问题,腾讯云早在几年前就已开始进行思考,并且着手打造了云边端一体化的超融合平台,目的是希望能够让业务可以更容易落地到边缘。

今天,我们就从以下三个部分展开,跟大家分享腾讯云在建设超融合平台时的一些经验:

  • 第一部分:主要介绍边缘计算有什么作用、业务落地边缘存在哪些挑战、以及为什么要有云边端一体化;
  • 第二部分:主要介绍腾讯云在打造超融合平台时的一些实战经验和进展;
  • 第三部分:介绍 3 个边缘业务落地案例。

云计算发展趋势

提到云计算,大家第一时间就会想起中心云计算。中心云计算是一种集中式架构,计算资源位于中心机房,由云厂商统一维护。那么,这种模式有什么好处呢?

  1. 业务方不再需要管理底层资源,更能聚焦于业务本身,降低了管理成本;
  2. 业务方可以灵活高效地申请、使用、退还底层资源,从整体上提高了资源利用率,降低了资源的使用成本。

而边缘计算,是一种分布式计算,计算资源分散在离数据源比较近的地方,达到就近提供服务的目的。从时间维度上看,边缘计算的发展可以分为 3 个阶段:

  1. 技术形成期,1998 - 2013。最早可以追溯到内容分发网络(CDN),主要用途把数据缓存在离用户近的位置,达到缩短数据下载时间,提高用户体验的目的。
  2. 快速发展期,2014 - 2017。由于满足万物互联的需求,引起国内外学术界和产业界的密切关注,各机构纷纷出台相关的白皮书。
  3. 实际落地期,2018 - ? 随着 5G 的发展,出现越来越多的落地场景,进入政府工作指导报告,基本上可以预见边缘计算会开始爆发。

边缘计算有什么用

前面我们讲了边缘计算是什么,有些人就会有这样一个疑问:既然我们已经有了中心云计算,为什么还需要边缘计算?边缘计算能带来什么价值呢?

其实随着技术不断地发展,云计算的范畴已经从中心不断地向边缘扩展,演变成了中心云-边缘云-端设备协同工作的架构模式。

为什么会发生这样变化呢?主要是因为需求和场景在不断变化,尤其是许多传统行业在信息化改造过程中提出来更多新需求,如:工业制造、港口物流、交通能源等等。

以智能制造为例,智能制造的本质就是设备智能化、信息化,整个系统的工作流程是:采集数据、处理数据、指导生产。这带来了两个方面的问题:

  1. 高实时性要求。很多工业数据具有极强的实时性,过期时间非常短,往往只有几毫秒,这就要求采集数据、数据处理、指导生产的整个过程需要在几毫秒内完成。如果上传到云端处理,然后从云端返回控制指令,整个过程就会耗时比较长,显然不能满足时效性要求,会造成严重的后果,比如制造出的产品精度不够,或者次品率比较高,所以就近处理数据是智能制造的核心。
  2. 海量数据如何处理。智能工控设备、传感器源源不断地产生工业产品及环境方面的数据,带来很高的传输和存储成本,这些成本甚至超过智能化带来的利润,反而成了工业往智能化转型的阻碍。另一方面,这些数据 90% 以上都是无效数据,如果可以尽可能早地筛选出有用数据,去除无效数据,就可以很好地降低传输和存储成本。

再举一个高清视频的例子,4K的高清视频需要至少 40M 带宽,带宽容量和成本是我们必须考虑的重要因素,相对于中心机房,边缘机房的总带宽容量要大,单价也更便宜,因此这类服务很适合部署在边缘。

总的来说,边缘计算可以带来4个方面的好处,容量更大、时延更低、成本更低、支持本地化处理。

边缘计算架构

前面我们讲了云计算在逐步演变成中心云-边缘云-端设备协同工作的模式,那新模式下的架构如何呢?

以腾讯云为例,中心云通常指的是 IDC 机房,边缘云依次会是 ec、oc、mec 机房,现场设备一般位于数据源附近,比如:家庭网关、交通灯路口、港口/园区/矿山内部。

通常物联设备与边缘端设备之间的时延可以控制在 2 ms内,适合处理实时性要求极高的业务数据,比如工业控制类的业务。

与边缘云之间的时延可以控制在 10ms 内,可以满足实时音视频、ARVR、云游戏的业务场景。

这就是边缘计算的大致架构情况。

带来的挑战

下面我们一起看一下边缘计算场景会带来哪些新挑战。

  1. 异构严重。在软硬件两方面都有体现,像中心云和边缘云通常采用x86和linux 标准发行版,而边缘资源由于需要考虑成本以及业务的特殊要求很可能是采用成本更便宜或者是定制化的软硬件方案。
  2. 规模庞大。根据各种权威机构预测,2025年全球物联设备数量会突破千亿,分布在全球各地。如何去管理这么大规模的设备也是一项很有挑战的任务
  3. 环境复杂。位于云机房的设备还好,很多终端设备常常位于恶劣的环境,你比如炼钢厂的很多设备长期处于高温环境、水利监测方面的设备部署环境往往都比较潮湿。设备网络环境也是各种各样,有线的、无线的,无线又有 WIFI、4G5G网络、zigbee等等。
  4. 标准不统一。很多地方还处于没有标准,或者是有很多标准但没有一种公认标准,尤其是在管理方式上极其不统一。

这些挑战带来的后果就是:

  1. 效率下降。包括研发测试、交付部署、升级运维等等
  2. 管理困难。规模很大,各方面环境很复杂,标准也很多,想要管好我们的资源也变得困难重重。
  3. 可靠性降低。边缘环境很恶劣,如何在恶劣的环境下保证服务质量也是一个难题

云边端一体化的意义

边缘场景有如此多的挑战,带来的影响就是业务落地非常困难,这个问题直接阻碍了行业的发展。为了降低业务落地门槛,促进行业顺利发展,云边端一体化的就显得很有必要。

一体化体现在多个方面:

  1. 统一管理。首先,我们要把复杂多变底层资源管理方案统一起来,尽量减少业务对底层细节的不必要感知,比如硬件架构、操作系统、网络环境等等。其次是提供的管理能力要尽可能与中心云保持统一,比如监控告警、发布运维等等各种业务常用的基础能力。
  2. 云边协同。在边缘计算场景下,把业务从中心下沉到边缘是很自然的事情,但是还不够。通常都需要让边缘和云协同工作起来,比如:把边缘的有用数据收集到中心进行分析处理,然后继续反馈到边缘也是非常有必要的。以AI场景为例,我们可以把推理放到边缘进行,然后从边缘收集数据在中心进行训练,训练好的模型又下发到边缘。另外,云上的能力也需要形成联动,比如把边缘的有用数据收集上来,在云上做呈现和再加工。
  3. 资源调度。边缘计算场景下资源很分散,负载随着时空不同而差异很大,如何根据时空差异对资源做合理有效的调节,使资源使用达到最佳效果也是一件很有意义的事情。合理的资源调度可以让系统变得更高效、稳定、低成本。

超融合平台的使命

上面我们一起探讨了边缘计算的挑战和云边端一体化的意义,腾讯云几年前就开始往这方面投入资源,经过多年沉淀逐步建设了囊括方方面面的超融合平台,接下来再和大家分享下腾讯云在超融合平台建设方面的实践。

在建设初期,大家思考得最多的问题就是什么是超融合平台,我们希望超融合平台给业务带来什么样的好处。经过长时间的摸索,我们确定了超融合平台的使命:让边缘资源像中心云资源一样容易管理。

简单来说就是,从平台层面屏蔽底层的复杂性,所有的基础能力尽可能与中心云对齐,从而让业务使用起来感受不到太多差异,业务方可以更加聚焦,把精力集中于具体业务研发,最终让所有的事情都变得简单高效。

如何达成这种效果

方向:
  1. 完全自研。从零开始,代价很高;不具有普适性,难以推广。
  2. 拥抱云原生。云原生是一种生态,囊括了方方面面的能力,我们可以基于这些能力,而不是重复造轮子,更聚焦于解决边缘场景的特殊性,达到事半功倍的效果。
方案:
  1. 使用原生 Kubernetes。并非针对边缘计算场景,直接在边缘使用会有一些问题。
  2. 魔改 Kubernetes。门槛高,代价大,兼容性问题不可忽视。
  3. 增强 Kubernetes。遵守 Kubernetes 标准,灵活,开放,学习成本低,使用起来容易。

TKE Edge

TKE Edge 是腾讯云基于原生 Kubernetes 研发的边缘计算容器系统,它的主要目的是屏蔽错综复杂的边缘计算物理环境,为业务提供一种统一的、标准的资源管理和调度方案。其部分能力已经开源为 SuperEdge 项目。

img

TKE Edge 有多个特点:

  1. Kubernetes 原生。以无侵入的方式将 Kubernetes 强大的容器编排、调度能力拓展到边缘端,其原生支持 Kubernetes,完全兼容 Kubernetes 所有 API 及资源,无额外学习成本。
  2. 边缘自治。提供 L3 级边缘自治能力,当边缘节点与云端网络连接不稳定或处于离线状态时,边缘节点可以自主工作,化解了网络不可靠所带来的不利影响。
  3. 分布式节点健康监测。是业内首个提供边缘侧健康监测能力的开源容器管理系统。SuperEdge 能在边缘侧持续守护进程,并收集节点的故障信息,实现更加快速和精准的问题发现与报告。此外,其分布式的设计还可以实现多区域、多范围的监测和管理
  4. 内置边缘编排能力。能够自动部署多区域的微服务,方便管理运行于多个地区的微服务。同时,网格内闭环服务可以有效减少运行负载,提高系统的容错能力和可用性
  5. 内网穿透。能够保证 Kubernetes 节点在有无公共网络的情况下都可以连续运行和维护,并且同时支持传输控制协议(TCP)、超文本传输协议(HTTP)和超文本传输安全协议(HTTPS)。

超融合平台

超融合平台是以底层IaaS为基础,以TKE Edge为粘接,集成大量腾讯云上能力和业务的边云联动平台,平台有三大特点:

  1. 开放性。在 IaaS 资源侧,除了可以接入腾讯的资源,还可以很方便地接入用户已有的计算资源:如其他云厂商服务器、用户自建机房、智能设备等等。
  2. 集成性。平台集成大量云上基础服务能力,云监控、云日志、云运维等,能满足大部分使用需求;另外还打通了腾讯云资源,边缘计算机器、腾讯云智能网关设备等等。
  3. 易用性。功能使用方式基本与中心云使用方式保持一致,无须学习额外的使用知识。

边缘资源建设情况

  1. 边缘计算机器(Edge Computing Machine,ECM)。该产品通过将计算能力从中心节点下沉到靠近用户的边缘节点,提供低时延、高可用、低成本的边缘计算服务,目前已开放 300+ 节点,全国覆盖。产品主页:https://console.cloud.tencent...
  2. 一体化中心。该产品以腾讯云自研的 Mini T-Block 的移动数据中心基础设施为载体,融合 5G、边缘计算、物联网等技术能力,以及引入腾讯云边缘计算 IaaS/PaaS/SaaS平台产品能力,支持云游戏、4K直播、机器人等5G 2C和2B业务,提供全面创新、可交付型的5G边缘计算整体解决方案。
  3. 边缘智能网关。该产品是腾讯面对物联网边缘应用场景的工业级设备,提供IoT设备接入、AI本地分析、边云协同等功能,具有小体积、高可靠、多网络、超静音、易管理等特性,适用于园区安防、智慧零售、电力巡检、智慧路灯、智能交通、水利监测、工业质检等场景。

边缘业务落地案例

音视频业务实践

  1. 资源量极大,分布极广,异构很严重。开发时需要考虑适配不同的硬件环境,测试的工作量成倍增加,发布上线更是相当麻烦。
  2. 如果是每个机房部署一套 K8s,一则是带来的额外资源开销成本不可忽视,二则会出现上千个集群基本上已经无法管理。
  3. 接入超融合平台后,通过容器化技术最大程度屏蔽掉底层资源异构,集群数量可以从上千个减少到几十套。开发、测试、发布运维成本下降明显。

工业云

工业云的底层是一个私有云机房,上面部署许多工业领域方面的管理系统。其中交付和运维是他们最头痛的两个问题。以往都是派遣交付团队去客户现场部署,交付一套系统少则半个月,日常运维、扩容等基本都需要去现场实施,效率很低,成本极高。

对接到超融合平台后,他们的交付精简成只需在用户环境中执行一条命令,日常运维等操作全部在云上完成。

另一个是工业增值业务,以往都是用户选中需要的增值业务,签合同,去现场部署,客户付钱,流程繁琐,周期很长。现在做出了云上工业电商模式,用户把业务加到购物车,自行下单后业务实时生效。

混合资源管理

这个场景的特点是资源类型很多,有云主机、自建机房、边缘智能设备,网络环境也很复杂:4/5G、单向网络,都有。

以车路协同为例,通常在一个区域有一个云中心,上面运行车路协同相关的系统管理服务;云中心之下是边缘云小机房,数量从几个到上百个不等,主要做数据存储;再下面是路口智能设备,运行 AI 推理方面的服务,负责处理路口摄像头视频数据;

以前的管理方式是在中心云和边缘云均部署一套 K8s,路口智能设备由于资源有限不足以部署完整的 Kubernetes 集群,未容器化。这场景两大主要痛点是:

  1. 集群数量太多,管理起来是一个沉重的负担。另一个是服务更新和配置升级很麻烦,需要一个一个集群操作,很容易遗漏。
  2. 路口智能设备由于未容器化,无论是服务升级还是线上 debug 均不方便。

由于超融合平台不要求边缘资源在同一内网,很方便就在同一个集群内同时管理中心云、边缘云、路口设备,很好地解决了上面提到的两个痛点。

【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!
查看原文

赞 0 收藏 0 评论 0

腾讯云原生 发布了文章 · 1月26日

使用 tke-autoscaling-placeholder 实现秒级弹性伸缩

背景

当 TKE 集群配置了节点池并启用了弹性伸缩,在节点资源不够时可以触发节点的自动扩容 (自动买机器并加入集群),但这个扩容流程需要一定的时间才能完成,在一些流量突高的场景,这个扩容速度可能会显得太慢,影响业务。 tke-autoscaling-placeholder 可以用于在 TKE 上实现秒级伸缩,应对这种流量突高的场景。

原理是什么?

tke-autoscaling-placeholder 实际就是利用低优先级的 Pod 对资源进行提前占位(带 request 的 pause 容器,实际不怎么消耗资源),为一些可能会出现流量突高的高优先级业务预留部分资源作为缓冲,当需要扩容 Pod 时,高优先级的 Pod 就可以快速抢占低优先级 Pod 的资源进行调度,而低优先级的 tke-autoscaling-placeholder 的 Pod 则会被 "挤走",状态变成 Pending,如果配置了节点池并启用弹性伸缩,就会触发节点的扩容。这样,由于有了一些资源作为缓冲,即使节点扩容慢,也能保证一些 Pod 能够快速扩容并调度上,实现秒级伸缩。要调整预留的缓冲资源多少,可根据实际需求调整 tke-autoscaling-placeholder的 request 或副本数。

有什么使用限制?

使用该应用要求集群版本在 1.18 以上。

如何使用?

安装 tke-autoscaling-placeholder

在应用市场找到 tke-autoscaling-placeholder,点击进入应用详情,再点 创建应用:

img

选择要部署的集群 id 与 namespace,应用的配置参数中最重要的是 replicaCountresources.request,分别表示 tke-autoscaling-placeholder 的副本数与每个副本占位的资源大小,它们共同决定缓冲资源的大小,可以根据流量突高需要的额外资源量来估算进行设置。

最后点击创建,你可以查看这些进行资源占位的 Pod 是否启动成功:

$ kubectl get pod -n default
tke-autoscaling-placeholder-b58fd9d5d-2p6ww   1/1     Running   0          8s
tke-autoscaling-placeholder-b58fd9d5d-55jw7   1/1     Running   0          8s
tke-autoscaling-placeholder-b58fd9d5d-6rq9r   1/1     Running   0          8s
tke-autoscaling-placeholder-b58fd9d5d-7c95t   1/1     Running   0          8s
tke-autoscaling-placeholder-b58fd9d5d-bfg8r   1/1     Running   0          8s
tke-autoscaling-placeholder-b58fd9d5d-cfqt6   1/1     Running   0          8s
tke-autoscaling-placeholder-b58fd9d5d-gmfmr   1/1     Running   0          8s
tke-autoscaling-placeholder-b58fd9d5d-grwlh   1/1     Running   0          8s
tke-autoscaling-placeholder-b58fd9d5d-ph7vl   1/1     Running   0          8s
tke-autoscaling-placeholder-b58fd9d5d-xmrmv   1/1     Running   0          8s

tke-autoscaling-placeholder 的完整配置参考下面的表格:

参数描述默认值
replicaCountplaceholder 的副本数10
imageplaceholder 的镜像地址ccr.ccs.tencentyun.com/library/pause:latest
resources.requests.cpu单个 placeholder 副本占位的 cpu 资源大小300m
resources.requests.memory单个 placeholder 副本占位的内存大小600Mi
lowPriorityClass.create是否创建低优先级的 PriorityClass (用于被 placeholder 引用)true
lowPriorityClass.name低优先级的 PriorityClass 的名称low-priority
nodeSelector指定 placeholder 被调度到带有特定 label 的节点{}
tolerations指定 placeholder 要容忍的污点[]
affinity指定 placeholder 的亲和性配置{}

部署高优先级 Pod

tke-autoscaling-placeholder 的优先级很低,我们的业务 Pod 可以指定一个高优先的 PriorityClass,方便抢占资源实现快速扩容,如果没有可以先创建一个:

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000
globalDefault: false
description: "high priority class"

在我们的业务 Pod 中指定 priorityClassName 为高优先的 PriorityClass:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 8
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      priorityClassName: high-priority # 这里指定高优先的 PriorityClass
      containers:
      - name: nginx
        image: nginx
        resources:
          requests:
            cpu: 400m
            memory: 800Mi

当集群节点资源不够,扩容出来的高优先级业务 Pod 就可以将低优先级的 tke-autoscaling-placeholder 的 Pod 资源抢占过来并调度上,然后 tke-autoscaling-placeholder 的 Pod 再 Pending:

$ kubectl get pod -n default
NAME                                          READY   STATUS    RESTARTS   AGE
nginx-bf79bbc8b-5kxcw                         1/1     Running   0          23s
【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!
查看原文

赞 0 收藏 0 评论 0

腾讯云原生 发布了文章 · 1月22日

被集群节点负载不均所困扰?TKE 重磅推出全链路调度解决方案

引言

在 K8s 集群运营过程中,常常会被节点 CPU 和内存的高使用率所困扰,既影响了节点上 Pod 的稳定运行,也会增加节点故障的几率。为了应对集群节点高负载的问题,平衡各个节点之间的资源使用率,应该基于节点的实际资源利用率监控信息,从以下两个策略入手:

  • 在 Pod 调度阶段,应当优先将 Pod 调度到资源利用率低的节点上运行,不调度到资源利用率已经很高的节点上
  • 在监控到节点资源率较高时,可以自动干预,迁移节点上的一些 Pod 到利用率低的节点上

为此,我们提供 动态调度器 + Descheduler 的方案来实现,目前在公有云 TKE 集群内【组件管理】- 【调度】分类下已经提供这两个插件的安装入口,文末还针对具体的客户案例提供了最佳实践的例子。

动态调度器

原生的 Kubernetes 调度器有一些很好的调度策略用来应对节点资源分配不均的问题,比如 BalancedResourceAllocation,但是存在一个问题是这样的资源分配是静态的,不能代表资源真实使用情况,节点的 CPU/内存利用率 经常处于不均衡的状态。所以,需要有一种策略可以基于节点的实际资源利用率进行调度。动态调度器所做的就是这样的工作。

技术原理

原生 K8s 调度器提供了 scheduler extender 机制来提供调度扩展的能力。相比修改原生 scheduler 代码添加策略,或者实现一个自定义的调度器,使用 scheduler extender 的方式侵入性更少,实现更加灵活。所以我们选择基于 scheduler extender 的方式来添加基于节点的实际资源利用率进行调度的策略。

scheduler extender 可以在原生调度器的预选和优选阶段加入自定义的逻辑,提供和原生调度器内部策略同样的效果。

架构

  • node-annotator:负责拉取 Prometheus 中的监控数据,定期同步到 Node 的 annotation 里面,同时负责其他逻辑,如动态调度器调度有效性衡量指标,防止调度热点等逻辑。
  • dynamic-scheduler:负责 scheduler extender 的优选和预选接口逻辑实现,在预选阶段过滤掉资源利用率高于阈值的节点,在优选阶段优先选择资源利用率低的节点进行调度。

实现细节

  1. 动态调度器的策略在优选阶段的权重如何配置?

    原生调度器的调度策略在优选阶段有一个权重配置,每个策略的评分乘以权重得到该策略的总得分。对权重越高的策略,符合条件的节点越容易调度上。默认所有策略配置权重为 1,为了提升动态调度器策略的效果,我们把动态调度器优选策略的权重设置为 2。

  2. 动态调度器如何防止调度热点?

    在集群中,如果出现一个新增的节点,为了防止新增的节点调度上过多的节点,我们会通过监听调度器调度成功事件,获取调度结果,标记每个节点过去一段时间的调度 Pod 数,比如 1min、5min、30min 内的调度 Pod 数量,衡量节点的热点值然后补偿到节点的优选评分中。

产品能力

组件依赖

组件依赖较少,仅依赖基础的节点监控组件 node-exporter 和 Prometheus。Prometheus 支持托管和自建两种方式,使用托管方式可以一键安装动态调度器,而使用自建 Prometheus 也提供了监控指标配置方法。

组件配置

调度策略目前可以基于 CPU 和内存两种资源利用率。

预选阶段

配置节点 5分钟内 CPU 利用率、1小时内最大 CPU 利用率,5分钟内平均内存利用率,1小时内最大内存利用率的阈值,超过了就会在预选阶段过滤节点。

优选阶段

动态调度器优选阶段的评分根据截图中 6个指标综合评分得出,6个指标各自的权重表示优选时更侧重于哪个指标的值,使用 1h 和 1d 内最大利用率的意义是要记录节点 1h 和 1d 内的利用率峰值,因为有的业务 Pod 的峰值周期可能是按照小时或者天,避免调度新的 Pod 时导致在峰值时间节点的负载进一步升高。

产品效果

为了衡量动态调度器对增强 Pod 调度到低负载节点的提升效果,结合调度器的实际调度结果,获取所有调度到的节点在调度时刻的的 CPU/内存利用率以后统计以下几个指标:

  • cpu_utilization_total_avg :所有调度到的节点 CPU 利用率平均值。
  • memory_utilization_total_avg :所有调度到的节点内存利用率平均值。
  • effective_dynamic_schedule_count :有效调度次数,当调度到节点的 CPU 利用率小于当前所有节点 CPU 利用率的中位数,我们认为这是一次有效调度,effective_dynamic_schedule_count 加 0.5分,对内存也是同理。
  • total_schedule_count :所有调度次数,每次新的调度累加1。
  • effective_schedule_ratio :有效调度比率,即 effective_dynamic_schedule_count/total_schedule_count
    下面是在同一集群中不开启动态调度和开启动态调度各自运行一周的指标变化,可以看到对于集群调度的增强效果。
指标未开启动态调度开启动态调度
cpu_utilization_total_avg0.300.17
memory_utilization_total_avg0.280.23
effective_dynamic_schedule_count21603620
total_schedule_count78607470
effective_schedule_ratio0.2730.486

Descheduler

现有的集群调度场景都是一次性调度,即一锤子买卖。后续出现节点 CPU 和内存利用率过高,也无法自动调整 Pod 的分布,除非触发节点的 eviction manager 后驱逐,或者人工干预。这样在节点 CPU/内存利用率高时,影响了节点上所有 Pod 的稳定性,而且负载低的节点资源还被浪费。

针对此场景,借鉴 K8s 社区 Descheduler 重调度的设计思想,给出基于各节点 CPU/内存实际利用率进行驱逐的策略。

架构

Descheduler 从 apiserver 中获取 Node 和 Pod 信息,从 Prometheus 中获取 Node 和 Pod 监控信息,然后经过Descheduler 的驱逐策略,驱逐 CPU/内存使用率高的节点上的 Pod ,同时我们加强了 Descheduler 驱逐 Pod 时的排序规则和检查规则,确保驱逐 Pod 时服务不会出现故障。驱逐后的 Pod 经过动态调度器的调度会被调度到低水位的节点上,实现降低高水位节点故障率,提升整体资源利用率的目的。

产品能力

产品依赖

依赖基础的节点监控组件 node-exporter 和Prometheus。Prometheus 支持托管和自建两种方式,使用托管方式可以一键安装 Descheduler,使用自建 Prometheus 也提供了监控指标配置方法。

组件配置

Descheduler 根据用户配置的利用率阈值,超过阈值水位后开始驱逐 Pod ,使节点负载尽量降低到目标利用率水位以下。

产品效果

通过 K8s 事件

通过 K8s 事件可以看到 Pod 被重调度的信息,所以可以开启集群事件持久化功能来查看 Pod 驱逐历史。

节点负载变化

在类似如下节点 CPU 使用率监控视图内,可以看到在开始驱逐之后,节点的 CPU 利用率下降。

最佳实践

集群状态

拿一个客户的集群为例,由于客户的业务大多是内存消耗型的,所以更容易出现内存利用率很高的节点,各个节点的内存利用率也很不平均,未使用动态调度器之前的各个节点监控是这样的:

动态调度器配置

配置预选和优选阶段的参数如下:

在预选阶段过滤掉 5分钟内平均内存利用率超过 60%或者 1h内最大内存利用率超过 70%的节点,即 Pod 不会调度到这些这些节点上。

在优选阶段将 5分钟平均内存利用率权重配置为 0.8,1h 和1d 内最大内存利用率权重配置为 0.2、0.2,而将 CPU 的指标权重都配置为 0.1。这样优选时更优先选择调度到内存利用率低的节点上。

Descheduler配置

配置 Descheduler 的参数如下,当节点内存利用率超过 80%这个阈值的时候,Descheduler 开始对节点上的 Pod 进行驱逐,尽量使节点内存利用率降低到目标值 60% 为止。

集群优化后状态

通过以上的配置,运行一段时间后,集群内各节点的内存利用率数据如下,可以看到集群节点的内存利用率分布已经趋向于均衡:

【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!
查看原文

赞 0 收藏 0 评论 0

腾讯云原生 发布了文章 · 1月21日

一文读懂 TKE 及 Kubernetes 访问权限控制

你有了解过Kubernetes的认证授权链路吗?是否对TKE的权限控制CAM策略、服务角色傻傻分不清楚?本文将会向你介绍腾讯云TKE平台侧的访问控制Kubernetes访问控制链路,以及演示如何将平台侧账号对接到Kubernetes内。

当你在使用腾讯云容器服务TKE(Tencent Kubernetes Engine)的时候,如果多人共用一个账号的情况下,是否有遇到以下问题呢?

  • 密钥由多人共享,泄密风险高。
  • 无法限制其他人的访问权限,其他人误操作易造成安全风险。

为了解决以上问题,腾讯云CAM(Cloud Access Management)提供了主账号和子账号的认证体系以及基于角色的权限控制。

而不同的子账号对于TKE平台侧资源的控制粒度比较粗(cluster实例级别),又会遇到以下问题:

  • 同一个集群由多子账号可访问,无法保证集群资源级别、命名空间级别的读写控制。
  • 集群的高权限子账户无法对低权限子账户进行授权管理。

为了解决以上两个问题,TKE针对平台侧资源Kubernetes资源分别进行相应的访问控制管理。

平台侧访问控制

首先介绍下什么是平台侧资源,平台侧资源即Cluster资源CVM资源CLB资源VPC资源等腾讯云资源,而访问的用户主要分为用户服务角色载体

  1. 用户就是我们平时登录控制台的主账号、子账号或者协作者账号
  2. 服务角色是一种定义好带有某些权限的角色,可以将这个角色赋予某个载体,可以是某个其他账户,也可以是腾讯云下一个产品的服务提供者,CAM会默认为产品提供一个预设的载体和默认的角色,例如TKE的默认角色就是TKE_QCSRole,而载体就是ccs.qcloud.com

而这个角色有什么用处呢?举个TKE的例子,比如TKE的service-controller会Watch集群内的Service资源,如果需要创建LoadBalance类型的Service,会通过云API购买并创建CLB资源,而service-controller是TKE平台为用户部署的,去访问云API需要有身份,这个身份就是ccs.qcloud.com载体,而权限则需要用户给载体授予一个角色,即TKE_QCSRole。只有用户在授权TKE载体之后,TKE才可以通过服务扮演的方式代替用户购买CLB。
下面我会简单为你介绍如何给用户授权,以及如何给TKE平台授予角色

定制策略

TKE通过接入CAM,对集群的API接口级别进行权限细分,需要您在CAM控制台对子账户进行不同的权限授予。同时TKE也在CAM侧提供了预设的权限,提供您默认选择,例如:

也可以自定义策略,具体策略定制请参考CAM产品介绍文档

例如拥有只读权限的子账户尝试修改集群名称,将会在API接口时校验CAM权限失败

划分用户组

可以依据团队的职责划分好用户组,将之前规划好的自定义策略绑定到一个用户组上,来方便的进行权限管理。
例如:有新同学入职时可方便的加入指定用户组(如运维组),就可以获取到该用户组的权限,避免了繁琐的权限配置操作。

授予TKE角色权限

使用TKE容器服务需要授予TKE平台为您操作CVMCLBVPCCBS等权限,所以首次访问TKE控制台需要确保同意授权,即创建预设角色TKE_QCSRole,此角色默认授予TKE载体,该载体会通过CAM获取操作您集群的临时密钥,来进行相应的云API操作。

更多

更多丰富的平台侧访问控制用法请访问CAM产品说明文档

Kubernetes访问控制

介绍完平台侧资源的访问控制,我们再来看看TKE集群内的资源如何进行权限管理。当不同的子账户都拥有访问同一个TKE Kubernetes集群权限之后,如何保证不同的子账户,对于集群内资源拥有不同的角色和权限呢?让我们首先从社区的Kubernetes访问链路来分析整个过程,从而向您介绍TKE是如何实现容器服务子账户对接Kubernetes认证授权体系的。

Overview

首先从宏观的角度看下Kubernetes的请求链路是如何进行的。图片来源于k8s社区官网。

可以大概了解到一个请求的链路是依次通过Authentication(认证,简称Authn)、Authorization(授权,简称Authz)、AdmissionControl(准入控制),从而获取到后端持久化的数据。

从图中可以看到Authn、Authz、AdmissionControl是由多个模块组成的,每个步骤都有多种方式构成的。

在进入认证模块之前会将HTTP的Request进行构建context,而context中就包含了用户的RequestInfo,userInfo、Verb、APIGroup、Version、Namespace、Resource、Path等。

带着这些信息,下面我们来一次看下准入过程中的每个步骤吧。

Kubernetes认证

认证的过程的证明user身份的过程。

Kubernetes中有两类用户,一类是ServiceAccount,一类是集群真实的用户。

ServiceAccount账户是由Kubernetes提供API(资源)进行创建和管理的,ServiceAccount可以认为是特殊的Secret资源,可用户集群内资源访问APIServer的认证所用。通过可以通过mount的方式挂载到Pod内进行使用。

真实的用户通常是从外部发起请求访问APIServer,由管理员进行管理认证凭证,而Kubernetes本身不管理任何的用户和凭证信息的,即所有的用户都是逻辑上的用户,无法通过API调用Kubernetes API进行创建真实用户。

Kubernetes认证的方式众多,常见的有TLS客户端证书双向认证、BearerToken认证、BasicAuthorization或认证代理(WebHook)

所有的认证方式都是以插件的形式串联在认证链路中,只要有一种认证方式通过,即可通过认证模块,且后续的认证方式不会被执行。

在此处参考一点点Kubernetes APIServer Authentication模块的代码,可以发现,任何的认证方式都是一下Interface的实现方式都是接收http Request请求,然后会返回一个user.Info的结构体,一个bool,以及一个error

// Request attempts to extract authentication information from a request and returns
// information about the current user and true if successful, false if not successful,
// or an error if the request could not be checked.
type Request interface {
   AuthenticateRequest(req *http.Request) (user.Info, bool, error)
}

user.Info中包含了用户的信息,包括UserName、UUID、Group、Extra。

bool返回了用户是否通过认证,false的话即返回无法通过认证,即返回401错误。

error则返回了当Request无法被检查的错误,如果遇到错误则会继续进行下一种注册的方式进行认证。

如果认证通过,则会把user.Info写入到到请求的context中,后续请求过程可以随时获取用户信息,比如授权时进行鉴权。

下面我会以Kubernetes代码中的认证方式顺序,挑选几项认证方式,并结合TKE开启的认证方式来向你介绍TKE创建的Kubernetes集群默认的认证策略。

Basic Authentication

APIServer启动参数--basic-auth-file=SOMEFILE指定basic认证的csv文件,在APIServer启动之后修改此文件都不会生效,需要重启APIServer来更新basic authentication的token。csv文件格式为:token,user,uid,"group1,group2,group3"

请求时,需要指定HTTP Header中Authentication为Basic,并跟上Base64Encode(user:passward)值。

x509客户端证书

APIServer启动参数--client-ca-file=SOMEFILE指定CA证书,而在TKE的K8s集群创建过程中,会对集群进行自签名CA密钥和证书用于管理,如果用户下发的客户端证书是由此CA证书的密钥签发的,那么就可以通过客户端证书认证,并使用客户端证书中的CommonName、Group字段分别作为Kubernetes的UserInfo中Username和Group信息。

目前TKE对接子账户都是通过自签名的CA凭证进行签发子账户Uin对应CN的客户端证书。

Bearer Token

Bearer Token的认证方式包含很多,比如启动参数指定的、ServiceAccount(也是一种特殊的BeaerToken)、BootstrapToken、OIDCIssure、WebhookToken

1. 默认指定Token csv文件

APIServer启动参数--token-auth-file=SOMEFILE指定Bearer Token认证的csv文件。和Basic Authentication方式相似,只不过请求APIServer时,指定的HTTP认证方式为Bearer方式。此Bearer后直接跟passward即可。csv文件格式为:password,user,uid,"group1,group2,group3"

请求时,需要指定HTTP Header中Authentication为Bearer,并跟上Base64Encode(user:passward)值。

2. ServiceAccount

ServiceAccount也是一种特殊beaer token,ServiceAccount在Kubernetes中是一种资源,创建一个ServiceAccount资源之后默认会创建一个Secret资源,而Secret资源中就包含了一个JWT格式的Token字段,以Bearer Token的方式请求到Kube-APIServer,Kube-APIServer解析token中的部分user信息,以及validate以下ServiceAccount是否存在即可进行认证检查。这种方式即之前提到的“两种用户”中常见的集群内认证方式,ServiceAccount,主要用于集群内资源访问APIServer,但不限于集群内。

3. BootstrapToken

此项开关在Kubernetes v1.18版本中才为stable版本,此类Token是专门用来引导集群安装使用的,需要配合controller-manager的TokenCleaner。

目前TKE默认开启此配置。

4. OpenID Connect Tokens

OIDCToken的认证方式是结合OAuth2向身份提供方获取ID Token来访问APIServer。

如需要开启此项功能,需要在APIServer的启动参数中指定oidc的配置参数,例如--oidc-issuer-url指定oidc身份提供方的地址,--oidc-client-id指定身份提供方侧的账户ID,--oidc-username-claim身份提供方的用户名。

具体可参考Kubernetes官方文档,目前公有云TKE没有使用此参数对接腾讯云账户,因为涉及用户需要主动登录授权后才可返回Id Token,和当前官网交互冲突,可以在后续CLI工具中实现。

5. Webhook Token Server

Webhook Token是一种hook的方式来校验是否认证通过。

APIServer启动参数--authentication-token-webhook-config-file--authentication-token-webhook-cache-ttl来分别指定Webhook地址以及token的cache ttl。

若APiServer开启此方式进行认证校验,则在接受到用户的Request之后,会包装Bearer Token成一个TokenReview发送给WebHookServer,Server端接收到之后会进行校验,并返回TokenReview接口,在status字段中进行反馈是否通过校验通过和user.Info信息。

总结

以上即为Kubernetes APIServer认证的几种方式,TKE在每种认证方式都有支持。供用户灵活使用。

目前TKE正在推使用x509客户端证书方式来进行认证管理,以方便进行对接子账户的创建、授权管理、更新。

Kubernetes授权

Kubernetes的授权模式支持一下几种,和认证一样,参考开始说的RequestInfo context,可知用户Reqeust的context除了认证需要的userInfo,还有一些其他的字段例如Verb、APIGroup、APIVersion、Resource、Namespaces、Path……

授权就是判断user是否拥有操作资源的相应权限。

Kubernetes支持AlwaysAllow、AlwaysDeny、Node、ABAC、RBAC、Webhook授权Mode,和认证一样,只要有一种鉴权模块通过,即可返回资源。

在这里重点介绍下面两种方式

RBAC

RBAC(Role-Based Access Control),Kubernetes提供ClusterRole、Role资源,分别对应集群维度、Namespace维度角色权限管控,用户可以自定义相应的ClusterRole、Role资源,绑定到已经认证的User之上。

如下tke:pod-reader ClusterRole,定义了该角色可以访问core apigroup下面对pods资源的get/watch/list操作

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: tke:pod-reader
rules:
- apiGroups: [""] # "" 指定核心 API 组
  resources: ["pods"]
  verbs: ["get", "watch", "list"]
  
---
apiVersion: rbac.authorization.k8s.io/v1
# 此角色绑定使得用户 "alex" 能够读取 "default" 命名空间中的 Pods
kind: ClusterRoleBinding
metadata:
  name: alex-ClusterRole
subjects:
- kind: User
  name: alex
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: tke:pod-reader # 这里的名称必须与你想要绑定的 Role 或 ClusterRole 名称一致
  apiGroup: rbac.authorization.k8s.io

通过以上的yaml配置,通过认证模块到达授权模块的requestInfo中userInfo信息是alex的请求,在授权模块中走到RBAC授权模块时,则会进行查询集群的ClusterRole/ClusterRoleBinding信息。进行判断是否拥有context相应操作的权限。

TKE的对接子账户的权限授权策略就是使用的Kubernetes原生的RBAC进行对子账户资源访问控制,这样符合原生,符合有K8s使用习惯的用户。

WebHook

Webhook模式是一种基于HTTP回调的方式,通过配置好授权webhook server地址。当APIServer接收到request的时候,会进行包装SubjectAccessReview请求Webhook Server,Webhook Server会进行判断是否可以访问,然后返回allow信息。

以下是kubernetes社区一个例子,以供参考。

{
  "apiVersion": "authorization.k8s.io/v1beta1",
  "kind": "SubjectAccessReview",
  "spec": {
    "resourceAttributes": {
      "namespace": "kittensandponies",
      "verb": "get",
      "group": "unicorn.example.org",
      "resource": "pods"
    },
    "user": "alex",
    "group": [
      "group1",
      "group2"
    ]
  }
}
{
  "apiVersion": "authorization.k8s.io/v1beta1",
  "kind": "SubjectAccessReview",
  "status": {
    "allowed": true
  }
}

目前TKE没有考虑使用Webhook的模式,但是Webhook模式提供了强大的灵活性,比如对接CAM,实现K8s权限对接到平台侧,但是也有一定的风险和挑战,比如依赖CAM的稳定性;请求延迟、缓存/TTL的配置;CAM action配置与K8s权限对应关系。此项授权模式仍然在考虑中,有需求的用户可以反馈。

准入控制

什么是admission controller?

In a nutshell, Kubernetes admission controllers are plugins that govern and enforce how the cluster is used.

Admission controllers是K8s的插件,用来管理和强制用户如何来操作集群。

Admission controllers主要分为两个phase,一个是mutating,一个是validating。这两个阶段都是在authn&authz之后的,mutating做的变更准入,就是会对request的resource,进行转换,比如填充默认的requestLimit?而validating admission的意思就是验证准入,比如校验Pod副本数必须大于2。

API Server请求链路:

ValidatingAdmissionWebhooks and MutatingAdmissionWebhooks, both of which are in beta status as of Kubernetes 1.13.

k8s支持30多种admission control 插件 ,而其中有两个具有强大的灵活性,即ValidatingAdmissionWebhooksMutatingAdmissionWebhooks,这两种控制变换和准入以Webhook的方式提供给用户使用,大大提高了灵活性,用户可以在集群创建自定义的AdmissionWebhookServer进行调整准入策略。

TKE中1.10及以上版本也默认开启了ValidatingAdmissionWebhooksMutatingAdmissionWebhooks

了解更多Admission Controller参考这里

kubernetes权限对接子账户

TKE权限实现对接子账户主要的方案是:x509客户端认证+Kubernetes RBAC授权

认证

每个子账户都拥有单独的属于自己的客户端证书,用于访问KubernetesAPIServer。

  • 用户在使用TKE的新授权模式时,不同子账户在获取集群访问凭证时,即前台访问集群详情页或调用DescribeClusterKubeconfig时,会展示子账户自己的x509客户端证书,此证书是每个集群的自签名CA签发的。
  • 该用户在控制台访问Kubernetes资源时,后台默认使用此子账户的客户端证书去访问用户Kubernetes APIServer。
  • 支持子账户更新自己的证书。
  • 支持主账户或集群tke:admin权限的账户进行查看更新其他子账户证书。

授权

TKE控制台通过Kubernetes原生的RBAC授权策略,对子账户提供细粒度的Kubernetes资源粒度权限控制。

  • 提供授权管理页,让主账号集群创建者默认拥有管理员权限,可以对其他拥有此集群DescribeCluster Action权限的子账户进行权限管理。
  • 并提供预设的ClusterRole。

    • 所有命名空间维度:

      • 管理员(tke:admin):对所有命名空间下资源的读写权限, 对集群节点,存储卷,命名空间,配额的读写权限, 可子账号和权限的读写权限
      • 运维人员(tke:ops):对所有命名空间下控制台可见资源的读写权限, 对集群节点,存储卷,命名空间,配额的读写权限
      • 开发人员(tke:dev):对所有命名空间下控制台可见资源的读写权限
      • 受限人员(tke:ro):对所有命名空间下控制台可见资源的只读权限
      • 用户自定义ClusterRole
    • 指定命名空间维度:

      • 开发人员(tke:ns:dev): 对所选命名空间下控制台可见资源的读写权限, 需要选择指定命名空间。
      • 只读用户(tke:ns:ro):对所选命名空间下控制台可见资源的只读权限, 需要选择指定命名空间。

  • 所有预设的ClusterRole都将带有固定label:cloud.tencent.com/tke-rbac-generated: "true"
  • 所有预设的ClusterRoleBinding都带有固定的annotations:cloud.tencent.com/tke-account-nickname: yournickname,及label:cloud.tencent.com/tke-account: "yourUIN"

更多

当然,除了TKE控制台提供的预设授权策略,管理员也可以通过kubectl操作ClusterRole/Role来实现自定义角色的灵活配置细粒度权限,以及操作ClusterRoleBinding/RoleBinding进行权限绑定,绑定到任意的角色权限之上。

例如你想设置CAM侧用户组为productA产品的pod-dev的用户权限只能够get/list/watch product-a命名空间下的pods资源,则你可以这样操作:

  • 创建自定义ClusterRole/Role:dev-pod-reader,yaml实例如下,文件名为developer.yaml

    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole # 这里使用ClusterRole可以复用给产品其他命名空间
    metadata:
      name: pod-dev # pod-dev此角色为只能读取pod的开发
    rules:
    - apiGroups: [""] # "" 指定核心 API 组
      resources: ["pods"]
      verbs: ["get", "watch", "list"]
    
  • 使用kubectl或者通过TKE控制台YAML创建资源创建上述Role
  • 绑定dev用户组下的dev1、dev2、dev3用户,绑定自定义权限pod-dev到product-a命名空间下
  • 从此dev1,dev2,dev3用户则只能使用get/list/watch访问product-a下的pods资源

$ kubectl --kubeconfig=./dev.kubeconfig get pods
Error from server (Forbidden): pods is forbidden: User "10000001xxxx-1592395536" cannot list resource "pods" in API group "" in the namespace "default"
$ kubectl --kubeconfig=./dev.kubeconfig get pods -n product-a
No resources found.


### 参考

1. [kubernetes认证介绍](https://kubernetes.io/docs/reference/access-authn-authz/authentication/)
2. [kubernetes授权介绍](https://kubernetes.io/docs/reference/access-authn-authz/authorization/)
3. [kubernetesRBAC介绍](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)
>【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!
查看原文

赞 0 收藏 0 评论 0

腾讯云原生 发布了文章 · 1月20日

一文读懂 Kubernetes APIServer 原理

前言

整个Kubernetes技术体系由声明式API以及Controller构成,而kube-apiserver是Kubernetes的声明式api server,并为其它组件交互提供了桥梁。因此加深对kube-apiserver的理解就显得至关重要了。

整体组件功能

kube-apiserver作为整个Kubernetes集群操作etcd的唯一入口,负责Kubernetes各资源的认证&鉴权,校验以及CRUD等操作,提供RESTful APIs,供其它组件调用:

kube-apiserver包含三种APIServer:

  • aggregatorServer:负责处理 apiregistration.k8s.io 组下的APIService资源请求,同时将来自用户的请求拦截转发给aggregated server(AA)
  • kubeAPIServer:负责对请求的一些通用处理,包括:认证、鉴权以及各个内建资源(pod, deployment,service and etc)的REST服务等
  • apiExtensionsServer:负责CustomResourceDefinition(CRD)apiResources以及apiVersions的注册,同时处理CRD以及相应CustomResource(CR)的REST请求(如果对应CR不能被处理的话则会返回404),也是apiserver Delegation的最后一环

另外还包括bootstrap-controller,主要负责Kubernetes default apiserver service的创建以及管理。

接下来将对上述组件进行概览性总结。

bootstrap-controller

  • apiserver bootstrap-controller创建&运行逻辑在k8s.io/kubernetes/pkg/master目录
  • bootstrap-controller主要用于创建以及维护内部kubernetes default apiserver service
  • kubernetes default apiserver service spec.selector为空,这是default apiserver service与其它正常service的最大区别,表明了这个特殊的service对应的endpoints不由endpoints controller控制,而是直接受kube-apiserver bootstrap-controller管理(maintained by this code, not by the pod selector)
  • bootstrap-controller的几个主要功能如下:

    • 创建 default、kube-system 和 kube-public 以及 kube-node-lease 命名空间
    • 创建&维护kubernetes default apiserver service以及对应的endpoint
    • 提供基于Service ClusterIP的检查及修复功能(--service-cluster-ip-range指定范围)
    • 提供基于Service NodePort的检查及修复功能(--service-node-port-range指定范围)
// k8s.io/kubernetes/pkg/master/controller.go:142
// Start begins the core controller loops that must exist for bootstrapping
// a cluster.
func (c *Controller) Start() {
    if c.runner != nil {
        return
    }
    // Reconcile during first run removing itself until server is ready.
    endpointPorts := createEndpointPortSpec(c.PublicServicePort, "https", c.ExtraEndpointPorts)
    if err := c.EndpointReconciler.RemoveEndpoints(kubernetesServiceName, c.PublicIP, endpointPorts); err != nil {
        klog.Errorf("Unable to remove old endpoints from kubernetes service: %v", err)
    }
    repairClusterIPs := servicecontroller.NewRepair(c.ServiceClusterIPInterval, c.ServiceClient, c.EventClient, &c.ServiceClusterIPRange, c.ServiceClusterIPRegistry, &c.SecondaryServiceClusterIPRange, c.SecondaryServiceClusterIPRegistry)
    repairNodePorts := portallocatorcontroller.NewRepair(c.ServiceNodePortInterval, c.ServiceClient, c.EventClient, c.ServiceNodePortRange, c.ServiceNodePortRegistry)
    // run all of the controllers once prior to returning from Start.
    if err := repairClusterIPs.RunOnce(); err != nil {
        // If we fail to repair cluster IPs apiserver is useless. We should restart and retry.
        klog.Fatalf("Unable to perform initial IP allocation check: %v", err)
    }
    if err := repairNodePorts.RunOnce(); err != nil {
        // If we fail to repair node ports apiserver is useless. We should restart and retry.
        klog.Fatalf("Unable to perform initial service nodePort check: %v", err)
    }
    // 定期执行bootstrap controller主要的四个功能(reconciliation)  
    c.runner = async.NewRunner(c.RunKubernetesNamespaces, c.RunKubernetesService, repairClusterIPs.RunUntil, repairNodePorts.RunUntil)
    c.runner.Start()
}

更多代码原理详情,参考 kubernetes-reading-notes

kubeAPIServer

KubeAPIServer主要提供对内建API Resources的操作请求,为Kubernetes中各API Resources注册路由信息,同时暴露RESTful API,使集群中以及集群外的服务都可以通过RESTful API操作Kubernetes中的资源

另外,kubeAPIServer是整个Kubernetes apiserver的核心,下面将要讲述的aggregatorServer以及apiExtensionsServer都是建立在kubeAPIServer基础上进行扩展的(补充了Kubernetes对用户自定义资源的能力支持)

kubeAPIServer最核心的功能是为Kubernetes内置资源添加路由,如下:

  • 调用 m.InstallLegacyAPI 将核心 API Resources添加到路由中,在apiserver中即是以 /api 开头的 resource;
  • 调用 m.InstallAPIs 将扩展的 API Resources添加到路由中,在apiserver中即是以 /apis 开头的 resource;
// k8s.io/kubernetes/pkg/master/master.go:332
// New returns a new instance of Master from the given config.
// Certain config fields will be set to a default value if unset.
// Certain config fields must be specified, including:
//   KubeletClientConfig
func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) (*Master, error) {
    ...
    // 安装 LegacyAPI(core API)
    // install legacy rest storage
    if c.ExtraConfig.APIResourceConfigSource.VersionEnabled(apiv1.SchemeGroupVersion) {
        legacyRESTStorageProvider := corerest.LegacyRESTStorageProvider{
            StorageFactory:              c.ExtraConfig.StorageFactory,
            ProxyTransport:              c.ExtraConfig.ProxyTransport,
            KubeletClientConfig:         c.ExtraConfig.KubeletClientConfig,
            EventTTL:                    c.ExtraConfig.EventTTL,
            ServiceIPRange:              c.ExtraConfig.ServiceIPRange,
            SecondaryServiceIPRange:     c.ExtraConfig.SecondaryServiceIPRange,
            ServiceNodePortRange:        c.ExtraConfig.ServiceNodePortRange,
            LoopbackClientConfig:        c.GenericConfig.LoopbackClientConfig,
            ServiceAccountIssuer:        c.ExtraConfig.ServiceAccountIssuer,
            ServiceAccountMaxExpiration: c.ExtraConfig.ServiceAccountMaxExpiration,
            APIAudiences:                c.GenericConfig.Authentication.APIAudiences,
        }
        if err := m.InstallLegacyAPI(&c, c.GenericConfig.RESTOptionsGetter, legacyRESTStorageProvider); err != nil {
            return nil, err
        }
    }
    ...
    // 安装 APIs(named groups apis)
    if err := m.InstallAPIs(c.ExtraConfig.APIResourceConfigSource, c.GenericConfig.RESTOptionsGetter, restStorageProviders...); err != nil {
        return nil, err
    }
    ...
    return m, nil
}

整个kubeAPIServer提供了三类API Resource接口:

  • core group:主要在 /api/v1 下;
  • named groups:其 path 为 /apis/$GROUP/$VERSION
  • 系统状态的一些 API:如/metrics/version 等;

而API的URL大致以 /apis/{group}/{version}/namespaces/{namespace}/resource/{name} 组成,结构如下图所示:

kubeAPIServer会为每种API资源创建对应的RESTStorage,RESTStorage的目的是将每种资源的访问路径及其后端存储的操作对应起来:通过构造的REST Storage实现的接口判断该资源可以执行哪些操作(如:create、update等),将其对应的操作存入到action中,每一个操作对应一个标准的REST method,如create对应REST method为POST,而update对应REST method为PUT。最终根据actions数组依次遍历,对每一个操作添加一个handler(handler对应REST Storage实现的相关接口),并注册到route,最终对外提供RESTful API,如下:

// m.GenericAPIServer.InstallLegacyAPIGroup --> s.installAPIResources --> apiGroupVersion.InstallREST --> installer.Install --> a.registerResourceHandlers
// k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go:181
func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService) (*metav1.APIResource, error) {
    ...
    // 1、判断该 resource 实现了哪些 REST 操作接口,以此来判断其支持的 verbs 以便为其添加路由
    // what verbs are supported by the storage, used to know what verbs we support per path
    creater, isCreater := storage.(rest.Creater)
    namedCreater, isNamedCreater := storage.(rest.NamedCreater)
    lister, isLister := storage.(rest.Lister)
    getter, isGetter := storage.(rest.Getter)
    ...
    // 2、为 resource 添加对应的 actions(+根据是否支持 namespace)
    // Get the list of actions for the given scope.
    switch {
    case !namespaceScoped:
        // Handle non-namespace scoped resources like nodes.
        resourcePath := resource
        resourceParams := params
        itemPath := resourcePath + "/{name}"
        nameParams := append(params, nameParam)
        proxyParams := append(nameParams, pathParam)
        ...
        // Handler for standard REST verbs (GET, PUT, POST and DELETE).
        // Add actions at the resource path: /api/apiVersion/resource
        actions = appendIf(actions, action{"LIST", resourcePath, resourceParams, namer, false}, isLister)
        actions = appendIf(actions, action{"POST", resourcePath, resourceParams, namer, false}, isCreater)
        ...
    }
    ...
    // 3、从 rest.Storage 到 restful.Route 映射
    // 为每个操作添加对应的 handler
    for _, action := range actions {
        ...
        switch action.Verb {
        ...
        case "POST": // Create a resource.
            var handler restful.RouteFunction
            // 4、初始化 handler
            if isNamedCreater {
                handler = restfulCreateNamedResource(namedCreater, reqScope, admit)
            } else {
                handler = restfulCreateResource(creater, reqScope, admit)
            }
            handler = metrics.InstrumentRouteFunc(action.Verb, group, version, resource, subresource, requestScope, metrics.APIServerComponent, handler)
            ...
            // 5、route 与 handler 进行绑定    
            route := ws.POST(action.Path).To(handler).
                Doc(doc).
                Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
                Operation("create"+namespaced+kind+strings.Title(subresource)+operationSuffix).
                Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
                Returns(http.StatusOK, "OK", producedObject).
                // TODO: in some cases, the API may return a v1.Status instead of the versioned object
                // but currently go-restful can't handle multiple different objects being returned.
                Returns(http.StatusCreated, "Created", producedObject).
                Returns(http.StatusAccepted, "Accepted", producedObject).
                Reads(defaultVersionedObject).
                Writes(producedObject)
            if err := AddObjectParams(ws, route, versionedCreateOptions); err != nil {
                return nil, err
            }
            addParams(route, action.Params)
            // 6、添加到路由中    
            routes = append(routes, route)
        case "DELETE": // Delete a resource.
        ...
        default:
            return nil, fmt.Errorf("unrecognized action verb: %s", action.Verb)
        }
        for _, route := range routes {
            route.Metadata(ROUTE_META_GVK, metav1.GroupVersionKind{
                Group:   reqScope.Kind.Group,
                Version: reqScope.Kind.Version,
                Kind:    reqScope.Kind.Kind,
            })
            route.Metadata(ROUTE_META_ACTION, strings.ToLower(action.Verb))
            ws.Route(route)
        }
        // Note: update GetAuthorizerAttributes() when adding a custom handler.
    }
    ...
}

kubeAPIServer代码结构整理如下:

1. apiserver整体启动逻辑 k8s.io/kubernetes/cmd/kube-apiserver
2. apiserver bootstrap-controller创建&运行逻辑 k8s.io/kubernetes/pkg/master
3. API Resource对应后端RESTStorage(based on genericregistry.Store)创建k8s.io/kubernetes/pkg/registry
4. aggregated-apiserver创建&处理逻辑 k8s.io/kubernetes/staging/src/k8s.io/kube-aggregator
5. extensions-apiserver创建&处理逻辑 k8s.io/kubernetes/staging/src/k8s.io/apiextensions-apiserver
6. apiserver创建&运行 k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/server
7. 注册API Resource资源处理handler(InstallREST&Install®isterResourceHandlers) k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints
8. 创建存储后端(etcdv3) k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/storage
9. genericregistry.Store.CompleteWithOptions初始化 k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/registry

调用链整理如下:

更多代码原理详情,参考 kubernetes-reading-notes

aggregatorServer

aggregatorServer主要用于处理扩展Kubernetes API Resources的第二种方式Aggregated APIServer(AA),将CR请求代理给AA:

这里结合Kubernetes官方给出的aggregated apiserver例子sample-apiserver,总结原理如下:

  • aggregatorServer通过APIServices对象关联到某个Service来进行请求的转发,其关联的Service类型进一步决定了请求转发的形式。aggregatorServer包括一个GenericAPIServer和维护自身状态的Controller。其中GenericAPIServer主要处理apiregistration.k8s.io组下的APIService资源请求,而Controller包括:

    • apiserviceRegistrationController:负责根据APIService定义的aggregated server service构建代理,将CR的请求转发给后端的aggregated server
    • availableConditionController:维护 APIServices 的可用状态,包括其引用 Service 是否可用等;
    • autoRegistrationController:用于保持 API 中存在的一组特定的 APIServices;
    • crdRegistrationController:负责将 CRD GroupVersions 自动注册到 APIServices 中;
    • openAPIAggregationController:将 APIServices 资源的变化同步至提供的 OpenAPI 文档;
  • apiserviceRegistrationController负责根据APIService定义的aggregated server service构建代理,将CR的请求转发给后端的aggregated server。apiService有两种类型:Local(Service为空)以及Service(Service非空)。apiserviceRegistrationController负责对这两种类型apiService设置代理:Local类型会直接路由给kube-apiserver进行处理;而Service类型则会设置代理并将请求转化为对aggregated Service的请求(proxyPath := "/apis/" + apiService.Spec.Group + "/" + apiService.Spec.Version),而请求的负载均衡策略则是优先本地访问kube-apiserver(如果service为kubernetes default apiserver service:443)=>通过service ClusterIP:Port访问(默认) 或者 通过随机选择service endpoint backend进行访问:

    func (s *APIAggregator) AddAPIService(apiService *v1.APIService) error {
      ...
        proxyPath := "/apis/" + apiService.Spec.Group + "/" + apiService.Spec.Version
        // v1. is a special case for the legacy API.  It proxies to a wider set of endpoints.
        if apiService.Name == legacyAPIServiceName {
            proxyPath = "/api"
        }
        // register the proxy handler
        proxyHandler := &proxyHandler{
            localDelegate:   s.delegateHandler,
            proxyClientCert: s.proxyClientCert,
            proxyClientKey:  s.proxyClientKey,
            proxyTransport:  s.proxyTransport,
            serviceResolver: s.serviceResolver,
            egressSelector:  s.egressSelector,
        }
      ...
        s.proxyHandlers[apiService.Name] = proxyHandler
        s.GenericAPIServer.Handler.NonGoRestfulMux.Handle(proxyPath, proxyHandler)
        s.GenericAPIServer.Handler.NonGoRestfulMux.UnlistedHandlePrefix(proxyPath+"/", proxyHandler)
      ...
        // it's time to register the group aggregation endpoint
        groupPath := "/apis/" + apiService.Spec.Group
        groupDiscoveryHandler := &apiGroupHandler{
            codecs:    aggregatorscheme.Codecs,
            groupName: apiService.Spec.Group,
            lister:    s.lister,
            delegate:  s.delegateHandler,
        }
        // aggregation is protected
        s.GenericAPIServer.Handler.NonGoRestfulMux.Handle(groupPath, groupDiscoveryHandler)
        s.GenericAPIServer.Handler.NonGoRestfulMux.UnlistedHandle(groupPath+"/", groupDiscoveryHandler)
        s.handledGroups.Insert(apiService.Spec.Group)
        return nil
    }
    // k8s.io/kubernetes/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy.go:109
    func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
        // 加载roxyHandlingInfo处理请求  
        value := r.handlingInfo.Load()
        if value == nil {
            r.localDelegate.ServeHTTP(w, req)
            return
        }
        handlingInfo := value.(proxyHandlingInfo)
      ...
        // 判断APIService服务是否正常
        if !handlingInfo.serviceAvailable {
            proxyError(w, req, "service unavailable", http.StatusServiceUnavailable)
            return
        }
        // 将原始请求转化为对APIService的请求
        // write a new location based on the existing request pointed at the target service
        location := &url.URL{}
        location.Scheme = "https"
        rloc, err := r.serviceResolver.ResolveEndpoint(handlingInfo.serviceNamespace, handlingInfo.serviceName, handlingInfo.servicePort)
        if err != nil {
            klog.Errorf("error resolving %s/%s: %v", handlingInfo.serviceNamespace, handlingInfo.serviceName, err)
            proxyError(w, req, "service unavailable", http.StatusServiceUnavailable)
            return
        }
        location.Host = rloc.Host
        location.Path = req.URL.Path
        location.RawQuery = req.URL.Query().Encode()
        newReq, cancelFn := newRequestForProxy(location, req)
        defer cancelFn()
       ...
        proxyRoundTripper = transport.NewAuthProxyRoundTripper(user.GetName(), user.GetGroups(), user.GetExtra(), proxyRoundTripper)
        handler := proxy.NewUpgradeAwareHandler(location, proxyRoundTripper, true, upgrade, &responder{w: w})
        handler.ServeHTTP(w, newReq)
    }
    $ kubectl get APIService           
    NAME                                   SERVICE                      AVAILABLE   AGE
    ...
    v1.apps                                Local                        True        50d
    ...
    v1beta1.metrics.k8s.io                 kube-system/metrics-server   True        50d
    ...
    # default APIServices
    $ kubectl get -o yaml APIService/v1.apps
    apiVersion: apiregistration.k8s.io/v1
    kind: APIService
    metadata:
      labels:
        kube-aggregator.kubernetes.io/automanaged: onstart
      name: v1.apps
      selfLink: /apis/apiregistration.k8s.io/v1/apiservices/v1.apps
    spec:
      group: apps
      groupPriorityMinimum: 17800
      version: v1
      versionPriority: 15
    status:
      conditions:
      - lastTransitionTime: "2020-10-20T10:39:48Z"
        message: Local APIServices are always available
        reason: Local
        status: "True"
        type: Available
    
    # aggregated server    
    $ kubectl get -o yaml APIService/v1beta1.metrics.k8s.io
    apiVersion: apiregistration.k8s.io/v1
    kind: APIService
    metadata:
      labels:
        addonmanager.kubernetes.io/mode: Reconcile
        kubernetes.io/cluster-service: "true"
      name: v1beta1.metrics.k8s.io
      selfLink: /apis/apiregistration.k8s.io/v1/apiservices/v1beta1.metrics.k8s.io
    spec:
      group: metrics.k8s.io
      groupPriorityMinimum: 100
      insecureSkipTLSVerify: true
      service:
        name: metrics-server
        namespace: kube-system
        port: 443
      version: v1beta1
      versionPriority: 100
    status:
      conditions:
      - lastTransitionTime: "2020-12-05T00:50:48Z"
        message: all checks passed
        reason: Passed
        status: "True"
        type: Available
    
    # CRD
    $ kubectl get -o yaml APIService/v1.duyanghao.example.com
    apiVersion: apiregistration.k8s.io/v1
    kind: APIService
    metadata:
      labels:
        kube-aggregator.kubernetes.io/automanaged: "true"
      name: v1.duyanghao.example.com
      selfLink: /apis/apiregistration.k8s.io/v1/apiservices/v1.duyanghao.example.com
    spec:
      group: duyanghao.example.com
      groupPriorityMinimum: 1000
      version: v1
      versionPriority: 100
    status:
      conditions:
      - lastTransitionTime: "2020-12-11T08:45:37Z"
        message: Local APIServices are always available
        reason: Local
        status: "True"
        type: Available
  • aggregatorServer创建过程中会根据所有kube-apiserver定义的API资源创建默认的APIService列表,名称即是$VERSION.$GROUP,这些APIService都会有标签kube-aggregator.kubernetes.io/automanaged: onstart,例如:v1.apps apiService。autoRegistrationController创建并维护这些列表中的APIService,也即我们看到的Local apiService;对于自定义的APIService(aggregated server),则不会对其进行处理
  • aggregated server实现CR(自定义API资源) 的CRUD API接口,并可以灵活选择后端存储,可以与core kube-apiserver一起公用etcd,也可自己独立部署etcd数据库或者其它数据库。aggregated server实现的CR API路径为:/apis/$GROUP/$VERSION,具体到sample apiserver为:/apis/wardle.example.com/v1alpha1,下面的资源类型有:flunders以及fischers
  • aggregated server通过部署APIService类型资源,service fields指向对应的aggregated server service实现与core kube-apiserver的集成与交互
  • sample-apiserver目录结构如下,可参考编写自己的aggregated server:

    staging/src/k8s.io/sample-apiserver
    ├── artifacts
    │   ├── example
    │   │   ├── apiservice.yaml
          ...
    ├── hack
    ├── main.go
    └── pkg
    ├── admission
    ├── apis
    ├── apiserver
    ├── cmd
    ├── generated
    │   ├── clientset
    │   │   └── versioned
                  ...
    │   │       └── typed
    │   │           └── wardle
    │   │               ├── v1alpha1
    │   │               └── v1beta1
    │   ├── informers
    │   │   └── externalversions
    │   │       └── wardle
    │   │           ├── v1alpha1
    │   │           └── v1beta1
    │   ├── listers
    │   │   └── wardle
    │   │       ├── v1alpha1
    │   │       └── v1beta1
    └── registry
    • 其中,artifacts用于部署yaml示例
    • hack目录存放自动脚本(eg: update-codegen)
    • main.go是aggregated server启动入口;pkg/cmd负责启动aggregated server具体逻辑;pkg/apiserver用于aggregated server初始化以及路由注册
    • pkg/apis负责相关CR的结构体定义,自动生成(update-codegen)
    • pkg/admission负责准入的相关代码
    • pkg/generated负责生成访问CR的clientset,informers,以及listers
    • pkg/registry目录负责CR相关的RESTStorage实现

更多代码原理详情,参考 kubernetes-reading-notes

apiExtensionsServer

apiExtensionsServer主要负责CustomResourceDefinition(CRD)apiResources以及apiVersions的注册,同时处理CRD以及相应CustomResource(CR)的REST请求(如果对应CR不能被处理的话则会返回404),也是apiserver Delegation的最后一环

原理总结如下:

  • Custom Resource,简称CR,是Kubernetes自定义资源类型,与之相对应的就是Kubernetes内置的各种资源类型,例如Pod、Service等。利用CR我们可以定义任何想要的资源类型
  • CRD通过yaml文件的形式向Kubernetes注册CR实现自定义api-resources,属于第二种扩展Kubernetes API资源的方式,也是普遍使用的一种
  • APIExtensionServer负责CustomResourceDefinition(CRD)apiResources以及apiVersions的注册,同时处理CRD以及相应CustomResource(CR)的REST请求(如果对应CR不能被处理的话则会返回404),也是apiserver Delegation的最后一环
  • crdRegistrationController负责将CRD GroupVersions自动注册到APIServices中。具体逻辑为:枚举所有CRDs,然后根据CRD定义的crd.Spec.Group以及crd.Spec.Versions字段构建APIService,并添加到autoRegisterController.apiServicesToSync中,由autoRegisterController进行创建以及维护操作。这也是为什么创建完CRD后会产生对应的APIService对象
  • APIExtensionServer包含的controller以及功能如下所示:

    • openapiController:将 crd 资源的变化同步至提供的 OpenAPI 文档,可通过访问 /openapi/v2 进行查看;
    • crdController:负责将 crd 信息注册到 apiVersions 和 apiResources 中,两者的信息可通过 kubectl api-versionskubectl api-resources 查看;
    • kubectl api-versions命令返回所有Kubernetes集群资源的版本信息(实际发出了两个请求,分别是https://127.0.0.1:6443/api以及https://127.0.0.1:6443/apis,并在最后将两个请求的返回结果进行了合并)

      $ kubectl -v=8 api-versions 
      I1211 11:44:50.276446   22493 loader.go:375] Config loaded from file:  /root/.kube/config
      I1211 11:44:50.277005   22493 round_trippers.go:420] GET https://127.0.0.1:6443/api?timeout=32s
      ...
      I1211 11:44:50.290265   22493 request.go:1068] Response Body: {"kind":"APIVersions","versions":["v1"],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0","serverAddress":"x.x.x.x:6443"}]}
      I1211 11:44:50.293673   22493 round_trippers.go:420] GET https://127.0.0.1:6443/apis?timeout=32s
      ...
      I1211 11:44:50.298360   22493 request.go:1068] Response Body: {"kind":"APIGroupList","apiVersion":"v1","groups":[{"name":"apiregistration.k8s.io","versions":[{"groupVersion":"apiregistration.k8s.io/v1","version":"v1"},{"groupVersion":"apiregistration.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"apiregistration.k8s.io/v1","version":"v1"}},{"name":"extensions","versions":[{"groupVersion":"extensions/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"extensions/v1beta1","version":"v1beta1"}},{"name":"apps","versions":[{"groupVersion":"apps/v1","version":"v1"}],"preferredVersion":{"groupVersion":"apps/v1","version":"v1"}},{"name":"events.k8s.io","versions":[{"groupVersion":"events.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"events.k8s.io/v1beta1","version":"v1beta1"}},{"name":"authentication.k8s.io","versions":[{"groupVersion":"authentication.k8s.io/v1","version":"v1"},{"groupVersion":"authentication.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"authentication.k8s.io/v1"," [truncated 4985 chars]
      apiextensions.k8s.io/v1
      apiextensions.k8s.io/v1beta1
      apiregistration.k8s.io/v1
      apiregistration.k8s.io/v1beta1
      apps/v1
      authentication.k8s.io/v1beta1
      ...
      storage.k8s.io/v1
      storage.k8s.io/v1beta1
      v1
      
      • kubectl api-resources命令就是先获取所有API版本信息,然后对每一个API版本调用接口获取该版本下的所有API资源类型

        $ kubectl -v=8 api-resources
         5077 loader.go:375] Config loaded from file:  /root/.kube/config
         I1211 15:19:47.593450   15077 round_trippers.go:420] GET https://127.0.0.1:6443/api?timeout=32s
         I1211 15:19:47.602273   15077 request.go:1068] Response Body: {"kind":"APIVersions","versions":["v1"],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0","serverAddress":"x.x.x.x:6443"}]}
         I1211 15:19:47.606279   15077 round_trippers.go:420] GET https://127.0.0.1:6443/apis?timeout=32s
         I1211 15:19:47.610333   15077 request.go:1068] Response Body: {"kind":"APIGroupList","apiVersion":"v1","groups":[{"name":"apiregistration.k8s.io","versions":[{"groupVersion":"apiregistration.k8s.io/v1","version":"v1"},{"groupVersion":"apiregistration.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"apiregistration.k8s.io/v1","version":"v1"}},{"name":"extensions","versions":[{"groupVersion":"extensions/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"extensions/v1beta1","version":"v1beta1"}},{"name":"apps","versions":[{"groupVersion":"apps/v1","version":"v1"}],"preferredVersion":{"groupVersion":"apps/v1","version":"v1"}},{"name":"events.k8s.io","versions":[{"groupVersion":"events.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"events.k8s.io/v1beta1","version":"v1beta1"}},{"name":"authentication.k8s.io","versions":[{"groupVersion":"authentication.k8s.io/v1","version":"v1"},{"groupVersion":"authentication.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"authentication.k8s.io/v1"," [truncated 4985 chars]
         I1211 15:19:47.614700   15077 round_trippers.go:420] GET https://127.0.0.1:6443/apis/batch/v1?timeout=32s
         I1211 15:19:47.614804   15077 round_trippers.go:420] GET https://127.0.0.1:6443/apis/authentication.k8s.io/v1?timeout=32s
         I1211 15:19:47.615687   15077 round_trippers.go:420] GET https://127.0.0.1:6443/apis/auth.tkestack.io/v1?timeout=32s
         https://127.0.0.1:6443/apis/authentication.k8s.io/v1beta1?timeout=32s
         I1211 15:19:47.616794   15077 round_trippers.go:420] GET https://127.0.0.1:6443/apis/coordination.k8s.io/v1?timeout=32s
         I1211 15:19:47.616863   15077 round_trippers.go:420] GET https://127.0.0.1:6443/apis/apps/v1?timeout=32s
         ...
         NAME                              SHORTNAMES   APIGROUP                       NAMESPACED   KIND
         bindings                                                                      true         Binding
         endpoints                         ep                                          true         Endpoints
         events                            ev                                          true         Event
         limitranges                       limits                                      true         LimitRange
         namespaces                        ns                                          false        Namespace
         nodes                             no                                          false        Node
         ...
        • namingController:检查 crd obj 中是否有命名冲突,可在 crd .status.conditions 中查看;
        • establishingController:检查 crd 是否处于正常状态,可在 crd .status.conditions 中查看;
        • nonStructuralSchemaController:检查 crd obj 结构是否正常,可在 crd .status.conditions 中查看;
        • apiApprovalController:检查 crd 是否遵循 Kubernetes API 声明策略,可在 crd .status.conditions 中查看;
        • finalizingController:类似于 finalizes 的功能,与 CRs 的删除有关;
  • 总结CR CRUD APIServer处理逻辑如下:

    • createAPIExtensionsServer=>NewCustomResourceDefinitionHandler=>crdHandler=>注册CR CRUD API接口:

      // New returns a new instance of CustomResourceDefinitions from the given config.
      func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) (*CustomResourceDefinitions, error) {
        ...
          crdHandler, err := NewCustomResourceDefinitionHandler(
            versionDiscoveryHandler,
              groupDiscoveryHandler,
            s.Informers.Apiextensions().V1().CustomResourceDefinitions(),
              delegateHandler,
            c.ExtraConfig.CRDRESTOptionsGetter,
              c.GenericConfig.AdmissionControl,
            establishingController,
              c.ExtraConfig.ServiceResolver,
            c.ExtraConfig.AuthResolverWrapper,
              c.ExtraConfig.MasterCount,
              s.GenericAPIServer.Authorizer,
              c.GenericConfig.RequestTimeout,
              time.Duration(c.GenericConfig.MinRequestTimeout)*time.Second,
              apiGroupInfo.StaticOpenAPISpec,
              c.GenericConfig.MaxRequestBodyBytes,
          )
          if err != nil {
              return nil, err
          }
          s.GenericAPIServer.Handler.NonGoRestfulMux.Handle("/apis", crdHandler)
          s.GenericAPIServer.Handler.NonGoRestfulMux.HandlePrefix("/apis/", crdHandler)
          ...
          return s, nil
      }
      
    • crdHandler处理逻辑如下:

      • 解析req(GET /apis/duyanghao.example.com/v1/namespaces/default/students),根据请求路径中的group(duyanghao.example.com),version(v1),以及resource字段(students)获取对应CRD内容(crd, err := r.crdLister.Get(crdName))
      • 通过crd.UID以及crd.Name获取crdInfo,若不存在则创建对应的crdInfo(crdInfo, err := r.getOrCreateServingInfoFor(crd.UID, crd.Name))。crdInfo中包含了CRD定义以及该CRD对应Custom Resource的customresource.REST storage
      • customresource.REST storage由CR对应的Group(duyanghao.example.com),Version(v1),Kind(Student),Resource(students)等创建完成,由于CR在Kubernetes代码中并没有具体结构体定义,所以这里会先初始化一个范型结构体Unstructured(用于保存所有类型的Custom Resource),并对该结构体进行SetGroupVersionKind操作(设置具体Custom Resource Type)
      • 从customresource.REST storage获取Unstructured结构体后会对其进行相应转换然后返回

        // k8s.io/kubernetes/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go:223
        func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
          ctx := req.Context()
          requestInfo, ok := apirequest.RequestInfoFrom(ctx)
          ...
          crdName := requestInfo.Resource + "." + requestInfo.APIGroup
          crd, err := r.crdLister.Get(crdName)
          ...
          crdInfo, err := r.getOrCreateServingInfoFor(crd.UID, crd.Name)
          verb := strings.ToUpper(requestInfo.Verb)
          resource := requestInfo.Resource
          subresource := requestInfo.Subresource
          scope := metrics.CleanScope(requestInfo)
          ...
          switch {
          case subresource == "status" && subresources != nil && subresources.Status != nil:
              handlerFunc = r.serveStatus(w, req, requestInfo, crdInfo, terminating, supportedTypes)
          case subresource == "scale" && subresources != nil && subresources.Scale != nil:
              handlerFunc = r.serveScale(w, req, requestInfo, crdInfo, terminating, supportedTypes)
          case len(subresource) == 0:
              handlerFunc = r.serveResource(w, req, requestInfo, crdInfo, terminating, supportedTypes)
          default:
              responsewriters.ErrorNegotiated(
                  apierrors.NewNotFound(schema.GroupResource{Group: requestInfo.APIGroup, Resource: requestInfo.Resource}, requestInfo.Name),
                  Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req,
              )
          }
          if handlerFunc != nil {
              handlerFunc = metrics.InstrumentHandlerFunc(verb, requestInfo.APIGroup, requestInfo.APIVersion, resource, subresource, scope, metrics.APIServerComponent, handlerFunc)
              handler := genericfilters.WithWaitGroup(handlerFunc, longRunningFilter, crdInfo.waitGroup)
              handler.ServeHTTP(w, req)
              return
          }
        }
        

更多代码原理详情,参考 kubernetes-reading-notes

Conclusion

本文从源码层面对Kubernetes apiserver进行了一个概览性总结,包括:aggregatorServer,kubeAPIServer,apiExtensionsServer以及bootstrap-controller等。通过阅读本文可以对apiserver内部原理有一个大致的理解,另外也有助于后续深入研究

Refs

【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!
查看原文

赞 2 收藏 0 评论 0

认证与成就

  • 获得 81 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-05-18
个人主页被 4.7k 人浏览