Serverless的微服务持续交付案例

本文是GitChat《Serverless 微服务的持续交付》部分内容已做修改。文章聊天实录请见:“顾宇:Serverless 微服务的持续交付解析

Serverless 风格微服务的持续交付(上):架构案例”中,我们介绍了一个无服务器风格的微服务的架构案例。这个案例中混合了各种风格的微服务。

架构图如下:

在这个架构中,我们采用了前后端分离的技术。我们把 HTML,JS, CSS 等静态内容部署在 S3 上,并通过 CloudFront 作为 CDN 构成了整个架构的前端部分。我们把 Amazon API Gateway 作为后端的整体接口连接后端的各种风格的微服务,无论是运行在 Lambda 上的函数,还是运行在 EC2 上的 Java 微服务,他们整体构成了这个应用的后端部分。

从这个架构图上我们可以明显的看到 前端(Frontend)和后端(Backend)的区分。

持续部署流水线的设计和实现

任何 DevOps 部署流水线都可以分为三个阶段:待测试,待发布,已发布

由于我们的架构是前后端分离的,因此我们为前端和后端分别构造了两条流水线,使得前后端开发可以独立。如下图所示:

(整体流水线)

在这种情况下,前端团队和后端团队是两个不同的团队,可以独立开发和部署,但在发布的时候则有些不同。由于用户是最后感知功能变化的。因此,为了避免界面报错找不到接口,在新增功能的场景下,后端先发布,前端后发布。在删除功能的场景下,前端先发布,后端后发布。

我们采用 Jenkins 构建我们的流水线,Jenkins 中已经含有足够的 AWS 插件可以帮助我们完成整个端到端的持续交付流水线。

前端流水线

前端持续交付流水线如下所示:

前端流水线的各步骤过程如下:

  1. 我们采用 BDD/ATDD 的方式进行前端开发。用 NightWatch.JS 框架做 端到端的测试,mochachai 用于做某些逻辑的验证。
  2. 我们采用单代码库主干(develop 分支)进行开发,用 master 分支作为生产环境的部署。生产环境的发布则是通过 Pull Request 合并的。在合并前,我们会合并提交。
  3. 前端采用 Webpack 进行构建,形成前端的交付产物。在构建之前,先进行一次全局测试。
  4. 由于 S3 不光可以作为对象存储服务,也可以作为一个高可用、高性能而且成本低廉的静态 Web 服务器。所以我们的前端静态内容存储在 S3 上。每一次部署都会在 S3 上以 build 号形成一个新的目录,然后把 Webpack 构建出来的文件存储进去。
  5. 我们采用 Cloudfront 作为 CDN,这样可以和 S3 相互集成。只需要把 S3 作为 CDN 的源,在发布时修改对应发布的目录就可以了。

由于我们做到了前后端分离。因此前端的数据和业务请求会通过 Ajax 的方式请求后端的 Rest API,而这个 Rest API 是由 Amazon API Gateway 通过 Swagger 配置生成的。前端只需要知道 这个 API Gateway,而无需知道API Gateway 的对应实现。

后端流水线

后端持续交付流水线如下所示:

后端流水线的各步骤过程如下:

  1. 我们采用“消费者驱动的契约测试”进行开发,先根据前端的 API 调用构建出相应的 Swagger API 规范文件和示例数据。然后,把这个规范上传至 AWS API Gateway,AWS API Gateway 会根据这个文件生成对应的 REST API。前端的小伙伴就可以依据这个进行开发了。
  2. 之后我们再根据数据的规范和要求编写后端的 Lambda 函数。我们采用 NodeJS 作为 Lambda 函数的开发语言。并采用 Jest 作为 Lambda 的 TDD 测试框架。
  3. 和前端一样,对于后端我们也采用单代码库主干(develop 分支)进行开发,用 master 分支作为生产环境的部署。
  4. 由于 AWS Lambda 函数需要打包到 S3 上才能进行部署,所以我们先把对应的构建产物存储在 S3 上,然后再部署 Lambda 函数。
  5. 我们采用版本化 Lambda 部署,部署后 Lambda 函数不会覆盖已有的函数,而是生成新版本的函数。然后通过别名(Alias)区分不同前端所对应的函数版本。默认的 $LATEST,表示最新部署的函数。此外我们还创建了 Prod,PreProd, uat 三个别名,用于区分不同的环境。这三个别名分别指向函数某一个发布版本。例如:函数 func 我部署了4次,那么 func 就有 4个版本(从1开始)。然后,函数 func 的 $LATEST 别名指向 4 版本。别名 PreProd 和 UAT 指向 3 版本,别名 Prod 在 2 版本。
  6. 技术而 API 的部署则是修改 API Gateway 的配置,使其绑定到对应版本的函数上去。由于 API Gateway 支持多阶段(Stage)的配置,我们可以采用和别名匹配的阶段绑定不同的函数。
  7. 完成了 API Gateway 和 Lamdba 的绑定之后,还需要进行一轮端到端的测试以保证 API 输入输出正确。
  8. 测试完毕后,再修改 API Gateway 的生产环境配置就可以了。

部署的效果如下所示:

(API Gateway + Lambda 配置)

无服务器微服务的持续交付新挑战

在实现以上的持续交付流水线的时候,我们踩了很多坑。但经过我们的反思,我们发现是云计算颠覆了我们很多的认识,当云计算把某些成本降低到趋近于 0 时。我们发现了以下几个新的挑战:

  1. 如果你要 Stub,有可能你走错了路。
  2. 测试金字塔的倒置。
  3. 你不再需要多个运行环境,你需要一个多阶段的生产环境 (Multi-Stage Production)。
  4. 函数的管理和 Nanoservice 反模式。

Stub ?别逗了

很多开发者最初都想在本地建立一套开发环境。由于 AWS 多半是通过 API 或者 CloudFormation 操作,因此开发者在本地开发的时候对于AWS 的外部依赖进行打桩(Stub) 进行测试,例如集成 DynamoDB(一种 NoSQL 数据库),当然你也可以运行本地版的 DynamoDB,但组织自动化测试的额外代价极高。然而随着微服务和函数规模的增加,这种管理打桩和构造打桩的虚拟云资源的代价会越来越大,但收效却没有提升。另一方面,往往需要修改几行代码立即生效的事情,却要执行很长时间的测试和部署流程,这个性价比并不是很高。

这时我们意识到一件事:如果某一个环节代价过大而价值不大,你就需要思考一下这个环节存在的必要性。

由于 AWS 提供了很好的配置隔离机制,于是为了得到更快速的反馈,我们放弃了 Stub 或构建本地 DynamoDB,而是直接部署在 AWS 上进行集成测试。只在本地执行单元测试,由于单元测试是 NodeJS 的函数,所以非常好测试。

另外一方面,我们发现了一个有趣的事实,那就是:

测试金字塔的倒置

由于我们采用 ATDD 进行开发,然后不断向下进行分解。在统计最后的测试代码和测试工作量的的时候,我们有了很有趣的发现:

  • End-2-End (UI)的测试代码占30%左右,占用了开发人员 30% 的时间(以小时作为单位)开发和测试。
  • 集成测试(函数、服务和 API Gateway 的集成)代码占 45%左右,占用了开发人员60% 的时间(以小时作为单位)开发和测试。
  • 单元测试的测试代码占 25%左右,占用了10%左右的时间开发和测试。

