跳到主要内容

4 篇博文 含有标签「kubernetes」

查看所有标签

聊聊如何让办公网络直连Kubernetes集群PodIP/ClusterIP/Service DNS等

· 阅读需 9 分钟
CarlJi
Coder|Blogger|Engineer|Mentor

想象一下,如果您日常使用的研发测试 Kubernetes 集群,能够有以下效果:

  • 在办公网络下直接访问 Pod IP
  • 在办公网络下直接访问 Service Cluster IP
  • 在办公网络下直接访问集群内部域名,类似 service.namespace.svc.cluster.local

会不会很方便,很优雅?

笔者近期就给内部的一个新集群做过类似的调整,特此分享一些心得。

PS: 这里的 直接访问/直连 指的是不借助 Ingress/hostnetwork:true/NodePort 等常规方式,直接访问 k8s 内部 IP or DNS,起到 网络拉平 的效果。

先决条件 - 三层路由方案

办公网段跟 Kubernetes 集群大概率是不同的网段,所以要想打通最自然的想法是依赖路由。相应的,Kubernetes 跨主机网络方案,我们最好也选择三层路由方案或者 Host-GW,而非 Overlay,不然数据包在封包解包过程中可能会失去路由方向。

我们的集群选用的是 Calico,且关闭了 IPIP 模式。具体的 IPPool 配置如下:

-> calicoctl get IPPool -o yaml
apiVersion: projectcalico.org/v3
items:
- apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
name: default-pool
spec:
blockSize: 24
cidr: 10.233.64.0/18
# 关闭IPIP模式
ipipMode: Never
natOutgoing: true
nodeSelector: all()
vxlanMode: Never
kind: IPPoolList

Calico RR(Route Reflectors)or Full-Mesh 模式?

网上的很多类似教程,上来都会引导大家先把集群改为 RR 模式,其实这不是必须的。大家可以思考下,RR 模式解决的问题是什么?是为了防止所有节点间都做 BGP 连接交换,浪费资源。但如果你的集群很小, 且已经是按 Full Mesh 模式部署了,到也没必要非得改为 RR 模式。Full Mesh 下所有的节点都是类似 RR 节点的效果,所以如果我们想选择作为 BGPPeer 交换的节点,选择任意节点就行。 比如,笔者的集群就选择了 Ingress 所在的节点,作为 BGPPeer。

~ calicoctl get BGPPeer -o yaml
apiVersion: projectcalico.org/v3
items:
- apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
name: peer-switch
spec:
# 交换机配置
asNumber: 65200
peerIP: 10.200.20.254
# 这个label是Ingress节点特有的
nodeSelector: node-role.kubernetes.io/ingress == 'ingress'
kind: BGPPeerList

从集群外部访问 Pod IP vs 从集群内部访问?

这个问题很关键,如果我们想从外部直接访问到集群内部的 Pod IP,那么首先需要搞清楚集群内的节点是如何畅通访问的。

以下面的节点为例,我们来看它的路由信息:

~ ip r
# 默认路由
default via 10.200.20.21 dev bond0 onlink
# 宿主机数据包路由
10.200.20.0/24 dev bond0 proto kernel scope link src 10.200.20.105
# 黑洞,防止成环
blackhole 10.233.98.0/24 proto bird
# 目的地址是10.233.98.3的数据包,走cali9832424c93e网卡
10.233.98.3 dev cali9832424c93e scope link
# 目的地址是10.233.98.4的数据包,走cali4f5c6d27f17网卡
10.233.98.4 dev cali4f5c6d27f17 scope link
# 目的地址是10.233.98.8的数据包,走cali8f10abc672f网卡
10.233.98.8 dev cali8f10abc672f scope link
# 目的地址是10.233.110.0/24网段的数据包,从bond0网卡出到下一跳10.200.20.107上
10.233.110.0/24 via 10.200.20.107 dev bond0 proto bird
# 目的地址是10.233.112.0/24网段的数据包,从bond0网卡出到下一跳10.200.20.106上
10.233.112.0/24 via 10.200.20.106 dev bond0 proto bird
# 目的地址是10.233.115.0/24网段的数据包,从bond0网卡出到下一跳10.200.20.108上
10.233.115.0/24 via 10.200.20.108 dev bond0 proto bird

