跳到主要内容

聊聊测试开发工程师的职责定位问题

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

网上有人会把测开定位成为 测试工具开发,主要是开发自动化测试工具或平台,用以帮助手动验收的同学提升效率。存在即合理,确实有一些团队或组织是这样建设的。但作为行业从业者,我们也应该认识到,这样是不全面的,有误导之嫌。

现实中的绝大部分测开还是定位在 保障业务迭代质量 上,因为这才是业务最需要的部分,也是最能直接产生价值的部分。而工具开发,属于 提效 范畴,当然也需要,但其紧迫性以及价值会因企业而不同,也会与企业所属的发展阶段息息相关。所以很多时候,工具具体能提效多少,都需要工具开发者能够想清楚,甚至要能显式证明。

也许大家会觉得,保障业务质量招聘测试就可以了啊,为啥要招聘测开呢?其实这是把质量保障这件事想 Low 了,从 最终服务于客户的质量 出发,要想做好质量保障真没那么容易。很多质量领域的事情,都不是单纯的测试能搞定的,但又是客户必须的,比如速度,可靠性等。测试仅仅只是质量保障的手段之一,还有很多其他的手段也仍然有效。就拿架构评审,代码 Review,发布审核等事项来说,要想参与无疑都需要质量同学对技术有更深的认知。笔者最近也在尝试通过发布审核来把控全局质量,所以也非常想看到有更多的质量同学往这方面发展,大家能彼此探讨。

不过,毕竟时代局限性仍在,从事质量领域的同学还是以测试为主。所以,此时如果企业想招聘一位技术比较好的测试同学,她一般 JD 都会选择叫 "测试开发工程师",因为这样能更好的匹配预期。要说明的是,在企业内部其实叫什么真的不重要,也没太有人关心,企业看中的始终是价值产出。私下里,我其实更喜欢 QA(Quality Assurance)这个称呼.因为我会觉得QA 包含 Ownership 意味,寓意我在负责某件事。而"测开"的叫法感觉跟"开发"一样,字面上更像个执行者,总是欠缺了那么一点主观能动性。

另外,当团队里所有的 QA 同学都有不错的开发水平时,你会发现,很多常见的"测开工具" "测开平台"需求就会显得不是那么刚需,甚至可能没必要。比如,相比通过 Web 或者 Excel 管理用例,直接用代码+Github 管理对经常写代码的同学来说可能更自然;执行用例也是,通过命令行,或者 Jenkins/Prow,也很方便;另外,Postman 可能会用的很少,因为对于经常与服务器打交道的同学,使用 curl 会更方便,其也基本够用;甚或者想搞个简单的压测,用系统自带的 AB 工具(Apache benchmarking tool)随手就完成了。凡此种种,笔笔皆是。

说到这里,有同学可能会问: 测开同学除了测试任务,就没有提效的要求吗?会不会大材小用?当然不是,在这种情况下,一些提效需求反而可能更具挑战性。因为这时候的提效目标就不是单指 QA 了,而是要面向全体工程师,甚至后者优先级更高,毕竟群体规模越大,投入产出比越高。另外就是需求越往上,需求的边界与传统的测试会越模糊,与业务越直接。比如发布灰度、质量运营、监测打点等等,这些系统都与质量有关,测开同学有能力的话当然也可以上。

总结来讲,对于测试开发的职责定位,我始终认为应该聚焦在 保障业务迭代质量,提升迭代效率 上,这点不应该变。而更具体的做法我会倾向于宣导:

  • 做业务的质检者, 关注检出率、漏出率,把控全局质量,为企业把好生产交付关。
  • 做工程研发专家, 保障业务迭代规范,加速迭代效率,关注软件工程技术的研发角色。

好像看起来挺难的,但不难的话又如何进步?

Go1.20 新版覆盖率方案解读

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

玩过 Go 覆盖率的同学当有所了解,Go 的覆盖率方案最初的设计目标仅是针对单测场景,导致其局限性很大。而为了适配更多的场景,行业内各种博客、插件、黑科技介绍也层出不穷。当然,过去我们也开源过 Go 系统测试覆盖率收集利器 - goc,算其中比较完善,比较系统的了。且从用户使用角度来看,goc 也确实解决了行业内很多同学的痛点。

而现在,Go 官方终于开始正式这个问题了。作者Than McIntosh 于今年 3 月份提出了新的覆盖率提案,且看当前实现进度,最快 Go1.20 我们就能体验到这个能力,非常赞。

基于作者的 Proposal,我们先来看看这个提案细节。

新姿势: go build -cover

需要明确的是,本次提案不会改变原来的使用姿势go test -cover,而是新增go build -cover使用入口。从这一变化我们不难看出,新提案主要瞄准的是 "针对程序级的覆盖率收集" ,而旧版的实际是 "仅针对包级别的覆盖率收集" ,二者设计目标有明显的差别。

在新姿势下,使用流程大体是:

