可视化架构设计——C4介绍

好多年前,同事徐昊说过的一句话给了我很大启发,他说“纸上的不是架构,每个人脑子里的才是”。这句话告诉我们,即便是天天工作在一个团队里的人,对架构的认识也可能是不一样的。每个人嘴上说的是类似的话,但心里想象的画面仍然是不一样的。在多年的工作中,我越来越认可这句话所揭示出的道理。软件开发是一个团队协作的工作,混乱的理解会造成架构的无意义腐化、技术债的无意识积累、维护成本的无价值上升。

最近听到一句话,“那些精妙的方案之所以落不了地,是因为没有在设计上兼容人类的愚蠢”。话糙理不糙,虽然最终人们选择的方案的思想都是在十年前甚至几十年前就已经存在的,然而在技术升级到足以“兼容”人类的愚蠢之前,这些思想只能在学术的故纸堆里睡大觉。当然话糙确实也会有一个问题,将一个思想性问题转化成了情绪性问题。人们容易把一些糟心的事情归因到人类的愚蠢,在宣泄完不满情绪后就停止思考了。作为知识工作者,我们的思维不能停步,我们需要思考到底人类有哪些愚蠢,分别用什么方法去避免或者“兼容”。

可以肯定彼此明明对自己开发的软件有不一样的认识却天天在一起讨论问题并试图把软件做好是一件愚蠢的事情,为了兼容这种愚蠢我们需要采用可视化的方法。

为什么需要可视化呢,主要还是语言不靠谱。人类语言真的是太随意了,只要你想,你可以说你见过一个方形的圆,并为此与别人辩论。但是无论如何你也画不出来一个方形的圆,这就是我们需要可视化的原因。

今天我们介绍一个工具,叫做C4 model,这是我近几年见到的一个比较难得跟我的认知有大量共鸣的工具。

该工具的作者在多年的咨询中经常发现,很多个人画出来的架构图都是不一样的,但也不是说谁画错了,而是每个人的抽象层次不一样。抽象层次这种东西,说起来好像存在,但真要说清楚还挺难,于是作者类比地图,提出了缩放的概念。(两年前我在教学生的时候提过同样的概念)如下图:

上面的四张地图就是想说明,当我们看待真实世界的“架构图”时候,也是要不停的缩放,在每一个层次刻意忽略一些细节才能表达好当前抽象层次的信息。所以他类比着把架构也提出了四个抽象层次:

从上到下依次是系统System、容器Container、组件Component和代码Code。(咦,那为什么叫C4呢,因为系统的图叫System Context,系统上下文图。为了凑四个C也是够拼的。)

基于这四个层次的抽象,C4模型由4张核心图和3张附属图组成,分别用于描述不同的场景,下面我们一一介绍一下。

四张核心图

系统上下文图

如上图所示,这个图表达的是你所开发的系统和它的用户以及它所依赖的系统之间的关系。从这个图上我们已经看出来C4图形的几个关键图形:

C4说穿了就是几个要素:关系——带箭头的线、元素——方块和角色、关系描述——线上的文字、元素的描述——方块和角色里的文字、元素的标记——方块和角色的颜色、虚线框(在C4里面虚线框的表达力被极大的限制了,我觉得可以给虚线框更大的扩展空间)。

通过在不同的抽象层次上,重新定义方块和虚线框的含义来将我们的表达限制在一个抽象层次上,从而避免在表达的时候产生抽象层次混乱的问题。

那么在系统上下文图里,方块指代的是软件系统,蓝色表示我们聚焦的系统,也就是我开发的系统(也可能是我分析的系统,取决于我是谁),灰色表示我们直接依赖的系统,虚线框表示的是企业的边界。通过这些图形化的元素表达我们可以看出来各个系统彼此之间的关系。

容器图