相信看了笔者的注释,大家应该很容易了解到以下信息:

  • 这台宿主机 IP 是 10.200.20.105,集群内其他的宿主机还有 10.200.20.106, 10.200.20.107, 10.200.20.108 等
  • 主机 10.200.20.105 上的 Pod IP 段是 10.233.98.0/24, 10.200.20.106 上是 10.233.112.0/24,10.200.20.107 上是 10.233.110.0/24
  • 目的地址是 10.233.98.3 的数据包走 cali9832424c93e 网卡,目的地址 10.233.98.4 的数据包走 cali4f5c6d27f17 网卡等

而这些信息实际解答了,容器数据包的 出和入 这个关键问题:

  • 比如想访问 Pod IP 为 10.233.110.7 的容器,宿主机自然知道下一跳是 10.200.20.107 上
  • 比如接收到了目的地址是 10.233.98.8 的数据包,宿主机自然也知道要把这个包交给 cali8f10abc672f 网卡。而这个网卡是 veth pair 设备的一端,另一端必然在目标 Pod 里

那这些路由信息是哪里来的呢?自然是 Calico 借助 BGP 的能力实现的。我们进一步想,如果外部节点也有这些信息,是不是也就自然知道了 Pod IP 在哪里了? 答案确实如此,其实总结基于 Calico 的网络打平方案,核心原理就是 通过 BGP 能力,将集群路由信息广播给外部。

而在具体的配置上,就比较简单了,只需要在两端配置好 BGP Peer 即可。

  • 先是集群这一侧,前面笔者已给出:

    ~ calicoctl get BGPPeer -o yaml
    apiVersion: projectcalico.org/v3
    items:
    - apiVersion: projectcalico.org/v3
    kind: BGPPeer
    metadata:
    name: peer-switch
    spec:
    # 交换机配置
    asNumber: 65200
    peerIP: 10.200.20.254
    # 这个label就是Ingress节点特有的
    nodeSelector: node-role.kubernetes.io/ingress == 'ingress'
    kind: BGPPeerList
  • 再就是外部,一般是交换机,使用类似下面的命令:

    [SwitchC] bgp 64513       # 这是k8s集群的ASN
    [SwitchC-bgp] peer 10.200.20.107 as-number 64513
    [SwitchC-bgp] peer 10.200.20.108 as-number 64513

    PS: 具体的交换机操作方式可以参考各品牌交换机官方文档

到这里,基本上我们已经打通了外部直接访问 Pod IP 的能力。当然,如果您的办公网络到交换机这一侧还有多个网关,您还需要在这些网关上设置合适的路由才行。

为什么 Service Cluster IP 还不能访问?

也许这时候您会发现,可以直连 Pod IP,但 Cluster IP 不可以,这是为什么呢?原来,默认情况 Calico 并没有广播 Service IP,您可以在交换机这一侧通过查看交换过来的 IP 段来确认这一点。

PS: 您是否注意到,k8s 主机节点上也没有 service 的 ip 路由,但为啥在集群内部访问 service 没有问题呢?

解决方案也简单,只要打开相关的设置即可, 类似如下:


~ calicoctl get bgpconfig default -o yaml
apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
name: default
spec:
asNumber: 64513
listenPort: 179
logSeverityScreen: Info
nodeToNodeMeshEnabled: true
# 这就是需要广播的service cluster IP 段
serviceClusterIPs:
- cidr: 10.233.0.0/18

打通内网 DNS,直接访问 Service 域名

直连 IP 虽然方便,但有时若想记住某服务的具体 IP 却不是那么容易。所以,我们将 K8s 内部的 DNS 域名也暴漏出来了,类似下面:

<service>.<namespaces>.svc.cluster.local

而这块的设置也相对简单,一般企业都有内网 DNS,只需要添加相应解析到 K8s 内部 DNS Server 即可。

总结

其实若想打造一个好用的研发测试集群,有很多的细节需要处理,笔者后续也会继续分享类似的经验,希望对大家有用。

参考链接

TopoLVM - 基于LVM的Kubernetes本地持久化方案,容量感知,动态创建PV,轻松使用本地磁盘

· 阅读需 8 分钟
CarlJi
Coder|Blogger|Engineer|Mentor

正文

研发测试场景下,一般追求的是一键快速起环境,横向动态复制,一人一套,随起随用,用完即走。作为使用方,其不用关心实际的物理资源是怎样的,环境起在哪里,只要声明自己的使用需求即可。但作为方案构建者以及 infrastructure 支撑,我们却深知,要想提供更好的解决方案,个中问题还有很多,且颇为不易。

