WeText项目:一个基于.NET实现的DDDCQRS与微服务架构的演示案例

Posted dotNET跨平台

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WeText项目:一个基于.NET实现的DDDCQRS与微服务架构的演示案例相关的知识,希望对你有一定的参考价值。

最近出于工作需要,了解了一下微服务架构(,MSA)。我经过两周业余时间的努力,凭着自己对微服务架构的理解,从无到有,基于.NET打造了一个演示微服务架构的应用程序案例,并结合领域驱动设计(DDD)以及命令查询职责分离(CQRS)体系结构模式,对事件驱动的微服务系统架构进行了一些实战性的探索。现将自己的思考和收获整理成文,分享给大家。

微服务架构

在介绍源代码之前,我还是想谈谈微服务架构,虽然网上有很多有关微服务架构的讨论,但我觉得在此再多说一些还是有必要的。大师级人物Martin Fowler在他谈论微服务的个人主页上提到,微服务并没有一个非常明确的定义。事实上有很多种分布式系统的实现都可以被看成(或者说勉强看成)是面向微服务架构的。就我个人而言,我觉得微服务架构应该满足以下几个特征:

  • 整个系统被分为多个业务功能相对独立的(,或称单一化架构)的应用程序(也就是所谓的“微服务”),每个微服务通常遵循标准的分层架构风格或者基于事件驱动的架构风格,能够对自己相关的领域逻辑进行处理,使用本地数据库进行数据存储,并向上层提供相对独立的API接口或者用户界面。每个微服务还可以使用诸如缓存、日志等基础结构层设施,但如果是与其它的微服务公用这些设施,则该基础结构层设施需要满足下面的第三条特征

  • 各个微服务之间可以使用以下方式进行通信(参见:)

    • 同步方式:最为常见的是基于RESTful风格的API,也可以是跨平台、跨语言的

    • 异步方式:使用轻量级的消息通信机制,比如RabbitMQ、Redis等

  • 整个系统是“云友好”(cloud-friendly)的。所谓的“云友好”,是指:

    • 针对每个微服务,都应该避免单点失败的可能。例如针对一个系统中某个微服务A,需要有至少两个(或以上)的运行实例,并由API网关(API Gateway)或者负载均衡器(Load Balancer)根据一定的规则(比如各个A的运行实例的健康程度等)将来自客户端的服务请求分配到任意一个A的运行实例上完成处理

    • 针对每个微服务,管理员可以根据一些特定的实时技术指标对这些应用程序的部署进行调整。例如,购物网站的查询服务负载明显要高于订单管理服务,那么管理员可以根据实际情况,增加查询服务的部署量(比如部署3个查询服务的实例),同时减少订单管理服务的部署量。与整个系统单一采用一体化架构相比,这样做的好处是显而易见的,它能够充分利用云端服务器资源,使得每个微服务都能够运行在合理的资源配置状态下,减少资源浪费

    • 公有基础结构层服务设施也应该满足避免单点失败的条件,例如数据库服务需要配置Replication/Clustering,消息队列也需要使用类似的fault tolerance策略