当我们放大一个系统,就会看到容器,如上图所示,C4模型认为系统是由容器组成的。我个人认为,容器是C4模型最大的创举,尤其是在这个单体架构快速崩塌的时代。所谓容器,既不是Docker的容器,也不是JavaEE里的容器,而是借用了进程模型,代指有自己独立进程空间的一种存在。不管是在服务器上的单独进程空间,还是在浏览器里的单独进程空间,只要是单独的进程空间就可以看作一个容器。当然如果你容器化做得好,Docker的Container和这个Container可以一一对应。有了这个概念的存在我们就可以更清晰的去表达我们的架构,而不是总是用一些模糊的东西。

组件图

当我们放大一个容器,我们就会看到组件,如上图所示。组件在这里面很好的把接口和它的实现类打包成一个概念来表达关系。我个人觉得有时候一些存在于代码中,但又不是接口的某些东西,比如Service、Controller、Repository之类也可以用组件图来表达,如果你学了一些没有明确抽象层次的架构知识或者一些单体时代的遗留经验的时候,你可以画出来一些组件图,来印证自己的理解,如下图,是我画的自己对DDD战术设计里面的一些概念的理解:

比起模糊的堆砌在一起的文字,这种表达要清晰的很多,哪怕我的理解是不对的,也容易指出和讨论。

代码图

代码图没什么可说的,就是UML里的类图之类很细节的图。一般是不画的,都是代码生成出来。除非非常重要的且还没有写出代码的组件才画代码图。

以上就是C4的核心图,我们可以看到四种不同的抽象层次的定义会让我们更容易固定住我们讨论的层次,这点上我觉得C4是非常有价值的。

三张扩展图

架构设计设计要考虑的维度很多,仅四张核心图是不够的,所以作者又提供了三张扩展图,可以让我们关注更多的维度。

系统景观图

看得出来,系统景观图是比上下文图更丰富的系统级别的表达。不像上下文图只关注聚焦系统和它的直接关系,连一些间接相关的系统都会标示出来,那些系统的用户以及用户之间的关系也会标示出来,只是内部的用户会用灰色标记。

这个图有什么用呢?在我们分析一个企业的时候,我们需要一个工具帮助我们把一家公司给挖个底掉,做到完全穷尽,才能看到企业的全景图,从而理解局部的正确定位以做好局部设计为全局优化服务。之前我试过以四色建模的红卡、事件风暴的事件两种工具来教人掌握这种能力,一般来说,程序员学员都无法快速掌握这种顺藤摸瓜的分析技巧,毕竟跟程序员的思维还是有些差异的。但是用了系统景观图之后,学员就毫不费力的掌握了这种分析能力,所以我后来都是用这个图来教程序员探索企业的数字化全景图,效果极好,推荐给大家。

动态图

动态图不同于其他表达静态关系的图,它是用来表达动态关系的,也就是不同的元素之间是如何调用来完成一个业务的。所以动态图不仅仅适用于一个层面上,它在系统级、容器级和组件级都可以画,表达的目标是不一样的。

我之前曾经写过名为《像机器一样思考》的一系列文章,在文中也发明了类似的图,不同于本文中关系线上标注的是调用的方法、函数,我更关注的是数据,使用效果也很好。

什么时候用动态图呢?举个小例子,我之前做一个内部的小系统,团队中只有一个有经验的工程师带着十多个毕业生,我便要求他们在开始工作之前都画出动态图来,交由有经验的工程师去评估他们的思路是否正确,如果有问题,就在开始之前就扼杀掉烂设计。不管是毕业生还是初级工程师,改代码的能力都比写代码的能力要差很多,所以将烂设计扼杀在实现之前还是有帮助的。

部署图

前面的几张图都是站在开发的角度思考,但是一个没有充分思考过部署的架构很容易变成一个运维的灾难。所以作者提供了一个部署图。考虑到DevOps运动如火如荼,这个图可以变成很好的Dev和Ops之间沟通的桥梁。我们在实操中发现,Dev和Ops关注点的不同、语言的不一致,在这张图上表现得非常清楚。