$ go build -o myapp.exe -cover ...
$ mkdir /tmp/mycovdata
$ export GOCOVERDIR=/tmp/mycovdata
$ <run test suite, resulting in multiple invocations of myapp.exe>
$ go tool covdata [command]

整体逻辑也比较清晰:

  1. 先编译出一个经过插桩的被测程序
  2. 配置好覆盖率输出的路径,然后执行被测程序。到这一步程序本身就会自动的输出覆盖率结果到上述路径了
  3. 通过 go tool covdata 来处理覆盖率结果

这里的子命令 covdata 是新引入的工具。而之所需要新工具,主要还是在新提案下,输出的覆盖率文件格式与原来的已有较大的差别。

新版覆盖率格式

先来看旧版的覆盖率结果:

  mode: set
cov-example/p/p.go:5.26,8.12 2 1
cov-example/p/p.go:11.2,11.27 1 1
cov-example/p/p.go:8.12,10.3 1 1
cov-example/p/p.go:14.27,20.2 5 1

大家当比较熟悉,其是文本格式,简单易懂。

每一行的基本语义为 "文件:起始行.起始列,结束行.结束列 该基本块中的语句数量 该基本块被执行到的次数"

但缺点也明显,就是 "浪费空间". 比如文件路径 cov-example/p/p.go, 相比后面的 counter 数据,重复了多次,且在通常的 profile 文件,这块占比很大。

新提案在这个方向上做了不少文章,实现细节上稍显复杂,但方向较为清晰。

通过分析旧版的每一行能看出,本质上每一行会记录两类信息,一是定位每个基本块的具体物理位置,二是记录这个基本块的语句数量和被执行的次数。虽然执行的次数会变化,但是其他的信息是不变的,所以全局上其实只要记录一份这样的信息就好,而这就能大大的优化空间,

所以,新版覆盖率它实际会实际输出两份文件,一份就是 meta-data 信息,用于定位这个被测程序所有包、方法等元信息,另一份才是 counters,类似下面:

➜  tmp git:(master) ✗ ls -l
total 1280
-rw-r--r-- 1 jicarl staff 14144 Nov 28 17:02 covcounters.4d1584597702552623f460d5e2fdff27.8120.1669626144328186000
-rw-r--r-- 1 jicarl staff 635326 Nov 28 17:02 covmeta.4d1584597702552623f460d5e2fdff27

这两份文件都是二进制格式,并不能直观的读取。但是借助covdata工具,可以轻松转化为旧版格式,比较优雅。类似:

go tool covdata textfmt -i=tmp -o=covdata.txt

ps: tmp 是覆盖率文件所在目录。

真 • 全量覆盖率

一个标准的 go 程序,基本上由三种类型的代码包组成:

  • 自身代码
  • 第三方包,通过 mod 或者 vendor 机制引用
  • go 标准库

在过去,几乎所有的工具都只关注业务自身代码的插桩,鲜少关注第三方包,更别说 go 官方标准库了。这在大部分场景下是没问题的,但有时有些场景也有例外,比如 SDK 相关的项目。因为这时候 SDK 会作为 Dependency 引入,要想对其插桩就需要额外的开发量。还比如一些 CLI 程序,执行完命令之后,立马就结束了,也是非常不利于覆盖率收集的。

这些问题都是很现实的,且我们在 goc 项目中也收到过真实的用户反馈:

不过,现在好了,新版覆盖率方案也有实际考虑到这些需求,它实际会做到 支持全量插桩+程序退出时主动输出覆盖率结果 的原生方式,非常值得期待。

更多覆盖率使用场景支持: 合并(merge)、删减(subtract)、交集(intersect)

在实际处理覆盖率结果时,有很多实用的场景,在新提案中也有提及,比如支持:

  • 合并多次覆盖率结果 go tool covdata merge -i=<directories> -o=<dir>
  • 删减已经覆盖的部分 go tool covdata subtract -i=dir1,dir2 -o=<dir>
  • 得到两份结果的交集 go tool covdata intersect -i=dir1,dir2 -o=<dir>

在过去,这些场景都需要依赖第三方工具才行,而在新方案中已经无限接近开箱即用了。

不过更复杂的场景,类似远程获得覆盖率结果等(类似 goc 支持的场景),看起来新方案并没有原生支持。这个问题,笔者也在 issue 讨论中提出,看看作者是否后续有解答。

展望与不足

值得注意的是新提案的实现是通过 源码插桩+编译器支持 的方式来混合实现的,与原来go test -cover 纯源码改写的方式有了较大的变化。

另外作者提到的 test "origin" queries 功能还是非常让我兴奋的,因为有了它,若想建立 测试用例到源码的映射 会变得简单很多,甚至更进一步的 精准测试,也变的更有想象空间。不过这个功能不会在 Go1.20 里出现,只能期待以后了。

