网站可扩展架构设计——微服务

从公众号转载,关注微信公众号掌握更多技术动态

---------------------------------------------------------------

一、微服务的引入

当项目开始进行多个功能的开发,需要大规模地扩张开发人员,如果这个时候继续采用单体应用架构,多个功能模块混杂在一起开发、测试和部署的话,会导致不同功能之间相互影响,一次打包部署需要所有的功能都测试成功才能上线。

1.引入微服务的原因

(1)降低系统的耦合性

DRY: DON't repeat yourself避免重复代码,服务内部不要违反DRY,跨服务之间可以适当违反DRY

每一个服务都是一个独立的个体,服务间通过网络调用通信,加强服务的隔离性,避免了服务的高度耦合。一个服务的改动不会影响到其它服务,对于一个服务来说应该考虑什么时候暴露什么时候隐藏,过多暴露会导致服务间的耦合性加强,同时调用的api应该与具体技术无关。

  • 每个微服务都很小,这样能聚焦一个指定的业务功能或业务需求。微服务是有功能意义的服务,易于被一个开发人员理解,修改和维护。

  • 微服务能使用不同的语言开发,不同技术开发,可存储到不同数据库。在影响小的服务上使用新技术,体验技术的好处的同时不会影响整个系统的运行。

  • 每一个微服务可以部署在不同的机器上,由于分散了集齐的压力所以微服务能部署中低端配置的服务器上。同时简化了部署,只需要重新部署修改的服务即可。

  • 更容易对服务进行扩展,当系统需要扩展的时候,不需要更改整个系统,只需要找到扩展需求相关的服务进行修改,这样避免了单体架构修改一处,整个系统出现错误的情况。一个服务产生故障不会影响到其它服务。

(2)服务可以得到重用,服务可组合,可重用

(3)人性

不过单从人性角度思考,如果你独立负责一个核心微服务的话,那么你的安全感和自尊都是最大化的。唯一能够支持多名研发维护同一个服务的理由,就是服务本身对公司的价值太大,它上面承载的业务量,对稳定性的要求,对服务连续性的要求,大到可以忽略研发资源的成本的时候;或者是从性能角度来看,服务无法拆分,它得复杂度大到可以支持多个研发维护的时候。在这种情况下,多个研发维护同一个服务的布局才是合理的。

但是,我们大多数人所在的公司并不是像阿里腾讯这样处于垄断地位的互联网公司,甚至哪怕你在阿里和腾讯,你也很可能不在公司最主流的现金流业务中,那么多个人维护一个服务的配置,我认为就不合理。

当然,有些人认为一个服务上只有一名研发会有稳定性风险,我觉得这个理由不成立。微服务的隔离性和有分布式架构带来的稳定性,是能抗住个别同学离职的压力的。而且服务粒度越小,这种承受能力越大。

2.微服务架构的缺点

随着服务的不断增大也会引发更为复杂的问题。

  • 服务的调用链长,问题定位难(当发生问题的时候,很难确定该问题是由哪个服务产生的)。

  • 多服务运维难度,随着服务的增加,运维的压力也在增大;系统部署依赖问题;

  • 服务的调用链长,性能下降(网络延迟和带宽的影响)

  • 当服务数量增加,若没有服务治理,服务间关系复杂,管理复杂性增加。

  • 没有自动化支撑无法快速交付

  • 需求变更困难,小小的改动可能会影响的范围变大

  • 服务器网络抖动。当用户大量访问时(服务1承受大量访问、服务1内部调用服务2也承受了大量访问)可能会出现某一服务因为访问量过大,响应出现延迟情况,而调用的服务又没有快速得到响应,则会出现时间访问过长情况。

  • 服务器网络繁忙

 服务1大量访问,服务2不能及时响应用户,导致异常出现,网络繁忙

在不使用 RPC 的情况下,代码调用开销仅仅只是一个函数调用的开销,不考虑内联优化的情况下,是一个纳秒级的开销。而将其换成 RPC 调用后,调用开销直接飙升到了毫秒级

解决方案:服务治理中间件(dubbo/SpringCloud) (SOA)

当服务器启动时,服务2会把所有的对象通过 服务治理中间件(dubbo/SpringCloud)注册给注册中心(zookper),而以后每次请求服务2获得访问对象时,就直接从注册中心中获取,不需要再去访问服务2。就解决了服务器网络抖动、网络繁忙的缺点。注册中心是一个数据库,存储我们所有的服务。

3.微服务数据管理

(1)论微服务的数据库管理

通常情况下,在每个服务都有自己的缓存和数据库,并且缓存和数据库是相互独立且透明的,因此,共享缓存与共享数据库是不对的。那如果服务 A 需要获取服务 B 的数据怎么办?服务 B 提供一个获取该数据的 API 接口,而服务 A 通过调用该接口进行业务组装。

(2)微服务的数据一致性

如何保证多个微服务的数据的一致性是一个必须面对的问题。例如,“话题系统”的数据变更的同时要保证“用户动态系统”的数据级联更新。如果,这个时候“用户动态系统”发生宕机,或者网络连接异常、网络超时,就会导致数据的不一致。目前,分布式事务并没有很好的解决方案,难以满足数据强一致性,一般情况下,保证系统经过一段较短的时间的自我恢复和修正,数据最终达到一致。这个自我恢复和修正的方式,可以在每次更新的时候进行修复,或者采取周期性的进行校验操作来保证。

