资深架构师解析springcloud分布式微服务的实现

分布式系统

微服务就是原来臃肿的项目拆分为多个模块互不关联。如:按照子服务拆分、数据库、接口,依次往下就更加细粒度,固然运维也就愈来愈难受了。java

分布式则是偏向与机器将诺大的系统划分为多个模块部署在不一样服务器上。web

微服务和分布式就是做用的“目标不同”。算法

微服务与Cloud

微服务是一种概念,spring-cloud是微服务的实现。spring

微服务也不必定必须使用cloud来实现,只是微服务中有许多问题,如:负载均衡、服务注册与发现、路由等等。数据库

而cloud则是将这些处理问题的技术整合了。api

Spring-Cloud 组件

Eureka缓存

Eureka是Netifix的子模块之一,Eureka有2个组件,一个EurekaServer 实现中间层服务器的负载均衡和故障转移,一个EurekaClient它使得与server交互变得简单。服务器

Spring-Cloud封装了Netifix公司开发的Eureka模块来实现服务注册和发现。网络

经过Eureka的客户端 Eureka Server维持心跳链接,维护能够更方便监控各个微服务的运行。架构

角色关系图

Eureka使用

客户端

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
server:
port: 4001
eureka:
client:
serviceUrl:
defaultZone: http://localhost:3000/eureka/ #eureka服务端提供的注册地址 参考服务端配置的这个路径
instance:
instance-id: admin-1 #此实例注册到eureka服务端的惟一的实例ID
prefer-ip-address: true #是否显示IP地址
leaseRenewalIntervalInSeconds: 10 #eureka客户须要多长时间发送心跳给eureka服务器,代表它仍然活着,默认为30 秒 (与下面配置的单位都是秒)
leaseExpirationDurationInSeconds: 30 #Eureka服务器在接收到实例的最后一次发出的心跳后,须要等待多久才能够将此实例删除,默认为90秒

spring:
application:
name: server-admin #此实例注册到eureka服务端的name

服务端

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-
netflix-eureka-server</artifactId>
</dependency>
yml文件声明 
server:
port: 3000
eureka:
server:
enable-self-preservation: false #关闭自我保护机制
eviction-interval-timer-in-ms: 4000 #设置清理间隔
(单位:毫秒 默认是60*1000)
instance:
hostname: localhost 
client:
registerWithEureka: false #不把本身做为一个客户端注册到本身身上
fetchRegistry: false #不须要从服务端获取注册信息
(由于在这里本身就是服务端,并且已经禁用本身注册了)
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka
在SpringBoot 启动项目中加入注解:@EnableEurekaServer 
就能够启动项目了,访问对应地址就能够看到界面。

Eureka 集群

服务启动后Eureka Server会向其余服务server 同步,当消费者要调用服务提供者,则向服务注册中心获取服务提供者的地址,而后将提供者的地址缓存到本地,下次调用时候直接从本地缓存中获取

yml 服务端

server:
port: 3000
eureka:
server:
enable-self-preservation: false #关闭自我保护机制
eviction-interval-timer-in-ms: 4000 #设置清理间隔
(单位:毫秒 默认是60*1000)
instance:
hostname: eureka3000.com 
client:
registerWithEureka: false #不把本身做为一个客户端
注册到本身身上
fetchRegistry: false #不须要从服务端获取注册信息
(由于在这里本身就是服务端,并且已经禁用本身注册了)
serviceUrl:
defaultZone: http://eureka3001.com:3001/eureka,
http://eureka3002.com:3002/eureka
(这里不注册本身,注册到其余服务上面觉得会同步。)

yml 客户端

