跳到主要内容

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

· 阅读需 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,聊聊研发效能那些事。

参考资料

往期推荐

觉得不错,欢迎关注:

我们是如何做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 精神,做好整个项目的质量保证工作。