我们还可以引入可靠的消息队列,只要保证当前“话题系统”的可靠事件投递并且消息中间件确保事件传递至少一次,那么订阅这个事件的消费者(用户动态系统)保证事件能够在自己的业务内被消费即可。在消费者(用户动态系统)处理的过程中出现异常,可以将事件放入重试队列并根据具体的策略进行失败重试,如果多次重试失败可以写入错误日志并主动通知开发人员进行手工介入。注意的是,这个过程要保证可靠事件投递与避免重复消费,其中尤其重要是接口要保证幂等性,例如支付系统不能因为重复收到支付事件而导致多次支付。

此外,我们可以采用业务补偿等方式保证数据的一致性。

  • 区分系统调用错误和业务失败:远程调用失败,不代表下游系统没有接收请求,更不能做为业务失败依据,需要严格区分系统调用错误和业务失败。

  • 可重试:任何一行代码执行时都有可能因系统重启而中断,所以需要支持可重试。

  • 异步处理必须增加核对:最终一致性离不开恢复重试策略,也需要有系统间数据核对用于及时发现数据不一致,同时在核对时需要增加处理时效的监控,及时发现长时间未处理成功的数据。

(3)微服务管理

值得注意的是,需要对服务依赖关系进行有效的管理,打造一个有序的微服务体系。否 则的话,东一个服务,西一个服务,这样会让系统变得碎片化,难以维护和扩展。

二、服务之间的协作方式

假设有一个系统,当用户注册一个账号后我们会分别通过短信和邮件的方式通知用户注册成功,系统中存在三个服务分别是账号服务、短信服务、邮件服务。

1.编排

有一个总指挥服务去告驱动各个服务。在这个例子中账号服务就是一个大脑,当用户注册成功之后,账号服务会取调用短信服务和邮件服务给用户发送短信和邮件通知。

优点

  • 总指挥服务可以对当前进行到哪一步进行跟踪

缺点

  • 中心服务承担了太多职责。

  • 这种架构风格容易产生少量的“上帝”服务,而与其打交道的服务则通常沦为营养不良的、基于CRUD的贫血服务。

2.协同(推荐使用)

告知各个服务的职责,服务通过监控事件或者消息订阅来触发对应的操作。在上述例子中,账号服务完成账号的注册之后可能只需要向消息中间件发送一条注册成功的消息,当订阅了这类消息的服务(邮件服务和短信服务)就可以执行已经定义好的业务逻辑了。

优点

  • 可以显著的解耦,只需要发布一个一个的事件即可。如果其他服务关系某个事件的话,只需要简单的订阅即可。

  • 可以降低耦合度,灵活性高。

缺点

  • 完整的业务流程很难从代码中看出来,最好维护一份完整的文档。

  • 需要做一些额外的工作来监控流程是否正常的执行。

3.六种常见的微服务架构设计模式

(1)聚合器微服务设计模式

聚合器调用多个服务实现应用程序所需的功能。它可以是一个简单的Web页面,将检索到的数据进行处理展示。它也可以是一个更高层次的组合微服务,对检索到的数据增加业务逻辑后进一步发布成一个新的微服务,这符合DRY原则。另外,每个服务都有自己的缓存和数据库。如果聚合器是一个组合服务,那么它也有自己的缓存和数据库。聚合器可以沿X轴和Z轴独立扩展。

(2)代理微服务设计模式

在这种情况下,客户端并不聚合数据,但会根据业务需求的差别调用不同的微服务。代理可以仅仅委派请求,也可以进行数据转换工作。

(3)链式微服务设计模式

这种模式在接收到请求后会产生一个经过合并的响应,如下图所示:

在这种情况下,服务A接收到请求后会与服务B进行通信,类似地,服务B会同服务C进行通信。所有服务都使用同步消息传递。在整个链式调用完成之前,客户端会一直阻塞。因此,服务调用链不宜过长,以免客户端长时间等待。

(4)分支微服务设计模式

这种模式是聚合器模式的扩展,允许同时调用两个微服务链,如下图所示:

(5)数据共享微服务设计模式

自治是微服务的设计原则之一,就是说微服务是全栈式服务。但在重构现有的“单体应用(monolithic application)”时,SQL数据库反规范化可能会导致数据重复和不一致。因此,在单体应用到微服务架构的过渡阶段,可以使用这种设计模式,如下图所示:

在这种情况下,部分微服务可能会共享缓存和数据库存储。不过,这只有在两个服务之间存在强耦合关系时才可以。对于基于微服务的新建应用程序而言,这是一种反模式。

(6)异步消息传递微服务设计模式

虽然REST设计模式非常流行,但它是同步的,会造成阻塞。因此部分基于微服务的架构可能会选择使用消息队列代替REST请求/响应

4.避免同级服务互相调用