server:
port: 4001
eureka:
client:
serviceUrl:
defaultZone:http://localhost:3000/eureka/,http://
eureka3001.com:3001/eureka,http://eureka3002.com:3 002
/eureka #eureka服务端提供的注册地址 参考服务端配置的这个路径
instance:
instance-id: admin-1 #此实例注册到eureka服务端的惟一的实例ID
prefer-ip-address: true #是否显示IP地址
leaseRenewalIntervalInSeconds: 10 #eureka客户须要多长时间发
送心跳给eureka服务器,代表它仍然活着,默认为30 秒 (与下面配置的单位都是秒)
leaseExpirationDurationInSeconds: 30 #Eureka服务器在
接收到实例的最后一次发出的心跳后,须要等待多久才能够将此实例删除,默认为90秒

spring:
application:
name: server-admin #此实例注册到eureka服务端的name

CAP定理

C:Consistency 一致性
A:Availability 可用性
P:Partition tolerance 分区容错性
这三个指标不能同时达到

Partition tolerance

分区容错性,大多数分布式系统都部署在多个子网络。每个网络是一个区。区间的通讯是可能失败的如一个在本地,一个在外地,他们之间是没法通讯的。分布式系统在设计的时候必需要考虑这种状况。

Consistency

一致性,写操做后的读取,必须返回该值。如:服务器A1和服务器A2,如今发起操做将A1中V0改成V1,用户去读取的时候读到服务器A1获得V1,若是读到A2服务器可是服务器

仍是V0,读到的数据就不对,这就不知足一致性。

因此让A2返回的数据也对,的让A1给A2发送一条消息,把A2的V0变为V1,这时候无论从哪里读取都是修改后的数据。

Availability

可用性就是用户只要给出请求就必须回应,不论是本地服务器仍是外地服务器只要接收到就必须作出回应,无论数据是不是最新必须作出回应,负责就不是可用性。

C与A矛盾

一致性和可用性不能同时成立,存在分区容错性,通讯可能失败。

若是保证一致性,A1在写操做时,A2的读写只能被锁定,只有等数据同步了才能读写,在锁按期间是不能读写的就不符合可用性。

若是保持可用性,那么A2就不会被锁定,因此一致性就不能成立。

综上 没法作到一致性和可用性,因此系统在设计的时候就只能选其一。

Eureka与Zookeeper

Zookeeper遵循的是CP原则保持了一致性,因此在master节点由于网络故障与剩余“跟随者”接点失去联系时会从新选举“领导者”,选取“领导者”大概会持续30-120s的时间,且选举的时候整个zookeeper是不可用的。致使在选举的时候注册服务瘫痪。

Eureka在设计的时候遵循AP可用性。Eureka各个接点是公平的,没有主从之分,down掉几个几点也没问题,其余接点依然能够支持注册,只要有一台Eureka在,注册就能够用,只不过查询到的数据可能不是最新的。Eureka有自我保护机制,若是15分钟以内超过85%接点都没有正常心跳,那么Eureka认为客户端与注册中心出现故障,此时状况多是

Eureka不在从注册列表移除由于长时间没有瘦到心跳而过时的服务。

Eureka仍然可以接收注册和查询,但不会同步到其余接点。

当网络稳定后,当前的 实例注册信息会更新到其余接点。

Ribbon

rebbon主要提供客户端的负载均衡,提供了一套完善的客户端的配置。Rebbin会自动帮助你基于某种规则(如:简单的轮询,随机连接等)。

服务端的负载均衡是一个url经过一个代理服务器,而后经过代理服务器(策略:轮询,随机 ,权重等等),反向代理到你的服务器。

客户端负载均衡是经过一个请求在客户端已经声明了要调用那个服务,而后经过具体算法来完成负载均衡。

Ribbon使用

引入依赖,Eureka以及把Ribbon集成在里面。

使用Ribbon只有在RestTemplate上面加入@LoadBalanced注解。

Feign负载均衡

feign是一个声明式的webService客户端,使用feign会让编写webService更简单,就是定义一个接口加上注解。

feign是为了编写java http客户端更加简单,在Ribbon+RestTemplate此基础上进一步封装,简化了使用Spring Cloud Ribbon时,自动封装服务调用客户端的开发量。

Feign使用

引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
在启动类上加@EnableFeignClients
而后在接口上加@FeignClient("SERVER-POWER")注解其中参数就是服务的名字。

