浅谈微服务的简史及内部框架组成

作者: 金桔文案 | 来源:发表于2017-10-31 10:37 被阅读24次

代码更新越来越快,代码模块之间的界限很模糊,可替代的代码多不胜数。因为这个情形,内聚性 - 单一职责原则:相同原因而变化的东西放在一起,因不同原因变化的东西分离开来;微服务将这个理念应用到独立的服务上,根据业务的边界来确定服务的边界。

微服务是SOA的一种特定方法

a.一个微服务就是一个独立的实体,可以独立部署
b.服务之间通过网络进行通讯
c.服务彼此间可以独立的进行修改,服务的部署不应该引起消费方的变动
d.服务暴露过多,会造成和消费方的紧耦合

微服务的优点如下:

a.技术异构性: 尝试新技术,降低风险
b.系统中组件不可用,不会造成级联故障
c.扩展:对服务进行针对性的扩展
d.简化部署:特定代码部署,不影响系统整体,快速回滚
e.组织结构匹配: 不同的团队负责不同的服务
f.可组合性: 对不同的场景组合服务

微服务的模块

Erlang的模块化能力惊人;难度比较大,很容易会和其他代码耦合在一起

微服务的分解技术

a.分布式系统的复杂性
b.部署、测试、监控的投入
c.类型分布式事务和CAP的考虑

微服务的共享库

对重复代码进行分包组织,工具类,重复业务代码类。缺点如下:
a.无法使用异构技术
b.每次更新,需要将相关的程序重新部署
c.公共任务并且不属于业务代码,可以这样做,但如果涉及服务间的通讯,会成为耦合点

微服务的要求的标准

建议确保所有的服务使用同样的方式报告健康状态 及 监控相关的数据,标准化,隐藏具体技术实现,日志服务和监控服务一样,要集中化使用统一的接口协议。

如何建模服务?


概念 & 准则

松耦合

独立修改部署而不影响系统的其他部分,限制服务间的调用数量,除了性能问题,过度通讯会造成紧耦合高内聚,改变某个行为,只需要在一个地方进行修改,就可以尽快发布,快速修改,低风险发布。

bounded Context(衔接上下文)

每个限界上下文分为两部分,一部分不需要和外部通讯,一部分需要。 有明确的接口,决定了暴露哪些模型给其他界限上下文模块边界就可以成为绝佳的微服务候选,熟练了之后,可以省掉在单块系统中先使用模块的这个步骤,而直接使用单独的服务思考限界上下文时,不应该从共享数据的角度来考虑,而应该从这些上下文能提供的功能来考虑,否则会演变成基于模型, 从而导致贫血、基于CRUD的操作逐步划分上下文,一开始识别粗粒度的限界上下文,而这些限界上下文会嵌套一系列子限界上下文,两种做法:(1)嵌套上下文不直接对外可见,用的还是粗粒度上下文的功能,但发出的请求被透明的映射到其他服务上(更好的测试);(2)将子限界上下文单独拆分成服务

微服务的共享的隐藏模型:

财务和仓库两个限界上下文,会对仓库的 库存模型存在交集,针对库存模型, 应该存在内部和外部两种表示方式,不暴露所有属性共享特定模型,不共享内部表示可以避免潜在的紧耦合,一旦发现了领域内部的限界上下文,一定要使用模块对其进行建模,同时使用共享模型和隐藏模型。

微服务的演化:需要注意细则

架构师类似城市规划师,专注在大方向上,有限情况参与到具体的开发,不关注每个区域内发生的事,更关注区域之间的事情(服务之间的交互),未来的变化很难预见,对所有可能性进行预测,不如做一个允许变化的计划系统设计方面的决定通常是取舍。为了和更大的目标保持一致,制定一些具体的规则,称为原则。原则作为指导,约束是很难被改变的。显示指出两者,并定期回顾是否要修正。编写文档是有用的,配上真实的代码范例,需要架构师提供一些温和的指导,让团队自行决定何时偿还债务,维护一个债务列表,并定期回顾。偏离原则:针对某个场景记录下来,当例外很多次出现,考虑修改原则,如果架构师和团队小组存在分歧,大部分情况要认同小组的决定。

微服务的其他

对于一个新系统而言,可以使用一段时间单系统,避免后期的修复代价。将一个已有的代码库划分为微服务,比从头开始构建微服务要简单得多。