两个同级服务相互依赖,A挂了,B还能自治吗?注意,他们是同级服务。同级服务需要明确的几点是:

  • 同级服务不允许直接依赖,必须完成单元的业务实现,比如你是订单服务,那只允许集成订单相关的概念,同层其他业务概念不允许入侵到这个服务。

  • 增加一层集成层(BFF/Application层)做业务集成,产品提的业务需要在这一层实现,体现为产品提的Use Case作为单元测试实现在代码中。在这一层中做各个服务之间的集成。比如你的业务场景是:可以直接购买一件商品。原来的调用链是

【商城(前端)】->【商品目录】->【订单】->【支付]

增加了这一层之后变为:

【商城(前端)】->【商城(BFF/App)】->【商品目录】 【商城(BFF/App)】->【订单】 【商城(BFF/App)】->【支付】

这样就能确保业务流完全在【商城(BFF/App)】中,其他的服务解耦(因为就没有同级调用了),确保了,如果支付挂了,订单、商品目录是不受影响的。

三、微服务设计要点

1.单一职责

微服务架构中的每个服务,都是具有业务逻辑的,符合高内聚、低耦合原则以及单一职责原则的单元,不同的服务通过“管道”的方式灵活组合,从而构建出庞大的系统。

2.轻量级通信

服务之间通过轻量级的通信机制实现互通互联,而所谓的轻量级,通常指语言无关、平台无关的交互方式。

对于轻量级通信的格式而言,我们熟悉的 XML 和 JSON,它们是语言无关、平台无关的;对于通信的协议而言,通常基于 HTTP,能让服务间的通信变得标准化、无状态化。目前大家熟悉的 REST(Representational State Transfer)是实现服务间互相协作的轻量级通信机制之一。使用轻量级通信机制,可以让团队选择更适合的语言、工具或者平台来开发服务本身。

注意:在微服务中确定可能会公用的工具类做成jar包,而实体类放到各自的服务之中进行维护,从而确保各个服务的依赖最小。

3.独立性

每个服务在应用交付过程中,独立地开发、测试和部署。在单块架构中所有功能都在同一个代码库,功能的开发不具有独立性;当不同小组完成多个功能后,需要经过集成和回归测试,测试过程也不具有独立性;当测试完成后,应用被构建成一个包,如果某个功能存在 bug,将导致整个部署失败或者回滚。

在微服务架构中,每个服务都是独立的业务单元,与其他服务高度解耦,只需要改变当前服务本身,就可以完成独立的开发、测试和部署。

4.进程隔离

单块架构中,整个系统运行在同一个进程中,当应用进行部署时,必须停掉当前正在运行的应用,部署完成后再重启进程,无法做到独立部署。

有时候我们会将重复的代码抽取出来封装成组件,在单块架构中,组件通常的形态叫做共享库(如 jar 包或者 DLL),但是当程序运行时,所有组件最终也会被加载到同一进程中运行。

在微服务架构中,应用程序由多个服务组成,每个服务都是高度自治的独立业务实体,可以运行在独立的进程中,不同的服务能非常容易地部署到不同的主机上。

理论上所有服务可以部署在同一个服务器节点,但是并不推荐这么做,因为微服务架构的主旨就是高度自治和高度隔离。

四、微服务应用4个设计原则

1.AKF拆分原则

Y 轴 :就是我们所说的微服务的拆分模式,就是基于不同的业务拆分。

    当服务数量增多时,服务调用关系变得复杂。为系统添加一个新功能,要调用的服务数也变得不可控,由此引发了服务管理上的混乱。所以,一般情况下,需要采用服务注册的机制形成服务网关来进行服务治理。系统的架构将变成下图所示:

X 轴 :指的是水平复制,很好理解,就是将单体系统多运行几个实例,做个集群加负载均衡的模式。

Z 轴 :是基于类似的数据分区,比如一个互联网打车应用突然用的人多了,用户量激增,集群模式撑不住了,那就按照用户请求的地区进行数据分区,北京、上海、四川等多建几个集群。以生产福特公司汽车的工厂来举例,为了发展在中国的业务,或者利用中国的廉价劳动力,在中国建立一个完整的子工厂,与美国工厂一样,负责完整的汽车生产。这就是一种 Z 轴扩展。基于地理位置就近提供服务,进而大幅度降低请求的时延,比如常见的 CDN 就是这么提升用户体验的。

2.前后端分离

  • 前端变化远比后端变化频繁。

  • 前端团队和后端团队分属两个领导班子,技能点差异很大

  • 前端效果绚丽/跨设备兼容要求高

    前后端分离原则,简单来讲就是前端和后端的代码分离也就是技术上做分离,我们推荐的模式是最好直接采用物理分离的方式部署,进一步促使进行更彻底的分离。不要继续以前的服务端模板技术,比如JSP ,把Java JS HTML CSS 都堆到一个页面里,稍复杂的页面就无法维护。这种分离模式的方式有几个好处:

  • 前后端技术分离,可以由各自的专家来对各自的领域进行优化,这样前端的用户体验优化效果会更好。

  • 分离模式下,前后端交互界面更加清晰,就剩下了接口和模型,后端的接口简洁明了,更容易维护。

  • 前端多渠道集成场景更容易实现,后端服务无需变更,采用统一的数据和模型,可以支撑前端的web UI\ 移动App等访问。