相比传统的一体化架构系统,微服务架构系统有着以下一些优势:

  • 每个微服务都相对较小,这样更加便于开发和调试

  • 每个微服务都相对独立,这样不仅可以使开发人员仅关注在某个业务处理部分,而且还可以针对每个微服务自己的特征,采用不同的技术实现(比如部分微服务使用C#实现,部分使用Java或者Python等)

  • 这种独立性使得微服务在容错隔离方面也有很好的表现:比如某个微服务出现了crash等问题,不会导致整个系统不可用,这符合BASE()理念

  • 由于相对独立,微服务架构的设计能够更方便地部署到云环境中

  • 微服务的独立性还为敏捷开发提供了很好的支持。比如每个服务都可以单独开发单独部署,同时项目团队还能根据成员本身的技术专长来平衡开发和测试资源

当然,它也有一些不足:

  • 开发人员需要应对由分布式架构带来的复杂性。比如如果微服务间采用异步的消息通信机制进行通信,那么就需要遵循由这种消息机制所引入的开发模式(创建消息处理器Message Handler,转发消息等)。此外,这种架构为测试工作也带来了很多不方便的因素,例如当某些测试用例(Test Cases)需要涵盖多个微服务的业务时,就需要关注弱一致性分布式事务的执行结果,而这往往是比较复杂的。更进一步,这种测试工作还需要多个团队的协调才能顺利进行,当各个团队分布在全球各个国家各个地区时,协调工作更是变得复杂甚至难以进行

  • 在生产环境中部署、安装和管理基于微服务架构的系统也不是件容易的事情。这需要客户方有着较强的专业技术背景和解决问题的能力。当然,一种更好的方式是以SaaS的方式直接将服务提供给消费者

  • 较多的资源消耗。出于隔离和容错需要,微服务有可能被部署为N个实例,每个实例运行于独立的虚拟系统中。假设部署策略不当造成系统资源存在一定的浪费,那么这种浪费也有可能被扩大N倍

有关微服务架构的内容暂时就写这么多吧,微服务架构现在比较火爆,大家也可以直接上网查阅相关资料,英语比较好的朋友建议直接上英文网站去搜索学习,有很多精华文章和精彩讨论。架构本身就是仁者见仁智者见智,不同的人有不同的理解,产生了不同的观点,有些观点可能在有些场景下更为合适,但换个场景又体现了它的弱势。但不管怎样,我想说的是,无论选择什么架构,它总有优缺点,架构设计的难处就在于如何选择最为合适的模式、方法、技术来完成一整套系统开发的解决方案。更多情况下,整个应用系统更有可能是融合了多种技术多种架构风格的“生态圈”。对于你现在正在开发的项目,或许使用经典的三层架构最为合适。

WeText项目

有理论还需要实践。为此,我花了两周的业余时间,使用Visual Studio 2015开发了一个案例项目:。这个案例项目的业务还是很简单的:用户可以注册、登录,登录后可以修改个人信息,然后可以创建一些自己的Text(就是含有标题和文本内容的小笔记),还可以发送加好友申请给其他用户,等对方接受邀请后,可以将自己的Text分享给对方。到我写本文为止,Text分享部分还没有完成,但其它业务部分基本已经走通,可能还有不少Bug。

看到这里,你肯定会要吐槽了,这么简单的系统还需要花两周,搞出这么大动静,还有这么多Bug,居然还没搞完!是的,目前还不太完善,为什么?因为架构复杂,我是边思考边设计边Coding,或许使用CQRS的微服务架构并不适合这样的应用系统,甚至DDD也未必有用武之地。在这个项目上采用这么个架构风格,老实说,我只是为了实践一下。到目前为止,这个项目还有以下不足之处,还请各位读者忍耐一下。当然,它是开源的(Apache 2.0 License),你觉得没有尽兴的地方也欢迎参与讨论和贡献,提交Pull Request给我就行了。

  • CQRS的查询部分采用了关系型数据库,数据库访问层面没有使用ORM,仅实现了模式,但Table Data Gateway的实现是单表型结构,跨表查询无法完成JOIN操作:有兴趣的朋友可以基于已有的WeText项目自己实现另一套基于ORM的查询机制

  • 虽然Web程序主页上宣称采用了Event Sourcing,但实际上我没有在Event Store中记录任何事件,只是将聚合的最终状态保存在Event Store中(出于时间考虑,否则再搞一个月也不一定完得成,时间精力耗不起啊)。CQRS没有Event Sourcing,Oh my god!不过别惊讶,CQRS不一定非要采用Event Sourcing:有兴趣的朋友可以基于已有的WeText项目自己实现Event Sourcing的功能,但别忘了将Snapshot也一并搞定,这个非常重要!你还可以在WeText上使用成熟的Event Store框架来完成这部分功能。有结论了别忘了分享出来

  • CQRS的命令部分由RESTful API封装。由于命令执行是异步的(仅保证),而RESTful API是同步的,导致RESTful API无法返回命令执行的最终结果。我在考虑是否还需要引入诸如Akka这样的基于Actor模型的方案来解决这样的问题,但也不一定有效。还在寻求解决方案。有兴趣的朋友可以继续深入地考虑这个问题

  • 异常处理部分相对较弱:这部分我会继续加强

  • 前端界面(WeText.Web项目)相对较丑,也有一些缺陷,就是简单的使用ASP.NET MVC 5结合Bootstrap做的,没有使用TypeScript+AngularJS、React甚至是jQuery搞一些高大上的用户体验,基本满足对后端业务的支撑。有兴趣的朋友可以扔掉WeText.Web项目,仅使用WeText提供的服务自己开发自己的前端界面

  • 暂时还没有完全验证在云端的部署是否可行,理论上可行,但没有完全验证,等有结论了我再另外发文介绍吧

整体架构

首先,让我们从整体架构角度来了解一下WeText项目的整个结构,以及它所包含的各个组件。

上图中,蓝色部分表示与领域相关的概念,诸如聚合、规约、事件、Saga、仓储等;黄色部分表示微服务,目前有Accounts、Texting以及Social三个微服务;灰色部分表示基础结构层设施,包括基于Owin的Web API宿主程序、消息队列、Event Store以及数据库等;浅粉红色色块表示一个服务宿主进程(Service Host)。

  • 客户端程序通过RESTful API(Web API)将命令请求发送到服务端

  • 服务端通过API Gateway或者Load Balancer将请求转发到相应的微服务实例(API Gateway和Load Balancer没有体现在上图中,那是另一件事情,今后我会讨论)

  • Web API Controller将请求转换为CQRS的Command,派发到Command Queue

  • Command Handler获得Command消息,通过Repository访问Domain(这个过程会牵涉到Snapshot),执行命令操作

  • Repository在保存聚合时,会将操作所产生的事件存储到Event Store(这个过程会牵涉到Snapshot),同时将领域事件派发到消息队列Event Queue

  • Event Handler在获取到消息后,执行消息相关操作,在Event Handler中会触发Saga状态的转换,Saga状态变化后,会产生状态变化领域事件,这个领域事件的Event Handler又会触发另一个Command的发生(理论上应该是在Saga中直接触发Command,但Saga本身也应该是聚合根,因此由Saga直接操作Command派发明显不合理,这部分内容之后再讨论)

  • Event Handler会根据需要同时更新Query Database(也就是上图中normalize的步骤)

  • 客户端的查询请求会直接经由RESTful API(Web API),通过Table Data Gateway访问Query Database直接完成

对于Service Host,在上图中它同时为三个服务实例提供了宿主环境。事实上,WeText的设计允许Service Host仅宿主其中的某个或者某几个实例,而多个Service Host又可以被部署到多个不同的物理机器上,例如:

于是,在整个环境中,我们有一个Accounts服务实例、两个Texting服务实例和两个Social服务实例。至少在单点失败和服务器资源平衡方面提供了解决方案,当然也带来了不少问题。比如:

  • 如何配置API Gateway的路由,使得客户端请求能够根据指定的策略派发到相应的微服务实例上完成处理?

  • 对于具有多个实例的微服务,基于Pub/Sub的消息订阅机制如何避免事件或者命令的重复处理?

这些问题我会在后续文章中讨论。

另外,你会认为基础结构层设施存在单点失败可能,比如RabbitMQ或者数据库。其实这些成熟的产品都有自己的解决方案,比如做数据库集群。或者干脆直接使用AWS或者Azure提供的PaaS服务(消息队列、存储等)。因此,解决这个问题并不困难。

开始

为了能够更好地了解WeText整个项目的架构和所使用的技术,建议提前对以下内容做些了解:

  • 领域驱动设计(DDD)

  • 命令查询职责分离(CQRS)

  • 微服务架构(MSA)

  • 消息通信模式():可以参考RabbitMQ官方的学习资料:

接下来,重要的事情,算了,就说一遍吧,请使用git将项目代码克隆到本地:

(c)2006-2024 SYSTEM All Rights Reserved IT常识