一开始我们以为我们走入了“蛋筒冰激凌反模式”或者“纸杯蛋糕反模式”但实际上:

  1. 我们并没有太多的手动测试,绝大部分自动化。除了验证手机端的部署以外,几乎没有手工测试工作量。
  2. 我们的自动化测试都是必要的,且没有重复。
  3. 我们的单元测试足够,且不需要增加单元测试。

但为什么会造成这样的结果呢,经过我们分析。是由于 AWS 供了很多功能组件,而这些组件你无需在单元测试中验证(减少了很多 Stub 或者 Mock),只有通过集成测试的方式才能进行验证。因此,Serverless 基础设施大大降低了单元测试的投入,但把这些不同的组件组合起来则劳时费力 。如果你有多套不一致的环境,那你的持续交付流水线配置则是很困难的。因此我们意识到:

你不再需要多个运行环境,你只需要一个多阶段的生产环境 (Multi-Stage Production)

通常情况下,我们会有多个运行环境,分别面对不同的人群:

  1. 面向开发者的本地开发环境
  2. 面向测试者的集成环境或测试环境(Test,QA 或 SIT)
  3. 面向业务部门的测试环境(UAT 环境)
  4. 面向最终用户的生产环境(Production 环境)

然而多个环境带来的最大问题是环境基础配置的不一致性。加之应用部署的不一致性。带来了很多不可重现问题。在 DevOps 运动,特别是基础设施即代码实践的推广下,这一问题得到了暂时的缓解。然而无服务器架构则把基础设施即代码推向了极致:只要能做到配置隔离和部署权限隔离,资源也可以做到同样的隔离效果。

我们通过 DNS 配置指向了同一个的 API Gateway,这个 API Gateway 有着不同的 Stage:我们只有开发(Dev)和生产(Prod)两套配置,只需修改配置以及对应 API 所指向的函数版本就可完成部署和发布。

然而,多个函数的多版本管理增加了操作复杂性和配置性,使得整个持续交付流水线多了很多认为操作导致持续交付并不高效。于是我们在思考:

对函数的管理和“ Nanoservices 反模式 ”

根据微服务的定义,AWS API Gateway 和 Lambda 的组合确实满足 微服务的特征,这看起来很美好。就像下图一样:

但当Lambda 函数多了,管理众多的函数的发布就成为了很高的一件事。而且, 可能会变成“Nanoservice 反模式”:

Nanoservice is an antipattern where a service is too fine-grained. A nanoservice is a service whose overhead (communications, maintenance, and so on) outweighs its utility.

如何把握微服务的粒度和函数的数量,就变成了一个新的问题。而 Serverless Framework ,就是解决这样的问题的。它认为微服务是由一个多个函数和相关的资源所组成。因此,才满足了微服务可独立部署可独立服务的属性。它把微服务当做一个用于管理 Lambda 的单元。所有的 Lambda 要按照微服务的要求来组织。Serverless Framework 包含了三个部分:

  1. 一个 CLI 工具,用于创建和部署微服务。
  2. 一个配置文件,用于管理和配置 AWS 微服务所需要的所有资源。
  3. 一套函数模板,用于让你快速启动微服务的开发。

此外,这个工具由 AWS 自身推广,所以兼容性很好。但是,我们得到了 Serverless 的众多好处,却难以摆脱对 AWS 的依赖。因为 AWS 的这一套架构是和别的云平台不兼容的。

所以,这就又是一个“自由的代价”的问题。

CloudNative 的持续交付

在实施 Serverless 的微服务期间,发生了一件我认为十分有意义的事情。我们客户想增加一个很小的需求。我和两个客户方的开发人员,客户的开发经理,以及客户业务部门的两个人要实现一个需求。当时我们 6 个人在会议室里面讨论了两个小时。讨论两个小时之后我们不光和业务部门定下来了需求(这点多么不容易),与此同时我们的前后端代码已经写完了,而且发布到了生产环境并通过了业务部门的测试。由于客户内部流程的关系,我们仅需要一个生产环境发布的批准,就可以完成新需求的对外发布!

在这个过程中,由于我们没有太多的环境要准备,并且和业务部门共同制定了验收标准并完成了自动化测试的编写。这全得益于 Serverless 相关技术带来的便利性。

我相信在未来的环境,如果这个架构,如果在线 IDE 技术成熟的话(由于 Lambda 控制了代码的规模,因此在线 IDE 足够),那我们可以大量缩短我们需求确定之后到我功能上线的整体时间。

通过以上的经历,我发现了 CloudNative 持续交付的几个重点:

  1. 优先采用 SaaS 化的服务而不是自己搭建持续交付流水线。
  2. 开发是离不开基础设施配置工作的。
  3. 状态和过程分离,把状态通过版本化的方式保存到配置管理工具中。

而在这种环境下,Ops工作就只剩下三件事:

  1. 设计整体的架构,除了基础设施的架构以外,还要关注应用架构。以及优先采用的 SaaS 服务解决问题。
  2. 严格管理配置和权限并构建一个快速交付的持续交付流程。
  3. 监控生产环境。

剩下的事情,就全部交给云平台去做。


更多精彩洞见,请关注微信公众号:思特沃克

Share

忘记“规模化敏捷”

[摘要]

虽然敏捷软件开发理念已为业界普遍接受,但敏捷的大规模落地应用仍然是一个非常大的挑战。敏捷开发模式的标准化是规模化应用的一个重要前提,很多组织和企业都已经在开展这方面的工作。市场的需求也催生出了如SAFe、LeSS这样的“规模化敏捷”框架,但本质上,规模化敏捷是一个伪命题,这样的“标准化”并没有帮助我们解决落地时的两个核心问题:即面向市场和商业模式变化的业务科技合作,及云时代的企业科技架构。

当然如果你仍然认为敏捷的框架体系是最重要的,可能获取一个“敏捷国际认证”对你来说也是关键的,点击这里便可一键获取!

目录


  1. 敏捷走向生命周期的尽头
  2. 软件开发标准化的伤害
  3. 不忘敏捷初心
  4. 下一个“敏捷”长啥样?
  5. 敏捷的规模化落地

敏捷(Agile)是2001年由17位资深软件领域专家们一起发起的针对软件开发工作价值观的倡议。作为一种软件开发理念,与之伴随出现了很多实践框架和方法,如Scrum、Kanban和XP。而这些方法目前已经逐步成为了我们软件开发的事实标准,类似持续集成(Continuous Integration)这样10年前,被大家认为是“极限”的方法,现在也已经成了开发团队的一个标配。

值得一提的是很多开发组织里敏捷的大规模应用仍然是一个非常有挑战性的任务。软件自身的多样性和这个行业的高速发展,造成了敏捷方法落地的种种挑战。在过去10年时间里,我自己在这方面的咨询辅导工作或多或少跟适配团队和实践有关。相信在下一个10年,我们还需要持续去解决敏捷开发落地的问题。

市场的需求当然也催生出了一些所谓“规模化敏捷”框架,如SAFe和LeSS。很多需要解决敏捷大规模应用的组织,于是感觉有了框架和标准。也有很多企业,询问我支持什么样的规模化敏捷框架。

希望通过本文总结一下这两年来我持续表达的观点:规模化敏捷是一个伪命题!所谓伪命题就是不要为一个不存在的问题,去寻找一把看似精美的战斧——敏捷这把锤子,遇到组织级灭霸,可能不好使了。

敏捷正走向生命周期的尽头

说这句话的时候我自己也带着一些伤感, 这里并非是想哗众取宠地呐喊一句 “敏捷已死”。敏捷作为一种开发理念,已经成为了现代软件开发的基础。然而,软件开发作为接下来这个数字化时代的基建行业,仍然有很多超越当年敏捷所谈及的事务。从时下爆款的区块链,我们就可以看到完全不一样的“开发团队”——形象代言人和挖矿小分队,都成为了这个开发团队里必不可少的成员。

什么标志着敏捷走向了生命周期的尽头?为什么不是持续演进成为敏捷2.0呢?

咨询行业对理念和方法的生命周期是有着最快感知和反馈的(如下图所示)。一旦一个理念和方法成为了事实标准,那么咨询公司需要做的事情就是体系化总结,通过标准化来帮助目标企业更快落地。一个我们都知道的案例,就是华为在IBM的帮助下,在上个世纪成功规模化应用了IPD。但即使有了华为这样的成功案例,IPD在后续中国企业落地时也是普遍失败的,原因是下一代的方法显然已经显现出了更大的优势。IPD有着完整的流程、方法和实践定义,当年的敏捷相比之下是混乱之极的,但仍然不可阻挡一波拥抱敏捷精神的互联网企业快速崛起。

​ (图示:开发方法随着行业成熟度提升而经历的生命周期。一种方法的成熟某种意义上只是为下一次创新做准备。当突变发生时,新方法将超越现行的主流方法,形成新的生命周期。)

当下的敏捷是何其相似!各大咨询公司蜂拥而至,都开始了“敏捷咨询”,资深的敏捷专家们开始总结大而全的框架,生怕遗漏了任何一个时髦概念——“价值流”大家都谈就加一个,“DevOps”火就先放里面。当你艳羡框架的完整时,往往需要警惕这个框架的时代价值,别忘了你做敏捷的初衷是什么?

在这个问题上,我在ThoughtWorks曾经纠结了好多年,每次有“写框架、卖咨询”的冲动时,都先后被老马(Martin Fowler)和Jim Highsmith这样的敏捷宣言签署者给拍了回来。确实也要感谢他们,能够站在行业发展的角度Let Agile Go。

早在10年前,华为给我的命题是:用敏捷开发改造IPD。记得我们最后“成功”把大家痛恨的“软需”改造成了用户故事(User Story),并构建起来了一条嵌入式系统的持续集成流水线。虽然现在看来,那是多么简单的任务,但当时大家还是激动地出了一本“葵花宝典”,记录了整个改造过程。听到这个名字大家可能都会笑而不语,XXX之后的IPD还是IPD吗?

同样的事情可能马上就会发生。有一天,中国的BATJ们可能会说:用XXX改造敏捷。我相信这个XXX不会是敏捷2.0,而我不希望成为那个在汇报中必须听到“敏捷改造成功”的管理者(即使团队写了另外一本“葵花宝典”)。

软件开发标准化的伤害

之前总是会顾忌得罪圈子里的老伙伴们,好在很多敏捷圈子的老一代们都已经去了各大知名企业做管理者(侧面印证了敏捷方法的成熟),敏捷顾问又是一波新人,所以今天才写这篇文章。再则最近几次关于AI的研讨和培训,着实让我觉得不能成为敏捷的“遗老遗少”,所以除了自省也希望鞭策更多的人。

企业里的很多管理者会说规模化敏捷框架至少给出了标准,让我们有章可循。很不幸的是这个“标准化”对目前的软件开发是有害的。标准化的基础在软件开发这个行业目前是不存在的,这个行业的基础知识积累还远不能支持有效的标准化,在未来很长一段时间软件开发都会是世界上最大的手工行业(可笑的现实~)。

谈标准化时我们必须跳出自身行业,看看别的成熟行业是怎么成功标准化的。程序员从心底是抵触“码农”这个可能未来的,所以暂且不说建筑行业。咱们看看临床医疗,算是一个标准化程度相当高的产业,但一个豪斯医生这样的天才也需要至少10年的培养,学习各个标准步骤;也没有人会写出《七天学会胆结石移除》或者《心脏起搏器的10种安装》这样的“速成”秘籍。从行业知识积累角度,临床已经是一个成熟的成年人,有着过去四五十代的知识积淀,尚且如此,何况软件开发仍然像个正在探索世界的小孩子,才经历了不超过四代人的知识积淀!

而显然,我们不应该以小孩子探索期得到的经验为依据,就开始进行行业范围大规模的标准化。CMM的失败在于软件学术业觉得软件开发就像算法论证一样,找到了nlog(n)的最佳算法就是普世的。但很可惜底层的运算环境,仍然会从单个冯诺依曼模型变成池化的云,再变成量子计算……

所以,让敏捷完成其历史使命,不要把敏捷标准化成另外一个CMM。软件开发行业需要下一个“敏捷”,下一个基于新知识积累的创新理念和方法!

不忘敏捷初心

当年的敏捷通过宣言的形式发表也是煞费苦心。在我们批判敏捷没有框架、没有标准的时候其实应该感谢17位参与者的克制和包容。我们很容易被自己十多年的经验所蒙蔽双眼,一边告诉别人要持续改进,一边却总认为自己这套是包治百病的。

他们的初心是值得学习的,我们每一代从业者都是在为这个行业的日益成熟积累经验。我经常拿开发和测试同事们开玩笑,“警告”他们未来不是成为“软件开发劳动力”,就是被AI所取代。但实际上软件开发的未来,包括职业的分工,都是一个未知数。持续学习的开放心态和着眼实践经验积累的学习方式,是软件开发在这个历史时代必须的。

另外一个值得提出的初心就是对软件开发“管理”的认知。由于软件开发并没有太多的先验知识,所以管理很多时候是会产生副作用;因为背后的标准并不具备普世性,随着生产工具的进步很快就成了畔脚石。用COBOL主机开发的方式去管理基于JavaScript的前端开发毫无疑问是偏执的。

这对我们的管理者提出了很高的要求,于是有一些企业高管开始要求全体管理者必须上手一线代码实践。当然这些做法都是不得已而为之,管理者的“初心”,是希望大家能深入理解这个年轻而高速发展的行业,直面缺乏标准化的现实,把自己日常的管理工作变成是去持续改进,做一个领导者,而不是所谓标准的监察者。

下一个“敏捷”长啥样?

那么,大家就会问下一个突破在哪里?未来的软件开发是什么样子的?很抱歉没有人可以准确预见发展的趋势,这个问题也可能不是大家目前最关心的。但既然写这个话题,我总还是要分享一些自己的思考,权且留作日后回顾的笑谈。

实际操作层面的标准化

随着老马即将出版的《重构第二版》,我们会发现实际代码层面的评判标准日益成熟。我们早已走过了代码能工作就是好的阶段,即使在过去最混乱的JavaScript领域(重构再版就是基于JavaScript的!)。在软件架构和代码结构方面,我们会逐步看到业界共识的质量标准。这两年基于GitHub等开源平台的兴盛为这个层面的标准化提供了契机。

当然这个层面的标准化也是最有可能被AI应用所颠覆的,毕竟全球最大的“手工业”必然会是AI技术应用最有利可图的行业之一。

行业层面的定制化

云已经是不争的软件开发基础设施,这样的水平切割已经形成了实际意义上的IaaS、PaaS和SaaS的分化。就我个人过去两年体验来说,各层软件在整个软件生命周期定义上还是存在明显差异的。比如构建一个IaaS层的基础服务,较之一个SaaS层的应用服务,在需求管理和发布上线领域就存在着显著不同。

在水平分层的基础上,我们越来越多地感受到了商业领域业务模式不同对软件开发的影响。前一段时间我写了一篇《银行业IT的敏捷转身》引起了行业的广泛关注,其原因就是金融行业在数字化时代越来越依赖软件,而从业人员发现他们的敏捷运用跟其它行业存在着很大不同。

这样的不同存在于方方面面,比如我经常挤兑同事吴雪峰的 “抛弃型演进式架构”,也许在区块链这样的投机领域就是一个真命题。

在一横一纵的行业化背景下,软件开发本身还需要很多的经验积累,短期标准化的必要性不大。对于一些更为传统的产业,在中国也面领着“互联网+”的冲击,存在更多不确定性,尝试着去标准化一个定制的“XXX行业的规模化敏捷”模式,也可能是弊大于利的。

敏捷的规模化落地

希望看到这里你理解了我为什么说 “规模化敏捷” 是一个伪命题。当大家在热议HBR关于敏捷的话题,认为敏捷就是真理的时候,需要理解我们现在的挑战是如何在一个大型开发组织里落地敏捷。而这个落地,不是靠套用一种 “规模化敏捷” 框架就能解决的。目前看,有这么三件事情是必须做的:

1. 建立持续改进的内部力量

这是软件开发组织最重要的管理举措(没有之一)。在行业缺乏足够先验知识积淀的今天,作为从业者我们能做的,就是让组织持续积累经验,并且具备从经验中学习和改善的能力。如果说我希望参与一项标准化工作,那就是行业里对教练的标准化“定义”,因为我希望帮助整个行业明确这个角色或团队在组织里存在的必要性及重要性。

2. 系统思考整个开发过程

软件开发不存在管理和技术的区分。由于立项、审计等多方面的传统制约因素,很多咨询公司人为地划分了所谓管理咨询和技术咨询,帮助刚开始合作的团队去理解如何落地敏捷。于是有了“只做管理”或“只关注技术”这样的说法。

事实上软件开发和很多工程制造业在这方面的差距是巨大的,即使Scrum这样的简单管理框架在落地时,也是受制于技术架构约束的;我们还没有办法让技术足够标准化,从而不约束管理。自从目睹了有人挥舞Kanban做“纯管理提升”产生的悲惨现场,再有任何团队说“这次只做管理”,我都坚决拒绝。

3. 为创新建立安全试错环境

面对未知最好的方式就是探索,而探索意味着大多数时候都是失败的,毕竟从失败中我们学习到更多知识。很多组织和企业说我们一直在做啊,我们每年都会有创新项目,投资一两个小团队去做。这个数字化时代,软件行业的创新应该是中国改革开放式的,即使不能用“雨后春笋”这样的形容词,也应该是全员参与的。已经有很多的案例告诉大家为什么隔离出来一两个小团队“专职”创新是不可行的了。规模化创新意味着创新不仅仅是IT的事情,是组织各个部门、各个角色共同的愿景;也意味着我们承认创新是一个不停试错的领域,从错误中我们提升成功的可能(让统计学发挥作用)。

所以虽然我反对“规模化敏捷”,但我却站队了“规模化创新”!

最后,让我们摆正心态,让敏捷成为软件开发历史进程中的一块儿重要垫脚石;让我们持续探索,为软件开发领域的知识累积添砖加瓦,并共同期待新一代创新理念和方法的诞生!


更多精彩洞见,请关注微信公众号:ThoughtWorks商业洞见

Share

和情绪作伴

“作为一个职业人,你不应该把情绪带进工作里。作为女性,可能会很难,但是,职场上只有角色的区分,没有性别的差异。”

我想很多女性在自己的职业生涯中,都会遭遇情绪管理的问题,上面这是我的同事在我一次工作情绪失控后对我讲起的话,那也是我第一次意识到情绪的杀伤力。

那是在09 年,我在新加坡做项目。

当时的项目干系人比较复杂,来自多个部门,诉求多样化还多变,甚至有些诉求还互相冲突和矛盾。我们每两周给客户做一次showcase(成果展示),对我来说那是很大的挑战,既要展示产品功能,又要应对来自不同需求方的责难。

客户方的项目负责人是个法国人,每次的showcase都是他和我一起参加,尽管他很少发言,但是他的出席仍然会让我心安。

有一次,我们的showcase进入了一种很艰难的境地,开发的优先级在不同部门的干系人之间没有被协调好,以至于客户源源不断的扔出挑战、责难、挖苦,很明显,是因为项目经理在管理干系人的期望时出现了疏漏,所以我将求助的目光投向他。

但让我意想不到的是,他转而向我发难,抱怨产品的各种不给力,但那些产品功能是他定的,优先级是他拍的。

我欲哭无泪,成了妥妥的背锅侠。

当天晚上,我抑制不住愤怒,和团队大倒苦水,也把收集到的负面反馈统统倾倒给了团队,现在想来,当时的负能量足以把整个团队淹没,要知道,那是在项目最艰难也最需要士气的时期。 在我的影响下,有几位同事对项目失去了信心,纷纷提出想退出项目,包括我自己。

压抑不住的时候,不妨表达出来

后来又经历了一个项目,整个项目期间和客户合作非常顺利,就在离项目结束还有三天的时候,客户和老板谈了一次话。

会后客户找到当时担任项目经理的我,表达了对产品的不满意,挑出了一些“莫须有”的问题,这和几天前他表达的对产品的信心以及喜爱大相径庭。

在客户勉强拼凑出的“理由”里我渐渐清楚了原因,项目结束意味着整个技术团队离场,在没有技术人员支持的情况下客户对产品的维护存有担忧,但他又不想追加维护费用,所以只好找一些借口把团队留住。

我本能的反应是愤怒,所以一边抵御着他的强词夺理,一边做好了攻击的准备。

但经验告诉我,站在他的立场,在资源受限的条件下,他也只是无助又无能为力。

那是一次很艰难的谈话,耗时很长,曾有几次,我的声音提高了八度,感觉就要控制不住了,也开始表现出不耐烦的情绪,但理性告诉我,再继续下去,我肯定会失控,与其失控,不如主动表达。

我说:“听你讲完这些,我是愤怒的,产品开发的过程,我们一起付出努力,合作愉快,但就在项目结束的前夕,听你对产品做出这样的评价,我非常难过;我非常理解技术人员的撤离会让你担忧,但是在你提供不了任何关于产品有问题的确凿事实的前提下,单方面宣布项目结束不了,让我对你以及你的公司很失望;我们的合作基于信任,请理解我此刻的情绪和感受。”

奇迹般的,客户没有继续咄咄逼人,也没有再继续寻找借口。

尽管我当时还不知道项目会如何收尾,但和客户表达完情绪后,感觉自己像打了场胜仗,当晚和团队讲战斗经过的时候,一直笑着,像在讲别人的故事。

后来项目按时结束,客户也一直和我保持着私人的联系。

有时想想,情绪控制不住的时候,与其让它失控,伤人伤己,不如为它找个出口,主动表达出来,或许会是一种更强大的力量。

掩藏不住情绪的时候,不如真实

还是和客户有关的一个故事。

是客户方的一个高层领导,他并不经常出现,除非发生了两种情况:项目有了大的风险,或者他 对项目存在极大的不满。

那个项目压力很大,工作强度也很高,最大的挑战是项目进行到一半的时候,需求膨胀了一倍。 多次和这位高管交涉,对方口头答应追加合同款,但是一直没有落实。

在一次争议很大的showcase之后,我好不容易和他约了面谈的时间,本意是沟通showcase中的争议,但是他的时间一直更改,最后召见我的时候,是在一个很尴尬的场合,他刚对自己的下属发完火,会议室内还弥漫着硝烟。

最为关键的是两个下属如惊弓之鸟,他也并没有让下属退场的意思。

也就是说,我要在其他人在场的情况下和大老板交谈。

计划好的内容已经不适合在那个场合沟通,就在我盘算聊什么的时候,他对我发难了,他全盘推翻了以前的约定,提出了新的诉求,说的头头是道,有理有据。对软件项目来说,那是灭顶的灾难。

我俩的地位不对等,隔着无数个层级,商业上他是客户,我能明显感觉到在我面前,他有意展示压倒性的优势,但我没有退路,只能迎上去。

我打开曾经约定的文件,帮他回忆当时是怎么达成一致的,何时何地,由谁提的意,如何做的决定,以及当时都说了什么。 回忆的时候,我脑海中浮现出在这过程中无数的反反复复,那些反复不但没有带来好的结果,还给团队制造了很多麻烦,团队的辛苦也是因为那些反复。

为了约到他的时间,我费了不少周折,眼看他要离场了,我却没有机会表达我想说的内容。 心里的委屈此起彼伏。

我说,我希望我们再一次澄清约定,并且遵守约定,团队已经做了最大的努力,也会继续努力下去,但是团队也很不容易。

说到“不容易”的时候突然就哽咽了,委屈的像个不更事的小姑娘。但意识到自己失态之后,我立即把情绪压了下去。

客户敏锐的觉察到了我的情绪,正了正身体,非常专心的和我完成了接下来的谈话。

就在当天,他竟然发信确认了追加的合同款。

后来才知道,他以为我执意找他是为了催合同款,为了让我开不了口他也故意不配合,是我掩饰不住的情绪,让他最终改变了主意。

我没有办法找他询问他当时看到的我是什么样子,但我想,那一刻,我应该很真实。

很多人说,在职场,情绪化是很不专业的表现,但我认为,只要我们的情绪没有伤害到别人,它就可以存在,因为情绪代表着最真实的自己。

这么多年,我也一直很感激,并且发自内心的尊重那些,在职场上能尊重别人情绪的人。

换一种角度来看待情绪,情绪也许是一件包装好的礼物,有时候包装得很丑,但你要先接受她,再去打开这份礼物。

或许,你会看到意想不到的内容。

Share

前端不止:Web性能优化 – 关键渲染路径以及优化策略

我问你:“当你从搜索引擎的结果页面选择打开一条搜索结果时,你觉得多长时间之后,如果页面还处于白屏或者没有加载到关键信息,你会选择关掉这个窗口?”

《Designing for Performance》的作者 Lara Swanson 在2014年写过一篇文章《Web性能即用户体验》,她在文中提到“网站页面的快速加载,能够建立用户对网站的信任,增加回访率,大部分的用户其实都期待页面能够在2秒内加载完成,而当超过3秒以后,就会有接近40%的用户离开你的网站”。

Google和亚马逊的研究表明,Google页面加载的时间从0.4秒提升到0.9秒导致丢失了20%流量和广告收入,对于亚马逊,页面加载时间每增加100毫秒就意味着1%的销售额损失。可见,页面的加载速度对于用户可能的下一步操作是多么的举足轻重。

想一想,如果你希望你的网站在一秒钟之内呈现用户想看的关键信息,有哪些可行的手段?Minify,压缩,雪碧图等等。

Google的Web性能工程师 Ilya Grigorik 会告诉你,你只需要理解浏览器的关键渲染路径。

页面性能可能是一个感性的东西

页面的性能,看似是一个理性和量化的概念,实则也来自于用户的感知,主观的评价,是一个偏感性的东西。

(参考自Google关键渲染路径)

如果页面可以做到优先显示与用户操作有关的内容,就可以让用户更快速的感知到操作得到响应,这个过程叫做“优化关键渲染路径”。

什么是关键渲染路径

我记得,有一个非常经典的面试题叫做:《当浏览器地址栏输入URL并回车后,发生了什么?》。

关键渲染路径就是描述浏览器从收到 HTML、CSS 和 JavaScript 字节开始,到如何使用HTML、CSS 和 JavaScript 在屏幕上渲染像素的中间过程。

如果我们能够优化这条路径,就能让页面更快速的展示内容,给用户更好的体验。

全景图

我们先尝试站在高处,看一眼关键渲染路径的全景图,这样能够快速的领略一个大致轮廓和一些关键概念。

文档对象模型 (DOM)

DOM概念之于Web开发人员再熟悉不过了,当浏览器发出请求并接收到HTML文档后,它会有这样一个流程来构建DOM:字节 → 字符 → 令牌 → 节点 → 对象模型。

以下面这段代码为例:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>

浏览器接收到HTML请求的返回结果,根据预定的流程解析HTML,文档中的“开标签”,比如<html><head>等会转换成一个令牌(Token),然后令牌转换成节点对象(Node)。

这个令牌解析并转换为节点对象的过程,也是每个节点建立关系(树形结构)的过程。例如:head的令牌出现在html令牌之后,但其闭标签出现在html闭标签之前,这就意味着headhtml的子节点,以此类推,建立节点的父子关系。

这个过程在浏览器中,叫做“Parse HTML”。

CSS 对象模型 (CSSOM)

当DOM捕获了页面的内容,我们还需要知道页面如何展示这些内容,所以需要构建CSS 对象模型(CSSOM)。

浏览器解析DOM,遇到了link标签,发现它引用了一个外部样式资源:style.css,于是浏览器会向外部请求样式资源,然后进行后续的DOM构建工作。

CSS 被视为阻塞渲染的资源,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕。

CSSOM有着一个和DOM构建相似的流程:字节 → 字符 → 令牌 → 节点 → CSS对象模型。

以下面的CSS样式为例,它会根据具体解析规则,将CSS文档转换成下面的树形结构:

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }

这种树形结构让CSS有层级继承关系,子节点会继承父节点的样式。

前面谈到CSS会阻塞浏览器的渲染过程,因为渲染树的构建同时需要DOM和CSSOM,所以当浏览器请求的style.css返回之后,浏览器会开始解析样式表,并重新计算样式(Recalculate Style),将CSS转换成CSSOM,然后进行后续的操作。

值得注意的是,CSSOM运算是一个非常复杂的过程,性能消耗会比较大,所以你会常常听到“老人们”说写样式“尽量使用classid,保证层级扁平,减少过度层叠”,而且越是通用的CSS样式,执行速度越快,越是具体(选择器)的CSS样式,则执行速度越慢。

DOM + CSSOM = 渲染树

渲染树和DOM树不同,它只会捕获一些页面上可见的元素,比如,Headerdisplay:none的元素不会放在渲染树中。

渲染树的构建会从DOM的根节点开始遍历,对于不可见节点会忽略,然后在CSSOM中找到每个对应节点的样式规则并应用,最后输出的渲染树会包含所有的可见内容和样式信息,如下图:

布局和绘制

有了渲染树,浏览器会进入布局和绘制阶段。

布局就是弄清每个对象在页面视窗(Viewport)上的确切大小和位置,它的输出是一个“盒模型”,里面准确的捕获每一个元素在页面视窗中的位置和尺寸。

在布局工作完成之后,浏览器会开始绘制,将渲染树转换成屏幕上的像素,这样,我们就能在浏览器中看到页面的内容。

短暂回顾一下“关键渲染路径”的步骤

  1. 处理 HTML 标记并构建 DOM 树。
  2. 处理 CSS 标记并构建 CSSOM 树。
  3. 将 DOM 与 CSSOM 合并成一个渲染树。
  4. 根据渲染树来布局。
  5. 将各个节点绘制到屏幕上。

当DOM或者CSSOM发生变化的时候,浏览器就需要再次执行一次上面的步骤。

JavaScript

到目前为止,我们还没涉及到JavaScript,但它在整个关键渲染路径中扮演着非常重要的角色,就如全景图中画的那样,我们从一段简单的代码开始:

<body>
   <p>Hello <span>web performance</span> students!</p>
   <script>
     var span = document.getElementsByTagName('span')[0];
     span.textContent = 'javascript';
   </script>
</body>

一个大家都知道的重要事实是:脚本在文档的何处插入,就在何处执行。

当HTML解析过程中遇到一个script标记时,它会暂停DOM构建,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。也就是说,执行内联的JavaScript会阻塞页面的首次渲染。

现在我们假设,这段JavaScript是外部资源。

<body>
   <p>Hello <span>web performance</span> students!</p>
   <script src="write.js"></script>
</body>

则浏览器的渲染会阻塞直到write.js的请求返回后,并执行JavaScript后,继续。

需要注意的是,在网页中引入JavaScript脚本有一个微妙事实,就是JavaScript不仅可以读取和修改DOM属性,还可以读取和修改CSSOM属性。

前面我们提到CSS是阻塞渲染的资源,当它和JavaScript一起出现在页面上时,会发生这样的事情:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path: Script</title>
  </head>
  <body>
    <p>Hello <script>document.write('web performance')</script> students!</p>
  </body>
</html>

在浏览器解析HTML构建DOM过程中,发现了link标签,于是发出请求获取style.css,然后继续构建DOM,此时,它发现script标签,由于JavaScript可能会访问样式属性,所以它会阻止JavaScript的执行直到styles.css返回并完成CSSOM构建,然后执行这一段JavaScript代码,再继续后面DOM的构建和相关渲染操作。

于是styles.css的请求不仅阻塞后面的渲染,还阻塞了DOM的构建。

如果将这段JavaScript作为外部资源,就是一个比较典型的页面结构:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path Render</title>
  </head>
  <body>
    <p>Hello <span>web performance</span></p>
    <script src="app.js"></script>
  </body>
</html>

JavaScript和CSS资源请求是并行的,但仍然需要等到CSSOM构建完成之后,JavaScript才可以执行,然后在进行后面的渲染工作。于是,当 DOM、CSSOM 和 JavaScript 执行之间有大量的依赖关系时,就很可能导致浏览器在处理及渲染网页时出现延迟。

优化策略

我们花了大量的篇幅来理解浏览器的渲染过程,理解DOM,CSSOM,渲染树,浏览器绘制,分析HTML,CSS和JS在渲染过程中的关系,我相信你已然受益匪浅,现在,我们来运用这些知识加速你的网站。

第一步,分析你的网站渲染状况

我们以Google为例,通过Chrome的Performance工具查看页面渲染情况,如下图,你应该可以清晰的看到图中有四条竖线,他们分别是什么含义呢?

(Google主页的性能分析情况)

  1. 绿色竖线,代表First Paint,即浏览器开始进行像素的绘制
  2. 黄色竖线,代表First Meaningful Paint(首次有效绘制)用户可以开始看到部分内容,但绘制仍在继续
  3. 蓝色竖线,代表大家比较熟悉的DOMContentLoaded
  4. 红色竖线,代表load,页面加载完成

优秀的网站都能够把“首次有效渲染”做到1秒之内完成,这样能够让用户更快的看到所请求的页面得到响应。如果你的网站“首次有效渲染”超过1秒,那么就非常有必要重新分析一下网站的关键渲染路径是否合理。

第二步,分析关键渲染路径

在关键渲染路径中,我们通常要关注三个点:

  1. 页面首次渲染需要的关键资源数量
  2. 关键资源的大小
  3. 关键渲染路径的往返次数(Roundtrip)

我们的策略也非常简单,就是减少关键资源数量,降低资源大小,减少关键路径的往返次数。

关键渲染的资源一般是阻止屏幕首次渲染HTML,CSS和JavaScript,所以最重要也是最难的部分的是你需要根据自己网站的实际情况分析,哪些是页面绘制的所必须的,哪些是无关的。

第三步,根据分析采取优化手段

1、减少关键资源的大小

我们首先从最简单也是最直接的减少关键资源的大小开始:

对于所有的资源(HTML,JavaScript,CSS,Image等),你都应该用上三大绝招:Minify,Compression和Cache,这里不过多的赘述里面的细节。

这一点对于HTML来说,非常关键,HTML作为渲染的关键资源,消除或者延迟加载肯定不太可能(这里指的是非局部渲染的关键HTML),能够做到是消除无用代码(比如:注释)和最小化代码(Minify)以及动态局部渲染等。

(Google对页面的HTML进行了压缩)

2、延迟JavaScript非阻塞资源加载

JavaScript和CSS都是阻塞渲染的资源,对于已经鉴别出的对于首次渲染没有起到关键作用的代码,我们首先想到的是要延迟它的加载,让它脱离关键渲染路径。

首先,对于阻塞渲染的JavaScript,应该将它放置在页面body的底部,为什么呢?

JavaScript可以查询和操作DOM和CSSOM,正如前面介绍的,HTML解析过程中构建DOM,当遇到JavaScript就停止DOM构建执行JavaScript,如果被执行的JavaScript是放置在head附近,那么很可能要被操作或者查询的DOM还没有构建到DOM当中。

而对于,非阻塞渲染的JavaScript,我们应该采用异步的方式加载,如下:

  <script src="script.js"></script>
  <script async src="script.js"></script>
  <script defer src="myscript.js"></script>
</body>
</html>

方式一:即阻塞的JavaScript,HTML解析过程中遇到script标签,发出网络请求获取script.js,在网络请求返回后,解析并执行script.js,然后浏览器继续HTML解析。

方式二:async,完全的异步操作,HTML解析遇到该标签后,发出网络请求,但不阻止HTML解析和其后面的渲染操作,当JavaScript请求返回后立刻执行,且不等待HTML解析或其他操作的完成。所以,如果脚本中有DOM操作,就并不适合。比较适合的场景是Google Analytics。

方式三:defer,HTML的解析和对JavaScript资源的网络请求是并行的,但它会等待HTML解析完成之后,才执行脚本。

(图片参考自:Asynchronous and deferred JavaScript execution explained « Peter Beverloo)

不过,async和defer,他们对浏览器的兼容性有一定的要求,但仍然应该使用它们,同时可以采用退而求其次的延迟代码执行的方法(比如:on DOMContentLoad后),特别是与首次渲染无关的计算逻辑和功能。

3、尽早和按需的加载CSS

你可能在思考,有没有异步加载CSS的需求?我认为不应该有,页面应该只引用与该页面相关的样式文件。(只不过很多时候,我们将所有的CSS都打包在了一个压缩的CSS文件中了。)目前,已经有许多帮助你分析关键渲染路径上的所需要的CSS的工具:grunt-criticalcsscriticalcriticalCSS,在线CRPCSS工具。

前面已经提到,CSS是阻塞渲染的资源,在CSSOM完全解析完成之前,浏览器不可能开始屏幕的绘画。

所以,我们应该尽早的开始对样式资源的请求,将它尽早、尽快地下载到客户端,这样解释了为什么我们看到样式资源的link标签一般都放在head表中:

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Sample Site</title>
  <link href="style.css" rel="stylesheet">
</head>

CSSOM的运算是一个非常复杂和相对耗时的过程,但它也有一个特点,就是可能只有在特定的情况下才会起作用,比如:响应式设计的页面。

对于响应式页面,我们可以考虑将不同媒体上的样式分离,在<link>中使用媒体查询,浏览器仍然会下载对应的资源,但是可以避免不必要的CSSOM解析导致对渲染的阻塞。

<link href="style.css" rel="stylesheet" media="all">
<link href="portrait.css" rel="stylesheet" media="orientation:portrait">
<link href="print.css" rel="stylesheet" media="print">

同时,我们还应该避免在首次渲染的CSS样式中使用@import指令,因为它只有在收到并解析完带有@import规则的CSS资源之后,才会发现导入的 CSS 资源,这个时候就会重新请求,从而增加了关键渲染路径的往返次数。

4、内联CSS来提高渲染性能

到目前为止,我们已经做到了识别关键渲染资源,将非关键资源延迟加载或者不加载。那么,减少关键路径的往返次数是什么意思?其实就是减少关键渲染资源从服务器端到客户端的往返次数。比如,外链的JS和CSS文件以前CSS的@import,在页面渲染的过程中,都会重新去服务器端请求。这其实,和我们常说的减少http请求量(合并http请求)类似,但是我么从渲染路径的角度来理解这样一种性能的消耗。

根据这样的逻辑,我们很容易就想到可以将渲染必备CSS内联到HTML中,来减少渲染路径的往返次数。

实际上不少的优秀网站都采用了在head内联样式的做法:Google,百度,淘宝,京东。

(百度和Google将样式inline在head中)

关于内联样式还有更进一步的做法,在文章的一开始就提到,优化关键渲染路径就是要优先显示和用户先关内容。

所以,我们可以考虑仅仅将当前屏幕展示的内容(above-the-fold,一屏)所需的CSS内联到HTML的head中,然后采用异步的方式加载整个页面所需要的完整CSS,以便用户能够更快的看到首屏出现的内容。(inlining-critical-css-for-better-web-performance

5、一个神奇的数字14kb

在最开始我们提到,要减小关键资源的大小,那么多小比较合适呢?(废话,当然是越小越好)。

其实,有一个神奇的数字14kb,它是怎么来?

HTTP的传输层协议是TCP,TCP协议有一个慢启动的过程,即它在第一次传递数据时,只能同时传递14kb的数据块,所以当数据超多14kb时,TCP协议传递数据实际是多次的往返(roundtrip)。如果能够将渲染所需要的资源控制在14kb之内,那么就能TCP协议启动时,一次完成数据的传递。

其他Web资源和关键渲染路径的关系

你一定会思考,除了HTML,JavaScript和CSS,Web页面还包含许多其他的资源,比如:图片,网络字体(Icon Font),他们和关键渲染路径的关系是什么?

大家对图片加载感受都应该大致一样,它会在页面加载过程中或完成后,逐步显示,也就是说它不是阻塞渲染的资源,它的痛点主要在于质量和资源大小的权衡,以及请求数量带来的性能消耗(雪碧图)。

网络字体,在网络加载比较慢的情况下,用户可能会感受到字体或者图形的变化(Icon Font)。其实,浏览器在渲染树构建完成之后,会指示需要哪些字体在网页上渲染指定文本,然后分派字体请求,浏览器执行布局并将内容绘制到屏幕上,如果字体尚不可用,浏览器可能不会渲染任何文本像素,待字体可用之后,再绘制文本像素,当然,不同浏览器之间实际行为有所差异,这里不在赘述,请参考文章尾部的资料链接。

总结

优化关键渲染路径的最终目的是优先显示和用户操作相关的内容,减少低优先级资源对浏览器渲染的阻塞,从而尽早显示用户真正关心的关键内容。页面性能就是用户体验的一个重要维度,尝试用感性的思维去思考理性的代码,也许真的能受益不少。


参考资料:

  1. Google关键渲染路径
  2. 《Web性能即用户体验》
  3. https://peter.sh/experiments/asynchronous-and-deferred-javascript-execution-explained/

更多精彩洞见,请关注微信公众号:思特沃克

Share

亲历者说:敏捷?我被洗脑了吗?

几年之前我还是个野生程序员的时候,对“敏捷”这个词是有些抗拒的。那时候,要么是没有想法、懒得去理会,要么就是主观上拒绝:

肯定又是些无所事事的人弄出来的无聊概念,帮他们自己刷存在感的东西!

敏捷,就是那些咨询公司弄出来给别人洗脑的嘛,那些理念太空,根本无法落地!

那些一大堆概念都是些什么鬼?条条框框太多了,运作起来太麻烦了!

不用敏捷,我们现在不也挺好的吗?敏捷跟我有什么关系?

但后来我却选择加入了 ThoughtWorks,这个传说中的敏捷大本营,一方面因为很多出名的书都是 ThoughtWorks 的人出的,另一方面也想亲入虎穴一探究竟。而如今,历经敏捷项目的洗礼,我已经成为专职的一线咨询师,为众多大型企业提供敏捷转型过程中的技术指导。

他们

在一些朋友看来,我自从换了工作,就开始在群里转发一些敏捷相关的文章,发表一些感言。在转发这些内容的时候,我经常用到的叙事口吻是“他们”。

他们的代码真的写了好多测试。

有时候要开一整天的会,我真不知道他们是怎么撑下来的!

感觉跟着他们一起做测试驱动开发好像没那么难。

这段时间里,我让自己成为一个“警惕的观察者”。不管是主观上的警觉,还是故意在外人面前将自己打造成这样的一个形象。深怕在我还没有觉察到的时候就已经被敏捷洗脑了;同时也希望在曾经的好友面前以尽量理性、中立和客观(理中客)的形象示人:不过,这不妨碍在他们看来,我已经被洗脑了。

后来我了解到,这如同学习新知识过程“守破离”中“守”的阶段。“守”的过程既是获取新认知的过程,也是与过去的认知做比较和更新的过程。观察现象——对比质疑——私下学习——拨开疑云,大体就是这样的不段重复,在不断了解新实践的过程中,也同步去认识它、理解它。

渐渐地,一系列疑问得以解答,使得最终我接纳了敏捷开发思想,并认为它是适用于现代开发团队中的工作方法。

疑问

在过去我呆过的团队中,一直有两个无法解答的问题。那时作为技术经理,我经常被别人问到的问题,也是我自己无法用经验回答的问题。

  • 做完这个功能,你估计需要多少时间?
  • 为什么大家在办公室显得很安静、气氛有点压抑?

在成功学的洗脑课程中,有一句被强调最多的话:“失败一定有原因,而成功一定有方法!”那么,我们过去回答不了的上面这些问题,以及由它们导致的管理上的难题,其根本原因又是什么呢?获得管理上成功的方法又是什么呢?我带着这两个问题离开了之前深度参与的创业项目。与朋友分享了要探索新征程的想法之后,他真诚地邀请我加入他的创意,并希望由我来带领团队一起续写新的故事。我猛然间发现,其实虽然之前在团队里担任所谓技术经理的职位,但我真正给团队带来的帮助似乎更多的只是一个有经验的工程师给新手的指导。那时候,第三个疑问产生了:

  • 如何去做好一个团队带头人?应该安排团队成员每天做什么?

这些疑问不得到解答,我就如同掉下井底的青蛙,虽然能听见外面世界的声音,却只能看到井口大小的世界。

答疑

加入新团队后不久,这些疑问就完全得到了解答。第一个要实现的需求就是一个“明星”功能,集成第三方系统的调查问卷。团队很快组织了需求计划会议,并细致地过了一遍第一期要完成的目标,实现这个目标要包含的业务范围,而具体又包含哪些步骤(用户故事)。

  • 目标:发出问卷链接,并将数据收回来。
  • 范围:选取模板、发送链接、收回数据、发送提醒邮件
  • 步骤:
    1.  管理员在外部系统中创建好模板(不需要开发)
    
    1. 用户可在 XX 页面中使用选项来选择问卷模板(系统自动将外部系统中的模板名字同步到本地系统)
    2. 用户可在 YY 页面中使用链接发送调查表单,客户收到包含链接的邮件
    3. 系统自动将外部系统中收到的数据同步到本地系统中以供使用
    4. 如果没收到提交数据,系统自动在7天后自动发出提醒邮件,客户再收到一封包含链接的邮件

接着开发人员和测试人员对还不够详尽的细节提出问题,讨论获得一致,一起对各个步骤大致估计所需时间。每个用户故事并不确定由谁来做,而是大家一起就其中的细节进行讨论,并就所需时间形成一致:有的人说需要 3 天,有的人觉得 2 天就够了。他们会叙述自己的想法,并最终达成共识。

这项活动,以及之后的迭代过程中,基于这个会议的开发过程解答了我第一个疑问。

他们有一个角色叫 BA,会写一个个的用户故事来描述需求,一两天就可以完成一个故事。明确的前提条件和验收标准(从哪里开始做,做到哪里为止),让开发工作变得有节奏感,需求不清楚的时候随时就这个需求的范围进行讨论。

相比于拿别的产品做个演示,甩一句“就照这个做”,然后就天天催进度、做出来之后又说不对劲的产品经理,有一个专门负责业务、随时可以叫过来讨论的 BA 让开发人员感到倍感轻松。

江湖上传言说敏捷不需要文档,原来是谬误。敏捷并没有说不需要文档,只是说认为团队成员之间的沟通协作比详尽的文档更重要。所以,用户故事仍然是会包含必要的描述内容的。要写清楚为什么要做这项功能,以及验收标准等。

团队一起估计时间的过程,不光可以消除特定人估计时的无助感,更重要的是它让所有人都了解用户故事的细节,在后续开发中谁都可以参与开发。

相对较小的用户故事,估计起来要比一整个功能(比如对整个调查问卷功能进行估计)估计起来靠谱得多。即使有特定的用户故事估计不准确,其影响范围也是可控的。

把所有故事的估计时间相加,即为整个功能所需要花费的时间。

估计只是帮助做计划的一种方法,在后续开发过程中,如果发现比当初估计的要复杂,或者简单,需要与 BA、PM 等角色一起更新这个估计值,从而帮助团队及时完善一开始制定的迭代计划(如果必要,可以加入一些,或者减去一些,或者修改一些)。

这样,我发现开发团队的时间原来是需要管理的,而管理时间这件事其实也需要花些心思才能做好。这时候,如果你问我某项功能需要多久做好?我会告诉你,让我来拆分一下功能,粗略估计就成为了可能。

而后面的其他疑问也很快得到了解答。关于团队气氛,如果一个团队里每个成员都在闷头做自己的工作,独自面对自己的交付压力和技术挑战,成员之间互相帮不上忙,他们的气氛一定不会太好。而如果所有人通力配合工作在相同的功能上,一起理解消化业务,一起解决技术问题,共同做技术决策,并分担解决缺陷(BUG)的责任和压力,那么团队的气氛怎么会不好呢?

最后一个问题,关于团队。

团队里大家的角色是如何分配的,规模又应该多大?团队之间应该如何配合?这就不难回答了。典型的业务功能团队,以及后来出现的微服务团队,都很好的诠释了团队结构和规模问题。有一个论述产品设计和组织结构关系的康威定律,值得我们深入思考。团队带头人?我突然反应过来,一定要有这个角色么?如果大家都能很好地运作了,那其实这个所谓带头人的作用是很淡化的,这也就是所谓的自组织团队了。如果一定要一个带头人,那他的职责一定是确保这样一种自组织的机制在团队中持续地运作下去。

所以,我被洗脑了吗?

也许你可以这样认为。

作者我现在是接受了敏捷思想的,其中还有一些工具和方法,我还在持续学习过程中。不过,“洗脑”这个词本身其实具有一定的预设立场,它是那些质疑者的说法。

那么,重新回到问题本身。敏捷是什么呢?它会将人们洗脑吗?

敏捷不是什么宗教,它只是一种生产软件的思路,一种倡议。它倡议,通过加强团队成员间的交流协作,尽快交付高质量、有价值的软件,让团队以良好的响应性来拥抱软件的变化。为了符合这种思路,它一般又会有一些典型的实践方式。我们可以说哪些实践是由敏捷方法所推荐的,因此是“敏捷的”;而哪些实践是不推荐的,因此是“不够敏捷的”。但不会说哪种是好的,哪种是不好的。

比如,敏捷的:

  • 自主提交代码,尽早集成
  • 自动化一切,包括环境初始化
  • 代码由团队共享,随时重构和优化

不敏捷的:

  • 逐次代码提交都需要他人审查并批准的管控
  • 手动部署生产环境
  • 不让他人修改自己编写的代码

但这些“不敏捷”的条目,基于团队具体情况,可能实际操作起来更可行,就可以根据目前的阶段去施行,并向着更敏捷的方向去持续改进。类似地还有,敏捷不会说团队一定要开站会,站会一定要在早上开等等……相反,如果要求团队一定要做某件事,其实它与敏捷思想是背道而弛的。我们应该遵循敏捷理念去对实践进行改良,以适应团队实际情况。

事实上,“敏捷”一词来源于英语 Agile,这一英文词汇也类似于中文中的形容词“敏捷”一词,其适应性相当广泛。人们往往用它来形容业务的灵活性,思路的开放性等。因此对于敏捷来说,并不存在什么洗脑不洗脑的说法。它只是一种风格,一种态度。只要你运用这种思路和风格去让团队协作越来越好,开发出来的软件的质量越来越好,那就是敏捷的。敏捷中典型的具体实践方法有 Scrum)、XP 和 Lean 等。此外,近年被广为谈论的 DevOps,也已经成为了敏捷软件方法的典型实践。


更多精彩洞见,请关注微信公众号:思特沃克

Share