核心思想是前端HTML页面通过AJAX调用后端的RESTFUL API接口并使用JSON数据进行交互。

  • Web服务器:一般指像Nginx,Apache这类的服务器,他们一般只能解析静态资源;

  • 应用服务器:一般指像Tomcat,Jetty,Resin这类的服务器可以解析动态资源也可以解析静态资源,但解析静态资源的能力没有web服务器好;

一般都是只有web服务器才能被外网访问,应用服务器只能内网访问。

开发流程

  • 产品经理/领导/客户提出需求

  • UI做出设计图

  • 前后端约定接口&数据&参数

  • 前后端并行开发(无强依赖,可前后端并行开发,如果需求变更,只要接口&参数不变,就不用两边都修改代码,开发效率高)

  • 前后端集成

  • 前端页面调整

  • 集成成功

  • 交付

请求方式

  • 浏览器发送请求

  • 直接到达html页面(前端控制路由与渲染页面,整个项目开发的权重前移)

  • html页面负责调用服务端接口产生数据(通过ajax等等,后台返回json格式数据,json数据格式因为简洁高效而取代xml)

  • 填充html,展现动态效果,在页面上进行解析并操作DOM。

①未分离时代(各种耦合)

早期主要使用MVC框架,Jsp+Servlet的结构图如下:

大致就是所有的请求都被发送给作为控制器的Servlet,它接受请求,并根据请求信息将它们分发给适当的JSP来响应。同时,Servlet还根据JSP的需求生成JavaBeans的实例并输出给JSP环境。JSP可以通过直接调用方法或使用UseBean的自定义标签得到JavaBeans中的数据。

需要说明的是,这个View还可以采用 Velocity、Freemaker 等模板引擎。使用了这些模板引擎,可以使得开发过程中的人员分工更加明确,还能提高开发效率。

那么,在这个时期,开发方式有如下两种:

方式一

方式二

②半分离时代

前后端半分离,前端负责开发页面,通过接口(Ajax)获取数据,采用Dom操作对页面进行数据绑定,最终是由前端把页面渲染出来。这也就是Ajax与SPA应用(单页应用)结合的方式

步骤如下:

  • 浏览器请求,CDN返回HTML页面;

  • HTML中的JS代码以Ajax方式请求后台的Restful接口;

  • 接口返回Json数据,页面解析Json数据,通过Dom操作渲染页面;

后端提供的都是以JSON为数据格式的API接口供Native端使用,同样提供给WEB的也是JSON格式的API接口。

那么意味着WEB工作流程是:

  • 打开web,加载基本资源,如CSS,JS等;

  • 发起一个Ajax请求再到服务端请求数据,同时展示loading;

  • 得到json格式的数据后再根据逻辑选择模板渲染出DOM字符串;

  • 将DOM字符串插入页面中web view渲染出DOM结构;

这些步骤都由用户所使用的设备中逐步执行,也就是说用户的设备性能与APP的运行速度联系的更紧换句话说就是如果用户的设备很低端,那么APP打开页面的速度会越慢。

为什么说是半分离的?因为不是所有页面都是单页面应用,在多页面应用的情况下,前端因为没有掌握controller层,前端需要跟后端讨论,我们这个页面是要同步输出呢,还是异步Json渲染呢?而且,即使在这一时期,通常也是一个工程师搞定前后端所有工作。因此,在这一阶段,只能算半分离。

首先,这种方式的优点是很明显的。前端不会嵌入任何后台代码,前端专注于HTML、CSS、JS的开发,不依赖于后端。自己还能够模拟Json数据来渲染页面。发现Bug,也能迅速定位出是谁的问题。

然而,在这种架构下,还是存在明显的弊端的。最明显的有如下几点:

  • JS存在大量冗余,在业务复杂的情况下,页面的渲染部分的代码,非常复杂;

  • 在Json返回的数据量比较大的情况下,渲染的十分缓慢,会出现页面卡顿的情况;

  • SEO( Search Engine Optimization,即搜索引擎优化)非常不方便,由于搜索引擎的爬虫无法爬下JS异步渲染的数据,导致这样的页面,SEO会存在一定的问题;

  • 资源消耗严重,在业务复杂的情况下,一个页面可能要发起多次HTTP请求才能将页面渲染完毕。可能有人不服,觉得PC端建立多次HTTP请求也没啥。那你考虑过移动端么,知道移动端建立一次HTTP请求需要消耗多少资源么?

③分离时代

在前后端彻底分离这一时期,前端的范围被扩展,controller层也被认为属于前端的一部分。在这一时期:

  • 前端:负责View和Controller层。

  • 后端:只负责Model层,业务/数据处理等。

可是服务端人员对前端HTML结构不熟悉,前端也不懂后台代码呀,controller层如何实现呢?这就是node.js的妙用了,node.js适合运用在高并发、I/O密集、少量业务逻辑的场景。最重要的一点是,前端不用再学一门其他的语言了,对前端来说,上手度大大提高。

可以就把Nodejs当成跟前端交互的api。总得来说,Nodejs的作用在mvc中相当于C(控制器)。Nodejs路由的实现逻辑是把前端静态页面代码当成字符串发送到客户端(例如浏览器),简单理解可以理解为路由是提供给客户端的一组api接口,只不过返回的数据是页面代码的字符串而已。