图上最大的的实线框不同于虚线框,它表达的是数据中心,当你开始考虑异地载备的时候它就有了意义。数据的同步、实例的数量都会影响部署图的内容。部署图基本都是容器级的,它能很好的表达出来容器到底部署了几个实例,部署在什么样的操作系统上,一个节点部署了几个容器之类,我们在实际使用中,发现需要考虑的信息太多,自己就抽象出了类似于亚马逊上实例规格的Small、Large之类的术语来表达机器配置,增进了开发和运维之间的交流准确性。

为什么C4值得推荐

够直观,对于程序员来说容易理解,容易使用。

我们在开头的时候说过,只有每个人脑子里的才是架构图,如果我们使用一个本身就很难达成一致理解的工具,那成员就会陷入理解的死循环。经过尝试教授不同工具,发现C4模型是最容易理解、最容易使用的工具。可能它的概念是复用了程序员已有的一些认知模型,程序员在学习后都可以迅速的使用起来,并问出一些高质量的问题。

总结

在思维的世界里,我们都是盲人,很多东西我们以为自己知道,实际上画出来之后,才发现很多东西没想到,或者想的是乱的,同时别人也才可以给我们反馈。

有了上面的这个工具,我们就可以开始可视化的架构设计之路了,但路上还有一个心魔需要战胜。在我们的文化里,出错是一件很丢人的事情,所以我们喜欢用一些模糊的描述避免被别人挑战,而可视化是让我们精确的描述出自己的理解,来欢迎别人的挑战。这一个坎不太容易跨过去,但是一旦跨过去、大家形成正向的互动之后,我们的进步速度会变得很快,从而把封闭的人远远的甩在后面,获得组织级的成长推力。我自己就在跟别人的交流之后获得了更深入的洞见,本文已经分享了一些,还有一些内容后续再跟大家分享。


注:文中图片均来自:https://c4model.com/

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

Share

细说API – 重新认识RESTful

如果你是一个客户端、前端开发者,你可能会在某个时间吐槽过后端工程师的API设计,原因可能是文档不完善、返回数据丢字段、错误码不清晰等。如果你是一个后端API开发者,你一定在某些时候感到困惑,怎么让接口URL设计的合理,数据格式怎么定,错误码怎么处理,然后怎么才能合适的描述我的API,API怎么认证用户的请求。

在前后端分离和微服务成为现代软件开发的大趋势下,API设计也应该变得越来越规范和高效。本篇希望把API相关的概念最朴素的方式梳理,对API设计有一个更全面和细致的认识,构建出更规范、设计清晰和文档完善的API。

重新认识API

广义的API(Application Programming Interface)是指应用程序编程接口,包括在操作系统中的动态链接库文件例如dll\so,或者基于TCP层的socket连接,用来提供预定义的方法和函数,调用者无需访问源码和理解内部原理便可实现相应功能。而当前通常指通过HTTP协议传输的web service技术。

API在概念上和语言无关,理论上具有网络操作能力的所有编程语言都可以提供API服务。Java、PHP、Node甚至C都可以实现web API,都是通过响应HTTP请求并构造HTTP包来完成的,但是内部实现原理不同。例如QQ邮箱就是通过使用了C构建CGI服务器实现的。

API在概念上和JSON和XML等媒体类型无关,JSON和XML只是一种传输或媒体格式,便于计算机解析和读取数据,因此都有一个共同特点就是具有几个基本数据类型,同时提供了嵌套和列表的数据表达方式。JSON因为更加轻量、容易解析、和JavaScript天生集成,因此成为现在主流传输格式。在特殊的场景下可以构造自己的传输格式,例如JSONP传输的实际上是一段JavaScript代码来实现跨域。

基于以上,API设计的目的是为了让程序可读,应当遵从简单、易用、无状态等特性,这也是为什么Restful风格流行的原因。