作者还提到了一些其他的限制和将来可能的改进,比如 Intra-line coverage, Function-level coverage, Taking into account panic paths 等,感兴趣的同学可以自行去 Proposal 文档查看。

聊聊如何让办公网络直连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 限制等,功能更加丰富。代码组织规范也更贴合云原生社区的方式,整体非常值得一探。

参考链接

往期推荐

Linux Troubleshooting 超实用系列 - Disk Analysis

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

关于磁盘的使用,实际生产中以下问题会较为常见:

  • No space left on device - 空间不足
  • Disk utilization 100% - 磁盘 I/O 过载
  • Too many open files - 文件句柄过多
  • Input/output error - 读写错误

而掌握常见的分析套路会事半功倍。

Disk usage

第一时间明确磁盘容量及使用情况总是没错的,这时候df -h 命令就比较方便:

$ df -h
Filesystem Size Used Avail Use% Mounted on
udev 48G 4.0K 48G 1% /dev
tmpfs 9.5G 8.55G 9.5G 90% /run
/dev/sda1 275G 234G 28G 90% /
/dev/sdd1 2.7T 1.6T 1.2T 57% /disk3
/dev/sdc1 3.6T 2.6T 1.1T 72% /disk1
/dev/sdb1 3.6T 4.2G 3.6T 1% /disk2

Use% 这个指标就比较清晰展示目标磁盘已经使用多少了。

注意,第三行的tmpfs文件系统比较特殊,其数据实际是存储在内存中而非磁盘。

Inode usage

有时候我们会发现明明磁盘有容量,但是程序仍然报No space left on device,这是因为什么呢?

答案大概率是 Inode 耗尽了。这时候可以通过df -i 来确认,比如:

$ df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
udev 12370103 518 12369585 1% /dev
tmpfs 12372788 611 12372177 1% /run
/dev/sda1 18317312 1941821 16375491 11% /
/dev/sdd1 183148544 181317058 1831468 99% /disk3
/dev/sdc1 244195328 153483 244041845 1% /disk1
/dev/sdb1 244195328 7496 244187832 1% /disk2

可以看到/disk3对应的目录其 Inode 已经使用 99%,很快就会耗尽。Inode 代表的是文件的 metadata 信息,若 inode 使用过多,通常意味着目录里小文件太多了。

PS: 不规范的容器化姿势比较容易出现这个问题,比如 Pod 一直在产生日志,且使用的是系统盘又不定期回收。

Disk utilization high

磁盘使用率高,一般是已经知道是哪个盘了,但如果不知道,使用iostat -x 1也能较清晰的查看到:

$ iostat -x 1
Linux 3.19.0-80-generic 2022年05月12日 _x86_64_ (24 CPU)

avg-cpu: %user %nice %system %iowait %steal %idle
5.85 0.00 3.60 4.83 0.00 85.72

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.00 237.00 441.00 48.00 56448.00 1388.00 236.55 0.97 1.98 1.02 10.83 1.00 48.80
sdb 0.00 26.00 2.00 186.00 8.00 93876.00 998.77 44.51 348.13 466.00 346.86 5.32 100.00
sdc 0.00 0.00 155.00 7.00 18132.00 16.00 224.05 6.62 47.95 46.71 75.43 4.02 65.20
sdd 0.00 30.00 8.00 8.00 900.00 212.00 139.00 0.10 6.25 3.50 9.00 6.00 9.60

PS: iostat -xd <device> 1 可以只查看某个设备。

可以看到 sdb 这块盘,其%util指标已经 100%。

但要注意,%util高并不严格意味着磁盘已经过载了,因为现代硬盘设备都有并行处理多个 I/O 请求的能力。要关注磁盘利用率,还需要关注await(再具体就是读r_await和写w_await指标),这个指标大致等于单个 I/O 所需的平均时间,所以如果它也很大,那磁盘一定是很繁忙了。

Which processes are using the specific disk?

实际场景中,面对磁盘负载高,我们通常需要做的是找到"罪魁祸首",判断其行为是否符合预期。

粗略的可以通过 iotop -oP 直接查看当前正在读写的进程。一般机器上有哪些程序,我们应该比较清楚,所以这时候可以大致判断出来:

$ iotop -oP
Total DISK READ : 173.26 M/s | Total DISK WRITE : 177.38 M/s
Actual DISK READ: 175.77 M/s | Actual DISK WRITE: 85.50 M/s
PID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
6929 be/4 root 168.67 M/s 168.57 M/s 0.00 % 76.51 % dd if=/dev/sda bs=4M count=100000 of=mbr.img
379 be/3 root 0.00 B/s 15.61 K/s 0.00 % 2.01 % [jbd2/sda1-8]

当然这种方式也存在一个问题,你是看不出目标进程具体使用哪块磁盘的。那怎么办呢?可以借助lsof +D <目录>命令,通过正在打开的文件句柄来识别进程:

$ lsof +D /disk2
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
prometheu 1705 root mem REG 8,17 72567556 234356807 /disk2/prometheus_dir/data/01G2J2YMJPY9HXMP5KSPW30MM1/chunks/000001
prometheu 1705 root mem REG 8,17 73620431 234356815 /disk2/prometheus_dir/data/01G19H692F7JN796CBQDSFVV1W/chunks/000001
prometheu 1705 root mem REG 8,17 73173252 234356814 /disk2/prometheus_dir/data/01G13QSNA21PYK2R6SC0BFYZYM/chunks/000001

然后通过pidstat -d 进一步分析这些进程的读写情况 :

$ pidstat -d
Linux 3.19.0-80-generic 2022年05月15日 _x86_64_(24 CPU)
16时21分37秒 UID PID kB_rd/s kB_wr/s kB_ccwr/s Command
16时21分59秒 0 1705 64.00 67.19 0.00 prometheus

kB_rd/skB_wr/s 这两个指标,能基本代表进程读写磁盘的速度。

Too many open files

相信后端同学大多都遇到过Too may open files的错误,因为高并发场景下,服务会建立很多连接,这时候就会很容易遇到这个错误。

可以通过ls -1 /proc/<pid>/fd | wc -l命令来查看当前进程已经打开了多少个文件:

$ ls -1 /proc/1705/fd | wc -l
1258

而若想查看某进程具体的句柄限制,可以通过命令cat /proc/<pid>/limits:

$ cat /proc/1705/limits
Limit Soft Limit Hard Limit Units
Max cpu time unlimited unlimited seconds
Max file size unlimited unlimited bytes
Max data size unlimited unlimited bytes
Max stack size 8388608 unlimited bytes
Max core file size 0 unlimited bytes
Max resident set unlimited unlimited bytes
Max processes 386565 386565 processes
Max open files 20240 20240 files

而若要调整这个限制,可以通过ulimit命令或修改系统文件/etc/security/limits.conf.

EIO (input/output error)

遇到这个错误,一般是物理磁盘坏了。可能是整个盘坏了不能读写,也有可能是某个 block 有问题。这时候通过dmesg -T查看内核日志,通常会有相应的 error 信息。

参考资料

  1. http://linuxperf.com/?p=156
  2. http://linuxperf.com/?p=40
  3. https://man7.org/linux/man-pages/man1/pidstat.1.html
  4. https://engineering.saltside.se/linux-troubleshooting-disk-analysis-2dc40c6c49b4

聊聊领导力与带团队的那些事

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

傅盛说 "人和人最大的差别是认知",说的是认知的力量,罗伯特·清崎说"赚不到认知以外的钱",描述了认知的局限。很明显,好的认知是力量,不好的认知是局限。但是认知又没法量化,所以有时候我们就会发现,很难认清自己,也很难学习别人。

曾经在一次内部会议上,我感慨"如何才能达到老师和老大们的认知高度呢?感觉光凭看书学习,好难啊。" 当时老大给出了回应,说 "关键在于选择"。

确实,如果能坚持选择正确的事情,不避艰难,选择长期价值,不趋小利,选择务实奋斗,不浮躁,进步是必然,只在早晚。

回首 2021 年,文章输出略少。来到 2022,新年新气象,希望给自己立个 flag,争取 Break。

作为今年的开篇,想聊聊工作上的一些认知、看法,谓之自省。

找到企业价值与团队成长的最佳战略落地点是团队 Leader 的核心工作

这应该是笔者近几年关于领导力和带团队方面最深的体悟了,没有之一。

作为团队 Leader,企业价值和团队成长必须两手抓,两手都要硬。产出不了价值,团队就不会有回报,做的事情没有一定的高度,大家个人方面就会成长不足,长期也会缺乏进步。而寻找最佳战略落地点,就是要找到那个团队关键着力点,要既能很好的达成职责和目标,还要能在技术先进上有一定的突破。

这点很重要,值得花精力慎重对待。

回头来看,云原生就是近几年我给团队找的主要战略方向了。抛开成绩不谈,通过这个战略,团队人员技术水平提高很多,且行业优势明显,这一点还是挺让我自豪的。

视人为人,超越伯乐

对于很多技术管理者来讲,这可能是最难的,包括我自己。

人心难测,但人性中最有力量。每个人都有自己的想法和诉求,而如何能团结一群人,发挥 1+1 > 2 的作用,非常考验功力。我们知道,现实中绝大多数问题归根结底是人的问题,但越是如此,领导者越不应该把问题简单定性在人上。因为,没有人是完美的,修人修心才是正途。

但,用人之长容易,培人之短甚难。我以前也有过尝试培人之短,深感不易。这也许就是为什么,很多人更认同“选对人比培养人更重要”的原因吧。

但,无论如此,任何时候都不应该丢失以人为本的心。

组织先行,倡导质量全员建设

自始至终,测试只是质量保障的手段,而不是目的。我们谈质量,实际谈的是结果质量,是产品能不能服务好最终用户,即使是面临突发情况,异常情况。