比如在过去,笔者就曾一度困扰于如何优雅的放开本地物理盘给业务使用这个问题,尤其是本地 HDD 数据盘。

这里有个背景,我们的 Kubernetes 研发测试集群是用线上退下来的过保机器搭建,然后七牛又搞云存储,所以我们的机器中很多那种多盘位的存储密集型机器(比如挂 12 块 4T 的盘)。所以如何更好的利用这些磁盘,就是个问题。 方案之一是把这些盘组成网络存储,然后通过七牛自身的云服务或者 ceph 等系统提供出去,这当然是可行的。不过有些业务其实就想单纯使用物理盘,那该怎么办呢?

纵观 Kubernetes 目前原生提供的几种方案,笔者发现都不完美:

  • Emptydir 非持久化方案,Pod 删除,EmptyDir 也会被清空。另外 Emptydir 使用的是 rootfs 的存储空间,而这个空间有可能是放在在系统盘上的,所以对它的使用当慎之又慎。
  • HostPath 持久化方案,但安全风险很高,官方不推荐。另外,从使用角度也不方便。比如作为用户,你首先要清楚知道目标系统的情况,知道具体的盘符和目录位置然后才能在 HostPath 里正确引用。但现实是从集群安全角度,用户不一定有直接登录机器的权限。另外即使你的 Pod 占了某个宿主机的目录,也不能排除别人二次占用,或者误操作。所以 HostPath 的使用场景实际很受限。
  • Local Persistent Volume 持久化方案,以 PVC/PV 的形式来使用本地存储。想法很好,但是不能动态创建 PV,对于 PV 提供者来说,心智负担较高。当然社区现在也有提供 Local Static Provisioner,某种程度上简化了这块的心智负担,但是仍然需要预先规划目录或者分区,略显不足。

笔者以为,理想中的本地磁盘使用方案,应当是按需申请,空间隔离,且自动化生命周期管理。这样才能既方便终端用户使用,也能减少运维支撑,提高效率。这里的关键技术点有三个:

  • 按需申请 意味着最好以 PVC+StorageClass 的模式来提供服务,做到 PV 动态创建。而要实现这点,容量感知 是关键,因为若调度到空间不足的节点上很明显是不合理的。最好能结合 PVC 申请的容量+Node 上的剩余容量,综合选择最优的节点来做绑定。
  • 空间隔离 要确保用户申请的空间大小一定是足额的,不被侵占的。从这里看,单纯的把文件系统的目录用作 PV 但容量上彼此不隔离显然不合适。
  • 自动化生命周期管理 动态 Provisioning 是强需。

综合以上三点,我们会发现基于 LVM 或分区技术+CSI 的实现,当是比较符合上述用户体验的,而TopoLVM就是这样一个项目。

地址: https://github.com/topolvm/topolvm

TopoLVM 是基于 LVM 的 Kubernetes 本地化磁盘方案,以 CSI 形式提供给用户使用。目前主要支持以下功能:

  • 动态 Provisioning
  • 支持原生数据块卷(Raw Block Volume)
  • Volume 伸缩

整体架构如下:

值得注意的是,在早期版本,为了能够动态感知节点上的剩余存储容量,TopoLVM 设计了个自定义扩展调度器(上图 topolvm-scheduler 部分),方便在 Scheduling 阶段为 Pod 绑定合适的 Node。而在 Kubernetes 1.21 之后,Kubernete 已经原生支持了 Storage Capacity Tracking的能力,这块的实现整体就变的优雅很多,topolvm-Scheduler 也就不再需要了。

当然,要想认知到 TopoLVM 的核心原理,除了了解 CSI 编写规范外,最重要的就是需要了解 LVM 相关技术。而正是因为通过 LVM 能够动态创建 LV,动态扩缩容,TopoLVM 才能支持动态 Provisioning 相关的能力。

不过,虽然作为开源项目 TopoLVM 已基本够用,但丰富度略显不足。而博云近期也开源了他们的云原生本地磁盘管理方案 Carina,看起来更完善一些。

项目地址: https://github.com/carina-io/carina

Carina 除了提供基于 LVM 的方案外,还支持裸盘分区方式,以及 IOPS 限制等,功能更加丰富。代码组织规范也更贴合云原生社区的方式,整体非常值得一探。

参考链接

往期推荐

聊聊 Kubernetes Pod or Namespace 卡在 Terminating 状态的场景