Feign集成Ribbon

利用Ribbon维护服务列表信息,融合了Ribbon的负载均衡配置,与Ribbon不一样的是Feign只须要定义服务绑定接口以声明的方式,实现简答的服务调用。

hystrix断路器

是一种用于处理分布式系统延迟和容错的开源库。在分布式系统中许多依赖不可避免的会调用失败,好比超时、异常等,断路器保证出错不会致使总体服务失败,避免级联故障。

断路器其实就是一种开关设置,相似保险丝,像调用方返回一个符合预期的、可处理的备选响应,而不是长时间等待或者抛出没法处理的异常,保证服务调用方线程不会被长时间 没必要要占用,从而避免了在分布式系统中蔓延,乃至雪崩。

微服务中 client->微服务A->微服务B->微服务C->微服务D,其中微服务B异常了,全部请求微服务A的请求都会卡在B这里,就会致使线程一直累积在这里,那么其余微服务就没有可用线程,致使整个服务器雪崩。

针对这方案有 服务限流、超时监控、服务熔断、服务降级

降级 超时

降级就是服务响应过长 ,或者不可用了,就是服务调用不了了,咱们不能把错误信息返回出来,或者长时间卡在哪里,因此要准备一个策略当发生这种问题咱们直接调用这个方法快速返回这个请求,不让他一直卡在那。

要在调用方作降级(要否则那个微服务都down掉了在作降级就没有意义)。

引入hystrix依赖

<dependency> 
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId> 
</dependency>
在启动类上加入@EnableHystrix 或者@EnableCircuitBreaker。
@RequestMapping("/feignPower.do") 
@HystrixCommand(fallbackMethod = "fallbackMethod") 
public Object feignPower(String name){ 
return powerServiceClient.power(); 
} 
fallbackMethod:
public Object fallbackMethod(String name){ 
System.out.println(name); 
return R.error("降级信息"); 
}
这里的降级信息具体内容根据业务需求来,好比返回一个默认的查询信息等等。
hystrix有超时监听,当你请求超过1秒 就会超时,这个是能够配置的

这里的降级信息具体内容根据业务需求来,好比返回一个默认的查询信息等等。

hystrix有超时监听,当你请求超过1秒 就会超时,这个是能够配置的

降级什么用

第一他能够监听服务有没有超时。第二报错了他这里直接截断了没有让请求一直卡在这个。

其实降级,当你系统迎来高并发的时候,这时候发现系统立刻承载不了这个大的并发 ,能够先关闭一些不重要 的微服务(就是在降级方法返回一个比较友好的信息)把资源让出来给主服务,其实就是总体资源不够用了,忍痛关闭某些服务,待过渡后再打开。

熔断限流

熔断就像生活中的跳闸,好比电路故障了,为了防止事故扩大,这里切断你的电源以避免意外发生。当一个微服务调用屡次,hystrix就会采起熔断 机制,不在继续调用你的方法,会默认短路,5秒后试探性的先关闭熔断机制,若是在这时候失败一次会直接调用降级方法,必定程度避免雪崩,

限流,限制某个微服务使用量,若是线程占用超过了,超过的就会直接降级该次调用。

Feign整合hystrix

feign默认支持hystrix,须要在yml配置中打开。
feign: 
hystrix: 
enabled: true

降级方法
@FeignClient(value = "SERVER-POWER", fallback = PowerServiceFallBack.class)
public interface PowerServiceClient {

@RequestMapping("/power.do")
public Object power(@RequestParam("name") String name);
}

在feign客户端的注解上 有个属性叫fallback 而后指向一个类 PowerServiceClient 
@Component
public class PowerServiceFallBack implements PowerServiceClient {
@Override
public Object power(String name) {
return R.error("测试降级");
}
}

Zuul 网关

zuul包含了对请求的路由和过滤两个主要功能

路由是将外部请求转发到具体的微服务实例上。是实现统一入口基础而过滤器功能负责对请求的处理过程干预,是实现请求校验等功能。

