Spring Cloud源于微服务架构概念的提出。所以在讲Spring Cloud之前,需要先谈谈微服务架构。
- 耦合严重:系统内往往使用API进行调用,代码耦合紧密导致维护困难,同时开发难度也骤增。
- 不够灵活:因为是单体式应用,所以对代码的每一次修改,都会导致整体应用的重新编译和部署(并且编译时间会随着代码量的增加而增加),这会使得开发和产品迭代成本上升。
- 扩容时资源浪费:对于极端场景,实际只需要对部分功能进行扩容。但是单体式应用不可拆分,只能以应用为单位进行扩容,浪费了机器资源。
- 服务独立开发和部署:以服务为单位进行开发,显然对整体系统了解的要求降低了。在开发过程中,只要专注于该服务需要完成的职责即可。另外,由于服务的独立性,所以可允许各服务自由选择自己的技术栈。
- 运维方便:通过对各个服务进行动态扩容,即可确保整体应用在极端场景下的稳定。
相比于单体式应用,微服务架构显然更加具有潜力。但是微服务架构也会面临一些实现问题,包括服务之间怎么感知,怎么调用,负载均衡等等。如果没有实现这些细节问题,那么微服务等同纸上谈兵。也正是这些问题催生出了Spring Cloud,当下最火热的微服务治理体系。Spring Cloud对于微服务的管理细节,针对性地提供了解决方案。使用Spring Cloud,就可以平滑地搭建基于微服务架构的应用。
Spring Boot
这里单独把Spring Boot列出来,是因为Spring Boot是当前最火热的开发微服务的框架,是Spring Cloud整个微服务治理框架的核心和基础。
在学习Spring Boot时,我第一个问题是Spring Boot和Spring的关系是什么?现在如果让我来回答这个问题,我的答案是:
- Spring:为Java EE提供了全面的基础支持,包括Spring IOC, Spring AOP, Spring MVC、 Spring JDBC 和 Spring Security等模块。开发者可自行选择使用的模块,并配合其他库(如MyBatis等)进行应用开发。
- Spring Boot:实现了自动配置,降低了项目搭建的复杂度,能够快速构建服务。
两者的区别在于:Spring中的各个模块就像是汽车零件。在Spring Boot出现前,用户需要自己组装这些零件,搭建出一辆完整的车。而Spring Boot就像是一个4S店,针对各种场景提供了诸如越野车、商务车等成品车,用户直接上门提车即可(如果用户对于某类车有定制化需求,只需进行些许改装即可)。Spring Boot提供了一系列Spring-boot-starter场景模板,减少了开发者配置项目的工作量,比如之前web项目一般是配置SSM(Spring MVC + Spring + MyBatis),而现在只需要依赖Spring-boot-start-web就能完成搭建。
Spring Boot使用也比较简单,仅需要在启动类上添加@SpringBootApplication就能完成自动扫描和启动;配置也比较简单,通常只需要改动properties或yml文件即可。
1 |
|
这里着重介绍下Spring Boot的主要特性:自动装配和Actuator。
自动装配
Spring Boot在启动时会自动装配,通过扫描所有jar包中的spring.factories,得到所有的AutoConfigure类,并开启对应的装配过程。一般来讲,AutoConfigure类的装配过程主要包括2步:
- 导入对应Properties类,并将配置信息复制到对应变量上。
- 根据Properties类属性和其他条件,AutoConfigure类将添加相关Bean。
自动装配使得通常情况下,开发者仅需在application.properties/application.yml中修改相应属性就可完成配置。
如果Spring Boot官方的spring-boot-starter不适用于应用场景,还可以自己创建starter,详情参见 自己写starter。
Actuator
1 | <dependency> |
一般在开发基于SpringBoot的Web应用时都会带上actuator。Actuator提供了许多生产级的特性,比如监控和健康检查等。具体来讲,Actuator以URL的形式(/actuator/xx)提供了一些接口,用于查看服务的各类信息,包括环境变量、线程dump等,具体可见Actuator官方文档。
服务注册和发现
微服务治理体系中,微服务是最小单位,但是往往某个业务功能需要多个微服务互相调用,这也意味着微服务之间需要能够相互感知,由此诞生了服务注册和发现。
- 服务注册:将某个服务的标识信息(通常是ip+端口)注册到某个公共组件中。
- 服务发现:某个服务从公共组件中获取其他服务的信息。
如果对此还不理解,那么我们做个比方:服务类似于一个个不同行业的企业,像建筑公司、维修公司等,这些公司会把自己的信息(包含电话号)注册到黄页中(这就是服务注册)。有一天,我们需要盖个新房,然后查黄页查到建筑公司的信息,打电话喊他们过来盖房子(这就是服务发现)。黄页就是上文定义中的公共组件。目前服务注册与发现中心有以下几种选择:
Eureka
Eureka是由Netflix开源的,分为两部分Eureka Server和Eureka Client。Eureka Server是服务注册中心;Eureka Client则部署在用户服务上,与Eureka Server进行交互,完成服务注册和发现过程。Eureka界面如下所示:
Eureka源于Netflix在AWS上微服务的实践,并根据AWS产生了region和zone的概念。
- region:可简单理解为地理上的分区,如华中华北地区等。
- zone:可以理解为region的具体机房。
对Eureka Client进行配置,可以实现同zone优先注册,也可以配合Ribbon完成同机房优先调用。
服务注册过程由Eureka Client主动发起,并定期发送心跳进行续约。如果Client连续多次无法更新lease,那么Eureka Server会在90秒内将该服务从注册表中删除。注册信息会在Eureka Server集群进行同步,这样不同zone内的服务能够相互发现。
Eureka的缺点主要有以下几个:
- Eureka不支持鉴权,无法对服务注册和发现的请求进行权限认证,这也意味着在Eureka层面服务可以互相调用,对于安全性是很大的风险。
- Eureka采用缓存设计,因此注册表中删除某些下线的服务的行为不是实时的,这就意味着服务消费端不能及时感知,需要依赖消费端的容错机制来保障运行。
- Eureka 2.0已经停止维护,未来可能也不会继续更新。
Zookeeper
Zookeeper是一个高可靠的分布式协调组件,其通常用于统一命名服务、统一配置管理、集合管理和分布式锁等。Zookeeper的核心功能通过两大类节点(大体可分为持久结点、短暂节点)+树型结构进行实现。
Zookeeper支持集群部署,节点间通过Zookeeper Atomic Broadcast(ZAB)协议进行同步,主要分为两部分:崩溃恢复和消息广播。
- 崩溃恢复:当leader出现网络中断、崩溃退出与重启等异常情况时采用该模式。新leader要具有集群中所有机器最高编号(zxid最大)的事务proposal(类似Raft)。选出leader之后,需要进行同步,将没有被follower同步的事务proposal逐个发送给follower,如果有不同的proposal,则进行回退操作。leader在正式工作前,会先确认事务日志中的所有proposal是否已经被集群中过半节点commit。leader服务器需要确保所有的follower能够接收到每一条proposal,并且能将所有已经提交的proposal应用到内存数据中。等到follower将所有尚未同步的事务proposal都从leader上同步过后并且应用到内存数据中以后,leader才会把该follower加入到真正可用的follower列表中。
- 消息广播:当leader选举成功后,同时集群中有过半的机器与该leade完成了状态同步之后,就会转入消息广播模式。消息广播类似二阶段提交过程。针对客户端的事务请求,leader服务器会为其生成对应的事务proposal,并将其发送给集群中其余节点,然后分别收集各自的选票,根据选票结果进行事务提交。
- 每个follower在接收到proposal之后,首先会以事务日志形式写到本地磁盘中,并且在写入成功后反馈给leader一个ack响应。leader收到超过半数的ack响应,就会广播一个commit消息给所有follower,通知它们进行事务提交,同时leader也会完成对事务的提交。
- 所有的follewer要么正常反馈leader提出的事务proposal,要么抛弃leader服务器。
- 在过半follower反馈ack之后就可以开始提交事务proposal 。
Zookeeper作为服务注册中心的缺点在于:Zookeeper为了保证高可靠性,追求CP,一旦遇上网络故障(leader节点与follower节点失去联系),剩余节点就会重新发起leader选举。选举持续时间较长,并且该期间整个Zookeeper集群不可用,那么就会导致服务注册和发现瘫痪。
总的来说zookeeper的设计理念(追求CP)跟注册中心的本质要求(AP)相悖,所以尽量不要使用zookeeper作为注册中心。详情参见: 为什么不应该使用ZooKeeper做服务发现
Consul
Consul是一套开源的分布式服务发现和配置管理系统,是由HashiCorp公司使用Go语言开发,其有如下特点:
- 服务发现:支持DNS和HTTP进行服务发现。
- 健康检查:提供不同的健康检查,包括web服务是否正常,本地节点内容利用率等。运维人员可以借助该功能监控集群健康。另外,服务发现组件也会基于健康检查结果进行路由规划。
- KV存储:使用简单的HTTP API就可实现动态配置、功能标记、协调、leader选举等功能。
- 安全服务沟通:Consul可以生成和分发TLS证书,用于服务间进行TLS连接,使用Intentions(意图)定义允许哪些服务进行通信。并且Intentions允许动态改变。
- 支持多数据中心
在Consul中的节点又称agent,分为两类client和server。server保存数据,client则负责健康检查,并将数据请求(如服务注册和发现)转发至server。server会对和client交互的信息进行聚合,并存储至catalog中。catalog维护了整个Consul集群的高层次视角,包括哪些服务可用,服务所在的节点,健康信息等等。
Consul集群中的节点通过Gossip协议(最终一致性协议,详情参见 Gossip协议)维护成员关系。每个节点都能了解集群内有哪些节点,这些节点是client还是server。
Consul的服务注册过程如下:
- 服务注册到Consul Client中。
- Consul Client并不直接处理注册请求,而是以RPC形式将请求转发給Consul Server。
- Consul Server处理注册请求时,会通过Raft协议同步至其他Server中。
Consul的服务发现过程如下:
- 服务向Consul Client发送服务发现请求。
- Consul Client将请求转发给Consul Server,并获得目标服务信息。
- Consul Client将目标服务信息反馈给发起请求的服务。
负载均衡
负载均衡是通过对请求流量分散到多个服务器来提高应用的性能和可靠性。常见的服务端负载均衡(即对客户端透明,由后端进行流量分发)有Nginx,LVS等。关于服务端负载均衡技术,主要是通过修改数据包目的Mac、NAT转发等进行实现。如果有想深入了解的,可以看下美团自研MGW的技术博客 MGW——美团点评高性能四层负载均衡。
Ribbon
Spring Cloud Ribbon是基于Netflix Ribbon实现的客户端负载均衡工具,主要是提供客户端的软件负载均衡算法和服务调用。
Ribbon一般和Eureka配合使用,通过给RestTemplate添加@LoadBalance,就可使用Ribbon。
1 |
|
Ribbon负载均衡的原理是:
- 当RestTemplate发起请求时,会被LoadBalancerInterceptor拦截,并将负载均衡逻辑交给loadBalancer。
- loadBalancer会根据当前服务实例列表以及对应的IRule(Ribbon默认是轮询),选择目标实例。
- 对目标实例发起请求,并对本次请求情况进行记录。
那么Ribbon是怎么获取服务实例呢?Ribbon会在初始化时,向Eureka注册中心获取服务注册列表,并且每10s一次向Eureka Client发送“ping”,来判断服务的可用性,如果服务的可用性发生改变或者服务数量和之前的不一致,则从注册中心更新或者重新拉取。
Ribbon提供了多种IRule,轮询、随机以及更复杂的算法。同时Ribbon还允许对不同服务采取不同的负载均衡算法,也允许用户实现IRule进行自定义负载均衡。
详情参见:深入理解Ribbon, Ribbon的负载均衡策略、原理和扩展。
简化服务调用
介绍到这,服务注册、发现等都清晰了,但是调用服务还是需要依赖RestTemplate发起请求才能获得结果。那么能否以一种更加简洁的方式进行服务调用呢?
OpenFeign
OpenFeign(基于Fegin)是一个声明式WebService客户端,使用它能更加简单地编写Web Service。使用了OpenFeign后,就可以直接以接口的方式调用服务,越过了RestTemplate主动调用的过程。简单来讲,OpenFegin对服务调用过程进行了包装,展现给程序员一个简单的接口,从此服务调用就好像在使用本地接口一样(具体的服务调用过程对用户透明)。
OpenFeign的使用方法是定义服务接口,然后在上面添加注解,主要有以下几步:
- 在主启动类上添加@EnableFeginClients
- 在接口上加@FeignClient,代码如下
1
2
3
4
5
6
"sayhello-service") (value =
public interface HelloClient {
"/sayHello") (value =
ServiceResult<String> sayHello();
}
对于HelloClient的sayHello方法调用,会转交给服务提供方相应URL的实现进行执行,并返回结果。
OpenFeign默认支持Ribbon,可使用Ribbon配置超时控制功能,还提供了日志功能,用于打印服务调用的具体信息,详细程度从低到高分别是None、BASIC、HEADERS和FULL。
服务熔断、降级、限流
如图5.1所示,微服务A调用微服务B和微服务C,B和C又调用其他的微服务。假如某个时刻流量激增,A服务的下流服务D率先崩溃,进而导致其上游服务B请求阻塞,耗尽资源,继续这样反向传播,那么就会导致微服务A不可用,也就是所谓的“雪崩效应”。
- 服务熔断:当下游服务因为某种原因变得不可用或响应过慢,上游服务为了保证自己整体服务的可用性,不再继续调用目标服务,而是直接返回,快速释放资源。如果目标服务状态好转,则恢复调用。
- 服务降级:如果是下游服务不可用或响应过慢,就调用对应的降级逻辑。如果是本地某些非核心功能需要消耗较多资源,那么就停掉这些非核心功能,优先保证核心功能。
- 服务限流:限制流量输入,使流量维持在服务器的承受范围内。
拿下棋比喻(引自降级-熔断-限流-傻傻分不清中的评论):
- 限流: 相当于尽量避免同时和两三个人同时下
- 熔断:相当于你的一颗卒被围死了,就不要利用其它棋去救它了,弃卒保帅,否则救他的棋也可能被拖死
- 降级:相当于尽量不要走用处不大的棋了,浪费走棋机会(资源),使已经过河的棋有更多的走棋机会(资源)发挥最大作用
三者之间的关系我觉得是:熔断是服务降级的一种方式;服务降级和限流都是应对流量压力的不同处理方法。详情见 降级-熔断-限流-傻傻分不清。
这里对限流再提一嘴,限流有点类似牺牲客户体验感来换取服务器的运行正常,其通用的方法有令牌桶算法,漏桶算法等(详情见 接口限流算法:漏桶算法&令牌桶算法)。
Hystrix
在分布式环境中,不可避免地会出现许多服务依赖项中的某些失败。Hystrix能保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。通过Hystrix能够实现服务降级和熔断。当某个服务单元发生故障时,断路器会向调用方返回一个符合预期的备选响应,而不是长时间的等待,这样就保证了调用方线程不会被不必要地占用。不过可惜的是,Hystrix目前停止更新,进入维护状态。
Hystrix服务降级
Hystrix通过设置fallbackMethod达到降级的效果,即当方法出问题时,替换成调用兜底方法。兜底方法的设置需要在业务类方法上添加@HystrixCommand:
1 | public class HelloService { |
以上设置能使hello方法执行时间超过3秒时,调用设定好的兜底方法。
Hystrix除了可以对单个方法设置兜底方法,也可以设置全局兜底方法,也可以对Fegin的WebService接口设置兜底实现类。
Hystrix熔断
熔断器主要依赖于上述三个状态变化,即当压力变大时,从Closed变为Open,当压力逐渐减小时从Open到Half Open,最后完全恢复时从Half Open到Closed。其实就相当保险丝,过载了就短路,然后试探性恢复。详细可见CircuitBreaker。
Hystrix的熔断设置也很简单,也是通过设置兜底方法实现的。只是@HystrixCommand配置略有不同:
1 | "helloTimeoutHandler", commandProperties = { (fallbackMethod = |
上述配置指的是超过10次调用中失败率达到60%时,熔断器的状态就会从Closed变为Open,直接调用降级fallback方法。在10秒后会尝试放行方法调用(此时状态为Half Open),如果此时方法正常返回,则熔断器变回Closed;如果失败,则变回Open。
服务网关
服务网关位于应用程序或服务(提供Rest API)之前的系统,用来管理授权、访问控制器和流量限制等,这样Rest API就被API网关保护起来,对调用者透明。这样的话,隐藏在API后的业务系统就可以专注于业务实现,而不用去处理这些基础控制策略。
Gateway
SpringCloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Zuul。SpringCloud Gateway的目标是提供统一的路由方式且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/指标和限流。
Gateway有三个基本概念:
- Route(路由):是构建网关的基本模块,由ID,目标URI,一系列断言和过滤器组成,如果断言为true则匹配该路由。
- Predicate(断言):可以跟HTTP中的所有内容(如请求头或请求参数)进行匹配,如果请求与断言匹配则进行路由。
- Filter(过滤):在请求被路由之前或之后,可以对请求/响应进行修改,有点类似拦截器。
Gateway支持yml配置和JavaBean,以下是JavaBean形式:
1 |
|
服务配置
服务配置指一套集中式、动态的配置管理设施,去管理大量微服务的配置信息。
Config
Spring Cloud Config就是面向分布式配置管理而生的,分为server和client两部分:
- server:又称分布式配置中心,是一个独立的微服务应用,用来连接配置服务器(默认是GIT,也支持SVN)并为客户端提供配置信息。
- client:通过指定的server来管理应用资源,以及业务相关的配置内容。会在启动时,拉取server中配置信息。
Config默认使用GIT来保存和更新配置,server端的yml配置为:
1 | spring: |
注意server是实时读取git仓库内容的,而client端则不是实时读取Server端内容,需要。当更新git仓库内容时,会带来动态刷新问题。需要引入Actuator的刷新功能,在配置更改后,手动发送Actuator刷新请求,使client重新从server拉取最新配置。
服务总线
在微服务架构中国,通常会使用轻量级消息代理构建一个共有的消息主题,并让系统中所有微服务实例连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称之为消息总线。在总线上的任何一个连接实例上,都可以方便地广播一些全局消息。这类全局消息能用于很多场景,比如上文提到的Config手动刷新问题。
Bus
Spring Cloud Bus就是服务总线的一种实现,可以配合Spring Cloud Config完成动态刷新。Bus支持Kafka和RabbitMQ。其配合Config的流程图如下所示:
上图已经很清楚,Bus的功能就是在监听发送给微服务的消息,从而将其广播至所有应用。当然也可以直接发送刷新请求给Bus,再由Bus进行广播。