最近出于工做须要,了解了一下微服务架构(Microservice Architecture,MSA)。我通过两周业余时间的努力,凭着本身对微服务架构的理解,从无到有,基于.NET打造了一个演示微服务架构的应用程序案例,并结合领域驱动设计(DDD)以及命令查询职责分离(CQRS)体系结构模式,对事件驱动的微服务系统架构进行了一些实战性的探索。现将本身的思考和收获整理成文,分享给你们。php
微服务架构
在介绍源代码以前,我仍是想谈谈微服务架构,虽然网上有不少有关微服务架构的讨论,但我以为在此再多说一些仍是有必要的。大师级人物Martin Fowler在他谈论微服务的我的主页上提到,微服务并无一个很是明确的定义。事实上有不少种分布式系统的实现均可以被当作(或者说勉强当作)是面向微服务架构的。就我我的而言,我以为微服务架构应该知足如下几个特征:html
- 整个系统被分为多个业务功能相对独立的一体化架构(Monolithic Architecture,或称单一化架构)的应用程序(也就是所谓的“微服务”),每一个微服务一般遵循标准的分层架构风格或者基于事件驱动的架构风格,可以对本身相关的领域逻辑进行处理,使用本地数据库进行数据存储,并向上层提供相对独立的API接口或者用户界面。每一个微服务还可使用诸如缓存、日志等基础结构层设施,但若是是与其它的微服务公用这些设施,则该基础结构层设施须要知足下面的第三条特征
- 各个微服务之间可使用如下方式进行通讯(参见:http://howtocookmicroservices.com/communication/)
- 同步方式:最为常见的是基于RESTful风格的API,也能够是跨平台、跨语言的Apache Thrift IDL
- 异步方式:使用轻量级的消息通讯机制,好比RabbitMQ、Redis等
- 整个系统是“云友好”(cloud-friendly)的。所谓的“云友好”,是指:
- 针对每一个微服务,都应该避免单点失败的可能。例如针对一个系统中某个微服务A,须要有至少两个(或以上)的运行实例,并由API网关(API Gateway)或者负载均衡器(Load Balancer)根据必定的规则(好比各个A的运行实例的健康程度等)未来自客户端的服务请求分配到任意一个A的运行实例上完成处理
- 针对每一个微服务,管理员能够根据一些特定的实时技术指标对这些应用程序的部署进行调整。例如,购物网站的查询服务负载明显要高于订单管理服务,那么管理员能够根据实际状况,增长查询服务的部署量(好比部署3个查询服务的实例),同时减小订单管理服务的部署量。与整个系统单一采用一体化架构相比,这样作的好处是显而易见的,它可以充分利用云端服务器资源,使得每一个微服务都可以运行在合理的资源配置状态下,减小资源浪费
- 公有基础结构层服务设施也应该知足避免单点失败的条件,例如数据库服务须要配置Replication/Clustering,消息队列也须要使用相似的fault tolerance策略
- 基于“云友好”的需求,衍生了一大堆的部署和运维问题,好比微服务自己的配置模式(每台机器配置多个实例?仍是每台机器配置一个实例?仍是每台虚拟机配置一个实例?又或者是将实例配置到相似Docker这样的容器中)?消息队列如何配置才能支持同一个微服务的多个实例不会重复处理相同的消息?基于RESTful的通讯如何让客户端找到动态改变的API地址?等等。我想,这些问题并无一个特定的答案,仍是须要根据实际状况来进行断定
相比传统的一体化架构系统,微服务架构系统有着如下一些优点:前端
- 每一个微服务都相对较小,这样更加便于开发和调试
- 每一个微服务都相对独立,这样不只可使开发人员仅关注在某个业务处理部分,并且还能够针对每一个微服务本身的特征,采用不一样的技术实现(好比部分微服务使用C#实现,部分使用Java或者Python等)
- 这种独立性使得微服务在容错隔离方面也有很好的表现:好比某个微服务出现了crash等问题,不会致使整个系统不可用,这符合BASE(Basically Available, Soft-state, Eventually consistency)理念
- 因为相对独立,微服务架构的设计可以更方便地部署到云环境中
- 微服务的独立性还为敏捷开发提供了很好的支持。好比每一个服务均可以单独开发单独部署,同时项目团队还能根据成员自己的技术专长来平衡开发和测试资源
固然,它也有一些不足:mysql
- 开发人员须要应对由分布式架构带来的复杂性。好比若是微服务间采用异步的消息通讯机制进行通讯,那么就须要遵循由这种消息机制所引入的开发模式(建立消息处理器Message Handler,转发消息等)。此外,这种架构为测试工做也带来了不少不方便的因素,例如当某些测试用例(Test Cases)须要涵盖多个微服务的业务时,就须要关注弱一致性分布式事务的执行结果,而这每每是比较复杂的。更进一步,这种测试工做还须要多个团队的协调才能顺利进行,当各个团队分布在全球各个国家各个地区时,协调工做更是变得复杂甚至难以进行
- 在生产环境中部署、安装和管理基于微服务架构的系统也不是件容易的事情。这须要客户方有着较强的专业技术背景和解决问题的能力。固然,一种更好的方式是以SaaS的方式直接将服务提供给消费者
- 较多的资源消耗。出于隔离和容错须要,微服务有可能被部署为N个实例,每一个实例运行于独立的虚拟系统中。假设部署策略不当形成系统资源存在必定的浪费,那么这种浪费也有可能被扩大N倍
有关微服务架构的内容暂时就写这么多吧,微服务架构如今比较火爆,你们也能够直接上网查阅相关资料,英语比较好的朋友建议直接上英文网站去搜索学习,有不少精华文章和精彩讨论。架构自己就是仁者见仁智者见智,不一样的人有不一样的理解,产生了不一样的观点,有些观点可能在有些场景下更为合适,但换个场景又体现了它的弱势。但无论怎样,我想说的是,不管选择什么架构,它总有优缺点,架构设计的难处就在于如何选择最为合适的模式、方法、技术来完成一整套系统开发的解决方案。更多状况下,整个应用系统更有多是融合了多种技术多种架构风格的“生态圈”。对于你如今正在开发的项目,或许使用经典的三层架构最为合适。linux
WeText项目
有理论还须要实践。为此,我花了两周的业余时间,使用Visual Studio 2015开发了一个案例项目:WeText。这个案例项目的业务仍是很简单的:用户能够注册、登陆,登陆后能够修改我的信息,而后能够建立一些本身的Text(就是含有标题和文本内容的小笔记),还能够发送加好友申请给其余用户,等对方接受邀请后,能够将本身的Text分享给对方。到我写本文为止,Text分享部分尚未完成,但其它业务部分基本已经走通,可能还有很多Bug。git
看到这里,你确定会要吐槽了,这么简单的系统还须要花两周,搞出这么大动静,还有这么多Bug,竟然还没搞完!是的,目前还不太完善,为何?由于架构复杂,我是边思考边设计边Coding,或许使用CQRS的微服务架构并不适合这样的应用系统,甚至DDD也未必有用武之地。在这个项目上采用这么个架构风格,老实说,我只是为了实践一下。到目前为止,这个项目还有如下不足之处,还请各位读者忍耐一下。固然,它是开源的(Apache 2.0 License),你以为没有尽兴的地方也欢迎参与讨论和贡献,提交Pull Request给我就好了。github
- CQRS的查询部分采用了关系型数据库,数据库访问层面没有使用ORM,仅实现了Table Data Gateway模式,但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项目的整个结构,以及它所包含的各个组件。sql

上图中,蓝色部分表示与领域相关的概念,诸如聚合、规约、事件、Saga、仓储等;黄色部分表示微服务,目前有Accounts、Texting以及Social三个微服务;灰色部分表示基础结构层设施,包括基于Owin的Web API宿主程序、消息队列、Event Store以及数据库等;浅粉红色色块表示一个服务宿主进程(Service Host)。mongodb
- 客户端程序经过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整个项目的架构和所使用的技术,建议提早对如下内容作些了解:
接下来,重要的事情,算了,就说一遍吧,请使用git将项目代码克隆到本地:
git clone https://github.com/daxnet/we-text.git
而后直接使用Visual Studio 2015打开WeText.sln文件便可。打开代码后,先别急着运行,让咱们先了解一下项目结构。
- Services目录:包含了三个微服务的项目:Accouts(用户帐户微服务)、Social(社交微服务)以及Texting(小笔记微服务)
- WeText.Common项目:包含了整个解决方案的基础库
- WeText.Domain项目:领域模型与命令、事件定义
- WeText.DomainRepositories项目:领域仓储的具体实现(MongoDB实现)
- WeText.Messaging.RabbitMq项目:基于RabbitMQ的消息系统实现
- WeText.Querying.MySqlClient项目:基于MySQL的Table Data Gateway实现,用于提供对MySQL数据库的CRU操做(暂不支持Delete)
- WeText.Querying.PostgreSQL项目:基于PostgreSQL的Table Data Gateway实现,用于提供对PostgreSQL数据库的CRU操做(暂不支持Delete)
- WeText.Service项目:微服务的宿主程序,启动后是一个控制台服务端程序(运行时先启动此项目)
- WeText.Tests项目:基于NUnit的单元测试项目,请直接忽略
- WeText.Web项目:前端用户界面项目,在WeText.Service项目启动后,再启动本项目
- 此外,在we-text根目录下,还有一个scripts的子目录,里面包含了针对MySQL和PostgreSQL数据库的初始化脚本(在写此文时,PostgreSQL脚本未加入,以后会加),在后面的安装步骤中会用到
外部依赖项(External Dependencies)
首先,WeText仅依赖于一些基础结构层设施所需的相关库,包括:
- MySql.Data
- RabbitMQ.Client
- MongoDB
- Npgsql
- Autofac相关
- Owin相关
- Newtonsoft Json
- log4net
- 与ASP.NET MVC相关的库
除此以外,没有使用任何应用层的开发框架和代码库,全部的代码都是原创而且包含在整个WeText的解决方案中。
其次,服务端基础结构层彻底选用诸如Owin、MySQL、RabbitMQ、PostgreSQL等这些可以跨平台的项目和产品,如此一来整个WeText服务端可以彻底部署在Linux环境中(其实这也是我想实践的一个部分,验证基于Mono的.NET服务器程序在Linux系统中是否有出色的表现)。没有使用SQL Server、Entity Framework这些目前更适合运行于Windows平台的产品。
安装与运行
首先,为了方便起见,强烈建议将全部的服务和程序安装在同一台机器上。请按如下步骤准备系统环境:
- 下载源代码(参考上面的git命令)
- 下载并安装RabbitMQ,安装过程使用默认配置(包括服务端口等等),有关RabbitMQ的安装,请参见:https://www.rabbitmq.com/download.html
- 下载并安装MongoDB,安装过程使用默认配置(包括服务端口等等),有关MongoDB的安装,请参见:https://docs.mongodb.org/manual/installation/,如安装后须要更改WeText的MongoDB配置,请移步到WeText.DomainRepositories项目下的MongoSetting.cs文件(写本文时仍是hard code在代码里,从此会移到App.config中)
- 下载并安装MySQL Community Edition(包含服务器和客户端),安装过程使用默认配置,root密码请采用P@ssw0rd。有关MySQL的安装,请参见:http://dev.mysql.com/doc/refman/5.7/en/installing.html,若是安装后须要更改WeText的MySQL配置,请直接修改WeText.Service项目的App.config文件
- 若是你打算使用PostgreSQL做为查询数据库,那么你只须要安装PostgreSQL便可,不须要安装MySQL。安装过程也请使用默认配置
- 使用we-text项目文件夹下scripts目录下的SQL脚本初始化对应的数据库,目前PostgreSQL的脚本尚未加进来,以后会添加
环境准备好以后,就能够试着启动项目了。
在Windows系统中启动并调试项目
- 使用Visual Studio 2015打开WeText.sln文件
- 启动WeText.Service项目,应该能看到如下画面:
- 启动WeText.Web项目,应该能看到如下画面:
- 尝试点击Register注册本身的帐户并登陆
在Linux中编译并启动服务器程序
注意:我目前尚未来得及测试使用WeText.Web站点访问部署在Linux上的服务器,仅试图在Linux环境中编译和启动服务器程序。Web站点程序(WeText.Web)自己暂不打算运行于Linux环境,之后能够尝试。
- 安装Mono,请根据该页面完成Mono的安装:http://www.mono-project.com/docs/compiling-mono/linux/。建议直接从Release Package安装,能够到http://download.mono-project.com/sources/mono/下载最新版本的Mono。WeText须要.NET Framework 4.6.1和C# 6.0的支持
- 一样须要根据上面的步骤准备系统环境,包括RabbitMQ、MongoDB以及查询数据库的安装和初始化
- 使用上面的git命令下载源代码
- 因为目前nuget在Mono下的支持仍是有些问题,有些package没法下载,建议能够先在Windows下编译WeText,而后将下载的packages目录上传到Linux中we-text\src目录下
- 进入we-text\src目录,使用该命令完成编译:xbuild /p:TargetFrameworkVersion=v4.6.1 /p:Configuration=ServerDebug WeText.sln。编译可能会出现部分警告,暂时请直接忽略
- 进入we-text\bin目录,执行./WeText.Service.exe命令,应该能看到如下画面:
- 如需尝试从WeText.Web站点访问Linux上的服务,暂时请在WeText.Web项目中查找http://localhost:9023/字符串,并将localhost替换成Linux主机的服务URL便可。
总结
本文首先简要介绍了微服务架构,并从总体架构、代码库的使用、环境准备和编译部署等方面介绍了WeText这个基于.NET实现的DDD、CQRS和微服务架构的演示案例。对微服务感兴趣的朋友欢迎试用本案例源代码,并欢迎参与更深刻的探讨。WeText目前仍是不太成熟,我也会逐步去完善这个案例,同时也会在此过程当中分享本身的心得体会,欢迎你们关注。