微服务的共享数据库

外部系统能够查看内部实现细节,并与其完全绑定在一起,所有服务都可以完成访问该数据库;但是如果修改数据库会导致消费方没有办法工作。需要大量的回归测试,但消费方和服务绑定在一起,那么就无法轻易的替换技术了。

微服务的集成

集成技术选型:
a.不应该选择那种对微服务具体实现技术有限制的集成方式
b.使服务易于消费方使用(提供客户端库可以简化使用,但是增加了耦合)
c.隐藏内部实现,避免服务方的任何修改都可能影响到消费方

微服务的同步与异步

同步:及时的得到操作的响应 ;请求/响应
异步:适用长时间的操作;基于事件

微服务的编排与协同

场景:创建用户的操作, 需要发放优惠券、创建银行账户、发送欢迎邮件

a.编排

使用客户服务作为中心,同步顺序的调用操作,能及时知道每一步是否成功,客户服务成为中心控制承担了太多职责,是中心枢纽和很多逻辑的起点。

b.协同

消除耦合,但没有明显的流程视图,无法保证每一步流程都正确执行,需要更多额外的工作,来构建一个与业务流程匹配的监控系统,

c.折中方案

使用异步回调的方式。

请求/响应的技术:

RPC

核心特点,使用本地调用的方式和远程进行交互。

核心思想是隐藏远程调用的复杂性,但是很多框架隐藏过头了;使用本地调用不会造成性能问题,但是RPC花大量的时间来对负荷和解封装,以及网络通信的时间,简单的把一个远程服务改造成跨服务的远程API往往会带来问题。

更糟的情况是: 开发人员不知道调用时远程调用,并对其进行使用
网络的出错模式不止一种,很难对问题进行定位

脆弱性:对象参数的修改,需要对客户端重新生成打桩,应用这些修改需要同时部署客户端和服务端,选用RPC,一定不要对远程调用过度抽象,确保可以独立的升级服务器,切记不要隐藏网络调用的事实。

REST

HTTP周边有一个很大的生态系统,包含很多支撑工具和技术,比如 Varnish HTTP缓存代理 / mod_proxy 负载均衡 / 大量的监控工具,HTTP也可以用来实现 RPC,比如soap就是基于HTTP进行路由的,只是使用了少量的HTTP特性,对于有些接口来说,HTML既可以做UI,也可以做API,建议使用XML,在工具上有很多支持。

springboot过多的约定带来了紧耦合


使用客户端库会增加复杂度,因为人们不自觉地回到基于HTTP的RPC思路上去了,然后构造出一堆共享库,在客户端和服务端之间共享代码是很危险的,在低延迟要求的服务中,HTTP的封装开销需要注意低延迟通信最好的选择是TCP编程REST得到序列化和反序列化需要自己实现,会成为消费者和服务端的耦合点。

微服务的基于异步的实现

增加开发流程的复杂度,需要额外的系统才能开发及测试,需要额外的专业知识和机器保持基础设计正常运行。

需要遵守原则

尽量让中间件简单,将逻辑放在自己的服务中,设置最大重试次数,失败的消息统一发送到一个地方,进行查看和重试,确保使用监控机制保证每个流程,然后对流程进行ID关联 (zookeeper)把关键领域的生命周期显示建模出来非常有用,不但可以在唯一的地方处理状态冲突,还可以在这些状态的基础上封装一些行为灾难性故障转移: 队列中存放了任务,消费者A处理崩溃,消费者B处理也崩溃,一个异常元素导致一系列的消费者崩溃。

DRY:避免重复代码


如果有相同代码做同样的事情,代码规模就会变大,从而降低可维护性

创建一个随处可用的共享库?

在微服务中是危险的,会导致耦合,客户端和服务端需要同时更新部署,但在服务间使用日志库代码不是问题,因为对外是不可见的,所以服务间使用共享库比重复代码还要可怕呢!

客户端库

如果要使用,要保证只包含处理底层传输协议的代码,比如服务发现和故障处理等等,千万不要把与目标服务相关的逻辑代码放到客户端库中。

按引用访问