以终为始,架构设计合不合理,代码实现优不优雅,产品姿势贴不贴切,都会影响最终的服务质量。

质量保障没有银弹,也不会有一劳永逸的解决方案,功夫都在平时。而如果能调动更多的人,心系质量,参与质量建设,不管从企业 ROI 还是技术文化构建上,都有一定的积极意义。

工程效率本质是为提升工程生产力服务的,面向的是全体工程人员,而不是单指 QA

很多时候,工程效率同学从组织分布上会离 QA 较近,这会导致工效同学的目光过于关注测试的一亩三分地,局限性比较大。但是,企业内工程属性更多的其实是研发人员,解决他们的痛点从价值规模上来看才是最大的。

不过,这类人又是最难服务的,有很多"古怪"的爱好。比如,一个不爽,分分钟钟就自己搞个轮子。所以应该如何做呢?

一定要深入到研发群体去,多观察,多交流,深挖痛点。真正把他们当作实际客户,以服务的心态来面对,才容易找到破局点,并形成突破。

很多时候,我个人是不推荐建设专服务于手动测试同学的所谓自动化测试平台的。作为技术人,以代码的形式呈现用例,管理用例,感觉已经足够了。

而保持团队技术密度,倡导技控先于人控,长期角度也会有更多的惊喜。

往期推荐

聊聊 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 资源删除的文章,写的非常好,推荐大家读读:

往期推荐

构建高效Presubmit卡点,落地测试左移最佳实践

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

樊登有一节课讲的挺有意思,说中国有个组织叫绩效改进协会,专门研究用技控代替人控的事情。其用麦当劳来举例子,他说麦当劳其实招人标准很低,高中文凭就可以,但是培养出来的人,三五年之后,每一个都是大家争抢的对象。为什么呢?因为麦当劳的标准化做的很好。其中有一个例子是说,麦当劳的汉堡,出炉之后,15 分钟之后没卖掉就扔了。大家想想,如果你是领导,你如何让员工遵循这个标准?而麦当劳的解决方案说起来也简单,汉堡做出来之后就放入保温箱,15 分钟后保温箱就会报警,通知餐厅经理该批次的汉堡过期了需要丢弃。

人是组织不确定性的最大来源,让人去记每个汉堡的出炉时间铁定不行。而我们做工程效能,质量保障的,也同样需要思考如何用技术手段消弭不确定性,以确保更好的做组织升维。

借此,今天就想谈谈如何通过强化 Presubmit 卡点模式,落地实实在在的测试左移,让提效言之有物。

PS: 针对 PR(github 叫法)/MR(gitlab 叫法)触发的 CI 检查就是 Presubmit 模式

Presubmit 卡点模式可以包含哪些检查?

  • 单元测试,必须要有。行业内的基本认知,普及度很高。
  • 静态检查,必须要有。每种语言都有其推荐的检查工具,建议配上。
  • 集成测试/e2e,强烈建议有。
  • 系统性能基准测试,如果你在 Presubmit 阶段,能够针对系统的核心 KPI,持续自动化的做性能基准测试,用于辅助 Code Review 阶段的代码合入。恭喜,你当属于行业领先地位。

相信单测、静态检查,大部分项目都有,此处不表。本篇想重点谈谈为什么要在 Presubmit 阶段引入后几种测试类型,以及行业参考。

为什么要在 Presubmit 阶段跑集测、e2e、甚至性能基准测试?

笔者有以下几点认知:

  • 提前发现回归问题,降低修复成本。Presubmit 阶段,是研发的编码阶段,在这个阶段,如果在测试未介入的情况下,就能提供充分、即时的质量反馈,必然可以极大的提高研发迭代效率。要知道很多时候,QA 和研发都是在并行工作,手上会有很多事情在排队处理。而如果一个回归缺陷在 QA 验收阶段才被发现,那可能已经过去一段时间了。研发需要重新将需求拾起,修复并再提测,很可能会浪费很多时间。这种情形下,隐性的成本损耗会非常大。
  • 保障提测标准,建立和谐产研关系。很多 QA 团队都会要求研发在提测上,要有一定的质量标准。这很好,但人非圣贤,孰能无过。尤其在时间紧,任务重时,必然会发生研发自测不充分,遗漏低级问题到 QA 手上的情况。所以,与其主观约定,不如用自动化建立标准。
  • 强化测试价值,增加曝光度 很多时候,QA 同学写了很多自动化,但是业务无感,研发无感。这时候,你会发现,自动化就成了 QA 同学手里的玩物,不能有效交付。但是如果在 Presubmit 阶段就充分执行,不断执行,尤其是能早早的检测出 bug 时,整个团队必然会更加关注集测产物。长此以往,认可度就会比较高。
  • 测试左移,真正优雅的保障入库代码质量。谈到测试左移,我看到了太多的流程范,意识流。大家很多时候会放大主观能动性,强调尽早的参与到项目早期。这一点没错,但流程还是依赖于人的值守,但人最是喜新善变。针对回归问题,如果能够建立行之有效的检测手段,必然可以极大的降低心智负担。