3.无状态服务

对于无状态服务,首先说一下什么是状态:如果一个数据需要被多个服务共享,才能完成一笔交易,那么这个数据被称为状态。进而依赖这个“状态”数据的服务被称为有状态服务,反之称为无状态服务。

那么这个无状态服务原则并不是说在微服务架构里就不允许存在状态,表达的真实意思是要把有状态的业务服务改变为无状态的计算类服务,那么状态数据也就相应的迁移到对应的“有状态数据服务”中。

场景说明:例如我们以前在本地内存中建立的数据缓存、Session缓存,到现在的微服务架构中就应该把这些数据迁移到分布式缓存中存储,让业务服务变成一个无状态的计算节点。迁移后,就可以做到按需动态伸缩,微服务应用在运行时动态增删节点,就不再需要考虑缓存数据如何同步的问题。

4.Restful通信风格

  • 无状态协议HTTP,具备先天优势,扩展能力很强。例如需要安全加密是,有现成的成熟方案HTTPS可用。

  • JSON 报文序列化,轻量简单,人与机器均可读,学习成本低,搜索引擎友好。

  • 语言无关,各大热门语言都提供成熟的Restful API框架,相对其他的一些RPC框架生态更完善。

五、合理拆分微服务

1.拆分原则

微服务架构 = 30%的SOA服务架构思想 + 20%的组件化架构思想 + 50%的领域建模思想

(1)高内聚,低耦合

对于服务的拆分并非越小越好,拆分粒度应该保证微服务具有业务的独立性与完整性,服务的拆分围绕业务模块进行拆分,并且针对不确定的边界应该先粗略拆分,再逐渐细化。例如将 VR 资讯系统进行服务拆分,分为资讯系统、话题系统、日报系统、百科系统四个微服务系统。

微服务也指一种种松耦合的、有一定的有界上下文的面向服务架构。也就是说,如果每个服务都要同时修改,那么它们就不是微服务,因为它们紧耦合在一起;如果你需要掌握一个服务大多的上下文场景使用条件,那么它就是一个有上下文边界的服务,这个定义来自DDD领域驱动设计。同时设计微服务调用链时不宜过长,以免客户端长时间等待,以及中间环节出现错误造成整个请求失败。

尽量做到一个微服务三个人负责开发,这样三个人可以进行有效的技术讨论,并且如果一个人请假,系统还有两个进行开发还可以支撑。

一个良好的微服务架构,对外各个服务之间是松耦合的,一个服务的更改不会影响到其它微服务的运行,同时服务调用接口形式尽量单一。对内每一个服务是高内聚的,相关的行为通过明确的界限上下分划分到同一个服务中。

单体架构拆分时,先按照包进行拆分,稳定后再做成服务。将相近影响服务放到一起,避免跨服务的调用数据层。静态的共享数据,比如国家可以放在一个共享的配置文件之中。对于多个服务共用的数据库表,应考虑是否能抽离成一个服务,然后再考虑是否能将表拆分成多张表,放到不同服务,但应该考虑事务问题。

(2)避免影响产品的日常功能迭代

拆分的过程,要尽量避免影响产品的日常功能迭代,也就是说,要一边做产品功能 迭代,一边完成服务化拆分。拆分只能在现有一体化系统的基础上,不断剥离业务独立部署。

  • 优先剥离比较独立的边界服务(比如短信服务、地理位置服务),从非核心的服务出 发,减少拆分对现有业务的影响,也给团队一个练习、试错的机会;

  • 当两个服务存在依赖关系时,优先拆分被依赖的服务。比方说,内容服务依赖于用户服 务获取用户的基本信息,那么如果先把内容服务拆分出来,内容服务就会依赖于一体化架构 中的用户模块,这样还是无法保证内容服务的快速部署能力。

所以正确的做法是要理清服务之间的调用关系,比如说,内容服务会依赖用户服务获取 用户信息,互动服务会依赖内容服务,所以要按照先用户服务,再内容服务,最后互动服务 的顺序来进行拆分。

2.拆分方式

(1)基于业务拆分(纵向拆分)最优先的标准

关联比较密切的业务适合拆分为一个微服务,而功能相对比较独立的业务适合单独拆分为一个微服务。保证高内聚力与松耦合

(2)横向拆分

从公共且独立功能维度拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源独立不与其他业务耦合。

(3)基于可扩展拆分

将成熟的服务拆分为稳定服务,将变化的服务和迭代的服务拆分为变动服务,稳定服务可以粒度粗一点。这样拆分目的是避免在开发迭代服务的时候,不小心影响已有的成熟服务。

(4)基于可靠性拆分

将可靠性要求高的和低的服务拆分开,然后重点保证核心服务的高可用,当然核心服务可以使多个。好处:避免非核心服务故障影响核心服务;核心服务高可用方案更简单,其中涉及的组件更少,并且核心服务占中的机器和带宽都变得小了,也可以解决高可用成本

(5)基于性能拆分

基于性能拆分和基于可靠性拆分类似,将性能要求高或者性能压力大的模块拆分出来,避免性能压力大的服务影响其他服务。常见的拆分方式和具体的性能瓶颈有关,可以拆分 Web 服务、数据库、缓存等。例如电商的抢购,性能压力最大的是入口的排队功能,可以将排队功能独立为一个服务。

