转眼已经2020,距离微服务这个词落地已经过去好多年!(我记得2017年就听过这个词)。然而今天我想想什么是微服务,其实并没有一个很好的定义。为什么这样说,按照微服务的定义:
微服务架构就是将一个庞大的业务系统按照业务模块拆分成若干个独立的子系统,每个子系统都是一个独立的应用,它是一种将应用构建成一系列按业务领域划分模块的,小的自治服务的软件架构方式,倡导将复杂的单体应用拆分成若干个功能单一、松偶合的服务,这样可以降低开发难度、增强扩展性、便于敏捷开发,及持续集成与交付活动。
根据这个定义,不难看出其实就是对复杂的业务系统统一做逻辑拆分,保持逻辑上的独立。那么逻辑上独立就是一个服务这样做真的是好吗,如何界定:小、独,还有要做一个事情,完成单一的业务,单一的功能要拆分出来,为了独立而独立会不会导致拆的过细?不同人有不同的见解,我们今天一起探讨微服务的过去和未来。
在没有微服务之前,我们最早的架构模式就是 MVC 模式,把业务逻辑分为:表示层,业务逻辑层,数据访问层。MVC模式随着大前端的发展,从一开始的前后端不分离,到现在的前后端分离逐渐演进。这种演进好的一点是剥离了不同开发语言的开发环境和部署环境,使得开发较为便利,部署更直接。然而问题是:这种模式仍然是单体应用模式,如果有一个改动需要上线,你不得不因为这个改动去考虑更多,因为你无法估量在这么大体量的代码中你的一个改动会不会引发蝴蝶效应。
另外很重要的一点就是移动互联网时代的到来,引发了用户数几何倍数暴增,传统的单体应用模式已经无法支撑用户量暴涨的流量冲击,互联网人不得不做出加机器的无赖之举,然而发现有的时候加机器都无法搞定问题,因为逻辑调用过于耦合导致调用链复杂。继而出现精简调用流程,梳理调用路径的举措,于是演变出微服务这个概念。
其实在没有微服务这个词出现之前, 我们也是这样干的,只是干的不彻底而已。比如说有一个信贷系统,信贷系统分为贷前,贷中,贷后三步:
在微服务未出现之前,我们大多是单体应用,基本上一个工程包含所有,无所不能,所以很臃肿上。述这些模块应该都是在一个工程中,但是按照业务做了代码上的拆分。另外就是 RPC 框架并为横空出世,如果有服务上的拆分,比如不同部门之间调用对方提供的服务,那么八九不离十肯定定义的是HTTP 接口,因为通用。但是某些时候大家又怕 HTTP 接口性能差,关键服务不敢用。
微服务出现之后,大家觉得按照模块分别部署好像是这么回事,同时默默在心里嘀咕,以前我只用发布一个工程,现在倒好,可能有个改动涉及3个服务,我一个小小的改动就要发布3次,是不是增加了工作量,另外我以前都不用调接口的,现在依赖了一堆别的系统,系统调用这么复杂,万一别的系统有问题,我岂不是就被耽搁了!
在这种质疑中大家虽有抱怨但是也没有放弃赶时髦,微服务开展的如火如荼,用户中心独立部署,风控系统单独成型,支付中心全公司统一独立,财务系统不再各个业务各自为战而是统筹公司各个业务线统一规划。按照业务抽象独立之后,大家发现好像是这么回事,用起来真香。虽然每次需要别的模块的时候就需要找对应模块进行接入,但是业务逻辑上清晰了呀,如果出了问题,不是自己的,那就是别人的,甩锅很方便的(笑)。
因为微服务是功能粒度上的拆分,必然导致拆分之后的模块变多。针对模块与模块之间的通信与维护,又演变出如下问题:
针对这些问题,业界也在发展中慢慢演进出几套通用的框架。理想中微服务框架应该具备这样的能力:
基于上述微服务框架应该具备的能力,我们来分析目前可以落地的微服务框架的具体实现。
目前国内用的最多的无外乎是两套框架:Dubbo,Spring Cloud。Dubbo大家都很熟悉,从开源到无人维护再到重新冲击Apache顶级项目。但是Dubbo更加准确来说是一个分布式服务框架,致力于提供 高效的RPC远程服务调用方案以及SOA服务治理方案 。说白了就是个 分布式远程服务调用框架 。
从Dubbo官网给的图来看Dubbo的整体架构:
模块注解:
从上图中不难看出Dubbo功能还是很明确的:服务注册于发现,服务监控。另外Dubbo也提供服务治理功能:
Dubbo提供了集群容错的能力,在管理后台可以快速的摘除失败的服务。
对于我们上面提到的一整套微服务应该提供的功能看,Dubbo只是提供了服务注册与服务发现的功能。不可否认在这一项功能中,Dubbo做的是非常优秀的。
Spring Cloud 基于 Spring Boot,为微服务体系开发中的架构问题,提供了 一整套的解决方案 ——服务注册与发现,服务消费,服务保护与熔断,网关,分布式调用追踪,分布式配置管理等。
目前Spring Cloud 支持的服务注册组件有 Consul,Eureka。Consul 不是 Spring 官方的项目,需要单独部署,Eureka 被 Spring 官方收录,本身属于 Spring Cloud 体系中。
下面列出可以被用作注册中心的组件他们的特性对比:
特性 | Euerka | Consul | Zookeeper | etcd |
---|---|---|---|---|
服务健康检查 | 可配支持 | 服务状态,内存,硬盘等 | (弱)长连接,keepalive | 连接心跳 |
多数据中心 | — | 支持 | — | — |
kv 存储服务 | — | 支持 | 支持 | 支持 |
一致性 | — | raft | paxos | raft |
cap | ap | cp | cp | cp |
使用接口(多语言能力) | http(sidecar) | 支持 http 和 dns | 客户端 | http/grpc |
watch 支持 | 支持 long polling/大部分增量 | 全量/支持long polling | 支持 | 支持 long polling |
自身监控 | metrics | metrics | — | metrics |
安全 | — | acl /https | acl | https 支持(弱) |
spring cloud 集成 | 已支持 | 已支持 | 已支持 | 已支持 |
Consul 官网中介绍了 Consul 的以下几个核心功能:
Consul 需要单独部署,而不是与Spring集成的组件。
Eureka 是 Spring Cloud NetFlix 默认的服务发现框架,但目前 2.0 版本已闭源,只剩下 1.9 版本的处于维护状态。Eureka 使用尽力而为同步的方式提供提供弱一致的服务列表。当一个服务注册时,Eureka 会尝试将其同步到其他节点上,但不提供一致性的保证。 因此,Eureka 可以提供过时的或是已不存在的服务列表(在服务发现场景下,返回旧的总比什么也不返回好)。
如果在 15分钟内超过85%的客户端节点都没有正常的心跳 ,那么 Eureka 就会认为客户端与注册中心出现了网络故障(出现网络分区),进入自我保护机制。
此时:
优点:
Eureka Server 可以很好的应对因网络故障导致部分节点失联的情况,而不会像 ZK 那样如果有一半不可用的情况会导致整个集群不可用。
微服务的拆分导致服务分散,如果一个大的业务要对外提供输出,每个服务单独对外提供调用对接入方不友好并且调用也会很复杂。所以出现了网关,网关主要实现请求的路由转发,负载均衡,统一校验,请求过滤等功能。
目前社区主流的网关有三个:Zuul,Kong,Spring Cloud GateWay。
Zuul 是 Netflix 公司的开源项目,Spring Cloud 在 Netflix 项目中也已经集成了 Zuul,依赖名叫:spring-cloud-starter-netflix-zuul。Zuul构建于 Servlet 2.5,兼容 3.x,使用的是阻塞式的 API,不支持长连接,比如 websockets。我们现在说的 Zuul 指 Zuul 1.x,Netflix 最新的 Zuul 2.x一直跳票,所以 Spring Cloud 在Zuul 2.x没有出的时候依靠社区的力量发展出了新的网关组件:Spring Cloud Gateway。
Zuul 的核心功能就是基于 Servlet 提供了一系列的过滤器:
Spring Cloud Gateway 构建于 Spring 5+,基于 Spring Boot 2.x 响应式的、非阻塞式的 API。同时,它支持 Websockets,和 Spring 框架紧密集成,开发体验相对来说十分不错。SpringCloud Gateway 是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
总体来说 Spring Cloud Gateway 与Zuul 功能差别不大,最大的出入是在底层性能的提升上。
Zuul 本身是基于 Servlet 容器来实现的过滤,Servlet采用的是单实例多线程的处理方案,Servlet会为每一个Request分配一个线程,如果当前线程比较耗时那么会一直等到线程处理完毕才会返回。所以说 Zuul 是基于servlet之上的一个阻塞式处理模型。
同步阻塞模型对于网关这种比较在意响应耗时和调用频繁的组件来说,必然会引发一些性能问题,所以Zuul 2已经做出了改良,从Zuul 2开始已经使用Netty。但是不幸的是Spring 官方已经对它的更新频率感到失望所以纵然更新了也没有被选用。
Spring Cloud Gateway 底层基于Webflux。Webflux模式替换了旧的Servlet线程模型。用少量的线程处理request和response io操作,这些线程称为Loop线程。Webflux的Loop线程,正好就是著名的Reactor 模式IO处理模型的Reactor线程,如果使用的是高性能的通信框架Netty,这就是Netty的EventLoop线程。
所以整体来看,Spring Cloud Gateway 的性能要比目前在用的 Zuul 高。但是Webflux的编程方式可能大家不是很能接收。
降级限流在微服务中属于银弹,一般不用,一旦用上那就是拯救宇宙般存在。
目前业界通用的降级限流工具主要有3款:Hystrix,Sentinel,Resilience4j。
Hystrix 的关注点在于以 隔离 和 熔断 为主的容错机制,超时或被熔断的调用将会快速失败,并可以提供 fallback 机制。Hystrix 是元老级别的存在,但是在18年11月 Netflix 官方宣布停止更新(就是这么不靠谱,说跳票就跳票)。虽然停止更新,但是社区又推出了新的替代工具:Resilience4j。
Resilience4j 的模块化做的比较好,将每个功能点(如熔断、限速器、自动重试)都拆成了单独的模块,这样整体结构很清晰,用户也只需要引入相应功能的依赖即可;另外resilience4j 是针对 Java 8 和函数式编程设计的,API 比较简洁优雅。同时与 Hystrix 相比,Resilience4j 增加了简单的限速器和自动重试特性,使用场景更加丰富。
相比 Hystrix , Resilience4j的优势在于:
Resilience4j 属于一个新兴项目,社区也在蓬勃发展。总的来说,Resilience4j 是比较轻量的库,在较小较新的项目中使用还是比较方便的,但是 Resilience4j 只包含限流降级的基本场景,对于非常复杂的企业级服务架构可能无法很好地 cover 住;同时 Resilience4j 缺乏生产级别的配套设施(如提供规则管理和实时监控能力的控制台)。
Sentinel 一款面向分布式服务架构的轻量级流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统自适应保护等多个维度来帮助用户保障服务的稳定性。
Sentinel 的核心思想:根据对应资源配置的规则来为资源执行相应的流控/降级/系统保护策略。在 Sentinel 中资源定义和规则配置是分离的。用户先通过 Sentinel API 给对应的业务逻辑定义资源,然后可以在需要的时候动态配置规则。
整体功能对比:
Sentinel | Hystrix | Resilience4j | |
---|---|---|---|
隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔离/信号量隔离 | 信号量隔离 |
熔断降级策略 | 基于响应时间、异常比率、异常数 | 基于异常比率 | 基于异常比率、响应时间 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于 RxJava) | Ring Bit Buffer |
动态规则配置 | 支持多种数据源 | 支持多种数据源 | 有限支持 |
扩展性 | 多个扩展点 | 插件的形式 | 接口的形式 |
基于注解的支持 | 支持 | 支持 | 支持 |
限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 | Rate Limiter |
流量整形 | 支持预热模式、匀速器模式、预热排队模式 | 不支持 | 简单的 Rate Limiter 模式 |
系统自适应保护 | 支持 | 不支持 | 不支持 |
控制台 | 提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等 | 简单的监控查看 | 不提供控制台,可对接其它监控系统 |
从上面的参照看,Sentinel 的功能相对要多一些,但是多并不意味着所有,合适的才是最好的,对于你用不到的功能,简单才是美丽。
统一配置中心概念的提出也是伴随着微服务架构出现才出现,单体应用的时候所有的配置都可以集成在服务之中,多应用的时候如果每个应用都持有一份配置可能会有相同配置冗余的情况;如果一共有2000台机器,如果一个配置发生更改,是否要登录每一台机器重新更改配置呢;另外,更多的配置必然会带来管理上的混乱,如果没有集中管理的地方必然会越来越乱。
分布式配置管理的本质基本上就是一种 推送-订阅 模式的运用。配置的应用方是订阅者,配置管理服务则是推送方。其中,客户端包括管理人员publish数据到配置管理服务,可以理解为添加/更新数据;配置管理服务notify数据到订阅者,可以理解为推送。配置管理服务往往会封装一个客户端库,应用方则是基于该库与配置管理服务进行交互。在实际实现时,客户端库可能是主动拉取(pull)数据,但对于应用方而言,一般是一种事件通知方式。
选型一个合格的配置中心,至少需要满足如下4个核心需求:
最开始我接触过的配置中心是淘宝的 Diamond,Diamond中的数据是简单的key-value结构。应用方订阅数据则是基于key来订阅,未订阅的数据当然不会被推送。
Diamond是无单点架构,在做更新配置的时候只做三件事:
本地的设计就是为了缓存,减少对数据库的压力。作为一个配置中心,高可用是最主要的需求。如何保持高可用,Diamond持有多层的数据存储,数据被存储在:数据库,服务端磁盘,客户端缓存目录,以及可以 手工干预
的容灾目录。 客户端通过API获取配置数据按照固定的顺序去不同的数据源获取数据:容灾目录,服务端磁盘,客户端缓存。
Diamond除了在容灾上做了很多方案,在数据读取方面也有很多特点。客户端采用推拉结合的策略在长连接和短连接之间取得一个平衡,让服务端不用太关注连接的管理,又可以获得长连接的及时性。
使用Diamond的流程:
发布配置:
读取配置:
Diamond server是无中心节点的逻辑集群,这样就能避免单点故障。Diamond的同质节点之间会相互通信以保证数据的一致性,每个节点都有其它节点的地址信息,其中一个节点发生数据变更后会响应的通知其他节点,保证数据的一致性。
为了保证高可用,client还会在app端缓存一个本地文件,这样即使server不可用也能保证app可用。Client不断长轮询server,获取最新的配置推送,尽量保证本地数据的时效性。
Client默认启动周期任务对server进行长轮询感知server的配置变化,server感知到配置变化就发送变更的数据编号,客户端通过数据编号再去拉取最新配置数据;否则超时结束请求(默认10秒)。拉取到新配置后,client会通知监听者(MessageListener)做相应处理,用户可以通过Diamond#addListener监听。
但是Diamond一般用途是做KV存储,如果用来做配置中心,他提供的能力不是太符合。
可以看到早期的配置中心处理的东西还是比较简单,那个时候业务没有那么复杂,读取配置和更新配置没有那么多花样,持久化存储和本地缓存,长连接更新就可以。但是现在的配置中心随着技术的发展承担的作用可能更多,
Spring Cloud Config 作为Spring官方提供的配置中心可能比较符合外国人的习惯:
Spring Cloud Config将不同环境的所有配置存放在git 仓库中,服务启动时通过接口拉取配置。遵循{ServiceID}-{profile}.properties的结构,按照profile拉取自己所需的配置。
当开发者修改了配置项之后,需要结合spring config bus将配置通知到对应的服务,实现配置的动态更新。
可以看到,Spring Cloud Config已经具备了一个配置中心的雏形,可以满足小型项目对配置的管理,但仍然有着很多局限性。配置使用git库进行管理,那么git库的权限如何来判断?不同环境的安全性也得不到保障。配置的添加和删除,配置项的汇总,也只能通过git命令来实现,对运维人员也并不友好。
Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。
Apollo 支持4个维度管理 Key-Value 格式的配置:
Apollo配置中心包括:Config Service、Admin Service 和 Portal。
上图简要描述了Apollo的总体设计,我们可以从下往上看:
客户端设计:
上图简要描述了Apollo客户端的实现原理:
apollo.refreshInterval
配置更新:
前面提到了Apollo客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。
长连接实际上是通过Http Long Polling实现的,具体而言:
考虑到会有数万客户端向服务端发起长连,在服务端使用了async servlet(Spring DeferredResult)来服务Http Long Polling请求。
服务调用链路分析在微服务中是幕后至关重要的使者,试想几百个服务掺杂在一起,你想屡出谁先调用了谁,谁被谁调用,如果没有一个可监控的路径,光凭脑子跟踪那的多累。基于这种需求,各路大神们集中脑汁展开遐想弄出一套分布式链路追踪神器来。
在介绍调用链监控工具之前,我们首先需要知道在微服务架构系统中经常会遇到两个问题:
打个比方说我们有两个服务:订单中心,库存中心。用户下单,先去查询库存系统,那么调用链路分析系统对于一个下单查询服务应该记录什么呢?我们造出如下一张调用链路请求记录表,表字段如下:
表字段说明:
id | span_id | p_span_id | service_name | api | stage | time_stamp |
---|---|---|---|---|---|---|
1 | uid1 | null | order-center | /shop/0001 | cs | t1 |
2 | uid2 | uid1 | shop-center | /getCount/0001 | sr | t2 |
3 | uid2 | uid1 | shop-center | /getCount/0001 | ss | t3 |
4 | uid3 | null | order-center | /shop/0001 | cr | t4 |
上表中的stage中的状态解释为:
根据这个表我们就能很快的分析上面提到的两个问题:
如果以上任何一步有问题,那么当前调用就不是完整的,我们必然能追踪出来;
通过每一步的调用时间进行分析,我们也必然知道阻塞在哪一步,从而对调用慢的地方进行优化。
现有的分布式Trace基本都是采用了google 的 Dapper 标准。
标准。
Dapper的思想很简单,就是在每一次调用栈中,使用同一个TraceId将不同的server联系起来。
一次单独的调用链也可以称为一个span,dapper记录的是span的名称,以及每个span的ID和父ID,以重建在一次追踪过程中不同span之间的关系。
对于一个特定的span,记录从Start到End,首先经历了客户端发送数据,然后server接收数据,然后server执行内部逻辑,这中间可能去访问另一个应用。执行完了server将数据返回,然后客户端接收到数据。
在整个过程中,TraceId和ParentId的生成至关重要。首先解释下 TraceId
和 ParentId
。 TraceId
是标识这个调用链的Id,整个调用链,从浏览器开始放完,到A到B到C,一直到调用结束,所有应用在这次调用中拥有同一个 TraceId
,所以才能把这次调用链在一起。
既然知道了这次调用链的整个Id,那么每次查找问题的时候,只要知道某一个调用的 TraceId
,就能把所有这个Id的调用全部查找出来,能够清楚的知道本地调用链经过了哪些应用,产生了哪些调用。但是还缺一点,那就是链。
基于这种需求,目前各大厂商都做出了自己的分布式追踪系统,目前国内开源的有阿里的鹰眼,美团的CAT,京东的Hydra,还有广为人知的个人开源Apache顶级项目SkyWalking。国外的有Zipkin,Pinpoint。
Spring Cloud Sleuth 实现了一种分布式的服务链路跟踪解决方案,通过使用Sleuth可以让我们快速定位某个服务的问题。简单来说,Sleuth相当于调用链监控工具的客户端,集成在各个微服务上,负责产生调用链监控数据。
通过Sleuth产生的调用链监控信息,让我们可以得知微服务之间的调用链路,但是监控信息只输出到控制台始终不太方便查看。所以我们需要一个图形化的工具,这时候就轮到Zipkin出场了。Zipkin是Twitter开源的分布式跟踪系统,主要用来收集系统的时序数据,从而追踪系统的调用问题。
Spring Cloud Slueth 聚焦在链路追踪和分析,将信息发送到Zipkin,利用 Zipkin的存储来存储信息,当然,Zipkin也可以使用ELK来记录日志和展示,再通过收集服务器性能的脚本把数据存储到ELK,则可以展示服务器状况信息。
pinpoint数据分析非常完备的。提供代码级别的可见性以便轻松定位失败点和瓶颈,对于执行的sql语句,都进行了记录。还可以配置报警规则等,设置每个应用对应的负责人,根据配置的规则报警,支持的中间件和框架也比较完备。Pinpoint 是一个完整的性能监控解决方案:有从探针、收集器、存储到 Web 界面等全套体系。
Pinpoint 提供有 Java Agent 探针,通过字节码注入的方式实现调用拦截和数据收集,可以做到真正的代码无侵入,只需要在启动服务器的时候添加一些参数,就可以完成探针的部署。
对于这一点,Zipkin 使用修改过的类库和它自己的容器(Finagle)来提供分布式事务跟踪的功能。但是,它要求在需要时修改代码。pinpoint是基于字节码增强的方式,开发人员不需要修改代码,并且可以收集到更多精确的数据因为有字节码中的更多信息。
相对来说,pinpoint界面显示的更加丰富,具体到调用的DB名,zipkin的拓扑局限于服务于服务之间。
SkyWalking 和 pinpoint有一种既生瑜何生亮的感叹。SkyWalking 逻辑上分为四部分:
探针基于不同的来源可能是不一样的(原生代理, SDK 以及 Zipkin, Jaeger 和 OpenCensus )、但作用都是收集数据、将数据格式化为 SkyWalking 适用的格式。
平台后端是一个支持集群模式运行的后台、用于数据聚合、数据分析以及驱动数据流从探针到用户界面的流程、平台后端还提供了各种可插拔的能力、如不同来源数据(如来自 Zipkin)格式化、不同存储系统以及集群管理、你甚至还可以使用观测分析语言来进行自定义聚合分析。
存储是开放式的、你可以选择一个既有的存储系统、如 ElasticSearch, H2 或 MySQL 集群(Sharding-Sphere 管理)、也可以选择自己实现一个存储系统。
用户界面对于 SkyWalking 的最终用户来说非常炫酷且强大、同样它也是可定制以匹配你已存在的后端的。
以上是微服务过程全链路过程中需要经历的阶段,当然还不包括发布系统的搭建,底层数据治理能力。所以提倡微服务可以,但是真的做起来不是所有公司都能做得到。小公司能做到服务拆分但是相应的配套设施不一定能跟上,大公司有人有钱有时间,才能提供这些基础设施。微服务的路任重道远,搭起来一套完整的设施并用于生产环境还是挺有挑战,新的一年希望我能够在践行微服务的路上走下去,只有走的完整才是微服务,走的不完整对于开发人员来说,那就是过度开发,就是灾难。