· 阅读需 11 分钟
CarlJi
Coder|Blogger|Engineer|Mentor

这个话题,想必玩过 kubernetes 的同学当不陌生,我会分 Pod 和 Namespace 分别来谈。

开门见山,为什么 Pod 会卡在 Terminating 状态?

一句话,本质是 API Server 虽然标记了对象的删除,但是作为实际清理的控制器 kubelet, 并不能关停 Pod 或相关资源, 因而没能通知 API Server 做实际对象的清理。

原因何在?要解开这个原因,我们先来看 Pod Terminating 的基本流程:

  1. 客户端(比如 kubectl)提交删除请求到 API Server
    • 可选传递 --grace-period 参数
  2. API Server 接受到请求之后,做 Graceful Deletion 检查
    • 若需要 graceful 删除时,则更新对象的 metadata.deletionGracePeriodSeconds 和 metadata.deletionTimestamp 字段。这时候 describe 查看对象的话,会发现其已经变成 Terminating 状态了
  3. Pod 所在的节点,kubelet 检测到 Pod 处于 Terminating 状态时,就会开启 Pod 的真正删除流程
    • 如果 Pod 中的容器有定义 preStop hook 事件,那 kubelet 会先执行这些容器的 hook 事件
    • 之后,kubelet 就会 Trigger 容器运行时发起TERMsignal 给该 Pod 中的每个容器
  4. 在 Kubelet 开启 Graceful Shutdown 的同时,Control Plane 也会从目标 Service 的 Endpoints 中摘除要关闭的 Pod。ReplicaSet 和其他的 workload 服务也会认定这个 Pod 不是个有效副本了。同时,Kube-proxy 也会摘除这个 Pod 的 Endpoint,这样即使 Pod 关闭很慢,也不会有流量再打到它上面。
  5. 如果容器正常关闭那很好,但如果在 grace period 时间内,容器仍然运行,kubelet 会开始强制 shutdown。容器运行时会发送SIGKILL信号给 Pod 中所有运行的进程进行强制关闭
  6. 注意在开启 Pod 删除的同时,kubelet 的其它控制器也会处理 Pod 相关的其他资源的清理动作,比如 Volume。而待一切都清理干净之后,Kubelet 才通过把 Pod 的 grace period 时间设为 0 来通知 API Server 强制删除 Pod 对象。

参考链接: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination

只有执行完第六步,Pod 的 API 对象才会被真正删除。那怎样才认为是**"一切都清理干净了"**呢?我们来看源码:

// PodResourcesAreReclaimed returns true if all required node-level resources that a pod was consuming have
// been reclaimed by the kubelet. Reclaiming resources is a prerequisite to deleting a pod from theAPI Server.
func (kl *Kubelet) PodResourcesAreReclaimed(pod *v1.Pod, status v1.PodStatus) bool {
if kl.podWorkers.CouldHaveRunningContainers(pod.UID) {
// We shouldn't delete pods that still have running containers
klog.V(3).InfoS("Pod is terminated, but some containers are still running", "pod", klog.KObj(pod))
return false
}
if count := countRunningContainerStatus(status); count > 0 {
// We shouldn't delete pods until the reported pod status contains no more running containers (the previous
// check ensures no more status can be generated, this check verifies we have seen enough of the status)
klog.V(3).InfoS("Pod is terminated, but some container status has not yet been reported", "pod", klog.KObj(pod), "running", count)
return false
}
if kl.podVolumesExist(pod.UID) && !kl.keepTerminatedPodVolumes {
// We shouldn't delete pods whose volumes have not been cleaned up if we are not keeping terminated pod volumes
klog.V(3).InfoS("Pod is terminated, but some volumes have not been cleaned up", "pod", klog.KObj(pod))
return false
}
if kl.kubeletConfiguration.CgroupsPerQOS {
pcm := kl.containerManager.NewPodContainerManager()
if pcm.Exists(pod) {
klog.V(3).InfoS("Pod is terminated, but pod cgroup sandbox has not been cleaned up", "pod", klog.KObj(pod))
return false
}
}

// Note: we leave pod containers to be reclaimed in the background since dockershim requires the
// container for retrieving logs and we want to make sure logs are available until the pod is
// physically deleted.

klog.V(3).InfoS("Pod is terminated and all resources are reclaimed", "pod", klog.KObj(pod))
return true
}

