Spring-Cloud项目的搭建
因为 spring-cloud 是基于 spring-boot 项目来的,所以我们项目得是一个 spring-boot 项目,至于 spring-boot 项目,这里我们先不讨论,这里要注意的一个点是 spring-cloud 的版本与 spring-boot 的版本要对应下图:
spring-boot:
org.springframework.boot spring-boot-starter-parent 2.0.2.RELEASE
spring-cloud:
org.springframework.cloud spring-cloud-dependencies Finchley.SR2 pom import
eureka是什么?
eureka 是 Netflflix 的子模块之一,也是一个核心的模块, eureka 里有 2 个组件,一个是 EurekaServer( 一个独立的项目 ) 这个是用于定位服务以实现中间层服务器的负载平衡和故障转移,另一个便是 EurekaClient (我们的微服务)它是用于与 Server 交互的,可以使得交互变得非常简单 : 只需要通过服务标识符即可拿到服务。
与 spring-cloud的关系:
Spring Cloud 封装了 Netflix公司开发的 Eureka 模块来实现服务注册和发现 ( 可以对比 Zookeeper) 。Eureka 采用了 C-S 的设计架构。 Eureka Server 作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用 Eureka 的客户端连接到 Eureka Server 并维持心跳连接。这样系统的维护人员就可以通过 Eureka Server 来监控系统中各个微服务是否正常运行。 SpringCloud 的一些其他模块(比如 Zuul )就可以通过 Eureka Server 来发现系统中的其他微服务,并执行相关的逻辑。
如何使用?
在 spring-cloud 项目里面加入依赖:
单机版 eureka 服务端依赖和配置+启动类
org.springframework.cloud spring-cloud-starter-netflix-eureka-server 2.0.2.RELEASE
server: port: 8081 eureka: server: enable-self-preservation: false #关闭自我保护机制 eviction-interval-timer-in-ms: 4000 #设置清理间隔(单位:毫秒 默认是60*1000) instance: hostname: eureka-server8001 #修改的本机hosts,可忽略 client: registerWithEureka: false #不把自己作为一个客户端注册到自己身上 fetchRegistry: false #不需要从服务端获取注册信息(因为在这里自己就是服务端,而且已经禁用自己注册了) serviceUrl: defaultZone: http://${eureka.instance.home}:${eureka.port}/eureka #可填写自己具体的local host + port
@SpringBootApplication @EnableEurekaServer public class EurekaServer8001 { public static void main(String[] args) { SpringApplication.run(EurekaServer8001.class); } }
eureka 客户端
org.springframework.cloud spring-cloud-starter-netflix-eureka-client 2.0.2.RELEASE server: port: 8082 eureka: client: serviceUrl: defaultZone: http://${eureka.instance.home}:${eureka.port}/eureka #可填写自己具体的local host + port #eureka服务端提供的注册地址 参考服务端配置的这个路径 instance: instance-id: order8082 #此实例注册到eureka服务端的唯一的实例ID prefer-ip-address: true #是否显示IP地址 leaseRenewalIntervalInSeconds: 10 #eureka客户需要多长时间发送心跳给eureka服务器,表明它仍然活着,默认为30 秒 (与下面配置的单位都是秒) leaseExpirationDurationInSeconds: 30 #Eureka服务器在接收到实例的最后一次发出的心跳后,需要等待多久才可以将此实例删除,默认为90秒 spring: application: name: order-server #此实例注册到eureka服务端的name
@SpringBootApplication @EnableEurekaClient public class OrderApplication6001 { public static void main(String[] args) { SpringApplication.run(OrderApplication6001.class); } }
集群 eureka 服务端依赖和配置+启动类
这是我自己的集群环境和修改本机的hosts文件
server: port: 8001 eureka: server: enable-self-preservation: false #关闭自我保护机制 eviction-interval-timer-in-ms: 4000 #设置清理间隔(单位:毫秒 默认是60*1000) instance: hostname: eureka-server8001 client: registerWithEureka: false #不把自己作为一个客户端注册到自己身上 fetchRegistry: false #不需要从服务端获取注册信息(因为在这里自己就是服务端,而且已经禁用自己注册了) serviceUrl: defaultZone: http://eureka-server8002:8002/eureka,http://eureka-server8003:8003/eureka,http://eureka-server8004:8004/eureka,http://eureka-server8005:8005/eureka
启动 eureka 集群后的主页面
红色说明没有本机eureka信息是因为eureka的server会把自己的注册信息与其他的server同步, 所以这里我们不需要注册到自己身上,因为另外两台服务器会配置本台服务器。
客户端 的注册配置(一部分)注册到所有 服务端机器
defaultZone: http://eureka-server8001:8001/eureka/,http://eureka-server8002:8002/eureka/,http://eureka-server8003:8003/eureka/,http://eureka-server8004:8004/eureka/,http://eureka-server8005:8005/eureka/
我们这里只截取了要改动的那一部分。 就是 原来是注册到那一个地址上面,现在是要写五个 eureka 注册地址,但是不是代表他会注册五次,因为我们 eureka server 的注册信息是同步的,这里只需要注册一次就可以了,但是为什么要写五个地址呢。因为这样就可以做到高可用的配置:打个比方有 5 台服务器。但是突然宕机了一台, 但是其他 4 台还健在,依然可以注册我们的服务,换句话来讲, 只要有一台服务还建在,那么就可以注册服务,这里 需要理解一下。这里效果图就不发了, 和之前单机的没什么两样,只是你服务随便注册到哪个 eureka server 上其他的 eurekaserver 上都有该服务的注册信息。
首先先看下ribbon 和 fegin 的定义
Spring Cloud Ribbon 是基于 Netflix Ribbon 实现的一套客户端 负载均衡的工具。简单的说, Ribbon 是 Netflix 发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将 Netflix 的中间层服务连接在一起。 Ribbon 客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出 Load Balancer (简称 LB )后面所有的机器, Ribbon 会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们也很容易使用 Ribbon 实现自定义的负载均衡算法。
Feign 是一个声明式 WebService 客户端。使用 Feign 能让编写 Web Service 客户端更加简单 , 它的使用方法是定义一个接口,然后在上面添加注解,同时也支持 JAX-RS 标准的注解。 Feign 也支持可拔插式的编码器和解码器。 SpringCloud 对 Feign 进行了封装,使其支持了 Spring MVC 标准注解和 HttpMessageConverters 。 Feign 可以与 Eureka 和Ribbon 组合使用以支持负载均衡。
org.springframework.cloud spring-cloud-starter-openfeign 2.0.2.RELEASE
下面通过两张图看下服务端的负载均衡和客户端的负载均衡有什么区别
服务端的负载均衡以Nginx 为例 客户端发起请求后通过算法分配到具体的某一台服务器
客户端的负载均衡以ribbon 为例 客户端先通过算法得到需要分配的某一台服务器再发起请求
我们先看下IRule 的实现者的负载均衡策略
我们按照客户端负载均衡的图片看下代码
order6001/order6002 //控制器 @RestController public class OrderController { @RequestMapping("/getOrder") @ResponseBody public Object getOrder(){ Map
map = new HashMap<>(); map.put("/getOrder","port::order6001"); return map; } } //启动类 @SpringBootApplication @EnableEurekaClient public class OrderApplication6001 { public static void main(String[] args) { SpringApplication.run(OrderApplication6001.class); } }
application.yml 参考上面的客户端的集群环境
shopping-cart3001/shopping-cart3002/shopping-cart3003
//IRule 负载均衡策略配置 @Configuration public class OrderRuleConfig { @Bean public IRule iRule(){ return new RoundRobinRule(); } }
//控制器 @RestController public class ShoppingCartController { @Autowired private ShoppingToOrderFeginClient shoppingToOrderFeginClient; @RequestMapping("/getShoppingCart") public Object getShoppingCart(){ Map
map = new HashMap<>(); map.put("/getShoppingCart","port::shopping-cart::3001"); return map; } //使用Fegin 客户端 跨系统shopping调用order服务 @RequestMapping("/ShoppingToOrderFeginClient") public Object getShoppingToOrderFeginClient(){ return shoppingToOrderFeginClient.getOrderFeginClient(); } } //Fegin 客户端配置 @FeignClient(name = "ORDER-SERVER") public interface ShoppingToOrderFeginClient { // 类似使用 dubbo 在Order系统中暴露服务提供调用 @RequestMapping("/getOrder") public Object getOrderFeginClient(); }
//启动类 @SpringBootApplication @EnableEurekaClient @RibbonClients({ @RibbonClient(name = "ORDER-SERVER", configuration = OrderRuleConfig.class) }) @EnableFeignClients public class ShoppingCartApplication3001 { public static void main(String[] args) { SpringApplication.run(ShoppingCartApplication3001.class); } }
user8000
@Configuration public class OrderRuleConfig { @Bean public IRule iRule(){ return new RoundRobinRule(); } }
@Configuration public class ShoppingRuleConfig { @Bean public IRule iRule(){ return new RoundRobinRule(); } }
@RestController public class UserController { @Autowired private OrderFeginClient orderFeginClient; @Autowired private ShoppingFeginClient shoppingFeginClient; @RequestMapping("/getUserToOrder")// user 调用 order 服务 @ResponseBody public Object getUserToOrder(){ return orderFeginClient.getOrderFeginClient(); } @RequestMapping("/getUserToShoppingCart")// user 调用 ShoppingCart 服务 @ResponseBody public Object getUserToShoppingCart(){ return shoppingFeginClient.getShoppingCartFeginClient(); } @RequestMapping("/getUserToShoppingCartToOrder") // user 调用 ShoppingCart 服务 ,ShoppingCart 再调用 Order服务 @ResponseBody public Object getShoppingToOrderFeginClient(){ return shoppingFeginClient.getShoppingToOrderFeginClient(); } }
@FeignClient(name = "ORDER-SERVER") public interface OrderFeginClient { @RequestMapping("/getOrder") public Object getOrderFeginClient(); }
@FeignClient(name = "SHOPPING-SERVER") public interface ShoppingFeginClient { @RequestMapping("/getShoppingCart") public Object getShoppingCartFeginClient(); @RequestMapping("/ShoppingToOrderFeginClient") public Object getShoppingToOrderFeginClient(); }
@SpringBootApplication @EnableEurekaClient @RibbonClients({ @RibbonClient(name = "ORDER-SERVER", configuration = OrderRuleConfig.class), @RibbonClient(name = "SHOPPING-SERVER", configuration = ShoppingRuleConfig.class) }) @EnableFeignClients public class UserApplication8000 { public static void main(String[] args) { SpringApplication.run(UserApplication8000.class); } }
eureka主界面
测试访问,由于负载均衡策略设置的为轮询,所以结果是循环切换
http://localhost:8000/getUserToOrder
{"/getOrder":"port::order6001"}/{"/getOrder":"port::order6002"}
http://localhost:8000/getUserToShoppingCart
{"/getShoppingCart":"port::shopping-cart::3001"}/
{"/getShoppingCart":"port::shopping-cart::3002"}/
{"/getShoppingCart":"port::shopping-cart::3003"}
Hystrix 断路器 组件
hystrix 是什么?Hystrix 是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等, Hystrix 能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。“ 断路器 ” 本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应( FallBack ),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
org.springframework.cloud spring-cloud-starter-netflix-hystrix 2.0.2.RELEASE
如果通过 nginx负载均衡算法 分配到的那一台服务器刚好404了怎么办呢,难道一直卡着吗,后面的所有请求都在浪费系统资源,最终导致系统崩溃
在多系统相互调用之间a->b->c->d,如果b服务404了 ,是不是也卡住了 ,后面的所有请求也都在浪费系统资源,最终导致系统崩溃
Hystrix 降级配置 @RequestMapping("/getUserToOrder") @ResponseBody @HystrixCommand(fallbackMethod = "getUserToOrderFallback") public Object getUserToOrder(){ return orderFeginClient.getOrderFeginClient(); } public Object getUserToOrderFallback(){ return "断路器生效,系统正在维护中"; }
启动类添加@EnableHystrix 注解
当调用getUserToOrder方法时该服务发生故障 那么Hystrix可以做到降级的作用 会执行getUserToOrderFallback方法,可以快速的响应
Hystrix 熔断配置 application.yml
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 20000
circuitBreaker:
requestVolumeThreshold: 5 #当前线程请求超过5次 熔断配置 半开失败一次后尝试调用,全开失败五次进入fallbackMethod方法
# sleepWindowInMilliseconds : 5000 时间
Hystrix 限流配置
hystrix 通过线程池的方式来管理你的微服务调用,他默认是一个线程池( 10 大小) 管理你的所有微服务,你可以给某个微服务开辟新的线程池:threadPoolKey 就是在线程池唯一标识, hystrix 会拿你这个标识去计数,看线程占用是否超过了, 超过了就会直接降级该次调用比如, 这里 coreSize 给他值为 5 那么假设你这个方法调用时间是 3s 执行完, 那么在 3s 内如果有超过 2 个请求进来的话, 剩下的请求则全部降级
@RequestMapping("/getUserToOrder")
@ResponseBody
@HystrixCommand(fallbackMethod = "getUserToOrderFallback"/*降级配置或者实现具体fegin接口*/,threadPoolKey = "order6001",
threadPoolProperties = {@HystrixProperty(name = "coreSize",value = "5")}/*限流配置 最多5个线程 其余fallback*/)
public Object getUserToOrder(){
return orderFeginClient.getOrderFeginClient();
}
配置通用的fallbackMethod 方法 即 实现 fegin 接口即可 eg:
@FeignClient(name = "ORDER-SERVER",fallback = OrderFeginClientImpl.class)
public interface OrderFeginClient {
@RequestMapping("/getOrder")
public Object getOrderFeginClient();
}
@Service
public class OrderFeginClientImpl implements OrderFeginClient {
@Override
public Object getOrderFeginClient() {
return "断路器生效,系统正在维护中";
}
}
feign: hystrix: enabled: true
Zuul 包含了对请求的路由和过滤两个最主要的功能:其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础 .Zuul 和 Eureka 进行整合,将 Zuul 自身注册为 Eureka 服务治理下的应用,同时从 Eureka 中获得其他微服务的消息,也即以后的访问微服务都是通过 Zuul 跳转后获得。注意: Zuul 服务最终还是会注册进 Eureka,也属于一个客户端
org.springframework.cloud spring-cloud-starter-netflix-eureka-client 2.0.2.RELEASE org.springframework.cloud spring-cloud-starter-netflix-zuul 2.0.2.RELEASE 启动类添加 @EnableZuulProxy 注解
zuul/zuul-server9001/zuul-server9002 application.yml 部分 其余和集群中的客户端配置一样
zuul-application.yml 部分 spring: application: name: zuul #此实例注册到eureka服务端的name zuul: prefix: /api # 添加前缀 ignored-services: "*"#user-server # 如果是 "*" 就是所有 routes: user: serviceId: zuul-server path: /zuul/**
zuul-server9001/zuul-server9002 application.yml 部分 spring: application: name: zuul-server #此实例注册到eureka服务端的name zuul: # prefix: /api # 添加前缀 ignored-services: "*"#user-server # 如果是 "*" 就是所有 routes: user: serviceId: user-server path: /user/**
user8000服务的控制器 @RequestMapping("/test") @ResponseBody public Object getTest(){ // 原始访问路径 http://localhost:8000/test // application.yml // spring:application:name: user-server #此实例注册到eureka服务端的name // 路由访问路径 http://localhost:8080/user-server/test // 设置路由规则后的访问路径 // http://localhost:8080/user/test // 设置zuul集群后的访问路径 // http://localhost:8080/api/zuul/user/test /** * zuul: * prefix: /api # 添加前缀 * ignored-services: user-server # 如果是 "*" 就是过滤所有 * routes: * user: * serviceId: user-server * path: /user/** */ return "路由测试访问"; }
zuul 的过滤器
过滤器 (fifilter) 是 zuul 的核心组件 zuul 大部分功能都是通过过滤器来实现的。zuul 中定义了 4 种标准过滤器类型,这 些过滤器类型对应于请求的典型生命周期。PRE :这种过滤器在请求被路由之前调用。可利用这种过滤器实现身份 验证、在 集群中选择请求的微服务、记录调试信息等。ROUTING :这种过滤器将请求路由到微服务。这种过滤器 用于构建发送给微服 务的请求,并使用 Apache HttpCIient 或 Netfilx Ribbon 请求微服务POST: 这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准 的 HTTP Header 、收集统计信息和指标、将响应从微服务发送给客户端等。ERROR :在其他阶段发生错误时执行该过滤器。如果要编写一个过滤器,则需继承 ZuulFilter 类 实现其中方法 :
@Componentpublic class LogFilter extends ZuulFilter {@Overridepublic String filterType () {return FilterConstants . ROUTE_TYPE ;}@Overridepublic int filterOrder () {return FilterConstants . PRE_DECORATION_FILTER_ORDER ;}@Overridepublic boolean shouldFilter () {return true ;}@Overridepublic Object run () throws ZuulException {RequestContext currentContext = RequestContext . getCurrentContext ();HttpServletRequest request = currentContext . getRequest ();String remoteAddr = request . getRemoteAddr ();System . out . println ( " 访问者 IP : " + remoteAddr + " 访问地址 :" + request . getRequestURI ());return null ;}}由代码可知,自定义的 zuul Filter 需实现以下几个方法。filterType: 返回过滤器的类型。有 pre 、 route 、 post 、 error 等几种取值,分别对应上文的几种过滤器。详细可以参考 com.netflix.zuul.ZuulFilter.filterType() 中的注释。filter0rder: 返回一个 int 值来指定过滤器的执行顺序,不同的过滤器允许返回相同的数字。shouldFilter :返回一个 boolean 值来判断该过滤器是否要执行, true 表示执行, false 表示不执行。run :过滤器的具体逻辑。禁用 zuul 过滤器 Spring Cloud 默认为 Zuul 编写并启用了一些过滤器,例如 DebugFilter 、 FormBodyWrapperFilter等,这些过滤器都存放在 spring-cloud-netflix-core 这个 jar 包 里,一些场景下,想要禁用掉部分过滤器,该怎么办呢? 只需在 application.yml 里设置 zuul...disable=true 例如,要禁用上面我们写的过滤器,这样配置就行了:zuul.LogFilter.pre.disable=trueeureka 集群主界面
zuul 容错与回退直接看官网的demo
class MyFallbackProvider implements FallbackProvider {@Overridepublic String getRoute () {// 制定为哪个微服务提供回退(这里写微服务名 写 * 代表所有微服务)return "*" ;}// 此方法需要返回一个 ClientHttpResponse 对象 ClientHttpResponse 是一个接口,具体的回退逻辑要实现此接口//route :出错的微服务名 cause :出错的异常对象@Overridepublic ClientHttpResponse fallbackResponse ( String route , final Throwable cause ) {// 这里可以判断根据不同的异常来做不同的处理, 也可以不判断// 完了之后调用 response 方法并根据异常类型传入 HttpStatusif ( cause instanceof HystrixTimeoutException ) {return response ( HttpStatus . GATEWAY_TIMEOUT );} else {return response ( HttpStatus . INTERNAL_SERVER_ERROR );}}private ClientHttpResponse response ( final HttpStatus status ) {// 这里返回一个 ClientHttpResponse 对象 并实现其中的方法,关于回退逻辑的详细,便在下面的方法中return new ClientHttpResponse () {@Overridepublic HttpStatus getStatusCode () throws IOException {// 返回一个 HttpStatus 对象 这个对象是个枚举对象, 里面包含了一个 status code 和 reasonPhrase信息return status ;}@Overridepublic int getRawStatusCode () throws IOException {// 返回 status 的 code 比如 404 , 500 等return status . value ();}@Overridepublic String getStatusText () throws IOException {// 返回一个 HttpStatus 对象的 reasonPhrase 信息return status . getReasonPhrase ();}@Overridepublic void close () {//close 的时候调用的方法, 讲白了就是当降级信息全部响应完了之后调用的方法}@Overridepublic InputStream getBody () throws IOException {// 吧降级信息响应回前端return new ByteArrayInputStream ( " 降级信息 " . getBytes ());}@Overridepublic HttpHeaders getHeaders () {// 需要对响应报头设置的话可以在此设置HttpHeaders headers = new HttpHeaders ();headers . setContentType ( MediaType . APPLICATION_JSON );return headers ;}};}}
最终调用图
HystrixDashbord 监控中心