微服务应该包含核心领域实体的全生命周期的相关操作,服务应该是关于该领域的唯一可靠来源,对服务发起一个资源的请求,然后保存在本地副本中,可能一段时间会失效,所以请求返回的结果,要保存一个指向原始资源的引用(比如一个资源URL),确保需要最新数据的时候可以有办法获取,总是通过一个服务去获取某个领域的信息,会造成过多的负载,但如果能够得到该领域的有效时间就是最好的。

版本管理

尽可能不做破坏性修改,使用良好的架构设计,鼓励客户端正确的行为,例如json传输数据,一些强类型语言会使用绑定技术,会将所有的字段绑定,无论消费者是否需要,当修改接口数据结构的时候会影响到消费者,可以使用XPath技术提取出想要的字段鲁棒性原则,每个模块都应该 宽进严出,发送的东西要严格,接收的东西要宽容,使用语义化的版本管理,格式如下:major.minor.patch ;major代表包含向后不兼容的修改; minor意味着新功能的增加 ; patch代表对缺陷的修改不同接口可以共存,发布一个破坏性修改的时候,可以部署一个包含新老接口的版本;但更建议在V1接口中转换后请求V2接口同时使用多个版本的服务。

BFF(Backed for frontends)为前端服务的后端

对于不同的客户端,使用聚合接口,对后台调用的服务进行编排,类似于一个专门的后台服务,比如Node程序,对JAVA后台的接口进行组合,也称作:分解单块系统,首先识别出单块后台系统明显的几个上下文;为他们创建包结构来表示,把已有的代码进行移动。

解决横跨不同上下文的表
a.打破外键约束,将访问变成逻辑外键,通过暴露的API进行交互
b.共享的静态数据,通过配置文件和代码中进行配置,不要放在公有包中

共享数据

a.不同的上下文会对同一张表进行读写操作:概念领域不是在代码中进行建模,相反是在数据库中隐式的建模,代表这个表是一个上下文,作为一个中间步骤,可以创建一个新的包最终变成一个服务
b.共享表:存在一个通用的行条目录表,不同上下文都用到了部分数据:可以分成两张表

重构数据库

a.先分离数据库结构,不对服务进行分离
b.对数据库的访问次数会变多,以前一个查询获得所有数据,现在要内存中进行组装

事务边界

一个事务可以帮助我们的系统从一个一致性状态 转移到另外一个一致性; 分离数据库之后,没有了原生的事务处理,解决方案:
a.再试一次:把失败的操作,记录在日志或者失败队列中,后面对他们尝试触发,要保证重新触发能够成功,最终一致性
b.终止整个操作:对上一个成功的操作进行补偿事务来抵消之前的操作,可靠性不佳
c.分布式事务:外部的事务管理器统一编排执行,常用算法是两阶段提交,可靠性也不佳
总结:是否真的需要强一致性? 是否要跨业务进行操作? 是否可以通过业务逻辑的处理避免事务,比如新增处理中的订单状态

报表

a.为了防止对主系统的影响,报表的查询使用副本; 缺点:共享数据库结构会抑制修改表结构的积极性
b.使用MongoDB或基于列的数据库来 保存副本

数据库分布在不同的系统中

a.通过服务调用来获取数据:少量的数据可以考虑在内存中进行组合
b.大数据读取:使用HTTP POST方法,携带一个位置信息,让服务器返回200,把获取的内容写入到文件中,然后保存在请求的位置上,客户端轮询请求,直到返回201,这样就减少了HTTP的开销
c.数据导出: 使用一个独立的服务,直接访问不同的微服务使用的数据库,导出到单独的报表系统中;在报表数据库中包含了所有的服务数据结构,然后可以使用视图之类的技术来创建一个聚合。
d.事件数据导出:在事件发生时就给报表系统发送数据,而不是周期性的导出,增量导入更高效。 缺点:数据量较大时不容易扩展
e.对数据导出的备份进行处理:可以使用Hadoop对数据处理后,存储起来

部署


持续集成(CI)
a.当构建失败之后,把修复CI当作第一优先级要处理的事情
b.集成需要测试,这样才能保证集成代码的正确性,不然只是对语法错误进行检查
c.每个微服务要有一个专有的CI,包含测试代码

构建流水线和持续交付(CD)

a.CD能够检查每次提交是否到达了部署生产环境的要求,并持续的把这些消息反馈给我们,把每次提交当成候选版本对待
b.在CD中,会把多阶段构建流水线的概念进行扩展,从而覆盖软件通过的所有阶段
c.编译及快速测试 -> 耗时测试 -> 用户验收测试 -> 性能测试 -> 生产环境