源码位置: https://github.com/kubernetes/kubernetes/blob/1f2813368eb0eb17140caa354ccbb0e72dcd6a69/pkg/kubelet/kubelet_pods.go#L923

是不是很清晰?总结下来就三个原因:

  1. Pod 里没有 Running 的容器
  2. Pod 的 Volume 也清理干净了
  3. Pod 的 cgroup 设置也没了

如是而已。

自然,其反向对应的就是各个异常场景了。我们来细看:

  • 容器停不掉 - 这种属于 CRI 范畴,常见的一般使用 docker 作为容器运行时。笔者就曾经遇到过个场景,用docker ps 能看到目标容器是Up状态,但是执行docker stop or rm 却没有任何反应,而执行docker exec,会报no such container的错误。也就是说此时这个容器的状态是错乱的,docker 自己都没法清理这个容器,可想而知 kubelet 更是无能无力。workaround 恢复操作也简单,此时我只是简单的重启了下 docker,目标容器就消失了,Pod 的卡住状态也很快恢复了。当然,若要深究,就需要看看 docker 侧,为何这个容器的状态错乱了。
    • 更常见的情况是出现了僵尸进程,对应容器清理不了,Pod 自然也会卡在 Terminating 状态。此时要想恢复,可能就只能重启机器了。
  • Volume 清理不了 - 我们知道在 PV 的"两阶段处理流程中",Attach&Dettach 由 Volume Controller 负责,而 Mount&Unmount 则是 kubelet 要参与负责。笔者在日常中有看到一些因为自定义 CSI 的不完善,导致 kubelet 不能 Unmount Volume,从而让 Pod 卡住的场景。所以我们在日常开发和测试自定义 CSI 时,要小心这一点。
  • cgroups 没删除 - 启用 QoS 功能来管理 Pod 的服务质量时,kubelet 需要为 Pod 设置合适的 cgroup level,而这是需要在相应的位置写入合适配置文件的。自然,这个配置也需要在 Pod 删除时清理掉。笔者日常到是没有碰到过 cgroups 清理不了的场景,所以此处暂且不表。

现实中导致 Pod 卡住的细分场景可能还有很多,但不用担心,其实多数情况下通过查看 kubelet 日志都能很快定位出来的。之后顺藤摸瓜,恢复方案也大多不难。

当然还有一些系统级或者基础设施级异常,比如 kubelet 挂了,节点访问不了 API Server 了,甚至节点宕机等等,已经超过了 kubelet 的能力范畴,不在此讨论范围之类。

还有个注意点,如果你发现 kubelet 里面的日志有效信息很少,要注意看是不是 Log Level 等级过低了。从源码看,很多更具体的信息,是需要大于等于 3 级别才输出的。

那 Namespace 卡在 Terminating 状态的原因是啥?

显而易见,删除 Namespace 意味着要删除其下的所有资源,而如果其中 Pod 删除卡住了,那 Namespace 必然也会卡在 Terminating 状态。

除此之外,结合日常使用,笔者发现 CRD 资源发生删不掉的情况也比较高。这是为什么呢?至此,那就不得不聊聊 Finalizers 机制了。

官方有篇博客专门讲到了这个,里面有个实验挺有意思。随便给一个 configmap,加上个 finalizers 字段之后,然后使用kubectl delete删除它就会发现,直接是卡住的,kubernetes 自身永远也删不了它。

参考: https://kubernetes.io/blog/2021/05/14/using-finalizers-to-control-deletion/#understanding-finalizers

原因何在?

原来 Finalizers 在设计上就是个 pre-delete 的钩子,其目的是让相关控制器有机会做自定义的清理动作。通常控制器在清理完资源后,会将对象的 finalizers 字段清空,然后 kubernetes 才能接着删除对象。而像上面的实验,没有相关控制器能处理我们随意添加的 finalizers 字段,那对象当然会一直卡在 Terminating 状态了。

自己开发 CRD 及 Controller,因成熟度等因素,发生问题的概率自然比较大。除此之外,引入 webhook(mutatingwebhookconfigurations/validatingwebhookconfigurations)出问题的概率也比较大,日常也要比较注意。

综合来看,遇 Namespace 删除卡住的场景,笔者认为,基本可以按以下思路排查:

  1. kubectl get ns $NAMESPACE -o yaml, 查看conditions字段,看看是否有相关信息
  2. 如果上面不明显,那就可以具体分析空间下,还遗留哪些资源,然后做更针对性处理
    • 参考命令: kubectl api-resources --verbs=list --namespaced -o name | xargs -n 1 kubectl get --show-kind --ignore-not-found -n $NAMESPACE