(6)基于安全边界

有特殊安全要求的功能,应从领域模型中拆分独立,避免相互影响。

(7)基于技术异构等因素

领域模型中有些功能虽然在同一个业务域内,但在技术实现时可能会存在较大的差异,也就是说领域模型内部不同的功能存在技术异构的问题。由于业务场景或者技术条件的限制,有的可能用.NET,有的则是 Java,有的甚至大数据架构。对于这些存在技术异构的功能,可以考虑按照技术边界进行拆分。

以上几种拆分方式不是多选一,而是可以根据实际情况自由排列组合,例如可以基于可靠性拆分出服务 A,基于性能拆分出服务 B,基于可扩展拆分出 C/D/F 三个服务,加上原有的服务 X,最后总共拆分出 6 个服务(A/B/C/D/F/X)。

3.微服务的演进策略

(1)绞杀者策略

绞杀者策略是一种逐步剥离业务能力,用微服务逐步替代原有单体系统的策略。它对单体系统进行领域建模,根据领域边界,在单体系统之外,将新功能和部分业务能力独立出来,建设独立的微服务。新微服务与单体系统保持松耦合关系。随着时间的推移,大部分单体系统的功能将被独立为微服务,这样就慢慢绞杀掉了原来的单体系统。

(2)修缮者策略

修缮者策略是一种维持原有系统整体能力不变,逐步优化系统整体能力的策略。它是在现有系统的基础上,剥离影响整体业务的部分功能,独立为微服务,比如高性能要求的功能,代码质量不高或者版本发布频率不一致的功能等。通过这些功能的剥离,我们就可以兼顾整体和局部,解决系统整体不协调的问题。

4.微服务改造过程

(1)准备阶段

①圈表

产品数据库有 100 多张表,圈表就是用来确定微服务具体包含哪些表,也就是确定服务的数据模型。在确定了表以后,微服务就负责这些表 的访问,当然微服务也不会访问其它的表,而业务系统后续将通过微服务的接口,实现对这些表的访问。

圈表是微服务改造中比较有挑战性的地方,它实际上对应了服务的边界划分。只是针对老系统做服务化改造的时候,更多的是从数据库表的角度来考虑划分,这样更好落地。

在微服务改造中,确定哪些表属于这个服务,会直接影响后续的所有改造工作,这需要有经验的业务架构师和数据架构师参与进来,通过深入地分析现有的业务场景和表的关系,才能对库表进行合理的划分。 所以,你可以发现,对现有系统的改造,服务的边界划分主要是从圈表入手的,而不是从一 个服务应该有哪些功能入手的,这一点和新服务设计是有所不同的。这有两方面原因:

  • 一方面,如果确定了服务包含哪些表,也就大致确定了服务有哪些功能,而表是现成 的,它比业务功能要直观很多,所以从表入手比较高效;

  • 另一方面,如果从表入手,构造的服务和表是对应的,服务包含的是完整的表,不会产 生一个表的一部分字段属于库存服务,而另一部分字段属于别的服务的情况,避免表字 段的拆分带来额外的复杂性。

值得注意的是,因为这是对现有系统的改造,为了避免一下子引入太多变化,先不对库 存的表结构进行调整,表结构的优化可以放在服务的升级版里做,这样对业务系统的影响也 最小。

②收集 SQL

在确定了哪些表属于库存服务后,会收集所有业务系统访问这些 表的 SQL 语句,包括它的业务场景说明、访问频率等等。库存微服务后续就针对这些 SQL 进行封装,提供相应的接口给业务系统使用。 这里,服务开发团队负责提供 SQL 收集的 Excel 模板,各业务系统开发团队负责收集具体 的 SQL。

③拆分 SQL

对于收集过来的 SQL 语句,有些 SQL 不仅仅访问圈定的这几张库存 表,还会和产品库中的其他表进行关联。 比如说,商品详情页需要展示商品详情,它会发起 SQL 查询商品基本信息表和库存表,一 次性获取商品的基本信息和库存数量。针对这种情况,就需要把查询语句拆分为两条 SQL,先查询商品表获取商品基本信息,再查询库存表获取库存数量。

对于这样的 SQL 语句,就要求各个业务团队先进行拆分,保证最后提供给服务开发团 队的 SQL,只包含访问库存的相关表。通过 SQL 拆分,我们切断了库存表和其他表的直接 联系,等后面微服务落地后,业务系统就可以通过接入微服务,完成现有 SQL 的替换。 SQL 拆分,会涉及一定的业务系统改造,这部分工作主要由各个研发团队负责,一般情况 下,性能可能会受些影响,但问题不是很大。

(2)实施阶段

①构建库存微服务

这里面包括了接口设计、代码开发、功能测试等步骤,服务开发团队会对业务方提供的 SQL 进行梳理,然后对接口做一定的通用化设计,避免为每个 SQL 定制一个单独的接口,以此保证服务的复用能力。 这部分工作由微服务开发团队负责,第一版的服务主要是做好接口设计,聚焦业务功能,以 保证服务能够落地,业务系统能够顺利对接为目标。将来,服务可以持续迭代,内部做各种 技术性优化,只要服务的接口保持不变,就不会影响业务系统。

