1、CAP原则又称CAP定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。
一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
分区容忍性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
CAP原则的精髓就是要么AP,要么CP,要么AC,但是不存在CAP。如果在某个分布式系统中数据无副本, 那么系统必然满足强一致性条件, 因为只有独一数据,不会出现数据不一致的情况,此时C和P两要素具备,但是如果系统发生了网络分区状况或者宕机,必然导致某些数据不可以访问,此时可用性条件就不能被满足,即在此情况下获得了CP系统,但是CAP不可同时满足 [1] 。
因此在进行分布式架构设计时,必须做出取舍。当前一般是通过分布式缓存中各节点的最终一致性来提高系统的性能,通过使用多节点之间的数据异步复制技术来实现集群化的数据一致性。通常使用类似 memcached 之类的 NOSQL 作为实现手段。虽然 memcached 也可以是分布式集群环境的,但是对于一份数据来说,它总是存储在某一台 memcached 服务器上。如果发生网络故障或是服务器死机,则存储在这台服务器上的所有数据都将不可访问。由于数据是存储在内存中的,重启服务器,将导致数据全部丢失。当然也可以自己实现一套机制,用来在分布式 memcached 之间进行数据的同步和持久化,但是实现难度是非常大的 [2] 。
2、配置文件中spring.profiles属性来定义多个不同的环境信息
3、 代表启动项目是在用测试环境的配置及环境
java -jar xxx.jar --spring.profiles.active=test
**
**
Register Service :服务注册中心,它是一个 Eureka Server ,提供服务注册和发现的功能
@EnableEurekaServer
@SpringBootApplication
public class RegistserverApplication extends SpringBootServletInitializer {
........}
Provider Service :服务提供者,它是一个 Spring Boot ,提供服务
@SpringBootApplication
@EnableHystrix //熔断
@EnableDiscoveryClient //发现服务
@EnableAspectJAutoProxy(exposeProxy = true)
@EnableAsync
@EnableFeignClients //feign
public class Application extends SpringBootServletInitializer {
@Bean
@LoadBalanced //负载均衡 Ribbon
RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
} }
Consumer Service :服务消费者,它是 一个Eureka Cient ,消费服务。
消费者应用从服务注册中心获取服务列表,从而使消费者可以知道去何处调用其所需要的服务
Eureka 的一些概念
(1) Register 一一服务注册
当"服务提供者"启动时就会通过REST请求的方式将自己注册到 Eureka Server 上,同时带上自身的元数据,比如 IP 地址、端口、运行状况指标的 Url、 主页地址等信息。Eureka Server接收到这个REST请求之后,将元数据信息存储在一个双层结构Map中,其中第一层的key是服务名,第二层的key是具体服务的实例名。(Eureka信息面板中一个服务有多个实例的情况,这些内容就是以这样的双层Map形式存储的)
(2)服务同步
如果有两个服务提供者分别注册到了两个不同的服务注册中心上,也就是,他们的信息分别被两个服务注册中心所维护。此时,由于服务注册中心之间因互相注册为服务,当服务提供者发送注册请求到一个服务注册中心时,它会将该请求转发给集群中相连的其他注册中心,从而实现注册中心之间的服务同步。通过服务同步,两个服务提供者的服务信息就可以通过这两天服务注册中心的任意一台获取到。
(3) Renewal一一服务续约
Eureka Client 在默认的情况下会每隔 30 秒发送一次心跳来进行服务续约。通过服务续约
来告知 Eureka Server 该Eureka Client 仍然可用,没有出现故障。正常情况下,如果 Eureka Server
在90 秒内没有收到 Eureka Client 的心跳, Eureka Server 会将 Eureka Client 实例从注册列表中
删除。注意:官网建议不要更改服务续约的间隔时间。
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
(4) Fetch Registries一一获取服务注册列表信息
当我们启动服务消费者的时候,它会发送一个REST请求到服务注册中心,来获取上面注册的服务清单。Eureka Client (消费者)从Eureka Server(服务注册中心) 获取服务注册表信息,井将其缓存在本地。 Eureka Client会使用服务注册列表信息查找其他服务的信息,从而进行远程调用。Eureka Server会维护一份只读的服务清单来返回给客户端,同时该缓存清单会(每30 秒) 更新一次。
eureka.client.fetch-registry=true //服务消费者获取服务
eureka.client.registry-fetch-interval-seconds=30 //缓存清单的更新时间
(5)服务调用
服务消费者在获取服务清单后,通过服务名可用获得提供服务的实例名和该实例的元数据信息。有了服务实例的详细信息,客户端可以根据自己的需要决定调用哪个实例,在Ribbon中会默认采用轮询的方式进行调用,从而实现客户端的负载均衡。
对于访问实例的选择,Eureka中有Region和Zone的概念,一个Region中可用包含多个Zone,每个服务客户端需要被注册到一个Zone中,所以每个客户端对应一个Region和一个Zone。在进行服务调用的时候,优先访问同处一个Zone中的服务提供方,若访问不到,就访问其他的Zone。
(6) Cancel --一服务下线
在系统运行过程中必然会面临关闭或重启服务的某个实例的情况,在服务关闭期间,我们自然不希望客户端会继续调用关闭了的实例。所以在客户端程序中,当服务实例进行正常的关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务端在接收到请求之后,将该服务状态设置为下线(DOWN),并把该下线事件传播出去。
关闭时调用以下代码:
DiscoveryManager . getinstance() .shutdownComponent();
( 7) Eviction一一服务剔除
有些时候,我们的服务实例并不一定会正常下线,可能由于内存溢出、网络故障等原因使得服务不能正常工作,而服务注册中心并未收到“服务下线”的请求。为了从服务列表中将这些无法提供服务的实例剔除,Eureka Server在启动的时候会创建一个定时任务,默认每隔一段时间(默认为60秒)将当前清单中超时(默认为90秒)没有续约的服务剔除出去。
细节
1、现有服务C希望调用服务A,服务C就需要向注册中心发起咨询服务请求,C便获得了服务A的两个可用位置192.168.0.100:8000和192.168.0.101:8000。当服务C要发起调用的时候,便从该清单中以某种轮询策略取出一个位置来进行服务调用,
这就是客户端负载均衡。实际的框架为了性能等因素,不会采用每次都向服务注册中心获取服务的方式,并且不同的应用场景在缓存和服务剔除等机制上也会有一些不同的实现策略
2、Spring Cloud Eureka 使用Netflix Eureka来实现服务注册与发现,它既包含了服务端组件,也包含了客户端组件,并采用java编写
3、 注册中心
@EnableEurekaServer
@SpringBootApplication
public class Application{......}
4、appication.yml
eureka.client.register-with-euerka:false //由于该应用为注册中心,所有设置为false,代表不向注册 中心注册自己
eureka.client.fetch-registry //由于注册中心的职责就是维护服务实例,它并不需要去检索服务,所以也设置为false
5、 在Spring Boot启动类中通过加上@EnableDiscoveryClient注解,激活euerka中的DiscoveryClient实现(自动化配置,创建DiscoveryClient接口针对Eureka客户端的EurekaDiscoveryClient实例)
@Autowired private DiscoveryClient client;
public class DiscoveryClient implements EurekaClient
6、一个微服务应用只可以属于一个Region,Region 和 Zone是一对多的关系,在获取了Region和Zone的信息之后,才开始真正加载Eureka Server的具体地址。
重点
1、高可用注册中心
Eureka Server 的高可用实际上就是将自己作为服务向其他服务注册中心注册自己,这样就可以形成一组相互注册的服务注册中心,以实现服务清单的互相同步,达到高可用的效果。下面我们就来尝试搭建高可用服务注册中心的集群,构建一个双节点的服务注册中心集群。
(一)创建application-peer1.properties,作为peer1服务中心的配置,并将serviceUrl直向peer2:
spring.application.name=euerka-server server.port=1111 eureka.instance.hostname=peer1 eureka.client.serviceUrl.defaultZone=http://peer2:1112/eureka/
(二)创建application-peer2.properties,作为peer2服务中心的配置,并将serviceUrl直向peer1:
spring.application.name=euerka-server server.port=1112 eureka.instance.hostname=peer2 eureka.client.serviceUrl.defaultZone=http://peer1:1111/eureka/
修改本机电脑/etc/hosts
127.0.0.1 peer1
127.0.0.1 peer2
通过spring.profiles.active属性来分别启动peer1和peer2:
java -jar eureka-server-1.0.0.jar --spring.profiles.active=peer1
java -jar eureka-server-1.0.0.jar --spring.profiles.active=peer2
此时访问http://localhost:1111或者http://localhost:1112都有两个节点(自己和对方节点)
在设置了多节点的服务注册中心之后,服务提供方还需要修改一些简单的配置才能将服务注册到Eureka Server集群中。
修改Spring Boot服务提供配置文件
spring.application.name=spring-boot-server
eureka.client.serviceUrl.defaultZone=http://peer1:1111/eureka/,http://peer2:1112/eureka/
启动该服务,通过访问http://localhost:1111或者http://localhost:1112,可用观察到spring-boot-server 服务同时被注册到peer1和peer2
**
**
Ribbon是将负载均衡逻辑以代码的形式封装到服务消费者的客户端上,服务消费者客户端维护了一份服务提供者的信息列表 ,这些服务端的清单来自于服务注册中心,有了信息列表,通过负载均衡策略将请求分摊给多个服务提供者,从而达到负载均衡的目的。
1、在Spring Boot启动类加上
@Bean
@LoadBalanced //负载均衡
RestTemplate restTemplate() {
return new RestTemplate();
}
2、通过Spring Cloud Ribbon 的封装,我们在微服务架构中使用客户端负载均衡调用非常简单,只需要下面两步:
(一)服务提供者只需要启动多个服务实例并注册到一个注册中心或者多个相关联的服务注册中心。
(二)服务消费者直接通过调用被@LoadBalanced注释修饰过的RestTemplate来实现面向服务的接口调用
这样,我们就可以将服务提供者的高可用以及服务消费者的负载均衡调用一起实现了。
3、Spring Cloud Ribbon 通过LoadBalancerInterceptor拦截器对RestTemplate的请求进行拦截,并利用Spring Cloud的负载均衡器LoadBalancerClient将以逻辑服务名为host的URL转换成具体的服务实例地址的过程。在使用Ribbon实现负载均衡的时候,实际使用的还是Ribbon中定义的ILoadBalancer接口的实现,自动化配置会采用ZoneAwareLoadBalancer的实例来实现客户端的负载均衡。
4、Ribbon实现负载均衡策略(父接口 IRule)
(一)AbstractLoadBalancerRule
负载均衡策略的抽象类,在该抽象类中定义了负载均衡器ILoadBalancer对象,该对象能够在具体实现选择服务策略时,获取到一些负载均衡中维护的信息来作为分配依据,并以此设计一些算法来实现针对特定场景的高效策略。
**
**
(@EnableFeignClients)
Feign 的源码实现过程如下。
(1) 首先通过@EnableFeignClients 注解开启 FeignClient 的功能。只有这个注解存在,才
会在程序启动时开启对@FeignClient 注解的包扫描
(2)根据 Feign的规则实现接口,并在接口上面加上@FeignClient
(3)程序启动后,会进行包扫描,扫描所有的@ FeignClient 注解 ,并将这些信息
注入 IoC 容器中。
(4)当接口的方法被调用时 ,通过 JDK 的代理来生成具体的 RequestTemplate 模板对象
(5) 根据 RequestTemplate 再生成 Http 请求的Request 对象
(6) Request 对象交给 Client 去处理 ,其中 Client 的网络请求框架可以是 HttpURLConnection、
HttpClient 和OkHttp
(7)最后 Client 被封装到 LoadBalanceClient 类,这个类结合类 Ribbon 做到了负载均衡
**
**
(@EnableHystrix)
设计原则
防止单个服务的故障耗尽整个服务的 Servlet 容器(例如 Tomcat)的线程资源。
快速失败机制,如果某个服务出现了故障,则调用该服务的请求快速失败,而不是线程等待。
提供回退(fallback)方案,在请求发生故障时,提供设定好的回退方案。
使用熔断机制,防止故障扩散到其他服务。
提供熔断器的监控组件 Hystrix Dashboard,可以实时监控熔断器的状态。
工作机制:
首先,当服务的某个 API 接口的失败次数 在一定时间内小于设定的阀值时,熔断器处于关闭状态,该 API 接口正常提供服务。
当该 API 接口处理请求的失败次数大于设定的阀值时, Hystrix 判定该 API 接口出现了故障,打开 熔断器,这时请求该 API
接口会执行快速失败的逻辑(即 fallback 回退的逻辑),不执行业 务逻辑,请求的线程不会处于阻塞状态。处于打开状态的熔断器,
一段时间后会处于半打开状态,并将一定数量的请求执行正常逻辑。剩余的请求会执行快速失败,若执行正常逻辑的 请求失败了,
则熔断器继续打开: 若成功了 ,则将熔断器关闭。这样熔断器就具有了自我修 复的能力。
**
**
(@EnableZuulProxy)
工作原理
Zuul是通过Servlet来实现的,Zuul通过自定义的ZuulServlet(类似于Spring MVC的DispatchServlet〕来对请求进行控制。
Zuul 的核心是一系列过滤器,可以在 Http 请求的发起和 响应返回期间执行一系列的过滤器。 Zuul 包括以下 4 种过滤器。
口 PRE 过滤器: 它是在请求路由到具体的服务之前执行的,这种类型的过滤器可以做 安全验证,例如身份验证、 参数验证等。
口 ROUTING 过滤器: 它用于将请求路由到具体的微服务实例。在默认情况下,它使用 Http Client 进行网络请求。
口 POST 过滤器:它是在请求己被路由到微服务后执行的。 一般情况下,用作收集统计 信息、指标,以及将响应传输到客户端。
口 ERROR 过滤器:它是在其他过滤器发生错误时执行的。
Zuul 采取了动态读取、编译和运行这些过滤器。 过滤器之间不能直接相互通信,而是通 过 RequestContext 对象来共享数据,
每个请求都会创建一个 RequestContext 对象。 Zuul 过滤器 具有以下关键特性。
口 Type (类型): Zuul 过滤器的类型,这个类型决定了过滤器在请求的哪个阶段起作用, 例如 Pre、 Post 阶段等。
口 Execution Order (执行顺序):规定了过滤器的执行顺序, Order 的值越小,越先执行。
口 Criteria (标准): Filter 执行所需的条件。
口 Action (行动〉: 如果符合执行条件,则执行 Action (即逻辑代码)。
使用方式
Zuul 是采用了类似于 Spring MVC 的 DispatchServlet 来实现的,采用的是异步阻塞模型, 所以性能比 Ngnix 差。
由于 Zuul 和其他 Netflix 组件可以相互配合、无缝集成, Zuul 很容易 就能实现负载均衡、智能路由和熔断器等功能。
在大多数情况下, Zuul 都是以集群的形式存 在的。由于 Zuul的横向扩展能力非常好,所以当负载过高时,可以通过添加实例来解决性 能瓶颈。
一种常见的使用方式是对不同的渠道使用不同的 Zuul 来进行路由,例如移动端共用一个 Zuul 网关实例, Web 端用另一个 Zuul 网关实例,
其他的客户端用另外一个 Zuul 实例进行路由。 这种不同的渠边用不同 Zuul 实例的架构如图 9-3 所示。 另外一种常见的集群是通过 Ngnix 和 Zuul 相互结合来做负载均衡。
暴露在最外面的是 Ngnix 主从双热备进行 Keepalive, Ngnix 经过某种路由策略,将请求路由转发到 Zuul 集群上, Zuul 最终将请求分发到具体的服务上。
**
**
(@EnableConfigServer)
Spring Cloud Bus 是用轻量的消息代理将分布式的节点连接起来,可以用于广播配置文件 的更改或者服务的监控管理。关键的思想就是,消息总线可以为微服务做监控,也可以实
现应用程序之间相互通信。 Spring Cloud Bus 可选的消息代理组建包括 RabbitMQ、 AMQP 和 Kafka 等。本节讲述的是用 RabbitMQ 作为 Spring Cloud 的消息组件
去刷新更改微服务的配置 文件。 为什么需要用 Spring Cloud Bus 去刷新配置呢? 如果有几十个微服务,而每一个服务又是多实例,当更改配置时,
需要重新启动多个微服务实例,会非常麻烦。 Spring Cloud Bus 的一个功能就是让这个过程变得简单,当远程 Git 仓 库的配置更改后
,只需要向某一个微服务实例发送一个 Post 请求,通过消息组件通知其他微 服务实例重新拉取配置文件。 当远程 Git 仓库的配置更改后,
通过发送 “ /bus/refresh” Post 请求给某一个微服务实例,通过消息组件,通知其他微服务实例,更新配 置文件。
@RestController
@RefreshScope
public class ConfigClientAppl工cat工on {
@Value (" ${ fool ” )
String foo;
@GetMapping (value = “/foo”)
public String hi() {
return foo;
}
**
**
(@EnableZipkinServer) (常见的链路追踪组件有 Google的Dapper 、Twitter的Zipkin ,以及阿里的 Eagleeye(鹰眼)等)
Spring Cloud Sleuth 采用了 Google 的开源项目 Dapper 的专业术语。
(1) Span: 基本工作单元,发送一个远程调度任务就会产生一个 Span, Span 是用一个 64 位 ID 唯一标识的, Trace 是用另一个 64 位 ID 唯一标识的。 Span 还包含了其他的信息,例如 摘要、时间戳事件、 Span 的 ID 以及进程 ID。
(2) Trace:由一系列 Span 组成的,呈树状结构。请求一个微服务系统的 API 接口 ,这个 API 接口需要调用多个微服务单元,调用每个微服务单元都会产生一个新的 Span, 所有由这 个请求产生的 Span 组成了这个 Trace。
( 3) Annotation:用于记录一个事件, 一些核心注解用于定义一个请求的开始和结束,这 些注解如下。
口 cs-Client Sent: 客户端发送一个请求,这个注解描述了 Span 的开始。
口 sr-Server Received:服务端获得请求并准备开始处理它,如果将其 sr 减去 cs 时间戳, 便可得到网络传输的时间。
口 ss-Server Sent:服务端发送响应, 该注解表明请求处理的完成(当请求返回客户端), 用 SS 的时间戳减去 sr 时间戳,便可以得到服务器请求的时间。
口 cr-Client Received: 客户端接收响应, 此时 Span 结束,如果 er 的时间戳减去 cs 时间 戳,便可以得到整个请求所消耗的时间。