《微服务设计》读书笔记之演化式架构师

2.0 上节回顾

上节主要介绍了什么是微服务,微服务与其他组合技术有何不同,以及它能带来的主要好处是什么。

2.1 不准确的比较

你总提及的那个词,它的含义与你想表达的意思并不一样。

架构师的一个重要的职责是,确保团队有共同的技术愿景,以帮助我们向客户交付他们想要的系统

在某些场景下,架构师只需要和一个团队一起工作,这时他们等同于技术领导者。在其他情况下,他们要对整个项目的技术愿景负责,通常需要协同多个团队之间,甚至是整个组织内的工作。

不管处于哪个层次,架构师这个角色都很微妙。在一般的组织中,非常出色的开发人员才能成为架构师,但通常会比其他角色招致更多的批评。

这个角色很难做好的原因何在呢?

我们的行业还很年轻,从计算机出现到现在也只有 70 年左右,因此我们需要在现存的行业中不断的寻找,帮助别人理解我们到底是做什么的。我们处于其他行业的中间地带,社会很难理解我们,我们自己也不清楚自己到底处于什么位置。

所以我们尝试借鉴其他行业。我们把自己称作软件“工程师”或者“建筑师”,但其实我们不是,对吧?建筑师和工程师所具备的精确性和纪律性是遥不可及的,而且他们在社会中的重要性也很容易理解。

此处省略 1000 字碎碎念,总之这些称呼都不太合适。

当我们把自己和工程师或者建筑师做比较时,很有可能会做出错误的决定。

不幸的是,架构师这个词已经被大众接收了,所以现在我们能够做的事情就是在上下文中去重新定义这个词的含义。

2.2 架构师的演化视角

与建造建筑物相比,在软件中我们会面临大量的需求变更,使用的工具和技术也具有多样性

我们创造的东西并不是在某个时间点之后就不再变化了,甚至在发布到生产环境之后,软件还能继续演化。

因此架构师必须改变那种从一开始就要设计出完美产品的想法,相反我们应该设计一个合理的框架,在这个框架下可以慢慢演化出正确的系统,并且一旦我们学到了更多知识,应该可以很容易地应用到系统中。

尽管前面说了不要跟其他行业做过多的比较,但是城市规划师可以更好的跟 IT 架构师相类比。城市规划师的职责是优化城镇布局,使其更易于现有居民生活,同时也会考虑一些未来的因素。为了达到这个目的,他需要收集各种各样的信息。城市规划师更多考虑的是人和公共设施如何从一个区域移到另一个区域,而不是具体在每个区域发生的事。

很多人把城市比作生物,因为城市会时不时地发生变化。当居民对城市的使用方式有所变化,或者受到外力的影响时,城市就会相应的演化。城市规划师应该尽量去预期可能发生的变化,但是也需要明白一个事实:尝试直接对各个方面进行控制往往不会奏效。

当用户对软件提出变更需求时,我们需要对其进行响应并作出相应的变化。未来的变化很难预见,所以与其对所有变化进行预测,不如做一个允许变化的计划。为此,应该避免对所有事情作出过于详尽的设计。

城市这个系统应该让生活在其中的住户感到开心。有一件经常被人们忽略的事实是:系统的使用者不仅仅是终端用户,还有工作在其上的开发人员和运维人员,他们也对系统的需求变更负责。

架构师的职责之一就是保证该系统适合开发人员在其上工作。(出自:Frank Buschmann)

城市规划师就像建筑师一样,需要知道什么时候他的计划没有得到执行。尽管他会引入较少的规范,并尽量少的对发展方向进行纠正,但是如果有人决定要在住宅区建造一个污水池,他应该能制止。

总结:我们的架构师应该像城市规划师那样专注在大方向上,只在很有限的情况下参与到非常具体的细节实现中来。他们需要保证系统不但能够满足当前的需求,还能够应对将来的变化。而且他们还应该保证在这个系统上工作的开发人员要和使用这个系统的用户一样开心。

2.3 分区

服务边界,或者是一些粗粒度的服务群组,相对于前面提到的区域的概念。

作为架构师,不应该过多的关注每个区域发生的事情,而应该关注区域之间的事情。

这意味着我们应该考虑不同的服务之间如何交互,或者说保证我们能够对整个系统的健康状态进行监控。

很多组织采用微服务是为了使团队的自治性最大化。

但是在区域之间,或者说传统架构图中的框图之间,我们需要非常小心,因为在这些地方犯的错误会很难纠正。

每一个服务内部可以允许团队自己选择不同的技术栈或者数据存储技术,当然其他问题也需要考虑。但是事实上也不会无限制地允许团队选择任意技术栈。比如如果需要支持 10 种不同的技术栈的话,可能会在招聘上遇到困难,或者很难在不同的团队之间交换人员。

如果一个服务决定通过 HTTP 暴露 REST 接口,另一个用的是 protocol buffers,第三个使用的是 Java RMI,那个作为一个服务的消费者就需要支持各种形式的交互,这对于消费者来说简直就是噩梦。这也是为什么我们应该 “担心服务之间的交互,而不要过于关注各个服务内部发生的事情”。

2.4 一个原则性的方法

规则对于智者来说是指导,对于愚蠢者来说是遵从。

做系统设计方面的决定通常都是在做取舍,而在微服务架构中,你要做很多的取舍!

基于要达到的目标去定义一些原则和实践对设计来说非常有好处。

2.4.1 战略目标

做一名架构师已经很困难了,但幸运的是,通常我们不需要定义战略目标!

战略目标关心的是公司的走向以及如何才能让自己的客户满意。

这些战略目标的层次一般都很高,但通常不会涉及技术这个层面,一般只在公司或部门层面制定。

2.4.2 原则

为了和更大的目标保持一致,我们会制定一些具体的规则,并称之为原则,它不是一成不变的。

举个例子,如果组织的一个战略目标是缩短新功能上线的周期,那个,一个可能的原则是,交付团队应该对整个软件生命周期有完全的控制权,这样他们就可以及时交付任何就绪的功能,而不受其他团队的现状。

一般来讲,原则最好不要超过 10 个,或者能够写在一张海报上,不然大家会很难记住。而且原则越多,它们发生重叠和冲突的可能性越大。

Heroku 的 12 Factors 就是一组能够帮助你在 Heroku 平台上创建应用的设计原则,当然它们在其他的上下文中可能也有用。

其中,有些原则实际上是为了让你的应用程序可以适应 Heroku 这个平台而引入的约束。约束是很难(或者说不可能)改变的,但原则也是我们自己决定的。

应该显式地指出哪些是原则,哪些是约束,这样用户就会清楚哪些是不能变的。

2.4.3 实践

我们通过相应的实践来保证原则能够得到实施,这些实践能够指导我们如何完成任务。

通常这些实践是技术相关的,而且是比较底层的,所以任何一个开发人员都能够理解。

这些实践包括代码规范、日志数据集中捕获或者 HTTP / REST 作为标准集成风格等。

由于实践比较偏技术层面,所以改变的频率会高于原则。

就像原则那样,有时候,实践也会反映出组织内的一些限制。比如你只支持 CentOS,那么相应的实践就应该考虑这个因素。

实践应该巩固原则。

2.4.4 将原则和实践相结合

有些东西对一些人来说是原则,对另外一些人来说则可能是实践。

比如,你可能会把使用 HTTP /REST 作为原则,而不是实践。

这些没什么问题,关键是要有一些重要的原则来指导系统的演化,同时也要有一些细节来指导如何实现这个原则。

对于一个足够小的群组,比如单个团队来说,将原则和实践结合是没问题的。

但是在一个大型组织中,技术和工作实践可能不一样,在不同的地方需要的实践可能也不同。不过这也没关系,只要它们都能够映射到相同的原则即可。

2.4.5 真实世界的例子

原则与实践的真实例子

这是帮一个客户画的图表。清楚的表示了目标、原则和实践之间的相互影响。几年之间,实践改动得很频繁,而原则基本上没怎么变。

可以把这样一个图表打印出来并共享给相关人员,其中每个条目都很简单,所以开发人员应该很容易记住它们。

尽管每条实践背后还有很多细节,但仅仅能把它们总结表述出来也是非常有用的。

上面提到的一些项可以使用文档来支撑,但大多数情况下我喜欢给出一些示例代码供人阅读、研究和运行,从而传递上面设计的那些信息。

更好的方式是,创造一些工具来保证我们所做事情的正确性。

2.5 要求的标准

当你浏览这些实践,并思考你需要做的取舍时,需要注意一个很重要的因素:系统允许多少可变性。

一种方法是,给出一个好服务的例子来阐释好服务的特点。

一种能够帮助我们实现平衡的方法就是:清楚的定义出一个好服务应有的属性。

2.5.1 监控

能够清晰地描绘出跨服务系统的健康状态非常关键。这必须在系统级别而非单个服务级别进行考虑。

往往在需要诊断一个跨服务的问题或者想要了解更大的趋势时,你才需要知道每个服务的健康状态。

简单起见,可以确保所有的服务使用同样的方式报告健康状态及其监控相关的数据。

你可能会选择使用推送机制,也就是说,每个服务主动把数据推送到某个集中的位置。你可以使用 Graphite 来收集指标数据,使用 Nagios 来检测健康状态,或者使用轮询系统来从各个节点收集数据。

但无论你的选择是什么,都尽量保持标准化。每个服务内的技术应该对外不透明,并且不要为了服务的具体实现而改变监控系统。

日志功能和监控情况类似:也需要集中式管理。

2.5.2 接口

选用少数几种明确的接口技术有助于新消费者的集成。

使用一种标准方式很好,两种也不太坏,但是 20 种不同的集成技术就太糟糕了。

这里说的不仅仅是关于接口的技术和协议。举个例子,如果你选用 HTTP / REST ,在 URL 中,你会使用动词还是名词?你会如何处理资源的分页?你会如何处理不同版本的 API ?

2.5.3 架构安全性

一个运行异常的服务可能会毁了整个系统,而这种后果是我们无法承担的。所以,必须保证每个服务都可以应对下游服务的错误请求。没有很好处理下游错误请求的服务越多,系统就会越脆弱。

你可以至少让每个下游服务使用它们自己的连接池,进一步让每一个服务使用一个断路器

返回码也应该遵守一定的规则。如果你的断路器依赖于 HTTP 返回码,并且一个服务决定使用 2XX 作为错误码,或者把 4XX 和 5XX 混用,那么这种安全措施就没什么意义了。

即使使用的不是 HTTP,也应该注意类似的问题。对以下几种请求做不同的处理可以帮助系统及时失败,并且也很容易追溯问题:

  • 正常并且被正确处理的请求;
  • 错误请求,并且服务识别出了它是错误的,但什么也没做;
  • 被访问的服务宕机了,所以无法判断请求是否正常。

如果我们的服务没有很好地遵守这些规则,那个整个系统就会更加脆弱。

2.6 代码治理

聚在一起,就如何做事情达成共识是一个好主意。但是,花时间保证人们按照这个共识来做事情就没那么有趣了,因为在各个服务中使用这些标准做法会成为开发人员的负担。

比较奏效的两种方式是:提供范例和服务代码模板。

2.6.1 范例

编写文档是有用的。但是开发人员更喜欢可以查看和运行的代码。

如果你有一些很好的实践希望别人采纳,那么给出一系列的代码范例会很有帮助。

这样做的一个初衷是:如果在系统中人们有比较好的代码范例可以模仿,那么他们也就不会错得很离谱。

理想情况下,提供的优秀范例应该来自真实项目,而不是一个专门实现的一个完美的例子。因为如果你的范例来自真正运行的代码,那么就可以保证其中所体现的那些原则都是合理的。

2.6.2 裁剪服务代码模板

如果能让所有的开发人员很容易地遵守大部分的指导原则,那就太棒了。一种可能的方式是,当开发人员想要实现一个新服务时,所有实现核心属性的那些代码都应该是现成的。

DropwizardKaryon 是两个基于 JVM 的开源微容器。它们的运行模式相似,会自动下载一系列第三方库,这些库提供一些特性,比如健康检查、HTTP 服务、提供指标数据接口等。这样你就有了一个可以从命令行启动的嵌入式 servlet 容器。这是一个很好的开始,但是你可以做得更多。在实际工作中,你可以使用 Dropwizard 和 Karyon 作为基础,然后根据自己的上下文加入更多的定制化特性。

举个例子,如果你想要断路器的规范化使用,那么就可以将 Hystrix 这个库集成进来。或者,你想要把所有的指标数据都发送到中心 Graphite 服务器,那么就可以使用像 Dropwizard’s Metrics 这样的开源库,只需要在此基础上做一些配置,响应时间和错误率等信息就会被自动的推送到某个已知的服务器上。

针对自己的开发实践裁剪出一个服务代码模板,不但可以提高开发速度,还可以保证服务的质量。

当然,如果你的组织使用多种不同的技术栈,那么针对每种技术栈都需要这样一个服务代码模板。你也可以把它当作一种在团队中巧妙地限制语言选择的方式。

需要注意的是,创建服务代码模板不是某个中心化工具的职责,也不是指导(即使是通过代码)我们应该怎样工作的职责。应该通过合作的方式定义出这些实践,所以你的团队也需要负责更新这个模块(内部开源的方式能够很好的完成这项工作)。

另外,如果越来越多的功能被加到一个中心化的框架中,直到这个框架变成一个不堪重负的怪兽,这会毁掉团队的士气和生产力。如果决定要使用一个裁剪的服务代码模板,一定要想清楚它的职责是什么。

理想情况下,应该是可以选择是否使用服务代码模板,但是如果强制团队使用它,一定要确保它能简化开发人员的工作,而不是使其复杂化。

还需要知道,重用代码可能引入的危险。在重用代码的驱动下,我们可能引入服务之间的耦合。

2.7 技术债务

有时候可能无法完全遵守技术愿景,比如为了发布一些紧急的特性,可能会忽略一些约束。其实这仅仅是另一个需要做的取舍而已。

我们的技术愿景有其本身的道理,所以偏离了这个愿景短期可能会带来收益,但是长期来看是要付出代价的。

可以使用技术债务的概念来帮助我们理解这个取舍,就像在真实世界中欠的债务要偿还一样,累积的技术债务也是如此。

不光走捷径会引入技术债务。有时候系统的目标会发生改变,并且与现有的实现不符,这种情况也会产生技术债务。

架构师的职责就是从更高的层次出发,理解如何做权衡。理解债务的层次对其系统的影响非常重要。

对于某些组织来说,架构师应该能够提供一些温和的指导,然后让团队自行决定如何偿还这些技术债务。而其他的组织就需要更加结构化的方式,比如维护一个债务列表,并定期回顾。

2.8 例外管理

原则和实践可以指导我们如何构建系统。那么,如果系统偏离了这些指导又会发生什么?

有时候我们会针对某个规则破一次例,然后把它记录下来。

如果这样的例外出现了很多次,就可以通过修改原则和实践的方式把我们的理解固化下来。

2.9 集中治理和领导

架构师的部分职责是治理。

如果说,架构师的一个职责是确保有一个技术愿景,那么治理就是要确保我们构建的系统符合这个愿景,并且在需要的时候还应对愿景进行演化。

架构师会对很多事情负责:

  • 他们需要确保有一组可以指导开发的原则,并且这些原则要与组织的战略相符。
  • 他们需要确保以这些原则为知道衍生出来的实践不会给开发人员带来痛苦。
  • 他们需要了解新技术,需要知道在什么时候做怎样的取舍。
  • 他们需要让同事也理解这些决定和取舍,并执行下去。
  • 他们需要花时间和团队一起工作,甚至是编码,从而了解所做的决定对团队造成了怎样的影响。

因此,可以由一个治理小组来做这些工作,并确定愿景。

一般来讲,治理是一个小组活动。它可以是与一个足够小的团队进行非正式的聊天,也可以是在比较大的范围内,与一个有着正式成员的小组进行结构化例会。在这些会议上,可以讨论前面提到的那些原则,有必要的化也可以对其进行修改。这个小组应该是由技术专家领导,并且要有一线人员的参与。这个小组也应该负责跟踪和管理技术风险。

一种可行的模式是:由架构师领导这个小组,但是每个交付团队都有人参加。架构师负责确保该组织的正常运作,整个小组都要对治理负责。这样职责就得到了分担,并且保证了有来自高层的支持。这也可以保证信息从开发团队顺畅地流入这个小组,从而保证小组做出更合理的决定。

2.10 建设团队

对于一个系统技术愿景的主要负责人来说,执行愿景不仅仅等同于做技术决定,和你一起工作的人自然会做这些决定。对于技术领导人来说,更重要的事情是帮助你的队友成长,帮助他们理解这个愿景,并保证他们可以积极地参与到愿景的实现和调整中来。

帮助别人成长的形式有很多种,微服务架构本身能够提供一种很好的形式。在微服务架构中存在多个自治的代码库,每个代码库都有自己独立的生命周期,这就给更多人提供了对单个服务负责的机会。

作者坚定的相信:伟大的软件来自伟大的人。所有如果你只是担心技术问题,那么恐怕你看到的问题远远不及一半。

2.11 小结

一个演进式架构师应该承担的责任:

  • 愿景:确保在系统中有一个经过充分沟通的技术愿景,这个愿景应该可以帮助你满足客户和组织的需求。
  • 同理心:理解你所做的决定对客户和同事带来的影响。
  • 合作:和尽量多的同事进行沟通,从而更好地对愿景进行定义、修订和执行。
  • 适应性:确保在你的客户和组织需要的时候调整技术愿景。
  • 自治性: 在标准化和团队自治之间寻找一个正确的平衡点。
  • 治理:确保系统按照技术愿景的要求实现。

演进式架构师应该理解,成功要靠不断地取舍来实现。总会存在一些原因需要你改变工作的方式,但是具体做哪些改变就只能依赖于自己的经验了。而僵化地固守自己的想法无疑是最糟糕的做法。

在下一章中,让我们带着对架构师全新的认识来考虑如何找寻微服务之间正确的边界。