导读:本文以某汽车产品代理商的线上销售和售后平台为例,介绍它在微服务转型过程中遇到的种种情况,希望能以此作为前车之鉴,让后来者避开不必要的趟坑。
前言
近年来,服务化和微服务的架构随着线上业务对响应变化和发布频率要求的不断提高已经变得日益常见。像DevOps和Docker等理念和技术的成熟也在为这一趋势推波助澜。许多企业对微服务思考的关注点也从最初的“该不该用”、“该如何用”逐渐转向“如何评价服务拆分的好坏”、“服务拆分后的数据如何治理”这样更加具体而实际的方面。纵观社区里与微服务有关的话题,既有一些适合大众阅读的概念入门科普文章,也有一些像“架构去中心化”、“消息异步解耦”、“最终一致性补偿”这类专业性的技术讨论,但却很少看到能够比较深入的介绍微服务实施经验,说清楚“什么地方会有什么坑”这类问题的实践性内容。
若将微服务的方方面面铺开,将是一个非常庞大而复杂的知识体系。万事开头难,如何迈出服务划分的第一步是对于那些从未采用过微服务的项目首先要面对的问题。虽然服务的划分本来是一件十分凭借架构师个人经验和对业务理解的主观工作,但其中仍然有些规律可循。根据服务的类型特点,可以有以下几种常见的方式:
根据业务进行建模,依据业务领域的边界划分,这也是当下微服务社区十分推崇的领域驱动设计(Domain Driven Design,简称DDD)方法
根据资源使用类型划分,这种方式主要使用在对硬件资源需求很高、或对特定硬件类型/区域地址等存在依赖的大型服务系统,例如系统的某些功能非常消耗CPU、或是某些功能必须在有加密狗设备的机器上运行。此时可以依据系统各部分对不同资源的需要作为边界进行最安全的划分,以最大化运行效率
根据数据边界划分,直接从数据表结构或数据源着手,依据数据归属关系界定服务。这种划分方法简单粗暴且能避免数据耦合,但容易导致潜在隐患,甚至可以被认为是一种反模式。仅仅对于强数据驱动的系统,如某些报表系统和多数据源ETL系统等适用,在实际案例中很少见
从现实上来说,我们平时遇到的绝大部分系统都是需要为特定终端用户群体服务的。因此,采用基于业务领域的建模的方法通常都会是个不错的选择,它也是目前在划分服务问题中的最主流的方法。不过即使有了理论指导的支撑,服务划分里的利弊权衡并不是非黑即白的事情,依然存在不少模棱两可之处,只有在犯下一些错误后才能摸索出适合自己系统的方法。本文将会聚焦在这些点。
没有什么能比一个具体而贴近真实的案例更具有代表性了。因此,我们将剖析一个传统行业企业的线上业务:某汽车产品代理商的线上销售和售后平台,介绍它在微服务转型过程中遇到的种种情况,希望能以此作为前车之鉴,让后来者避开不必要的趟坑。这个案例中讲述的场景原型并非完全来自同一个项目,而是从我们过去一年中所经历的多个项目的实际场景抽取出来的,去除了其中的敏感信息,并添加了基于真实情况的适当演绎,使之更加完整。简单介绍一下案例的背景:这是一家颇具规模的汽车代理商企业,承接多个国际一线品牌汽车的销售和周边资源整合的业务,具有庞大的实体店网点。其线上的IT业务系统已经存在了十余年,提供的功能从最初的进销存管理、客户信息管理等到现在的面向消费者客户的服务系统,十分复杂。系统中的线上销售和售后平台部分是相对比较新、且需求变化特别迅速的部分,也是目前出问题最频繁、收到抱怨最多的部分。
下面我们将从几个方面展开这个话题。
不要从数据库开始建模
传统软件开发中,数据模型被认为是整个系统的核心,业务逻辑仅仅是对数据的CRUD加上简单的计算呈现。有些项目团队的架构评审会花很多时间来讨论系统庞大的ER图(实体关系图)设计。但在微服务架构设计时,ER图并非最佳选择,特别是在服务划分时采用ER图进行建模甚至是十分有害的。
在领域驱动设计的实践中,有一个和ER图建模有些相似的环节,叫做“领域建模”。相比ER图建模通常只有开发人员参与,并从数据表的角度考虑模型的方法,领域建模的过程需要由产品的业务人员和核心开发人员共同参与,先梳理用户场景,然后从业务领域角度,逐步确定场景中的实体、关联和聚合等元素。其中的“实体、关联”与ER图中的“实体、关系”有些相似,但含义并不一致。领域模型中的“实体”本质上是业务场景中需要被持久化存储的对象,但存储方式不一定是数据库,更不一定是关系型数据库,而ER图中的“实体”最终对应的就是关系型数据库的表。领域模型中的“关联”是两个实体在业务上下文之间有业务含义的联系,而ER图中的“关系”只的是两张表之间的一个关联外键。
那么,使用ER图给微服务建模会存在什么问题呢?
首先,ER图设计时假定了使用的是关系型的数据库。微服务的一个特点在于它支持异构架构,系统的不同部分,依据实际需要可选择不同的编程语言、框架以及数据库的类型。关系型数据库采用平面表的结构,如果有两类嵌套关系的对象,只能使用关联表来表达两个实体之间的关系,然后通过复杂的SQL语句在查询时将多个表拼成更大的平面表,最后在业务代码里再分解到各个独立的对象里去。这些单独的表又可能在其他地方与另一个表存在关联查询。这样设计出来的表结构的冗余很低,且使用非常灵活,但正是这种灵活性往导致系统中多个业务逻辑表出现错中缠绕的关系,从而使剥离单独服务进行异构设计变得困难。
其次,ER图还会导致实体相似的不同业务逻辑在设计时被耦合在一起。举一个具体的例子,在汽车代理销售系统中,不同品牌汽车的购买流程后端实际上分别对接的是完全不同的分销渠道系统和逻辑流程,但它们在用户视图上所需要的信息比较一致,因此存储的数据结构也比较相似。这个系统在最初设计时采用了ER图的方式建模,由于ER图模型不关心业务层面上的东西,不同品牌汽车的实体数据看上去都是一种类型的数据,仅仅是一个品牌字段的差异而已,因此被理所当然的设计成了同一张表。上层逻辑实现的时候使用了大量的if-else语句来区分各种品牌的购买流程,结果使得多条完全不同的业务线糅合在同一个上下文里,后来的开发非常容易在这里错改、漏改代码。若当初使用的是领域建模,则不同的购车流程会自然的被划分到各种不同的用户场景,即使它们在数据结构上看起来基本相同,也会被识别为两个独立的上下文,这就会使得未来划分服务时能够更加容易。
最后,ER图设计的架构会使得系统的模块之间倾向于使用数据库集成,而非API集成。在ER图中没有明确划分模块和表的所有权关系,所有的数据表对所有的模块都是可见的,倘若不加额外约束,各个模块便都会轻易的读写其中的内容。数据集成并不会直接导致服务无法拆分,但由于数据的所有权不清晰,十分容易引发的意想不到的状况。还是举销售平台的例子,在ER图建模得到的模型中,有一张与购买记录相关的表。在一次销售业务的代码更新中,对购买结果的增加了字段,在测试过程中没有发现问题,结果上线几天以后,由于售后服务也在修改这个表而导致出现了脏数据,造成难以排查的故障。
当然,我们并非要完全否认ER图建模的价值,只不过通过数据库角度建立模型的过程容易倾向于设计出庞大的单体应用,因而不太适应于服务划分的目的。
端到端的划分服务
在拆服务时要端到端的划分,这是我们在设计微服务时经常听到的一句话。端到端的划分,指的是一个服务负责一个业务领域,这个功能领域的所有逻辑、数据都归它管,这有利于微服务的数据治理。与之相对的是MVC那样的横向服务划分,将所有数据归一块,所有逻辑归另一块,特别是在跨团队管理的情况,横向划分服务会带来十分高昂的协调和联调成本。
道理不必多说,还是讲个例子吧。在汽车销售平台的最初架构中,使用了典型的MVC三层结构,由于人比较多,分成了前台组、后台组,就时不时要出现新开发一个功能,一动底层数据结构,结果上层的另一个不相干页面挂了,一查发现原来那这个页面间接的用了同一个数据模型。比较典型的例子是,有一回负责后台的小组调整了汽车销售服务里面的汽车参数信息相关的对象结构,结果一上线,销售功能正常,试驾服务的页面挂了。原来是试驾功能的前端开发人员在处理汽车信息时候,看到销售模块有现成功能,就直接拿来复用了。这便是服务上下层分团队开发导致的问题。
此外,横向划分服务也不利于系统的开发效率的提升。不同业务服务对功能发布频率的的需求是不一样的,比如试驾平台经常推出新的优惠促销活动,需要尽快进行一次版本更新,于此同时售后服务有一个需要和第三方联调的功能也已经差不多完成并提交到代码仓库了,但由于需要协调第三方系统的时间,最近还不能够上线,此时两边的业务主管就要开掐了。类似这样的情况其实经常发生,通常使用特性分支、特性开关等流程或者技术手段能够规避一部分业务开发进度不一致的风险,但若要从根本上解决这种问题,还是需要端到端的按业务来划分服务。
最后,横向划分模块对于问题的追踪调试也不友好,几乎每次事故调查总是要穿插涉及在几个团队之间不停的协调开会,因为一个完整业务流总是要贯穿前后几个层的功能。
需要指出的是,端到端划分服务并非是说服务与服务之间都是平级的。实际上,服务之间可以再聚合成更高层级的组合服务的,以及在最顶端的API Gateway也可以算是一类服务。只不过,在核心业务层的这些服务,每个都单独提供了某项特定的业务价值。
识别核心业务服务
微服务的架构通常并非是一开始就重头设计出来的,而是先有整块的单体架构,随着业务的复杂度上升,才逐步拆分出来。业务领域建模除了能用来指导适当的服务划分,另一个重要的作用是让工作聚焦到核心的业务服务中。
无论多复杂系统都是为特定业务价值而存在的。在系统的实现中必然会存在与核心业务最相关的部分、辅助核心业务的部分、和非核心业务关系的部分。它们在领域驱动设计的术语中称为“核心域”、“支撑子域”和“通用子域”。在进行领域建模的时候就应该顺便识别出系统里的关键领域。将系统的业务领域罗列出来然后划分出重要性,这件事情听起来似乎是多此一举,甚至有点荒唐,但对于复杂系统,实际去做这件事带来的价值可能远比它看起来更大。
首先,当我们将一个复杂的系统的各种业务仔细清点到台面上以后,得到的列表往往会比许多人最初想象得长得多,它能提醒我们系统的复杂度是否已经过高了。其次,列举业务的过程也是开发者与业务人员沟通的过程,来自不同业务线的代表也许会为某部分业务的价值点发生争执,或是提供一些许多开发人员此前并不了解的细节信息,将这些问题当面讨论清楚并非什么坏事。此外,当我们真的去仔细思考一个业务系统的核心价值时,也许会得出使人意想不到的结果。
继续举例子,在汽车的销售和售后平台中,通过业务建模,可以划分出许多子领域:
在线购车(渠道A)
在线购车(渠道B)
在线购车(渠道C)
试驾预约
售后服务
用户反馈
订单系统
促销活动
车辆市价信息
车辆参数信息
4s店信息
用户信息管理
…
(说明:这里渠道指的是销售平台对接的一个第三方系统,其中每个渠道可以对应多个汽车品牌)
然后我们要从中识别出业务中的核心域。必须强调,服务领域划分仅仅是代表服务对系统关键业务贡献的价值,处于核心域中的那些服务应该是该系统业务成功的主要促成因素,从战略层面讲,企业应该在自己的核心领域上具有一定壁垒优势。经过讨论,开发者和业务人员最终得出让人大跌眼镜的结论,核心域部分的服务只有“试驾预约”和“售后服务”,因为这两项才是该企业最具竞争力的业务。而看起来十分重要的“在线购车”服务,由于并不具有特别的行业竞争优势而被划到了支撑子域中。正确的服务划分定位将对系统未来的发展策略产生积极的影响。
在这个系统中的“车辆市价信息”、“车辆参数信息”、“4s店信息”等服务都被划归到了通用子域。在通用子域中的服务并非最没有价值或是复杂度最低,而只是说明系统在这些服务领域中通常不具优势,因此这部分功能完全可以考虑外包开发或者购买第三方的现成服务。
使用符合业务结构的API
前面介绍业务建模的时候我们强调了使用“API集成”的必要性,以及它的反面形式“数据库集成”所带来的问题。在微服务的架构中,服务的技术选型可能是异构的,API的实现也会各有不同。除了Web应用比较流行的RESTful标准,还有像SOAP、ProtolBuf、MessageQueue等不同的协议与格式标准,它们都可以被作为服务之间通信的API。不同的API设计对服务使用的体验差别会很大,除去技术原因对API协议的倾向性,在设计和评价API方面依然有许多值得注意的地方。
一个好的API设计应该在接口的元数据中向用户提供尽可能多的有意义的业务信息,这里指的元数据包括例如API的名称、参数、标签等等用户在不需要专门查看文档就可以看到的内容。通常在各种不同的API协议里,总会存在一个相似的概念,就是目标路径。比如RESTful的URL地址,ProtolBuf的消息类型嵌套结构,MessageQueue名称里用斜线划分Topic路径的惯例等。以RESTful标准为例,可以试比较下面两种URL地址的差异。
第一种:/users/123/orders/123
第二种:/orders?id=123&user_id=123
显然前一种地址包含的信息更多,它告诉了访问者订单(orders)是属于用户(users)这个实体中的一个子实体,并且包含了一个潜在业务规则,即如果ID为123的这个用户不存在了,那么查询他下面的所有订单信息也是不具有业务意义的。实体之间的嵌套关系可以通过领域建模过程中的聚合识别。当然,这种URL结构有时会导致很长的API路径,但相比它所带来的业务语义,我们认为还是值得的。
类似的语义化例子还有例如:
PUT /services/questions/123
PUT /services/questions/123/comments/123
这是售后服务部分的两个API,分别用来更新提问的内容和提问的评论内容,它们也是按层级组织的。
有些通信协议中还提供了其他可以表明业务语义的元素。比如RESTful中使用GET/POST/PUT操作语义(查询/创建/修改),以及HTTP返回值中的语义信息等。在实际设计API时充分利用这些协议特性,能够使服务变得更加易用。
尾声
在这篇文章里,我们列举了一些微服务划分的实践中常见的反例和值得注意的问题,希望能为读者设计微服务架构时扫清一些阻碍。实际上,本文中提到的许多概念,包括微服务和领域驱动设计,服务的划分都只是其中的冰山一角,在冰水之下还有更多的内容值得我们在实践中去探索。
网友评论