在 Presubmit 阶段落地复杂测试类型,有哪些挑战?

好处很多,但落地也非易事。

  • 被测系统怎么建设? 不管是集成测试还是 e2e,被测对象都是较为完整的业务系统。而要在 Presubmit 阶段执行起来,就需要通过代码,自动构建和部署整套系统。另,业务通常会有多个仓库,多 PR/MR 同时执行,所以被测环境应该是按需而起,多套并行。
  • 资源哪里来? 既然是多套环境,必然会涉及到很多资源,资源哪里来,环境如何有效管理?PR 合并之后是不是应该自动回收等等问题。
  • 使用什么样的系统和姿势来构建? Presubmit 阶段对应的是 CI 系统,而 CI 的执行必然需要足够的快。毕竟,提个 PR,等十几分钟才有结果反馈,有点不雅。笔者经验认为,对于绝大部分的业务,CI 要控制在 10 分钟以内。

其实深入分析,问题还会有很多。但是挑战即机遇,方法总比困难多。只要价值足够大,收获才能足够多。

业界有哪些可以参考的?

七牛云

七牛云比较早的开始围绕 Presubmit 阶段建设各种质量反馈,落地测试左移。并且还进一步做了测试覆盖率收集和受影响服务分析,以及 devops 建设等工作。其使用的方案和工具大部已开源,比较有借鉴价值。

PS:想进一步了解细节的同学,可以搜索 MTSC2020 Topic: 基于云原生的测试左移技术实践 by 储培

开源工具:

谷歌

谷歌建设 Presubmit 模式的历史由来已久,其代码大仓的工作模式,也让其在这方面多了一些推陈出新。比如通过 Machine Learning / Probabilistic Safety 来筛选有效的执行用例,减少 Flaky tests 等。具体可以参考:

往期推荐

觉得不错,欢迎关注:

谈谈测试环境管理与实践

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

测试环境这个话题对于开发和测试同学一定不陌生,大家几乎每天都会接触。但是说到对测试环境的印象,却鲜有好评:

  • 环境不稳定,测试五分钟,排查两小时
  • 功能建设不全,导致验证不充分,遗漏缺陷
  • 多人共用,互相踩踏
  • 随手改动不入库,消极对待,缺乏敬畏之心

这些问题在行业内其实屡见不鲜。我甚至有听过运维同学"脏乱差"的评价。这里先不说他的评价是否有偏见,但是起码我认为,针对测试环境的管理有较大的改进空间,这是不争的事实。

而本文将重拾这个看起来老生常谈的话题,希望能系统化的阐述我的认知,以期与大家对齐。如果不对或者不完善的地方,欢迎提出,笔者将非常乐于与大家讨论。

首先我们要清晰的认知到,测试环境管理做的不好,不光有严重的质量风险,还会非常影响迭代效率,所以这件事情很重要。那在解决它之前,我们首先要去想想,对于测试环境我们到底有哪些诉求?

我们对测试环境的本质诉求是什么?

很明显,测试环境的定位就是满足产研侧的测试需求,保障产品迭代质量。所以从使用类型上,一般要支撑集成测试,系统测试,压力测试,甚至故障测试等。

而这些环境背后,其实都伴随着 非功能性要求 ,重点体现在:

  • 从使用者角度
    • 想用就有,不要等待
    • 要低维护,高稳定
      • 维护角度 - 我只关心我的测试需求,我不想干其他维护性工作
      • 稳定角度 - 我依赖的其他服务和业务要稳定,不要影响我测试
  • 从企业角度
    • 低成本,高效率
      • PS: 测试环境管理追求的是更高的研发迭代效率,但是成本是底线

除此之外,其实还有个非常关键的问题就是,要定义清楚测试环境管理的主体责任人是谁。这点很关键,没有责任人自然会滋生乱象。

  • 研发 虽经常使用测试环境,但从投入产出比上,组织一般还是希望研发同学能多投入精力做更多创造性的事情
  • 运维 本身负责线上环境的运维,可能有企业也会觉得把测试环境交给他们运维会顺水渠成,且现实确实是有不少企业就是这么干的。不过从人性的角度去分析,相比于线上环境,运维同学对测试环境的重视程度一定不够。而这也是为什么,很多企业的测试环境管理,也只是达到将就能用的水平的原因。
  • 测试 测试同学算是测试环境的主要使用者,对测试环境的管理理应负有直接责任。不过现实中,经常看到的是,测试同学因本身测试任务较多,且测试环境管理也要求具备一定的系统运维能力。导致相对而言,测试同学要想做好测试环境管理,也不容易。

