跳到主要内容

构建高效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,聊聊研发效能那些事。

参考资料

往期推荐

觉得不错,欢迎关注:

我们是如何做go语言系统测试覆盖率收集的

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

工程效能领域,测试覆盖率度量总是绕不开的话题,我们也不例外。在七牛云,我们主要使用 go 语言构建云服务,在考虑系统测试覆盖率时,最早也是通过围绕原生go test -c -cover的能力来构建。这个方案,笔者还曾在 MTSC2018 大会上有过专项分享。其实我们当时已经做了不少自动化工作,能够针对很多类型的代码库,自动插桩服务,自动生成 TestMain()等方法,但随着接入项目越来越多,以及后面使用场景的不断复杂化,我们发现这套还是有其先天局限,会让后面越来越难受:

  • 程序必须关闭才能收集覆盖率。如果将这套系统仅定位在收集覆盖率数据上,这个痛点倒也能忍受。但是如果想进一步做精准测试等方向,就很受局限。
  • 因为不想污染被测代码库,我们采取了自动化的方式,在编译阶段给每个服务生成类似 main_test.go 文件。但这种方式,其最难受的地方在于 flag 的处理,要知道 go test 命令本身会调用flag.Parse 方法,所以这里需要自动化的修改源码,保证被测程序的 flag 定义,要先于 go test 调用 flag.Parse 之前。但是,随着程序自己使用 flag 姿势的复杂化,我们发现越来越难有通用方案来处理这些 flag,有点难受。
  • 受限于go test -c命令的先天缺陷,它会给被测程序注入一些测试专属的 flag,比如-test.coverprofile, -test.timeout 等等。这个是最难受的,因为它会破坏被测程序的启动姿势。我们知道系统测试面对是完整被测集群,如果你需要专门维护一套测试集群来做覆盖率收集时,就会显得非常浪费。好钢就应该用在刀刃上,在七牛云,我们倡导极客文化,追求用工程师思维解决重复问题。而作为业务效率部门,我们自己更应该走在前列。

也是因为以上的种种考量,我们内部一直在优化这一套系统,到今天这一版,我们已从架构和实现原理上完成了颠覆,能够做到无损插桩,运行时分析覆盖率,当属非常优雅。

Goc - A Comprehensive Coverage Testing System for The Go Programming Language

一图胜千言:

使用goc run .的姿势直接运行被测程序,就能在运行时,通过goc profile命令方便的得到覆盖率结果。是不是很神奇?是不是很优雅?

这个系统就是goc, 设计上希望完全兼容 go 命令行工具核心命令(go buld/install/run)。使用体验上,也希望向 go 命令行工具靠拢:

以下是goc 1.0 版本支持的功能:

系统测试覆盖率收集方案

有了 goc,我们再来看如何收集 go 语言系统测试覆盖率。整体比较简单,大体只需要三步:

  • 首先通过goc server命令部署一个服务注册中心,它将会作为枢纽服务跟所有的被测服务通信。

  • 使用goc build --center="<server>" 命令编译被测程序。goc 不会破坏被测程序的启动方式,所以你可以直接将编译出的二进制发布到集成测试环境。

  • 环境部署好之后,就可以做执行任意的系统测试。而在测试期间,可以在任何时间,通过goc profile --center="<server>"拿到当前被测集群的覆盖率结果。 是不是很优雅?

goc 核心原理及未来

goc 在设计上,抛弃老的go test -c -cover模式,而是直接与go tool cover工具交互,避免因go test命令引入的一系列弊端。goc 同样没有选择自己做插桩,也是考虑 go 语言的兼容性,以及性能问题,毕竟go tool cover工具,原生采用结构体来定义 counter 收集器,每个文件都有单独的结构体,性能相对比较可靠。goc 旨在做 go 语言领域综合性的覆盖率工具以及精准测试系统,其还有很长的路要走:

  • 基于 PR 的单测/集测/系统覆盖率增量分析
  • 精准测试方向,有一定的产品化设计体验,方便研发与测试日常使用
  • 拥抱各种 CICD 系统

当前goc 已经开源了,欢迎感兴趣的同学,前往代码仓库查看详情并 Star 支持。当然,我们更欢迎有志之士,能够参与贡献,和我们一起构建这个有意思的系统。

