跳到主要内容

如何保障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并发的资料,从初学者到进阶,内容非常丰富,且权威。