找准了问题原因,然后做相应处理,kubernetes 自然能够清理对应的 ns 对象。不建议直接清空 ns 的 finalizers 字段做强制删除,这会引入不可控风险。

参考: https://github.com/kubernetes/kubernetes/issues/60807#issuecomment-524772920

相关阅读

前同事也有几篇关于 kubernetes 资源删除的文章,写的非常好,推荐大家读读:

往期推荐

Kubernetes e2e test and test framework

· 阅读需 11 分钟
CarlJi
Coder|Blogger|Engineer|Mentor

前言

Kubernetes的成功少不了大量工程师的共同参与,而他们之间如何高效的协作,非常值得我们探究。最近研究和使用了他们的e2e测试和框架,还是挺有启发的。

怎样才是好的e2e测试?

不同的人写出的测试用例千差万别,尤其在用例,可能由开发人员编写的情形下,其情形可想而知。要知道,绝大多数开发人员,可能并没有经历过大量测试用例场景的熏陶。所以如何持续输出高质量的e2e测试用例,确实是一个挑战。不过,Kubernetes社区非常聪明,他们抽象出来了一些共性的东西,来希望大家遵守。比如说

  1. 拒绝“flaky”测试 - 也就是那些偶尔会失败,但是又非常难定位的问题。
  2. 错误输出要详细,尤其是做断言时,相关信息要有。不过也不要打印太多无效信息,尤其是在case并未失败的情况。
  3. make case run in anywhere。这一点很重要,因为你的case是提交到社区,可能在各种环境下,各种时间段内运行。面对着各种cloud provider,各种系统负载情况。所以你的case要尽可能稳定,比如APICall,能异步的,就不要假设是同步; 比如多用retry机制等。
  4. 测试用例要执行的足够快。超过两分钟,就需要给这种测试打上[SLOW]标签。而有这种标签的测试用例,可以运行的场景就比较有限制了。谁又不希望自己写的用例都被尽可能的执行呢?很有激励性的一条规则。

另外,社区不过定下规则,还开发和维护了一系列的基础设施,来辅助上面规则的落地。我们接下来要讲的e2e框架就是其中之一。

e2e 验收测试

搞过测试的应该都知道,在面对复杂系统测试时,我们通常有多套测试环境,但是测试代码通常只有一份。所以为了能更好的区分测试用例,通常采取打标签的方式来给用例分类。这在Kubernetes的e2e里,这也不例外。

Kubernetes默认将测试用例分为下面几类,需要开发者在实际开发用例时,合适的使用。

  • 没标签的,默认测试用例是稳定的,支持并发,且运行足够快的
  • [Slow] 执行比较慢的用例.(对于具体的时间阈值,Kubernetes不同的文档表示不一致,此处需要修复)
  • [Serial] 不支持并发的测试用例,比如占用太多资源,还比如需要重启Node的
  • [Disruptive] 会导致其他测试用例失败或者具有破坏性的测试用例
  • [Flaky] 不稳定的用例,且很难修复。使用它要非常慎重,因为常规CI jobs并不会运行这些测试用例
  • [Feature:.+] 围绕特定非默认Kubernetes集群功能或者非核心功能的测试用例,方便开发以及专项功能适配

当然除了以上标签,还有个比较重要的标签就是[Conformance], 此标签用于验收Kubernetes集群最小功能集,也就是我们常说的MAT测试。所以如果你有个私有部署的k8s集群,就可以通过这套用例来搞验收。方法也很简单,通过下面几步就可以执行:

# under kubernetes folder, compile test cases and ginkgo tool
make WHAT=test/e2e/e2e.test && make ginkgo

# setup for conformance tests
export KUBECONFIG=/path/to/kubeconfig
export KUBERNETES_CONFORMANCE_TEST=y
export KUBERNETES_PROVIDER=skeleton

# run all conformance tests
go run hack/e2e.go -v --test --test_args="--ginkgo.focus=\[Conformance\]"

注意,kubernetes的测试使用的镜像都放在GCR上了,如果你的集群在国内,且还不带翻墙功能,那可能会发现pod会因为下载不了镜像而启动失败。

Kubernetes e2e test framework

研究Kubernetes的e2e测试框架,然后类比我们以往的经验,个人觉得,下面几点特性还是值得借鉴的:

All e2e compiled into one binary, 单一独立二进制

在对服务端程序进行API测试时,我们经常会针对每个服务都创建一个ginkgo suite来框定测试用例的范围,这样做的好处是用例目标非常清晰,但是随着服务数量的增多,这样的suite会越来越来多。从组织上,看起来就稍显杂乱,而且不利于测试服务的输出。

比如,我们考虑这么一个场景,QA需要对新机房部署,或者私有机房进行服务验证。这时候,就通常需要copy所有代码到指定集群在运行了,非常的不方便,而且也容易造成代码泄露。

kubernetes显然也会有这个需求,所以他们改变写法,将所有的测试用例都编译进一个e2e.test的二进制,这样针对上面场景时,就可以直接使用这个可执行文件来操作,非常的方便。

当然可执行文件的方便少不了外部参数的自由注入,以及整体测试用例的精心标记。否则,测试代码写的不规范,需要频繁的针对特定环境修改,也是拒不方便的。

Each case has a uniqe namespace, 每个case拥有唯一的空间

为每条测试用例创建一个独立的空间,是kubernetes e2e framework的一大精华。每条测试用例独享一个空间,彼此不冲突,从而根本上避免并发困扰,借助ginkgo的CLI来运行,会极大的提高执行效率。

而且这处代码的方式也非常优美,很有借鉴价值:

func NewFramework(baseName string, options FrameworkOptions, client clientset.Interface) *Framework {
f := &Framework{
BaseName: baseName,
AddonResourceConstraints: make(map[string]ResourceConstraint),
Options: options,
ClientSet: client,
}

BeforeEach(f.BeforeEach)
AfterEach(f.AfterEach)

return f
}

利用ginkgo 的BeforeEach的嵌套特定,虽然在Describe下就定义framework的初始化(如下),但是在每个It执行前,上面的BeforeEach才会真正执行,所以并不会有冲突:

var _ = framework.KubeDescribe("GKE local SSD [Feature:GKELocalSSD]", func() {
f := framework.NewDefaultFramework("localssd")
It("should write and read from node local SSD [Feature:GKELocalSSD]", func() {
...
})
})

当然e2e框架还负责case执行完的环境清理,并且是按需灵活配置。比如你希望,case失败保留现场,不删除namespace,那么就可以设置flag 参数 delete-namespace-on-failure为false来实现。

Asynchronous wait,异步等待

几乎所有的Kubernetes操作都是异步的,所以不管是产品代码还是测试用例,都广泛的使用了这个异步等待库:kubernetes/vendor/k8s.io/apimachinery/pkg/util/wait。这个库,实现简单,精悍,非常值得学习。

另外,针对测试的异步验证,其实ginkgo(gomega)本身提供的Eventualy,也是非常好用的。

Suitable logs,打印合适的log

Kubernetes e2e 主要使用两种方式输出log,一个是使用glog库,另一个则是framework.Logf方法。glog本身是golang官方提供的log库,使用比较灵活。但是这里主要推荐的还是Framework.Logf。因为使用此方法的log会输出到GinkgoWriter里面,这样当我们使用ginkgo.RunSpecsWithDefaultAndCustomReporters方法时,log不光输出到控制台,也会保存在junit格式的xml文件里,非常方便在jenkins里展示测试结果。

Clean code, 测试代码也可以很干净,优美

很多时候大家会觉得测试代码比较low,其实却不然。代码无所谓优劣,好坏还是依赖写代码的人。而且我想说,测试代码也是可以,并且应该写的很优美的,不然如何提升逼格?!。

我们从Kubernetes e2e能看到很多好的借鉴,比如:

  • 抽取主干方法,以突出测试用例主体
  • 采用数据驱动方式书写共性测试用例
  • 注释工整,多少适宜
  • 不输出低级别log
  • 代码行长短适宜
  • 方法名定义清晰,可读性强

Kubernetes环境普适性的e2e测试框架

现实中,如果需要围绕k8s工作,你可能需要一套,自己的测试框架。不管是测试各种自定义的controller or watcher,还是测试运行在k8s里运行的私有服务。这套框架都适用于你:

https://github.com/CarlJi/golearn/tree/master/src/carlji.com/experiments/k8s_e2e_mat_framework

逻辑改动很小,只是在原有kubernetes e2e 框架基础上抽取了最小集合。以方便快速使用。

是不是很贴心?