Zuul与Eureka进行整合,将zuul注册在Eureka服务治理下,同时从Eureka获取其余服务信息。(zuul分服务最终仍是注册在Eureka上)

路由

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
最后要注册在Eureka上因此须要引入eureka依赖
YML
server:
port: 9000
eureka:
client:
serviceUrl:
defaultZone: http://localhost:3000/eureka/ #eureka服务端提供的注册地址 参考服务端配置的这个路径
instance:
instance-id: zuul-0 #此实例注册到eureka服务端的惟一的实例ID
prefer-ip-address: true #是否显示IP地址
leaseRenewalIntervalInSeconds: 10 #eureka客户须要多长时间发送心跳给eureka服务器,代表它仍然活着,默认为30 秒 (与下面配置的单位都是秒)
leaseExpirationDurationInSeconds: 30 #Eureka服务器在接收到实例的最后一次发出的心跳后,须要等待多久才能够将此实例删除,默认为90秒

spring:
application:
name: zuul #此实例注册到eureka服务端的name 
启动类 @EnableZuulProxy

在实际开发当中咱们确定不会/server-power这样经过微服务调用,
可能只要一个/power就行了 
zuul: 
routes:
mypower: 
serviceId: server-power 
path: /power/** 
myorder: 
serviceId: server-order 
path: /order/**
注意/**表明是全部层级 /* 是表明一层。
通常咱们会禁用服务名调用
ignored-services:server-order 这样就不能经过此服务名调用,
不过这个配置若是一个一个通微服务名字设置太复杂
通常禁用服务名 ignored-services:“*”
有时候要考虑到接口调用须要必定的规范,好比调用微服务URL须要前缀/api,能够加上一个prefix
prefix:/api 在加上strip-prefix: false /api前缀是不会出如今路由中
zuul:
prefix: /api
ignored-services: "*"
stripPrefix: false
routes:
product:
serviceId: server-product
path: /product/**
order:
serviceId: server-order
path: /order/**

过滤器

过滤器(filter)是zuul的核心组件,zuul大部分功能是经过过滤器实现的,zuul中定义了4种标准过滤器类型,这些过滤器类型对应与请求的生命周期,

PRE:这种过滤器在请求路由前被调用,可利用过滤器进行身份验证,记录请求微服务的调试信息等。

ROUTING:这种过滤器将请求路由到微服务,这种过滤器用于构建发送给微服务请求,并使用 Apache HttpClient或Netfix Ribbon请求微服务。

POST:这种过滤器在路由微服务后执行,可用来相应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端。

ERROR:在其余阶段发送错误时执行过滤器

继承ZuulFilter

@Component
public class LogFilter extends ZuulFilter { 
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}

@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER+1;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
//被代理到的微服务
String proxy = (String)ctx.get("proxy");
//请求的地址
String requestURI = (String)ctx.get("requestURI");
//zuul路由后的url
System.out.println(proxy+"/"+requestURI);
HttpServletRequest request = ctx.getRequest();
String loginCookie = CookieUtil.getLoginCookie(request);
ctx.addZuulRequestHeader("login_key",loginCookie);
return null;
}
}

由此可知道自定义zuul Filter要实现如下几个方法。

filterType:返回过滤器类型,有pre、route、post、erro等几种取值

filterOrder:返回一个int值指定过滤器的顺序,不一样过滤器容许返回相同数字。

shouldFilter:返回一个boolean判断过滤器是否执行,true执行,false不执行。

run:过滤器的具体实现。

Spting-Cloud默认为zuul编写并开启一些过滤器。若是要禁用部分过滤器,只需在application.yml里设置zuul…disable=true,例如zuul.LogFilter.pre.disable=true

zuul也整合了了hystrix和ribbon的, 提供降级回退,继承FallbackProvider 类 而后重写里面的方法。

 

 

私信我免费领取更多java架构资料、源码、笔记

相关文章
相关标签/搜索