RESTful

REST(英文:Representational State Transfer,简称REST),RESTful是一种对基于HTTP的应用设计风格,只是提供了一组设计原则和约束条件,而不是一种标准。网络上有大量对RESTful风格的解读,简单来说Restful定义URI和HTTP状态码,让你的API设计变得更简洁、清晰和富有层次,对缓存等实现更有帮助。RESTful不是灵丹妙药,也不是银弹。

RESTful第一次被提出是在2000Roy Fielding的博士论文中,他也是HTTP协议标准制定者之一。从本质上理解RESTful,它其实是尽可能复用HTTP特性来规范软件设计,甚至提高传输效率。HTTP包处于网络应用层,因此HTTP包为平台无关的字符串表示,如果尽可能的使用HTTP的包特征而不是大量在body定义自己的规则,可以用更简洁、清晰、高效的方式实现同样的需求。

用我几年前一个真实的例子,我们为了提供一个订单信息API,为了更方便传递信息全部使用了POST请求,使用了定义了method表明调用方法:

返回定义了自己的状态:

大家现在来看例子会觉得设计上很糟糕,但是在当时大量的API是这样设计的。操作资源的动作全部在数据体里面重新定义了一遍,URL上不能体现出任何有价值的信息,为缓存机制带来麻烦。对前端来说,在组装请求的时候显得麻烦不说,另外返回到数据的时候需要检查HTTP的状态是不是200,还需要检查status字段。

那么使用RESTful的例子是什么样呢:

例子中使用路径参数构建URL和HTTP动词来区分我们需要对服务所做出的操作,而不是使用URL上的接口名称,例如 getProducts等;使用HTTP状态码,而不是在body中自定义一个状态码字段;URL需要有层次的设计,例如/catetory/{category_id}/products 便于获取path参数,在以后例如负载均衡和缓存的路由非常有好处。

RESTful的本质是基于HTTP协议对资源的增删改查操作做出定义。理解HTTP协议非常简单,HTTP是通过网络socket发送一段字符串,这个字符串由键值对组成的header部分和纯文本的body部分组成。Url、Cookie、Method都在header中。

几个典型的RESTful API场景:

虽然HTTP协议定义了其他的Method,但是就普通场景来说,用好上面的几项已经足够了

RESTful的几个注意点:

  • URL只是表达被操作的资源位置,因此不应该使用动词,且注意单复数区分
  • 除了POST和DELETE之外,其他的操作需要冥等的,例如对数据多次更新应该返回同样的内容
  • 设计风格没有对错之分,RESTful一种设计风格,与此对应的还有RPC甚至自定义的风格
  • RESTful和语言、传输格式无关
  • 无状态,HTTP设计本来就是没有状态的,之所以看起来有状态因为我们浏览器使用了Cookies,每次请求都会把Session ID(可以看做身份标识)传递到headers中。关于RESTful风格下怎么做用户身份认证我们会在后面讲到。
  • RESTful没有定义body中内容传输的格式,有另外的规范来描述怎么设计body的数据结构,网络上有些文章对RESTful的范围理解有差异

JSON API

因为RESTful风格仅仅规定了URL和HTTP Method的使用,并没有定义body中数据格式的。我们怎么定义请求或者返回对象的结构,以及该如何针对不同的情况返回不同的HTTP 状态码?

同样的,这个世界上已经有人注意到这个问题,有一份叫做JSON API开源规范文档描述了如何传递数据的格式,JSON API最早来源于Ember Data(Ember是一个JavaScript前端框架,在框架中定义了一个通用的数据格式,后来被广泛认可)。