不过,不管是哪个角色负责,其实症结还在 ROI 上。只要有充足的预算和人力,这些都不是问题。反之,就需要不断的优化和调整。

当然人力成本是组织层面的考量,今天我们先按下不表。这里重点聊聊如何从技术上解决这些问题。

业界的思路?

先来看看业界是怎么玩的。

阿里

阿里讲测试环境的文章不少,其中有一篇来自云效的文章,挺有借鉴价值。其重点聚焦了两个方向:

  • 通过项目环境复用公共基础环境的模式,来解决资源问题

  • 通过链路识别,请求染色,做到联调测试不串流量

当然,这些是借助阿里内部中间件实现的。不过在云原生环境下,其也开源了两个工具 kt-connect 和 virtual-environment,虽产品化程度做的不够,但整体还是比较有想法的。

百度

百度有篇文件介绍了其中间件技术在测试中的应用。文章说的比较清晰,这个中间件的架构是类似 istio 的模式,本质是通过代理来托管系统流量,从而实现控制链路的能力。而有了这个能力,对测试联调和环境复用自然就不在话下。同样的,对于录制/回放/mock/混沌等测试场景的能力实现上也能顺水渠成。

不过这个平台看起来有浓浓的背景局限,尤其是其控制平面的逻辑设计,感觉要玩转起来,需要一系列的基础设施的配合。所以这个应该是强百度业务和技术环境背景下的产物,对于使用者,也应该有一定的学习和理解成本。

商业化?

其他企业如有赞、喜马拉雅等,基本上也都是采用改造服务,通过路由策略来实现隔离组,从而达到环境复用的能力。

不过以上都是技术人的玩法,我在想测试环境管理这个方向有没有商业化价值呢?

大家看下图,来自站点www.testenvironmentmanagement.com:

(PS: 2019 年 4 月发布)

见名识意,这些都是国外主打 Test Environment Management(TEM)方向的企业,其中 Plutora 在 2011 年创立,2016 年融了 1340 万$. Enov8 始于 2008 年,正式创立于 2014 年。整体感觉活的都还不错。

研究这些企业会发现,他们会把价值重点落地在操作自动化,过程 Visibility,以及自服务和降低成本上。尤其是降低成本这块,会推出计算器,让企业主一目了然的看到,使用了他们的 TEM 方案会降低多少人力成本,多少资源成本等等。

另外,在 TEM 方向上,这些企业都会比较重视测试环境资源的自动或预约回收能力,以达到节约成本。这一点,感觉国内的玩家重视程度不够。

当然,目前国内互联网 ToB Saas 企业也开始方兴未艾,比如我前老大的创业公司www.koderover.com,其拳头产品云原生持续交付平台,也有关注TEM方向,值得推荐。

认知自醒,我们需要坚守哪些原则?

测试环境抛开全局管理一说,我认为作为使用者,最重要的还是坚守以下原则:

  • 重视服务部署环节,尽可能的遵循线上部署模式,比如:
    • 基础系统一致(系统版本,内核版本等)
    • 中间件版本和部署姿势一致 - 千万不要想当然
    • 部署工具一致*(PS: 坚决抵制那种通过 apt-get install 在机器上随意安装的行为)。*
    • 部署逻辑一致 - 模拟真实场景,避免测试遗漏(The wider the gap between test and production, the greater the probability that the delivered product will have more bugs/defects.), 包括:
      • 服务版本
      • 配置写法
      • 实例个数
      • 机房 or 区域情况等等 (PS: 切勿图省事,无脑部署最简单模式用于测试验收)
  • 谨记使用规范 - 改动一定要 入库, 入库, 入库

您觉得呢?

参考资料

往期推荐

觉得不错,欢迎关注:

聊聊Go代码覆盖率技术与最佳实践

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

"聊点干货"

覆盖率技术基础