测试


单元测试
通常只测试一个函数或者方法,通过TDD写的测试就属于这一类,不启动服务,对外部网络和文件使用也很有限;面向技术,对功能正常给出快速反馈。

服务测试

a.对于包含多个服务的系统,一个服务测试只测试其中一个功能
b.为了达到隔离性,需要为其他服务打桩,MOCK

端到端测试

a.会覆盖整个系统,通常需要打开一个浏览器来操作图形界面。
b.测试类型的比例:应该是不同数量级的
c.随着测试的范围扩大,遇到的可能情况也越多,发现脆弱测试时,应该竭尽全力去解决,避免异常正常化(对事情出错变得习以为常);当不能立即修复的时候,从测试套件中移除。
d.不要轻易删掉测试代码,除非你理解风险
e.测试场景,而不是故事:测试的重点放在核心的场景中,其他场景在服务测试中进行。

CDC

消费者驱动测试:定义消费者的期望,服务端没有达到预期将无法部署,有助于不同团队一起来编写代码

部署后再测试:

a.部署之前的测试不能保证零缺陷,部署只是在正式环境启动,不代表引入正常流量。
b.蓝绿部署 -> 冒烟测试 -> 切换流量
c.金丝雀发布:少量流量引入新部署的服务中,然后不断的调节流量来验证我们的功能性和非功能性。进行计分然后确认完全切换,简单的做法Nginx分流,复杂的复制生产环境请求
d.性能测试:原来的单次调用可能会变成多次调用,以及跨数据库,会影响到整个微服务调用链,所以比单块系统更加重要

微服务规模化的挑战


了解真正的需求:响应时间、延迟、可用性、数据持久性的 权衡

功能降级:当出现问题的其他处理方式

a.程序使用HTTP连接池来处理下游链接:如果某个下游请求故障,但是HTTP设置了超时时间,就会导致大量的请求堆积,所有的worker都在等待超时,阻止建立新的HTTP请求,导致系统大范围不可用
b.在分布式系统中,延迟是致命的

解决方案

a.正确的设置超时时间
b.实现资源隔离,使用不同的连接池
c.实现一个断路器,快速失败

断路器

对下游资源请求失败的次数到达一定数量,断路器打开,所有请求快速失败,一段时间发送请求成功,将会重置断路器

资源隔离

分配不同的资源,当某个部分资源耗尽不影响其他的组件

幂等

确保部分操作幂等安全性,Nginx的重试不包括POST请求

扩展

a.帮助处理失败,额外的程序保证正常运行
b.性能扩展,减少延迟增加负载

强大的主机

称之为垂直扩展,如果软件没有充分利用也是白搭

分散风险

不要把所有的微服务放在一个地方

负载均衡:SSL终止

通过HTTPS连接到负载均衡器后(Nginx),转到http server ,变成HTTP连接,提高性能。HTTP连接在局域网中,所有外部请求通过一个路由访问内部。

扩展数据库

a.扩展读操作:通过多个副本扩展,一般有一致性问题,确保可以接受
b.扩展写操作:对数据进行哈希,基于哈希分配到一个数据库中,缺点:查询复杂(mongo map/reduce),扩展困难
c.每个微服务一个单独的数据库实例,避免一个数据库实例分配多个数据库

缓存

代理服务器缓存,介于客户端和服务端之间;客户端缓存以及服务端缓存,一般都是三者混用

HTTP缓存:

a.对客户端的响应使用 cache-control指令:告诉是否缓存以及时限
b.设置Expire头部,指定一个日期,该日期之后失效
c.Etag用来标志资源是否匹配,有一种请求方式叫做条件GET

缓存失效

后台异步生成缓存,接受部分实时请求(服务端可能负载),其他请求快速失败,异步生成缓存

自动伸缩

不同的流量对服务进行自动伸缩。

CAP: consistency / availability / partition tolerance

一般是AP: 分区可用,最终一致性; CP:一致性 ,但是拒绝新请求

更多参考内容:http://www.roncoo.com/article/index

相关文章

网友评论

    本文标题:浅谈微服务的简史及内部框架组成

    本文链接:https://www.haomeiwen.com/subject/odovpxtx.html