JSON已经是最主流的网络传输格式,因此本文默认JSON作为传输格式来讨论后面的话题。JSONAPI尝试去提供一个非常通用的描述数据资源的格式,关于记录的创建、更新和删除,因此要求在前后端均容易实现,并包含了基本的关系类型。个人理解,它的设计非常接近数据库ORM输出的数据类型,和一些Nosql(例如MongoDB)的数据结构也很像,从而对前端开发者来说拥有操作数据库或数据集合的体验。另外一个使用这个规范的好处是,已经有大量的库和框架做了相关实现,例如,backbone-jsonapi ,json-patch。

没有必要把JSON API文档全部搬过来,这里重点介绍常用部分内容。

MIME 类型

JSON API数据格式已经被IANA机构接受了注册,因此必须使用application/vnd.api+json类型。客户端请求头中Content-Type应该为application/vnd.api+json,并且在Accept中也必须包含application/vnd.api+json。如果指定错误服务器应该返回415或406状态码。

JSON文档结构

在顶级节点使用data、errors、meta,来描述数据、错误信息、元信息,注意data和errors应该互斥,不能再一个文档中同时存在,meta在项目实际上用的很少,只有特别情况才需要用到,比如返回服务器的一些信息。

data属性

一个典型的data的对象格式,我们的有效信息一般都放在attributes中。

  • id显而易见为唯一标识,可以为数字也可以为hash字符串,取决于后端实现
  • type 描述数据的类型,可以对应为数据模型的类名
  • attributes 代表资源的具体数据
  • relationships、links为可选属性,用来放置关联数据和资源地址等数据

errors属性

这里的errors和data有一点不同,一般来说返回值中errors作为列表存在,因为针对每个资源可能出现多个错误信息。最典型的例子为,我们请求的对象中某些字段不符合验证要求,这里需要返回验证信息,但是HTTP状态码会使用一个通用的401,然后把具体的验证信息在errors给出来。

在title字段中给出错误信息,如果我们在本地或者开发环境想打出更多的调试堆栈信息,我们可以增加一个detail字段让调试更加方便。需要注意的一点是,我们应该在生产环境屏蔽部分敏感信息,detail字段最好在生产环境不可见。

常用的返回码

返回码这部分是我开始设计API最感到迷惑的地方,如果你去查看HTTP协议文档,文档上有几十个状态码让你无从下手。实际上我们能在真实环境中用到的并不多,这里会介绍几个典型的场景。

200 OK

200是一个最常用的状态码用来表示请求成功,例如GET请求到某一个资源,或者更新、删除某资源。 需要注意的是使用POST创建资源应该返回201表示数据被创建。

201 Created

如果客户端发起一个POST请求,在RESTful部分我们提到,POST为创建资源,如果服务器处理成功应该返回一个创建成功的标志,在HTTP协议中,201为新建成功的状态。文档规定,服务器必须在data中返回id和type。 下面是一个HTTP的返回例子:

在HTTP协议中,2XX的状态码都表示成功,还有202、204等用的较少,就不做过多介绍了,4XX返回客户端错误,会重点介绍。

401 Unauthorized

如果服务器在检查用户输入的时候,需要传入的参数不能满足条件,服务器可以给出401错误,标记客户端错误,需要客户端自查。

415 Unsupported Media Type

当服务器媒体类型Content-Type和Accept指定错误的时候,应该返回415。

403 Forbidden

当客户端访问未授权的资源时,服务器应该返回403要求用户授权信息。

404 Not Found

这个太常见了,当指定资源找不到时服务器应当返回404。

500 Internal Server Error

当服务器发生任何内部错误时,应当返回500,并给出errors字段,必要的时候需要返回错误的code,便于查错。一般来说,500错误是为了区分4XX错误,包括任何服务器内部技术或者业务异常都应该返回500。

HATEOAS

这个时候有些了解过HATEOAS同学会觉得上面的links和HATEOAS思想很像,那么HATEOAS是个什么呢,为什么又有一个陌生的名词要学。 实际上HATEOAS算作被JSON API定义了的一部分,HATEOAS思想是既然Restful是利用HTTP协议来进行增删改查,那我们怎么在没有文档的情况下找到这些资源的地址呢,一种可行的办法就是在API的返回体里面加入导航信息,也就是links。这样就像HTML中的A标签实现了超文本文档一样,实现了超链接JSON文档。