最后,父亲节快乐!

Contact me ?

高效测试框架推荐之Ginkgo

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

自 2015 年开始,七牛工效团队一直使用 Go 语言+Ginkgo的组合来编写自动化测试用例,积累了大约 5000+的数量。在使用和维护过程中,我们觉得 Ginkgo 的很多设计理念和功能非常赞,因此特分享给大家。

本篇不是该框架的入门指导。如果您也编写或维护过大量自动化测试用例,希望能获得一些共鸣.

BDD(Behavior Driven Development)

要说 Ginkgo 最大的特点,笔者认为,那就是对 BDD 风格的支持。比如:

	Describe("delete app api", func() {
It("should delete app permanently", func() {...})
It("should delete app failed if services existed", func() {...})

It's about expressiveness。Ginkgo 定义的 DSL 语法(Describe/Context/It)可以非常方便的帮助大家组织和编排测试用例。在 BDD 模式中,测试用例的标题书写,要非常注意表达,要能清晰的指明用例测试的业务场景。只有这样才能极大的增强用例的可读性,降低使用和维护的心智负担。

可读性这一点,在自动化测试用例设计原则上,非常重要。因为测试用例不同于一般意义上的程序,它在绝大部分场景下,看起来都像是一段段独立的方法,每个方法背后隐藏的业务逻辑也是细小的,不具通识性。这个问题在用例量少的情况下,还不明显。但当用例数量上到一定量级,你会发现,如国能快速理解用例到底是能做什么的,真的非常重要。而这正是 BDD 能补足的地方。

不过还是要强调,Ginkgo 只是提供对 BDD 模式的支持,你的用例最终呈现的效果,还是依赖你自己的书写。

进程级并行,稳定高效

相应的我们知道,BDD 框架,因为其 DSL 的深度嵌套支持,会存在一些共享上下文的资源,如此的话想做线程级的并发会比较困难。而 Ginkgo 巧妙的避开了这个问题,它通过在运行时,运行多个被测服务的进程,来达到真正的并行,稳定性大大提高。其使用姿势也非常简单,ginkgo -p命令就可以。在实践中,我们通常使用 32 核以上的服务器来跑集测,执行效率非常高。

这里有个细节,Ginkgo 虽然并行执行测试用例,但其输出的日志和测试报告格式,仍然是整齐不错乱的,这是如何做到的呢?原来,通过源码会发现,ginkgo CLI 工具在并行跑用例时,其内部会起一个监听随机端口的本地服务器,来做不同进程之间的消息同步,以及日志和报告的聚合工作,是不是很巧妙?

其他的一些 Tips

Ginkgo 框架的功能非常强大,对常见测试场景的都有比较好的支持,即使是一些略显复杂的场景,比如:

  • 在平时的代码中,我们经常会看到需要做异步处理的测试用例。但是这块的逻辑如果处理不好,用例可能会因为死锁或者未设置超时时间而异常卡住,非常的恼人。好在 Ginkgo 专门提供了原生的异步支持,能大大降低此类问题的风险。类似用法:

    It("should post to the channel, eventually", func(done Done) {
    c := make(chan string, 0)
    go DoSomething(c)
    Expect(<-c).To(ContainSubstring("Done!"))
    close(done)
    }, 0.2)
  • 针对分布式系统,我们在验收一些场景时,可能需要等待一段时间,目标结果才生效。而这个时间会因为不同集群负载而有所不同。所以简单的硬编码来 sleep 一个固定时间,很明显不合适。这种场景下若是使用 Ginkgo 对应的 matcher 库GomegaEventually功能就非常的贴切,在大大提升用例稳定性的同时,最大可能的减少无用的等待时间。

  • 笔者一直认为,自动化测试用例不应该仅仅是 QA 手中的工具,而应该尽可能多的作为业务验收服务,输出到 CICD,灰度验证,线上验收等尽可能多的场景,以服务于整个业务线。同样利用 Ginkgo 我们可以很容易做到这一点:

    • CICD: 在定义 suite 时,使用RunSpecWithDefaultReporters方法,可以让测试结果既输出到 stdout,还可以输出一份 Junit 格式的报告。这样就可以通过类似 Jenkins 的工具方便的呈现测试结果,而不用任何其他的额外操作。
    • TaaS(Test as a Service): 通过ginkgo build或者原生的go test -c命令,可以方便的将测试用例,编译成 package.test 的二进制文件。如此的话,我们就可以方便的进行测试服务分发。典型的,如交付给 SRE 同学,辅助其应对线上灰度场景下的测试验收。所以在测试用例的组织上,这里有个小建议,过往我会看到有同学会习惯一个目录就定义一个 suite 文件,这样编译出的二进制文件就非常多,不利于分发。所以建议不要定义太多的 suite,可以一条产品就一个 suite 入口,其他的用例包通过_导入进来。比如:

另外,值得说道的是,Ginkgo 框架在提供强大功能和灵活性的同时,有些地方也需要使用者特别留心:

  • DescribeTable功能是对 TableDriven 模式的友好支持,但它的原理是通过Entry在用例执行之前,通过反射机制来自动生成It方法,所以如果期望类似BeforeEach+It的原生组合来使用BeforeEach+Entry的话,可能在值类型的变量传递上,会不符合预期。其实,相较于DescribeTable+Entry的模式,我个人更倾向于通过方法+多个It的原生组合来写用例,虽然代码量显得有点多,但是用例表达的逻辑主题会更清晰,可读性较高。类似如下:
  • Ginkgo CLI 的 focus 和 skip 命令非常好用,能够灵活的指定想执行或者排除的测试用例。不过要注意的是,focus 和 skip 传入的是正则表达式,而适配这个正则的,是组成用例的所有的 Container 标题的组合(Suite+Describe+Context+It), 这些标题从外到里拼接成的完整字符串,所以使用时当注意。

都有谁在用 Ginkgo?

Ginkgo 的官方文档非常详细,非常利于使用。另外,我们看到著名的容器云项目Kubernetes也是使用 Ginkgo 框架来编写其 e2e 测试用例。

最后,如果您也使用 Go 语言来编写测试用例,不妨尝试下 Ginkgo。

性能测试必知必会

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

说到性能测试,我们到底是想谈论什么?

任何做产品的,都希望自己家的产品,品质优,性能好,服务海量用户,还不出问题。

任何使用产品的,都喜欢自己购买的产品功能全,性能优,不花一分冤枉钱。

不过理想很丰满,现实很骨感。实际产品的性能与开发周期,部署方式,软硬件性能等都息息相关。所以真正提到做性能测试的场景,多数是为满足特定需求而进行的度量或调优。

比如:

  • 针对交付客户的软硬件环境,提供性能测试报告,证明对客户需求的满足
  • 针对特定的性能瓶颈,进行针对性测试,为问题定位提供帮助
  • 重大功能迭代,架构设计上线前的性能评估

所有的这些场景,都隐含着对性能测试目标的确认,这一点非常重要。因为如果没有明确的测试目标,为了做而做,多数情况是没有价值的,浪费精力。

而性能测试的目标一般是期望支持的目标用户数量,负载,QPS 等等,这些信息一般可以从业务负责人或者产品经理处获得。当然如果有实际的业务数据支持,也可以据此分析得出。所以在开展性能测试之前,一定要先搞清楚测试目标

目标明确之后,如何开展性能测试?

有了性能测试目标,之后还需要进一步拆解,做到具体可执行。根据经验,个人认为性能测试的执行,最终会落地到以下两个场景:

  • 在特定硬件条件,特定部署架构下,测试系统的最大性能表现
  • 在相同场景,相同硬件配置下,与竞品比较,与过往分析,总结出优劣

不同的目的,做事的方式也不一样。

第一类场景,因为结果的不确定性,测试时需要不断的探索测试矩阵,找出尽可能优的结果。

第二类场景,首先需要理清楚,业界同类产品,到底比的是什么,相应的测试工具是什么,测试方法是什么。总之要在公平公正的条件下,遵循业界标准,得出测试结果,给出结论。

所有的性能测试场景,都需要有明确的分析与结论,以支持上述两个场景下的目的达成。测试场景要贴近实际的目标场景,测试数据要贴近实际的业务数据,最好就用目标业务场景下的数据来进行性能测试。

服务端性能测试到底要看哪些指标?

不同的领域,业务形态,可能关注的性能指标是不一样的,所以为了表述精确,我们这里只谈服务端的性能测试指标。

一般我们会用以下指标来衡量被测业务: QPS, 响应时间(Latency), 成功率,吞吐率,以及服务端的资源利用率(CPU/Memory/IOPS/句柄等)。

不过,这里有一些常识需要明确:

  • 响应时间不要用平均值,要用百分值。比如常见的,98 值(98th percentile)表示。
  • 成功率是性能数据采集标准的前提,在成功率不足的情况下,其他的性能数据是没意义的(当然这时候可以基于失败请求来分析性能瓶颈)。
  • 单独说 QPS 不够精确,而应结合响应时间综合来看。比如 "在响应时间 TP98 都小于 100ms 情况下,系统可以达到 10000qps" 这才有意义。
  • 性能测试一定要持续一定时间,在确保被测业务稳定的情况下,测出的数据才有意义。

要多体会下这些常识,实战中很多新手对这块理解不深,导致有时出的性能数据基本是无效的。

为什么性能测试报告一定要给出明确的软硬件配置,以及部署方式?

前面说到,性能数据是与软件版本,硬件配置,部署方式等息息相关的。每一项指标的不同,得出的数据可能是天差万别。所以在做性能测试时,一定要明确这些基础前置条件,且在后期的性能测试报告中,清晰的说明。

jmeter, ab, wrk, lotust, k6 这么多性能测试工具,我应该选择哪个?

业界性能测试数据工具非常多,不过适用的场景,以及各自特点会有不同。所以针对不同的性能测试需求,应当选择合适的性能工具。比如:

  • jmeter: 主要提供图形化操作以及录制功能,入门简单,功能也较强大。缺点是需要额外安装。
  • ab(apech benchmark): 简单好用,且一般系统内置了,应对简单场景已足够
  • lotust:简单好用,支持 python 编写自定义脚本,支持多 worker,图形化界面汇总性能数据。

这里不一一介绍工具,大家有兴趣的都可以自行去网上搜索。

其实笔者在实践过程中发现,其实绝大多数性能测试场景,都需要编码实现。所以如何优雅的结合现有的测试代码,环境,以及基础设施,来方便的进行性能测试反而是个可以考量的点。

笔者比较认可 Go+Prometheus+Kubernetes 的模式。首先 go 语言因其独有的并发模式,上手简单等特点,在云服务,服务端程序领域使用已经非常广了,采用其写脚本,也许与被测程序天然紧密结合。且服务端程序要想很好的运维,必然有一套完整的监控告警体系,而 Prometheus 基本是其中热度最高的,使用范围最广的,同时我们也可以将测试程序性能数据打点到 Prometheus,这样在计算 QPS,成功率等指标上,非常方便。

另外大家知道,在性能测试时,多数需要不断的调整 metrix,比如并发数,worker 数量等,来探测系统的性能表现,这时候如果将测试程序跑在 Kubernetes 上,就可以借助其能力,比如 Deployment,灵活的部署和水平扩展,体验相当优雅。

单机 10000 并发为什么可能不靠谱?

我们知道使用 goroutine,可以瞬间开很多并发,非常好用。于是可能就会有同学觉得用它做性能测试很方便,直接写个脚本,起超多的并发,去做性能测试。但这样真的靠谱吗?

虽然 go 语言的并发,通过 P,G,M 模型,在调度 goroutine 时,比较高效,但无论如何,任何的程序执行,最终消耗的都是系统资源,测试脚本也同样。所以单机上执行的并发效果,最终会受限于,你脚本的复杂程序,也就是对 CPU,IO,网络等系统资源的消耗。所以,并不是并发越多越好,一定是基于实际环境,通过不断调节并发数量,worker 数量等,来达到最佳姿势。

构建业务性能数据的持续可观测性对产品质量意义重大

一次专项性的性能分析,可以观察当前业务的性能表现,进一步的分析性能瓶颈,为之后的改进提供帮助,意义挺大。但只这样可能不够全面,因为指不定的某次迭代,句柄没关,goutinue 泄露,就会造成性能问题,如果我们没有常态化的检测手段,等上线后才发现,很明显不是我们想看到的。

所以更优雅的做法是,将性能测试常态化的持续运营,甚至可以做到每次 PR 触发,都自动执行性能测试,检测性能问题。

To-Be-Continued

性能测试对保障产品质量,提升用户体验意义重大。笔者这里只罗列了一些个人在实际工作中看的问题,以及一些体会,可能不全面。所以如果您有问题,欢迎抛出来,共同探讨。

参考资料

如何保障Go语言基础代码质量

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

为什么要谈这个 topic?

实践中,质量保障体系的建设,主要针对两个目标: 一是不断提高目标业务测试覆盖率,保障面向客户的产品质量;二就是尽可能的提高人效,增强迭代效率。而构建全链路质量卡点就是整个体系建设的核心手段。笔者用下图来描述这整个链路:

可以看到,虽然保障业务迭代的方向性正确排在最前面,但在具体操作上,这一步需要的是强化流程规范和构建企业文化,同时对各负责人技能培训,可以说多数是软技能。而保障基础代码质量环节发力于自动化建设链路之始,是可以通过技术手段来消灭潜在的质量问题,所以构建好的话能极大的降低心智负担,非常值得关注。

我们都知道,代码的好坏会直接影响到业务质量,团队协作,以及后期技术债等。有一个经典的图来描述代码质量的好坏,当能深切表达程序员的内心:

而同时我们相信,绝大部分程序员都有追求卓越的初心,且会尽可能的在自己能力范围内编写高质量的代码。

但是,保障基础代码质量光靠程序员的个人素质一定是不全面,是人就会犯错,可能会疏忽。我们最需要的是一种自动化的机制来持续确保不出问题。这也是自动化的魅力,一次构建,持续收获价值。

此类工具在业界一般叫 linter,不同的语言有不同的实现。本文主要探究 Go 语言相关的。 在介绍相关工具之前,我们先看看几个经典的代码坏味道: 这段代码常规运行不会有问题,但是在一些场景下循环执行,那可能就会有问题了, 我们来看看: (注:ex2 是上述代码编译出的可执行文件名字)

很明显,有句柄泄露。原因也很简单,http response 的 body 没有关闭。但这个关闭语句,一不注意也容易写错:

这时候如果百度挂了,上述程序程序就会因为空指针引用,造成非预期的 panic,非常的不优雅。所以正确的做法应该是在 err 判断之后再行关闭 body(关于 Client.Do 具体的各种限制,大家可以参考这里: https://golang.org/pkg/net/http/#Client.Do)

如此种种,此类小问题在实际编码活动中非常常见,且不容易一眼看出问题。甚至常规的测试可能也难检测出来,可谓非常棘手。好在 Go 语言的开发者们为我们想到了这一点,内置工具链中的 vet 命令,就能方便的检测到很多类似的问题。

还比如下面的代码场景,我在实际的测试用例和业务代码都看到过:

go vet 可以很容易检测出这个问题(其他 vet 功能,可以参考这里: https://golang.org/cmd/vet/)。

go 的工具链中,还有一个不得不提,那就是大名鼎鼎的 go fmt,其了却了其他语言经常陷入的代码风格之争,是 Go 语言生态构建非常巧妙的地方。另外 golint 也是 google 主推的 go 语言代码代码风格工具,虽非强制,但强烈建议新项目适用。

Go linters 业界现状

上面主要说到 Go 工具链的内置工具,还有一些非官方的工具也比较有名,比如 staticcheck, errcheck在 github 上 Star 都较多。此类工具有个专门的的 github 库,收集的比较全,参见 awesone-static-analysis

同时还有些项目旨在聚合此类工具,提供更方便的使用方式,以及一些酷炫的产品化。比如golangci-lint, 其衍生的商业化项目,可以自动针对 github PR 做代码审核,对有问题的地方自动 comments,比较有意思。

如何才能优雅的落地 linter 检查?

linter 工具必须为产品质量服务,不然就是做无用功。实践中,我们应该思考的是如何才能优雅的落地 linter 检查,如何才能建立有效的质量卡点。

推荐针对 PR,做代码检查,保障入库代码质量。基于 PR 做事情是我比较看好的,因为这是调动所有研发力量,天然契合的地方。且进一步讲,这也是测试基础设施更能体现价值的地方。

目前 Github 上有很多这方面的集成系统做的都比较好,能够快速的帮我们落地 PR 测的检查,比如 Travis, Circle CI 等。另外就是著名的 Kubernetes 社区,也自行构建了强大的 Prow 系统,其不光是基于 CICD 系统,还构建了 chat ops 模式,为参与 Kubernetes 的社区的贡献者提供了方便。

细看 Kubernetes 库,会发现,其会针对每个 PR 都做如下静态检查:

Kubernetes 只利用了官方的几款工具, 在检测准确性上比较有保障。有了这些检查点,也能倒逼研发人员关注提交代码的质量,会迫使其在本地或者 IDE 上就配置好检查,确保每次提交的 PR 都能通过检查,不浪费 CI 资源。这也是合格工程师的基本要求。

总结

高质量的代码是业务质量保障的基础。而编写高质量的代码是技术问题,同时也应该是企业文化问题。因为当大家都开始注重技术,注重代码质量时,自然会朝着精益求精的路上行进,视糟糕的代码为仇寇。

我的一位老板跟我说过,要做就做 Number One。而在没达到第一的时候,那就要向业界标杆看齐,比如 Netflix,Google,Facebook 等。当大家都非常注重自己代码质量时,工程师才有时间去关注解决更加系统性的问题,而不用一直在 Low Level 徘徊。笔者深以为然。

如何负责一个项目的质量保证工作

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

问题

通常,我在面试测试相关候选人时,除了技术等硬性标准外,我还非常希望候选人回答这么一个问题 ——如果让你负责一个项目的质量保证工作,你会怎么做?

之所以问这么个问题,主要是想考察候选人在过往的经历中,有没有全局性的思考如何把控一个项目的质量状况;有没有对自己日常的工作有个清晰的认识,甚或者有没有观察过你的 leader 或经理,他们是如何带项目的。这是个开放性的问题,不同行业,不同公司背景下的 QA 人员,得出的认识,可能会有不同。这里,我将谈谈我的理解。

从项目的一般生命周期说起

很多候选人听到我这个问题,一般会从项目的生命周期说起,将焦点聚焦在测试人员及其工作本身上。

比如会谈到测试人员要参与需求评定,充分理解需求。之后还要设计测试用例以及用例评审。最后就是基于用例做最后的验收测试。基于此,部分同学还会提到,需要做的测试种类,比如功能测试,性能测试。做移动端的同学还会提到各版本,各机型的兼容性测试等等。

这种说法确实没错,测试人员做好了这些工作,很大程度上会保障好项目质量。但通常这种模式,比较倾向于传统 QA,容易变成研发的下游。且实际表明,这种模式对 QA 人力有一定的要求。太少了,工作就开展不起来。按我观察到的现象来看,这种模式下,开发测试比,一般可以达到 2:1 甚或者 1.5:1.

很明显这种比例对创业公司来说太高了,创业公司一般追求的是极致的投入,以及更加极致的产出。而传统意义上,测试的产出却并不是那么明显。所以在追求质量保证的道路上,我们需要考虑是否还有其他道路呢?

影响项目的质量因素

仔细思考上面的描述,你会发现候选人默认将项目质量聚焦在测试人员身上, 而非项目本身。但做项目是个系统工程,涉及到的是方方面面。所以这里,我们不妨放大关注点,先不把目光局限在测试人员身上,而是考虑下这个问题的实质——影响项目的质量因素到底有哪些?

正所谓,过程决定结果。所以我认为做好过程质量,会让我们在追求项目质量的道路上事半功倍!

从过程质量出发,我将质量保证工作,简要的划分为下面几个环节,如图:

研发质量

研发阶段是项目最重要的时期,代表着一个项目从无到有,从 1 到 100 的研发及逐渐迭代的过程。做好这个阶段的质量保证工作,其正面意义毋庸置疑。

我推荐将这个阶段的工作按分层模式来搞,从最初的代码检查,到最终的 e2e 测试,性能测试等,全方位,立体化来逐渐保卫产品质量。这里的每一项工作都不是独立的。而应该按照持续集成,流水线的模式,对每一次的代码改动进行筛查和测试

测试同学这阶段的目标应该是保证这条流水线的畅通,以及部分测试工作的完善,比如测试框架,e2e 等。但不是说这里的每一项工作都要有测试同学来搞。而应该尽可能的发动开发和测试一起来协作。这样才会得到更高效率。

上线质量

也就是发布环节的产品质量保证。之所以把这个单拎出来,主要是面向服务端程序来说。因为这个过程是产品代码从研发到线上,真正面对用户的分水岭。这个环节处理不好,就很容易出问题。这里我将这个阶段,影响质量的因素,主要归结为版本控制,配置控制,以及上线流程三个方面,需要测试人员着重关注。当然,有同学会说,在我们公司,几个因素主要是运维部门在负责,但是测试作为质量监察者,和布道师,同样应时刻关注,且针对其中的问题或薄弱环节,着力推动和解决相关事宜。总之,项目质量相关的问题,QA 都应该有义务关注。

特别的,QA 在这个阶段最好能产出,或者协助产出,线上功能的冒烟测试集,以方便做发布后的及时验证。

线上质量

产品上线或者交付了,并不代表质量工作的完结,我们还应该时刻关注用户对产品的反馈。

应该定期组织线上 bug 分析,研究如何做才能避免这类 bug 的遗漏。对于线上事故,更要慎重对待,最好能对每一粒事故都给出测试端的改进。

还有一点可能大家比较忽视的就是,产品使用姿势分析。这一方面,虽然通常有专门部分来分析,但是如果有可能,我们同样应该关注,用户是如何使用我们产品的。这对我们在测试策略的制定上,非常具有指导意义。

对 QA 同学的技能要求

通过上面的分析,你会发现,要想做好这些工作,需要对 QA 同学提出更高的要求。

首先,技术要过关。在七牛,我们要求测试同学在技术上与开发并无二致。只有这样,你在质量布道和流程改进时,才会与开发同学产生更多的共鸣。同时,你还需要有一定的沟通技巧,和项目管理能力。测试同学面对是整个团队,要能适应每一位人员。在平时的技术沟通,需求讨论时,高效应对,维护好良好的人际关系,以方便后续工作的开展。但同时也要有全局意识,坚守质量底线,把控各个环节,防止出现质量漏洞。对质量工作的如何开展要有清晰的认识,不能被带偏。

篇后语

很多次,候选人都会问我,你们是手动测试多还是自动化测试多。我都会给他们强调,测试是对质量负责,不管是手动还是自动,都只是一种手段,依赖于测试人员的技术水平。我们希望所有的测试同学,都应该是以测试开发为标准,以质量布道为方向。用 owner 精神,做好整个项目的质量保证工作。

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 框架基础上抽取了最小集合。以方便快速使用。

是不是很贴心?

Go并发编程实践

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

前言

并发编程一直是Golang区别与其他语言的很大优势,也是实际工作场景中经常遇到的。近日笔者在组内分享了我们常见的并发场景,及代码示例,以期望大家能在遇到相同场景下,能快速的想到解决方案,或者是拿这些方案与自己实现的比较,取长补短。现整理出来与大家共享。

简单并发场景

很多时候,我们只想并发的做一件事情,比如测试某个接口的是否支持并发。那么我们就可以这么做:

func RunScenario1() {
count := 10
var wg sync.WaitGroup

for i := 0; i < count; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
doSomething(index)
}(i)
}

wg.Wait()
}

使用goroutine来实现异步,使用WaitGroup来等待所有goroutine结束。这里要注意的是要正确释放WaitGroup的counter(在goroutine里调用Done()方法)。

但此种方式有个弊端,就是当goroutine的量过多时,很容易消耗完客户端的资源,导致程序表现不佳。

规定时间内的持续并发模型

我们仍然以测试某个后端API接口为例,如果我们想知道这个接口在持续高并发情况下是否有句柄泄露,这种情况该如何测试呢?

这种时候,我们需要能控制时间的高并发模型:

func RunScenario2() {
timeout := time.Now().Add(time.Second * time.Duration(10))
n := runtime.NumCPU()

waitForAll := make(chan struct{})
done := make(chan struct{})
concurrentCount := make(chan struct{}, n)

for i := 0; i < n; i++ {
concurrentCount <- struct{}{}
}

go func() {
for time.Now().Before(timeout) {
<-done
concurrentCount <- struct{}{}
}

waitForAll <- struct{}{}
}()

go func() {
for {
<-concurrentCount
go func() {
doSomething(rand.Intn(n))
done <- struct{}{}
}()
}
}()

<-waitForAll
}

上面的代码里,我们通过一个buffered channel来控制并发的数量(concurrentCount),然后另起一个channel来周期性的发起新的任务,而控制的条件就是 time.Now().Before(timeout),这样当超过规定的时间,waitForAll 就会得到信号,而使整个程序退出。

这是一种实现方式,那么还有其他的方式没?我们接着往下看。

基于大数据量的并发模型

前面说的基于时间的并发模型,那如果只知道数据量很大,但是具体结束时间不确定,该怎么办呢?

比如,客户给了个几TB的文件列表,要求把这些文件从存储里删除。再比如,实现个爬虫去爬某些网站的所有内容。

而解决此类问题,最常见的就是使用工作池模式了(Worker Pool)。以删文件为例,我们可以简单这样来处理:

  • Jobs - 可以从文件列表里读取文件,初始化为任务,然后发给worker
  • Worker - 拿到任务开始做事
  • Collector - 收集worker处理后的结果
  • Worker Pool - 控制并发的数量

虽然这只是个简单Worker Pool模型,但已经能满足我们的需求:

func RunScenario3() {
numOfConcurrency := runtime.NumCPU()
taskTool := 10
jobs := make(chan int, taskTool)
results := make(chan int, taskTool)
var wg sync.WaitGroup

// workExample
workExampleFunc := func(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
res := job * 2
fmt.Printf("Worker %d do things, produce result %d \n", id, res)
time.Sleep(time.Millisecond * time.Duration(100))
results <- res
}
}

for i := 0; i < numOfConcurrency; i++ {
wg.Add(1)
go workExampleFunc(i, jobs, results, &wg)
}

totalTasks := 100 // 本例就要从文件列表里读取

wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < totalTasks; i++ {
n := <-results
fmt.Printf("Got results %d \n", n)
}
close(results)
}()

for i := 0; i < totalTasks; i++ {
jobs <- i
}
close(jobs)
wg.Wait()
}