截止到 Go1.15.2 以前,关于覆盖率技术底层实现,以下知识点您应该知道:

  • go 语言采用的是插桩源码的形式,而不是待二进制执行时再去设置 breakpoints。这就导致了当前 go 的测试覆盖率收集技术,一定是侵入式的,会修改目标程序源码。曾经有同学会问,插过桩的二进制能不能放到线上,所以建议最好不要。

  • 到底什么是"插桩"?这个问题很关键。大家可以任意找一个 go 文件,试试命令go tool cover -mode=count -var=CoverageVariableName xxxx.go,看看输出的文件是什么?

    • 笔者以这个文件为例https://github.com/qiniu/goc/blob/master/goc.go, 得到以下结果:

      	package main

      import "github.com/qiniu/goc/cmd"

      func main() {CoverageVariableName.Count[0]++;
      cmd.Execute()
      }

      var CoverageVariableName = struct {
      Count [1]uint32
      Pos [3 * 1]uint32
      NumStmt [1]uint16
      } {
      Pos: [3 * 1]uint32{
      21, 23, 0x2000d, // [0]
      },
      NumStmt: [1]uint16{
      1, // 0
      },
      }

      可以看到,执行完之后,源码里多了个CoverageVariableName变量,其有三个比较关键的属性: _ Count uint32 数组,数组中每个元素代表相应基本块(basic block)被执行到的次数 _ Pos 代表的各个基本块在源码文件中的位置,三个为一组。比如这里的21代表该基本块的起始行数,23代表结束行数,0x2000d比较有趣,其前 16 位代表结束列数,后 16 位代表起始列数。通过行和列能唯一确定一个点,而通过起始点和结束点,就能精确表达某基本块在源码文件中的物理范围 * NumStmt 代表相应基本块范围内有多少语句(statement) CoverageVariableName变量会在每个执行逻辑单元设置个计数器,比如CoverageVariableName.Count[0]++, 而这就是所谓插桩了。通过这个计数器能很方便的计算出这块代码是否被执行到,以及执行了多少次。相信大家一定见过表示 go 覆盖率结果的 coverprofile 数据,类似下面: github.com/qiniu/goc/goc.go:21.13,23.2 1 1

      这里的内容就是通过类似上面的变量CoverageVariableName得到。其基本语义为 "文件:起始行.起始列,结束行.结束列 该基本块中的语句数量 该基本块被执行到的次数"

依托于 go 语言官方强大的工具链,大家可以非常方便的做单测覆盖率收集与统计。但是集测/E2E 就不是那么方便了。不过好在我们现在有了https://github.com/qiniu/goc。

集测覆盖率收集利器 - Goc 原理

关于单测这块,深入 go 源码,我们会发现go test -cover命令会自动生成一个_testmain.go 文件。这个文件会 Import 各个插过桩的包,这样就可以直接读取插桩变量,从而计算测试覆盖率。实际上goc也是类似的原理(PS: 关于为何不直接用go test -c -cover 方案,可以参考这里https://mp.weixin.qq.com/s/DzXEXwepaouSuD2dPVloOg)。

不过集测时,被测对象通常是完整产品,涉及到多个 long running 的后端服务。所以 goc 在设计上会自动化会给每个服务注入 HTTP API,同时通过服务注册中心goc server来管理所有被测服务。如此的话,就可以在运行时,通过命令goc profile实时获取整个集群的覆盖率结果,当真非常方便。

整体架构参见:

代码覆盖率的最佳实践

技术需要为企业价值服务,不然就是在耍流氓。可以看到,目前玩覆盖率的,主要有以下几个方向:

  • 度量 - 深度度量,各种包,文件,方法度量,都属于该体系。其背后的价值在于反馈与发现。反馈测试水平如何,发现不足或风险并予以提高。比如常见的作为流水线准入标准,发布门禁等等。度量是基础,但不能止步于数据。覆盖率的终极目标,是提高测试覆盖率,尤其是自动化场景的覆盖率,并一以贯之。所以基于此,业界我们看到,做的比较有价值的落地形态是增量覆盖率的度量。goc diff 结合 Prow 平台也落地了类似的能力,如果您内部也使用 Kubernetes,不妨尝试一下。当然同类型的比较知名的商业化服务,也有 CodeCov/Coveralls 等,不过目前她们多数是局限在单测领域。

  • 精准测试方向 - 这是个很大的方向,其背后的价值逻辑比较清晰,就是建立业务到代码的双向反馈,用于提升测试行为的精准高效。但这里其实含有悖论,懂代码的同学,大概率不需要无脑反馈;不能深入到代码的同学,你给代码级别的反馈,也效果不大。所以这里落地姿势很重要。目前业界没还看到有比较好的实践例子,大部分都是解决特定场景下的问题,有一定的局限。

而相较于落地方向,作为广大研发同学,下面这些最佳实践可能对您更有价值:

  • 高代码覆盖率并不能保证高产品质量,但低代码覆盖率一定说明大部分逻辑没有被自动化测到。后者通常会增加问题遗留到线上的风险,当引起注意。
  • 没有普适的针对所有产品的严格覆盖率标准。实际上这更应该是业务或技术负责人基于自己的领域知识,代码模块的重要程度,修改频率等等因素,自行在团队中确定标准,并推动成为团队共识。
  • 低代码覆盖率并不可怕,能够主动去分析未被覆盖到的部分,并评估风险是否可接受,会更加有意义。实际上笔者认为,只要这一次的提交比上一次要好,都是值得鼓励的。

谷歌有篇博客(参考资料)提到,其经验表明,重视代码覆盖率的团队通常会更加容易培养卓越工程师文化,因为这些团队在设计产品之初就会考虑可测性问题,以便能更轻松的实现测试目标。而这些措施反过来会促使工程师编写更高质量的代码,更注重模块化。

最后,欢迎点击左下角详情按钮,加入七牛云 Goc 交流群,我们一起聊聊 goc,聊聊研发效能那些事。

参考资料

往期推荐

觉得不错,欢迎关注: