最近,阅读了文章《微服务架构在Netflix的应用:架构设计的经验教训》,引发了我对微服务的一些感想。大约这些感想平日都在大脑里装着没有声响,这篇文章算是酵母,投进去,发了酵,开始有些微醉薄醺的味道了。
这篇文章介绍了Cockcroft的微服务架构经验。Cockcroft是Battery Ventures公司的技术人员,是微服务和云架构方面著名的布道者,目前供职于Nginx技术咨询委员会。
一直以来,微服务虽然风生水起,不过却没有什么靠得住的定义可以得到多少人公认的。Cockcroft对微服务的定义却引起了我的注意。定义如下:
由松耦合的有相应语境的元素构成的一种面向服务的架构,松耦合意味着你可以独立更新这些服务。更新其中一个服务并不会改变其他的服务。
最后一句话可以看做是验证服务设计是否合理的一个标准。这里提到的“更新”,不仅意味着服务实现的变化,关键是它意味着“部署好的服务”的更新,如此才能体现服务的物理边界,而这正是微服务所要解决的单块架构的弊病。
在向微服务迁移的时候人们常常会把数据库的耦合看的过重,也就是所有服务都连的是同一个数据库,更新其中一个服务就意味着要改变数据库的schema。这种情况你需要对数据库进行拆分。
我只能表示部分同意这个观点,因为我们需要考虑数据库拆分的成本,因为它可能会带来维护成本的增加,此外还有拆分数据库会产生的影响,例如需要考虑数据的同步、分布式事务等系列问题。
在文章末尾,作者旗帜鲜明地强调了这一观点,并将其列入微服务架构设计最佳实践的第一原则:“每个微服务的数据单独存储”。但我觉得这个约束可能会导致微服务的粒度过粗,且可能导致微服务的设计者过度地考虑数据库Schema,而忘记从领域的角度去划分服务边界。正如Jan Stenberg的文章《微服务架构设计须避开工程师思维》就认为“开发者最好能站在用户的角度像一个设计师一样去设计规划”,他认为:
以数据层开始着手从内向外构建服务,然后通过业务逻辑将数据层往外迁移,这也就意味着API的设计工作接近尾声了。如果说真的是按照这个流程来执行的话,只能说制作这样一个API从业务逻辑上来说是有意义的,但是并不能满足庞大的用户需求。
服务的分离并不绝对代表数据应该分离,从具有悠久历史的ORM框架的存在就已经证明数据Schema与领域逻辑必然是“阻抗不匹配”的,强调这个约束,可能会导致设计陷入“数据驱动设计”的危险境地。
个人认为,降低数据约束的设计原则是尽可能避免多个服务对同一个数据存储进行写操作。而读操作则不在限制之列。在我们开发的产品中,CData微服务会在HDFS上创建Parquet格式的数据集,而另一个微服务则会对这个数据集的数据进行读取。虽然这两个服务共享了存储的数据,但读写却是分离(或者说命令查询分离)的,无论从性能、可扩展性还是其他质量属性去考虑,这样的设计都是无可厚非的。没有数据存储的约束,我们就可以从业务角度去划分微服务。
文中提到了Bounded Context,这是我非常认可的,甚至觉得DDD与Micro Service的理念是天然融合的,BC就是驱动出Domain Service的最佳起点。
在微服务这个概念兴起之前,我们有项目就已经通过BC驱动出各个粒度较小且松散耦合的Domain Service,并基于RESTful的架构风格设计服务API。所以微服务并不是什么时髦新潮的玩意儿,数十年前的DDD已经这么要求了,只是没有一个概念来清晰定位罢了。
如果我们再结合Robert Martin提出的Clean Architecture的概念,那么,一个微服务就与Application相对应,而Application则可以映射为一个Feature级别的Use Case,恰好对应了DDD分层架构中应用服务层中的应用服务。还可以引入Cockburn的六边形架构,它可以帮助我们分清楚服务的物理边界和逻辑边界。
许多人在理解物理边界时,会错误地以为只要模块分为独立的Jar包(或者.NET平台下的DLL)即可以认为是物理分隔,实则真正的物理分解是要从运行视图的角度去判断。在同一个JVM中运行的Jar包,都应该视为是逻辑分离,而非真正意义上的物理分离。微服务定义中的服务必须是物理分离的服务,如此才能满足真正意义的服务独立进化。
Netflix团队提出了几条设计和实现微服务架构的最佳实践:
- 每个微服务的数据单独存储
- 使用类似程度的成熟度来维护代码
- 每个微服务都单独进行编译构建
- 部署到容器之中
- 将服务器看做是无状态的
除了针对第一条原则我心存疑虑外,后面诸条原则我均表示一百二十分的赞成。