超链接JSON文档是我造的一个词,它的真是名字是Hypermedia As The Engine Of Application State,中文叫做超媒体应用程序状态的引擎,网上很多讲它。但是它并不是一个很高大上的概念,在RESTful和JSONAPI部分我们都贯穿了HATEOAS思想。下面给出一个典型的例子进一步说明:

如果在某个系统中产品和订单是一对多的关系,那我们给产品的返回值可以定义为:

从返回中我们能得到links中product的的资源地址,同时也能得到orders的地址,这样我们不需要客户端自己拼装地址,就能够得到请求orders的地址。如果我们严格按照HATEOAS开发,客户端只需要在配置文件中定义一个入口地址就能够完成所有操作,在资源地址发生变化的时候也能自动适配。

当然,在实际项目中要使用HATEOAS也要付出额外的工作量(包括开发和前后端联调),HATEOAS只是一种思想,怎么在项目中使用也需要灵活应对了。

参考链接

在文档中还定义了分页、过滤、包含等更多内容,请移步文档:

英文版:http://jsonapi.org/format/

中文版:http://jsonapi.org.cn/format/ (PS:中文版更新不及时,请以英文文档为准)


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

Share

需求的冰川

在面对客户、面对用户或是面对五花八门的产品时,我有时会忍不住问自己,到底什么是需求分析?这概念好像哪哪儿都有、无人不知无人不晓,又好像深不见底难以摸个透彻。那我们在谈论需求分析的时候,都在讨论些什么?

要谈论需求分析,先要说说需求本身这个概念。在我们的语境中,需求往往包含了两层意思:

  • 用户需求:从用户自身角度出发产生的“自以为的”需求
  • 产品需求:由综合提炼用户的真实需求而产生的符合组织和产品定位的解决方案

这样一来,重点显而易见:真实需求和解决方案。如何挖掘需求、如何确认需求和解决方案我们已经有了很多成熟的方法论。但真实的需求又是什么?如何知道我们拿到的就是所谓“真实的“需求?要想摆脱需求罗生门,无限接近用户真正的需求,让产品从能用到好用,恐怕第一大忌就是“想当然”。

“想当然”,很大程度上等于我们经常在讲的making assumptions。做合理的assumption并积极地验证假设从来不会造成问题;可怕的是没有意识到assumption的存在还自我感觉良好,这大概就是我们中文里“想当然”所包含的意思了。

拿到我们的项目交付语境中,“想当然”是什么?是懒做甚至不做用户调研关起门来做需求;是自以为了解用户也了解客户没经过验证就敲定了需求;是偏听偏信客户爸爸的话说什么就做什么。

用户研究与验证

了解用户/客户是个庞大的课题,当用户体验被不断强调,可能没有人会跳出来否认用户研究和验证的价值。但反观我们的实践,很多时候业务分析师在需求层面上对用户研究和验证的重视还远远不够。

造成这种缺失的一大原因在于角色的割裂。有人理所当然地认为用户研究与验证是设计师的事,毕竟他们的头衔才是“用户体验设计师”。在产品同质化严重的今天,“体验”二字包含的不单是界面好不好看,操作顺不顺畅,更是背后的业务逻辑和痛点把握。如果强行将需求分析和用户调研分割开来,我们所做的需求分析很可能是浮在真相表面的“假需求”,所谓用户体验更是无从谈起。

业务分析师常常被形容为产品和交付之间的桥梁,产品经理把握产品走向,聚焦产品成长;业务分析师则往返于产品经理和程序员之间,专注如何迅速有效地让产品落地。于是,有人理所当然地把用户研究与验证的锅扣在产品经理头上,认为他们作为产品的最终负责人,应该由他们去做用户反馈的收集,再传递给业务分析师。