⑤接入库存微服务

库存服务经过功能和性能验证以后,会由各个业务开发团队逐步 接入,替换原来的 SQL 语句。这部分工作主要由业务研发团队负责,难度不大,但需要耗 费比较多的时间。

⑥数据库独立

当服务接入完成,所有的 SQL 语句都被替换后,业务系统已经不会直接访问这些库存的表。这时就可以把库存相关的表,从原来的产品库中迁移出 来,部署成为一个物理上独立的数据库。业务系统是通过服务来访问数据库的,因此,这个 数据迁移对于业务系统来说是透明的,业务团队甚至都不用关心这些表的新位置。

通过库存表独立成库,可以从物理层面,切断业务团队对这些表的依赖,同时也可以 大幅度降低产品库的压力,特别是大促的时候,库存读写压力是非常大的,数据库独立也为 库存服务后续的技术优化打下了基础。 这部分工作主要由微服务开发团队和 DBA 一起配合完成,主要是要避免业务系统还有遗漏 的 SQL 语句,避免它们还在直接访问库存的表。

可以在迁库前,通过代码扫描做好相 应的检查工作。 改造完成后的库存微服务架构如下图所示,库存微服务一共包含了 15 张表,对外有 30 多 个接口,几十个业务系统接入库存服务。平时,库存服务会部署 50 个实例,大促时会部署 更多,我们很容易通过加机器的方式,实现库存服务的水平扩展。

5.分层与分割

(1)分层

  • 将系统在横向维度上切分成几个部分,每个部门负责一部分相对简单并比较单一的职责,然后通过上层对下层的依赖和调度组成一个完整的系统

  • 比如把电商系统分成:应用层,服务层,数据层。(具体分多少个层次根据自己的业务场景)

  • 应用层:网站首页,用户中心,商品中心,购物车,红包业务,活动中心等,负责具体业务和视图展示

  • 服务层:订单服务,用户管理服务,红包服务,商品服务等,为应用层提供服务支持

  • 数据层:关系数据库,nosql数据库 等,提供数据存储查询服务

  • 分层架构是逻辑上的,在物理部署上可以部署在同一台物理机器上,但是随着网站业务的发展,必然需要对已经分层的模块分离部署,分别部署在不同的服务器上,使网站可以支撑更多用户访问

(2)分割

  • 在纵向方面对业务进行切分,将一块相对复杂的业务分割成不同的模块单元

  • 包装成高内聚低耦合的模块不仅有助于软件的开发维护,也便于不同模块的分布式部署,提高网站的并发处理能力和功能扩展

  • 比如用户中心可以分割成:账户信息模块,订单模块,充值模块,提现模块,优惠券模块等

六、微服务注意事项

在服务化之前,业务通常都是本地API调用,本地方法调用性能损耗较小。服务化之后,服务提供者和消费者之间采用远程网络通信,增加了额外的性能损耗,业务调用的时延将增大,同时由于网络闪断等原因,分布式调用失败的风险也增大。如果服务框架没有足够的容错能力,业务失败率将会大幅提升。

除了性能、可靠性等问题,跨节点的事务一致性问题、分布式调用带来的故障定界困难、海量微服务运维成本增加等也是分布式服务框架必须要解决的难题。

1.服务框架

「功能点/服务框架」

Netflix/SpringCloud

Motan

gRPC

Thrift

Dubbo/DubboX

功能定位

完整的微服务框架

RPC框架,但整合了ZK或Consul,实现集群环境的基本服务注册发现

RPC框架

RPC框架

服务框架

支持Rest

是,Ribbon支持多种可拔插的序列号选择

支持RPC

是(Hession2)

支持多语言

是(Rest形式)

负载均衡

是(服务端zuul+客户端Ribbon),zuul-服务,动态路由,云端负载均衡Eureka(针对中间层服务器)

是(客户端)

是(客户端)

配置服务

Netfix Archaius,Spring Cloud Config Server 集中配置

是(Zookeeper提供)

服务调用链监控

是(zuul),zuul提供边缘服务,API网关

高可用/容错

是(服务端Hystrix+客户端Ribbon)

是(客户端)

是(客户端)

典型应用案例

Netflix

Sina

Google

Facebook

社区活跃程度

一般

一般

2017年后重新开始维护,之前中断了5年

学习难度

中等

文档丰富程度

一般

一般

一般

其他

Spring Cloud Bus为我们的应用程序带来了更多管理端点

支持降级

Netflix内部在开发集成gRPC

IDL定义

实践的公司比较多

2.业务最佳实践

要保证高性能,单依靠分布式服务框架是不够的,还需要应用的配合,应用服务化高性能实践总结如下:

  • 能异步的尽可能使用异步或者并行服务调用,提升服务的吞吐量,有效降低服务调用时延。

  • 无论是NIO通信框架的线程池还是后端业务线程池,线程参数的配置必须合理。如果采用JDK默认的线程池,最大线程数建议不超过20个。因为JDK的线程池默认采用N个线程争用1个同步阻塞队列方式,当线程数过大时,会导致激烈的锁竞争,此时性能不仅不会提升,反而会下降。

  • 尽量减小要传输的码流大小,提升性能。本地调用时,由于在同一块堆内存中访问,参数大小对性能没有任何影响。跨进程通信时,往往传递的是个复杂对象,如果明确对方只使用其中的某几个字段或者某个对象引用,则不要把整个复杂对象都传递过去。举例,对象A持有8个基本类型的字段,2个复杂对象B和C。如果明确服务提供者只需要用到A聚合的C对象,则请求参数应该是C,而不是整个对象A。

  • 设置合适的客户端超时时间,防止业务高峰期因为服务端响应慢导致业务线程等应答时被阻塞,进而引起后续其他服务的消息在队列中排队,造成故障扩散。

  • 对于重要的服务,可以单独部署到独立的服务线程池中,与其他非核心服务做隔离,保障核心服务的高效运行。

  • 利用Docker等轻量级OS容器部署服务,对服务做物理资源层隔离,避免虚拟化之后导致的超过20%的性能损耗。

  • 设置合理的服务调度优先级,并根据线上性能监控数据做实时调整。

3.研发团队协作问题

服务化之后,特别是采用微服务架构以后。研发团队会被拆分成多个服务化小组,例如AWS的Two Pizza Team,每个团队由2~3名研发负责服务的开发、测试、部署上线、运维和运营等。随着服务数的膨胀,研发团队的增多,跨团队的协同配合将会成为一个制约研发效率提升的因素。

(1)共用服务注册中心

为了方便开发测试,经常会在线下共用一个所有服务共享的服务注册中心,这时,一个正在开发中的服务发布到服务注册中心,可能会导致一些消费者不可用。解决方案:可以让服务提供者开发方,只订阅服务(开发的服务可能依赖其他服务),而不注册正在开发的服务,通过直连测试正在开发的服务。

(2)直连提供者

在开发和测试环境下,如果公共的服务注册中心没有搭建,消费者将无法获取服务提供者的地址列表,只能做本地单元测试或使用模拟桩测试。还有一种场景就是在实际测试中,服务提供者往往多实例部署,如果服务提供者存在Bug,就需要做远程断点调试,这会带来两个问题:

  • 服务提供者多实例部署,远程调试地址无法确定,调试效率低下。

  • 多个消费者可能共用一套测试联调环境,断点调试过程中可能被其他消费者意外打断。

解决策略:绕过注册中心,只测试指定服务提供者,这时候可能需要点对点直连,点对点直联方式将以服务接口为单位,忽略注册中心的提供者列表。

(3)多团队进度协同

假如前端Web门户依赖后台A、B、C和D 4个服务,分别由4个不同的研发团队负责,门户要求新特性2周内上线。A和B内部需求优先级排序将门户的优先级排的比较高,可以满足交付时间点。但是C和D服务所在团队由于同时需要开发其他优先级更高的服务,因此把优先级排的相对较低,无法满足2周交付。

在C和D提供版本之前,门户只能先通过打测试桩的方式完成Mock测试,但是由于并没有真实的测试过C和D服务,因此需求无法按期交付。

应用依赖的服务越多,特性交付效率就越低下,交付的速度取决于依赖的最迟交付的那个服务。假如Web门户依赖后台的100个服务,只要1个核心服务没有按期交付,则整个进度就会延迟。

解决方案:调用链可以将应用、服务和中间件之间的依赖关系串接并展示出来,基于调用链首入口的交付日期作为输入,利用依赖管理工具,可以自动计算出调用链上各个服务的最迟交付时间点。通过调用链分析和标准化的依赖计算工具,可以避免人为需求排序失误导致的需求延期。

(4)服务降级和Mock测试

在实际项目开发中,由于小组之间、个人开发者之间开发节奏不一致,经常会出现消费者等待依赖的服务提供者提供联调版本的情况,相互等待会降低项目的研发进度。

解决方案:服务提供者首先将接口定下来并提供给消费者,消费者可以将服务降级同Mock测试结合起来,在Mock测试代码中实现容错降级的业务逻辑(业务放通),这样既完成了Mock测试,又实现了服务降级的业务逻辑开发,一举两得。

(5)协同调试问题

在实际项目开发过程中,各研发团队进度不一致很正常。如果消费者坐等服务提供者按时提供版本,往往会造成人力资源浪费,影响项目进度。解决方案:分布式服务框架提供Mock桩管理框架,当周边服务提供者尚未完成开发时,将路由切换到模拟测试模式,自动调用Mock桩;业务集成测试和上线时,则要能够自动切换到真实的服务提供者上,可以结合服务降级功能实现。

(6)接口前向兼容性

由于线上的Bug修复、内部重构和需求变更,服务提供者会经常修改内部实现,包括但不限于:接口参数变化、参数字段变化、业务逻辑变化和数据表结构变化。在实际项目中经常会发生服务提供者修改了接口或者数据结构,但是并没有及时知会到所有消费者,导致服务调用失败。

解决方案:

  • 制定并严格执行《服务前向兼容性规范》,避免发生不兼容修改或者私自修改不通知周边的情况。

  • 接口兼容性技术保障:例如Thrift的IDL,支持新增、修改和删除字段,字段定义位置无关性,码流支持乱序等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值