在Go里,分发任务,收集结果,我们可以都交给Channel来实现。从实现上更加的简洁。

仔细看会发现,本模型也是适用于按时间来控制并发。只要把totalTask的遍历换成时间控制就好了。

等待异步任务执行结果

goroutine和channel的组合在实际编程时经常会用到,而加上Select更是无往而不利。

func RunScenario4() {
sth := make(chan string)
result := make(chan string)
go func() {
id := rand.Intn(100)
for {
sth <- doSomething(id)
}
}()
go func() {
for {
result <- takeSomthing(<-sth)
}
}()

select {
case c := <-result:
fmt.Printf("Got result %s ", c)
case <-time.After(time.Duration(30 * time.Second)):
fmt.Errorf("指定时间内都没有得到结果")
}
}

在select的case情况,加上time.After()模型可以让我们在一定时间范围内等待异步任务结果,防止程序卡死。

定时反馈异步任务结果

上面我们说到持续的压测某后端API,但并未实时收集结果。而很多时候对于性能测试场景,实时的统计吞吐率,成功率是非常有必要的。

func RunScenario5() {
concurrencyCount := runtime.NumCPU()
for i := 0; i < concurrencyCount; i++ {
go func(index int) {
for {
doUploadMock()
}
}(i)
}

t := time.NewTicker(time.Second)
for {
select {
case <-t.C:
// 计算并打印实时数据
}
}
}

这种场景就需要使用到Ticker,且上面的Example模型还能控制并发数量,也是非常实用的方式。

知识点总结

上面我们共提到了五种并发模式:

  • 简单并发模型
  • 规定时间内的持续并发模型
  • 基于大数据量的持续并发模型
  • 等待异步任务结果模型
  • 定时反馈异步任务结果模型

归纳下来其核心就是使用了Go的几个知识点:Goroutine, Channel, Select, Time, Timer/Ticker, WaitGroup. 若是对这些不清楚,可以自行Google之。

另完整的Example 代码可以参考这里:https://github.com/jichangjun/golearn/blob/master/src/carlji.com/experiments/concurrency/main.go

使用方式: go run main.go <场景>

比如 :

参考文档

这篇是Google官方推荐学习Go并发的资料,从初学者到进阶,内容非常丰富,且权威。