首先,我们应该承认产品的需求和运营是无法独立存在的,如果业务分析师和产品经理是纯粹的上传下达关系,分析师既不接触用户也不关注反馈,他甚至连“好”的定义都模棱两可,如何能分析好需求又怎么做好一个产品?其次,虽然产品经理们对自己的行业和领域有很深的了解,但对产品的规划设计和交付却很难面面俱到。他们不是不愿意做好用户研究与验证,而是不一定具备这种能力;即使做好了,也很难知道如何准确地把合适的信息传递给业务分析师和交付团队。

我们不止一次地强调“复合型人才”对产品构建和组织转型的重要性,在需求分析领域,用户研究和验证或许是呈给业务分析师的第一份考卷。

“了解用户”无法一劳永逸

在没有直接接触过用户、也没有参与过产品前期设计活动的时候,我曾经想当然地认为“了解用户”是售前团队、产品探索和规划设计团队的事情;我作为交付阶段的业务分析师,只要跟着项目计划保证交付就好了。后来有机会接触了更多项目更前期的阶段,开始认识到事实也许并非如此,但也并没有付诸实际行动。原因再简单不过:项目交付已经焦头烂额,花“额外的”精力去做用户研究和验证只能带来眼见的工作量负载而非立竿见影的成效,更别说有招致需求膨胀的可能,不如作罢。

在产品探索和规划设计阶段,由于时间紧、任务重,我们的用户研究与验证往往侧重在产品概念层面,确保产品方向性的正确。因此,即使在产品快速启动时产出了完整的需求列表和设计,也不意味着他们统统是ready to go的状态;更不意味着在交付的第二期、第三期,可以照搬当时的产出作为产品目标。“了解用户”无法一劳永逸,反之,它应该是持续的:在产品进入稳定的交付阶段后,业务分析师应该继续积极了解用户,不断验证并挖掘需求;用户和环境都在改变,该重新组织产品规划设计工作坊的时候,不能搪塞了事。

我目前所在的团队正在做一个听起来挺无聊的需求:给产品中的某功能换名字。我们的产品旨在帮助企业员工提高心理健康水平,在必要时进行干涉并提供援助。这个产品已经上线两个月,收到了不错的用户/客户反馈,但是从GA收集到的数据来看,发现我们当初产品设计阶段收到正面反馈的一个功能使用率并不理想。团队经过用户研究发现,从某种程度上看,是这个功能的名字出了问题。因为用户大多常常处在焦虑状态,这个叫“Goal”的小功能让人觉得压力山大,commitment很重以至于overwhelming,结果干脆不用;我们将在下次发布中,把“goal”换成“pathway”,让人觉得这是压力生活下的一条绿幽小径而非任务/目标。

Stay hungry, stay foolish

用户研究和验证的方法千千万,我不在这里一一列举。Stay hungry, stay foolish这句用烂了的话,恐怕是我能找到的最契合“BA怎么做用户研究和验证”的原则。面对客户的需求,多问一个为什么;面对用户的答案,多想一个为什么;能最大程度地避免“想当然”,或许就能最大程度地做好“用户研究和验证”

我们常常形容需求是冰川,露出海面的只是冰川一角。写到这里,我忽然想起去年夏天我在某客户的合作工厂做用户测试,工人们因为厂房过于炎热光着膀子也不带安全帽,我趁他们休息间隙想要和他们聊一聊,我走过去蹲在抽烟的人们旁边想要加入他们,但几乎于此同时,大家都站起来离开了。想要融化冰川,或许不只是挖掘那么简单。


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

Share

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

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

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

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

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

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

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

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

他们

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

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

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

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

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

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

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

疑问

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

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

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

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

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

答疑

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

所以,我被洗脑了吗?

也许你可以这样认为。

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

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

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

比如,敏捷的:

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

不敏捷的:

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

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

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


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

Share