文章目录
- 一、服务调用Feign
- 1.1 Feign的基本使用
- 1.2 Feign的属性配置
- 1.2.1 Ribbon配置
- 1.2.2 Hystrix配置
- 二、网关服务Zuul
- 2.1 Zuul的基本使用
- 2.1.1 请求路由
- 2.1.2 请求过滤
- 2.2 路由详解
- 2.2.1 传统路由配置
- 2.2.2 服务路由配置
- 2.2.3 服务路由的默认规则
- 2.2.4 自定义路由映射规则
- 2.2.5 路径匹配
- 2.2.6 路由前缀
- 2.2.7 Cookie与头信息
- 2.2.8 Hystrix和Ribbon支持
- 2.3 过滤器详解
- 2.3.1 请求的生命周期
- 2.3.2 核心过滤器
- 2.3.3 异常处理
- 2.3.4 禁用过滤器
- 2.3.5 用过滤器限流
- 三、动态配置Apollo
- 3.1 Apollo的四个维度
- 3.2 Apollo的使用
- 3.2.1 页面操作
- 3.2.2 Web中使用
- 四、Spring Cloud相关问题
- 4.1 SpringBoot和SpringCloud的区别
- 4.2 Spring Cloud断路器的作用
- 4.3 什么是Spring Cloud Gateway
- 4.4 Spring Cloud如何实现服务的注册
- 4.5 REST和RPC对比
- 4.6 说说RPC的实现原理
- 4.7 接口限流方案
本系列文章:
Spring(一)控制反转、两种IOC容器、自动装配、作用域
Spring(二)延迟加载、生命周期、面向切面、事务
Spring(三)父子容器、国际化、异步调用、定时器、缓存
Spring(四)Spring MVC
Spring(五)Spring Boot
Spring(六)Spring Cloud----Eureka、Ribbon、Hystrix
Spring(七)Spring Cloud----Feign、Zuul和Apollo
一、服务调用Feign
Spring Cloud Feign基于Netflix Feign实现, 整合了Spring Cloud Ribbon与Spring Cloud Hystrix。并且,它还提供了一种声明式的 Web 服务客户端定义方式。
1、Feign采用的是基于接口的注解。
2、Feign整合了Ribbon,具有负载均衡的能力。
3、Feign整合了Hystrix,具有熔断的能力。
在使用Ribbon时,通常都会利用它对RestTemplate的请求拦截来实现对依赖服务的接口调用,而RestTemplate已经实现了对HTTP请求的封装处理。Feign在此基础上做了进一步封装,在Feign的实现下,我们只需要创建一个接口并用注解的方式来配置它,即可完成对服务提供方的接口绑定,简化了在使用Ribbon时自行封装服务调用客户端的开发量。
简而言之,Feign是一种声明式、模板化的HTTP客户端。
Feign的一个关键机制就是使用了动态代理。其具体调用过程:
- 首先,如果你对某个接口定义了@FeignClient注解,Feign就会针对这个接口创建一个动态代理;
- 接着你要是调用哪个接口,本质就是会调用Feign创建的动态代理;
- Feign的动态代理会根据你在接口上的@RequestMapping等注解,来动态构造出你要请求的服务的地址;
- 最后针对这个地址,发起请求、解析响应。
Feign的优点:
- Feign采用的是基于接口的注解;
- Feign整合了Ribbon,具有负载均衡的能力;
- 整合了Hystrix,具有熔断的能力。
Ribbon和Feign的区别:
1、都是调用其他服务的,但方式不同。
2、启动类注解不同,Ribbon是@RibbonClient,feign的是@EnableFeignClients。
3、服务指定的位置不同,Ribbon是在@RibbonClient注解上声明,Feign则是在定义抽象方法的接口中使用@FeignClient声明。
4、调用方式不同,Ribbon需要自己构建http请求,模拟http请求然后使用RestTemplate发送给其他服务,步骤相当繁琐。Feign需要将调用的方法定义成抽象方法即可。
1.1 Feign的基本使用
- 1、引入Eureka和Feign的相关依赖
示例:
<dependency><groupid>org.springfrarnework.cloud</groupid><artifactid>spring-cloud-starter-eureka</artifactid>
</dependency>
<dependency><groupid>org.springframework.cloud</groupid><artifactid>spring-cloud-starter-feign</artifac七Id>
</dependency>
- 2、使用@EnableFeignClients 注解
通过@EnableFeignClients 注解开启 Spring Cloud Feign 的支待功能。示例:
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {public static void main(String(J args) {SpringApplication.run(ConsurnerApplication.class, args);}
}
- 3、使用@FeignClient注解
通过@FeignClient 注解指定服务名来绑定服务, 然后再使用 Spring MVC 的注解来绑定具体该服务提供的 REST 接口。示例:
//这里服务名不区分大小写, 所以使用 hello-service和HELLO-SERVICE 都是可以的
@FeignClient("hello-service")
public interface HelloService {@RequestMapping("/hello")String hello();
}
- 4、调用服务
创建一个 ConsumerController 来实现对 Feign 客户端的调用。 使用@Autowired 直接注入上面定义的 HelloService 实例, 并在 helloConsumer函数中调用这个绑定了 hello-service 服务接口的客户端来向该 服务发起/hello 接口的调用。
@RestController
public class ConsumerController {@AutowiredHelloService helloService;@RequestMapping(value = "/feign-consumer", method = RequestMethod.GET)public String helloConsumer() {return helloService.hello();}
}
- 参数绑定
现实系统中的各种业务接口比较复杂, 会在HTTP的各个位置传入各种不同类型的参数, 并且在返回请求响应的时候也可能是一个复杂的对象结构。
我们把服务提供方的接口修改下:
@RequestMapping(value= "/hellol", method= RequestMethod.GET)public String hello(@RequestParam String name) {return "Hello "+ name;}@RequestMapping(value= "/hello2", method= RequestMethod.GET)public User hello(@RequestHeader String name, @RequestHeader Integer age) {return new User(name, age);}@RequestMapping(value= "/hello3", method = RequestMethod.POST)public String hello(@RequestBody User user) {return "Hello "+ user.getNarne() + ", " + user. getAge();}
服务提供方中的User类中包含name和age2个属性。
在服务消费方一般也要创建一样的实体类(此处指User类)。然后, 在 HelloService 接口中增加对上述三个新增接口的绑定声明。示例:
@FeignClient("HELLO-SERVICE")
public interface HelloService {@RequestMapping("/hello")String hello();@RequestMapping(value= "/hellol", method= RequestMethod.GET)String hello(@RequestParam("name") String name) ;@RequestMapping(value= "/hello2", method= Reques七Method.GET)User hello(@RequestHeader("name") String name, @RequestHeader("age") Integer age);@RequestMapping(value = "/hello3", method= Reques七Method.POST)String hello(@RequestBody User user);
}
1.2 Feign的属性配置
1.2.1 Ribbon配置
由于SpringCloudFeign的客户端负载均衡是通过SpringCloudRibbon实现的,所以我们可以直接通过配置Ribbon客户端的方式来自定义各个服务客户端调用的参数。
- 全局配置
全局配置的方法非常简单, 我们可以直接使用ribbon.<key>=<value>
的方式来设置ribbon的各项默认参数。 比如, 修改默认的客户端调用超时时间:
ribbon.ConnectTimeout=500
ribbon.ReadTimeout=5000
- 指定服务配置
在使用SpringCloudFeign的时候,针对各个服务客户端进行个性化配置的方式与使用SpringCloudRibbon时的配置方式是一样的, 都采用<client>. Ribbon.key=value
的格式进行设置。 此处<client>
对应的客户端名称就是@FeignClient注解中的name或value属性值。比如上个小节中对应的@FeignClient(“HELLO-SERVICE”)注解对应的客户端,我们可以设置如下属性:
HELLO-SERVICE.ribbon.ConnectTimeout=500
HELLO-SERVICE.ribbon.ReadTimeout=2000
HELLO-SERVICE.ribbon.OkToRetryOnAllOperations=true
HELLO-SERVICE.ribbon.MaxAutoRetriesNextServer=2
HELLO-SERVICE.ribbon.MaxAutoRetries=1
在配置Ribbon的超时时间时,需要 让Hystrix的超时时间大于Ribbon的超时时间, 否则Hystrix命令超时后, 该命令直接熔断, 重试机制就没有任何意义了。
1.2.2 Hystrix配置
默认情况下,Spring CloudFeign会为将所有Feign客户端的方法都封装到Hystrix命令中进行服务保护。
- 全局配置
直接使用它的 默认配置前缀hystrix.command.default就可以进行设置, 比如设置全局的超时时间:
hystrix.command.default.execution.isolation.thread.timeoutinMilliseconds=5000
另外,在对Hystrix进行配置之前,我们需要确认feign.hystrix.enabled(默认值为true)参数没有被设置为false, 否则该参数设置会关闭 Feign客户端的Hystrix支持。
- 禁用Hystrix
在 Spring Cloud Feign 中, 可以 通 过 feign.hystrix.enabled=false来关闭Hystrix功能。 另外, 如果不想全局地关闭Hystrix支持, 而只想针对某个服务客户端关闭Hystrix 支待时, 需要通过使用@Scope (“prototype”)注解为指定的客户端配置Feign.Builder实例。
步骤1:构建一 个关闭Hystrix的配置类。
@Configuraion
public class DisableHystrixConfiguration {@Bean@Scope("proto七ype")public Feign.Builder feignBuilder() {return Feign.builder();}
}
步骤2:在@ FeignClient注解中,通过configuration参数引入上面实现的配置。示例:
@FeignClient(name="HELLO - SERVICE", configuration = DisableHystrixConfiguration.class)
public in七erface HelloService {
}
- 指定命令配置
对于Hystrix命令的配置,在实际应用时往往也会根据实际业务情况制定出不同的配置方案。 配置方法也跟传统的 Hystrix 命令的参数配置相似, 采用hystrix.command.<commandKey>
作为前缀。<commandKey>
默认情况下会采用Feign客户端中的方法名作为标识, 所以, 针对上一节介绍的尝试机制中对/hello接口的熔断超时时间的配置可以通过其方法名作为<commandKey>
来进行配置。示例:
hystrix.command.hello.execution.isolation.thread.timeoutinMilliseconds=5000
在使用指定命令配置的时候, 需要注意, 由于方法名很有可能重复, 这个时候相同方法名的Hystrix配置会共用,所以在进行方法定义与配置的时候需要做好一定的规划。当然,也可以重写Feign.Builder的实现,并在应用主类中创建它的实例来覆盖自动化配置的HystrixFeign.Builder实现。
- 服务降级配置
由于Spring Cloud Feign在定义服务客户端的时候与Spring Cloud和bbon有很大差别,HystrixCommand定义被封装了起来, 我们无法像之前介绍Spring CloudHystrix时, 通过@HystrixCommand注解的fallback参数那样来指定具体的服务降级处理方法。 但是, Spring Cloud Feign提供了另外一 种简单的定义方式。
步骤1:服务降级逻辑的实现只需要为 Feign 客户端的定义接口编写一个具体的接口实现类。比如为 HelloService 接口实现一个服务降级类 HelloServiceFallback, 其中每个重写方法的实现逻辑都可以用来定义相应的服务降级逻辑。示例:
@Component
public class HelloServiceFallback implements HelloService {@Overridepublic String hello() {return "error";}@Overridepublic String hello(@RequestParam("name") String name) {return "error";}@Overridepublic User hello(@RequestHeader("name") S七ring name, @RequestHeader("age"} Integer age) {return new User("未知", 0);}@Overridepublic String hello(@RequestBody User user) {return "error";}
}
步骤2:在服务绑定接口 HelloService 中, 通过 @FeignClient 注解的 fallback 属性来指定对应的服务降级实现类。示例:
@FeignClient{narne="HELLO-SERVICE", fallback= HelloServiceFallback.class)
public interface HelloService {@RequestMapping("/hello")String hello();@RequestMapping(value= "/hellol", method= RequestMethod.GET)String hello(@RequestParam("name") String name) ;@RequestMapping(value = "/hello2", method= RequestMethod.GET)User hello(@Reques七Header("name") String name, @RequestHeader("age") Integer age);@RequestMapping(value = "/hello3", method= RequestMethod.POST)String hello(@RequestBody User user);
}
- 压缩配置
Spring Cloud Feign支持对请求与响应进行 GZIP 压缩,以减少通信过程中的性能损耗。我们只需通过下面两个参数设置, 就能开启请求与响应的压缩功能:
feign.compression.request.enabled=true
feign.compression.response.enabled=true
同时, 我们还能对请求压缩做一些更细致的设置, 比如下面的配置内容指定了压缩的请求数据类型, 并设置了请求压缩的大小下限, 只有超过这个大小的请求才会对其进行压缩。示例:
feign.compression.request.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048
二、网关服务Zuul
Zuul包含了对请求的路由和过滤两个最主要的功能:其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。
如果前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务。
Zuul和Eureka进行整合,将Zuul自身注册为Eureka服务治理下的应用,同时从Eureka中获得其他微服务的消息,也即以后的访问微服务都是通过Zuul跳转后获得。
网关有的功能, Zuul基本都有,最关键的就是路由和过滤器。Zuul的主要作用可以分为两类:首先,对于路由规则与服务实例的维护问题。其次,对于类似签名校验、登录校验等问题。
- 1、路由
Spring Cloud Zuul通过与Spring Cloud Eureka进行整合, 将自身注册为Eureka服务治理下的应用, 同时从Eureka中获得了所有其他微服务的实例信息。 这样的设计非常巧妙地将服务治理体系中维护的实例信息利用起来, 使得将维护服务实例的工作交给了服务治理框架自动完成, 不再需要人工介入。
对于路由规则的维护, Zuul默认会将通过以服务名作为ContextPath的方式来创建路由映射,大部分情况下, 这样的默认设置已经可以实现我们大部分的路由需求, 除了一些特殊情况(比 如兼容一些老的URL)还需要做一些特别的配置 。 - 2、过滤
对于类似签名校验、 登录校验在微服务架构中的冗余问题。 理论上来说, 这些校验逻辑在本质上与微服务应用自身的业务并没有多大的关系, 所以它们完全可以独立成一个单独的服务存在, 只是它们被剥离和独立出来之后, 并不是给各个微服务调用, 而是在API网关服务上进行统一调用 来对微服务接口做前置过滤, 以实现对微服务接口的拦截和校验 。
SpringCloudZuul提供了 一套过滤器机制, 它可以很好地支持这样的任务。 开发者可以通过使用Zuul来创建各种校验过滤器,然后指定哪些规则的请求需要执行校验逻辑,只有通过校验的才会被路由到具体的微服务接口,不然就返回错误提示。 通过这样的改造,各个业务层的微服务应用就不再需要非业务性质的校验逻辑了, 这使得我们的微服务应用可以更专注于业务逻辑的开发, 同时微服务的自动化测试也变得更容易实现。
2.1 Zuul的基本使用
<dependency><groupid>org.springframework.cloud</groupid><artifactld>spring-cloud-starter-zuul</artifactid>
</dependency>
- 2、使用@EnableZuulProxy注解
一般在主类使用@EnableZuulProxy注解开启Zuul的API网关服务功能。示例:
@EnableZuulProxy
@SpringCloudApplication
public class Application {public static void main(String[] args) {new SpringApplicationBuilder(Application.class).web(true) .run(args);}
}
- 3、添加配置信息
在application.properties中配置Zuul应用的基础信息, 如应用名、 服务端口号等。示例:
spring.application.name=api-gateway
server.port=5555
完成上面的工作后, 通过Zuul实现的API网关服务就构建完毕了。
2.1.1 请求路由
- 传统路由方式
使用Spring CloudZuul实现路由功能非常简单, 只需要对 api-gateway服务增加一些关于路由 规则的配置, 就能实现传统的路由转发功能。示例:
zuul.routes.api-a-url.path=/api-a-url/**
zuul.routes.api-a-url.url=http://localhost:8080/
该配置定义了发往API 网关服务的请求中, 所有符合/api-a-url/**规则的访问都将被路 由转发到http://localhost:8080/地址上, 也就是说, 当我们访问http://localhost:5555/api-a-url/hello的时候, API网关服务会将该请求路由到http://localhost: 8080/hello 提供的微服 务接口上。 其中 , 配置属性zuul.routes.api-a-url.path 中的api-a-url部分为路由的名字,可以任意定义,但是一组path和url 映射关系的路由 名要相同。
- 面向服务的路由
很显然,传统路由的配置方式对于我们来说并不友好, 它同样需要运维人员花费大量的时间来维护各个路由 path与url的关系 。 为了解决这个问题, SpringCloudZuul实现了与SpringCloudEureka的无缝整合, 我们可以让路由的path不是映射具体的url, 而是让它映射到某个具体的服务 , 而具体的url则交给Eureka的服务发现机制去自动维护, 我们称这类路由为面向服务的路由。 在Zuul中使用服务路由也同样简单, 只需做下面这些配置。
在api-gateway的app巨ca巨on.proper巨es 配置文件中指定Eureka注册中心的位置, 并且配置服务 路由。示例:
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=hello-servicezuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceid=feign-consumereureka.client.serviceUrl.defaultZone=http://localhost:llll/eureka/
假如有两个微服务应用hello-service和feign-consumer, 在上面的配置中分别定义了 两个名为 api-a 和 api-b的路由来映射它们。 同时,通过指定EurekaServer服务注册中心的位置, 除了将自己注册成服务 之外, 同时也让Zuul能够获取hello-service和feign-consumer 服务的实例清单,以实现path映射服务, 再从服务中挑选实例来进行请求转发的完整路由机制。
通过上面的搭建工作, 我们已经可以通过服务网关来访问 hello-service 和feign-consumer这两个服务 了。 根据配置的映射关系, 分别向网关发起下面这些请求。
http://localhost: 5555/api-a/hello: 该 url符合
/api-a/**
规则, 由api-a路由负责转发, 该路由映射的serviceid为hello-service, 所以最终/hello请求会被发送到hello-service服务的 某个 实例上去。
http://localhost:5555/api-b/feign-consumer: 该url符合/api-b/**规则,由 api-b路由负责转发,该路由映射的service工d为feign-consumer,所以最终/feign-consumer请求会被发送到feign-consumer服务的某个 实例上去。
通过面向服务的路由配置 方式, 我们不需要再为各个路由维护微服务应用的具体 实例的位置, 而是 通过简单的path与serviceld的映射组合,使得维护工作变得非常简单。 这完全归功于Spring CloudEureka的服务发现机制,它使得API网关服务可以自动化完成服务实例清单的维护,完美地解决了对路由映射 实例的维护问题。
2.1.2 请求过滤
每个客户端用户请求微服务应用提供的接口时, 它们的访问权限往往都有 一 定的限制,系统并不会将所有的微服务接口都对它们开放。
较好的做法是通过前置的网关服务来完成这些非业务性质的校验。由于网关服务的加入, 外部客户端访问我们的系统已经有了统一入口, 既然这些校验与具体业务无关, 那何不在请求到达的时候就完成校验和过滤, 而不是转发后再过滤而导致更长的请求延迟。 同时, 通过在网关中完成校验和过滤, 微服务应用端就可以去除各种复杂的过滤器和拦截器了, 这使得微服务应用接口的开发和测试复杂度也得到了相应降低。
Zuul 允许开发者在API网关上通过定义过滤器来实现对请求的拦截与过滤,实现的方法非常简单,我们只需要继承 ZuulFi巨er 抽象类并实现它定义的4个抽象函数就可以完成对请求的拦截和过滤。
比如要实现一个简单的 Zuul 过滤器 , 它实现了在请求被路由之前检查HttpServletReque中是否有 accessToken 参数, 若有就进行路由, 若没有就拒绝访问, 返回 401 Unauthorized 错误。示例:
public class AccessFilter extends ZuulFilter {private static Logger log= LoggerFactory.getLogger(AccessFilter.class);@Overridepublic String filterType() {return "pre";}@Overridepublic int filterOrder () {return 0;}@Overridepublic boolean shouldFilter() {return true;}@Overridepublic Object run(){RequestContext ctx = RequestContext.getCurrentContext();HttpServletRequest request = ctx.getRequest();log.info("send{} request to{}", request.getMethod(),request.getRequestURL().toString());Object accessToken = request.getParameter("accessToken");if(accessToken == null){log.warn("access token is empty");ctx.setSendZuulResponse(false);Ctx.setResponseStatusCode(401);return null;}log.info("access token ok");return null;}
}
上面的4个重写方法分别定义了如下内容:
fillterType
: 过滤器的类型, 它决定过滤器在请求的哪个生命周期中执行。 这里定义为pre, 代表会在请求被路由之前执行。
filterOrder
: 过滤器的执行顺序。 当请求在 一个阶段中存在多个过滤器时, 需要根据该方法返回的值来依次执行。
shouldFilter
: 判断该过滤器是否需要被执行。 这里我们直接返回了true, 因此该过滤器对所有请求都会生效。 实际运用中我们可以利用该函数来指定过滤器的有效范围。
run方法: 过滤器的具体逻辑。 这里我们通过ctx.setSendZuulResponse(false)令zuul过滤该请求, 不对其进行路由, 然后通过 ctx.setResponseStatusCode(401)设置了其返回的错误码, 当然也可以进 一步优化我们的返回, 比如, 通过ctx.setResponseBody(body)对返回的body内容进行编辑等。
在实现了自定义过滤器之后, 它并不会直接生效, 我们还需要为其创建具体的Bean才能启动该过滤器, 比如, 在启动类中增加。示例:
@EnableZuulProxy
@SpringCloudApplication
public class Application {public static void main(String[] args) {new SpringApplicationBuilder(Application.class) .web(true) .run(args);}@Beanpublic AccessFilter accessFilter() {return new AccessFilter();}
}
通过上面的例子,可以将网关服务的功能可以总结为4条:
- 作为系统的统一入口,屏蔽了系统内部各个微服务的细节。
- 与服务治理框架相结合,实现自动化的服务示例维护以及负载均衡的路由转发。
- 实现接口权限检验与微服务业务逻辑的解耦。
- 通过服务网关中的过滤器,在各生命周期中去校验请求的内容,将原本在对外服务层做的检验前移,保证了微服务的无状态行,同时降低了微服务的测试难度,让服务本身更集中关注业务逻辑的处理。
2.2 路由详解
2.2.1 传统路由配置
传统路由配置方式就是在不依赖千服务发现机制的情况下, 通过在配置文件中具体指定每个 路由表达式与服务实例的映射关系来实现API网关对外部请求的路由。
没有Eureka等服务治理框架 的帮助,我们需要 根据服务实例的数量采用不同方式 的配置来实现路由规则,具体可分为:单实例配置、多实例配置。
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.url=http://localhost:8080/
该配 置实 现了对符合/user-service/** 规则的请求路径转发到 http://localhost:8080/ 地址的路由规则。 比如,当有一个请求http://localhost:5555/user-service/hello被发送到API网关上,由千/user-service/hello 能够被上述配置 的 path 规则匹配,所 以 API网关 会转发请求到http://localhost:8080/hello士也扛上。
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceId=user-service
ribbon.eureka.enabled=false
user-service.ribbon.listOfServers=http://localhost:8080/,http://localhost:8081/
该配 置实 现了对 符合 /user-service/** 规则 的 请 求 路 径 转 发 到http://localhost:8080/和http://localhost:8081/两个实例地址的路由规则。 它的配置 方式与服务 路由的配置 方式 一 样,都采用了zuul.routes.<route>.path
与zuul.routes.<route>.serviceId
参数对的映射方式,只是这 里的 serviceid 是由用户手工命名 的服 务 名称 , 配合ribbon.listOfServers 参数实现服务与实例的维护。由于存在多个实例,API网关在进行路由转发时需要实现负载均衡策略,于是这里还需要Spring Cloud Ribbon的配合。
由于在Spring Cloud Zuul中自带了对Ribbon的依赖, 所以我们只需做一些简单的配置即可,比如上面示例中关 于Ribbon的2个配置:
ribbon.eureka.enabled: 由于
zuul.routes.<route>.serviceId
指定的是服务名称,默认清况下Ribbon会根据服务发现机制来获取配置服务名对应的实例清单。 但是,该示例并没有整合类似Eureka之类的服务治理框架,所以需要将该参数设置为false, 否则配置 的serviceId获取不到对应实例的清单。
user-service.ribbon.listOfServers: 该参数内容与zuul.routes.<route>.serviceId
的配置相对应, 开头的 user-service 对应 了serviceId的值, 这两个参数的配置相当于在该应用内部手工维护了服务与实例的 对应关系。
不论是单实例还是多实例的配置方式, 我们都需要为每 一对映射关系指定 一个名称,也就是上面配置中的<route>
, 每 一个<route>
对应了 一条路由规则。 每条路由规则都需要通 过path属性来定义 一 个用来匹配客户端请求的路径表达 式 , 并通过url 或serviceId属性来指定请求表达式映射具体实例地址或服务名。
2.2.2 服务路由配置
Spring Cloud Zuul通过与Spring Cloud Eureka的整合, 实现了对服务实例的 自动化维护, 所以 在使用服务路由配置的时候, 我们不需要向传统路由配置方式那样为serviceId指定具体的服务实例地址,只需要通过zuul.routes.<route>.path
与zuul.routes.<route>.serviceId
参数对的方式进行配置即可。示例:
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceid=user-service
它实现了 对符合 / user-service/**规则的请求路径转发到名为user-service的服务实例上去的路由规则。
对于面向服务的路由配置, 除了使用path与serviceId映射的配置方式 之外, 还有 一种更简洁的配置方式:zuul.routes.<serviceId>=<path>
, 其中<serviceId>
用来指定路由的具体服务名,<path>
用来配置匹配的请求表达式。比如下面的例子, 它的路由规则等价于上面通过path与serviceId组合使用的配置方式。
zuul.routes.user-service=/user-service/**
我们可以直接将API网关也看作Eureka服务治理下的 一个普通微服务应用。它除了会将自己注册到Eureka服务注册中心上之外,也会从注册中心获取所有服务以及它们的实例清单。所以,在Eureka的帮助下,API网关服务本身就已经维护了系统中所有 serviceId与实例地址的映射关系。当有外部请求到达 API 网关的时候,根据 请求的 URL 路径找到最佳匹配的path规则, API 网关就可以知道要将该请求路由到哪个具体的 service Id上去。 由于在 API 网关中已经知道serviceid对应服务实例的地址清单, 那么只需要通过Ribbon的负载均衡策略, 直接在这些清单中选择一个具体的实例进行转发就能完成路由工作了。
2.2.3 服务路由的默认规则
通过Eureka与Zuul的整合已经为我们省去了维护服务实例清单的大量配置工作,剩下只需要再维护请求路径的匹配表达式与服务名的映射关系即可。 在实际的运用过程中会发现, 大部分的路由配置规则几乎都会采用服务名作为外部请求的前缀, 比如下面的例 子, 其中 path路径的前缀使用了user-service, 而对 应的 服 务 名称也是user-service 。
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceId=user-service
当我们为Spring Cloud Zuul构建的 API 网关服务引入Spring Cloud Eureka之后, 它为Eureka中的每个服务都自动创建一个默认路由规则, 这些默认规则的path会使用serviceId配置的服务名作为请求前缀, 就如上面的例子那样。
由于默认情况下所有Eureka上的服务都会被 Zuul自动地创建映射关系来进行路由,这会使得一 些我们不希望对外开放的服务也可能被外部访问到。 这个时候, 我们可以使用zuul.ignored-services参数来设置一个服务名匹配表达式来定义不自动创建路由的规则。 Zuul在自动创建服务路由的时候会根据该表达式来进行判断, 如果服务名匹配表达式, 那么 Zuul 将跳过 该 服务 , 不为 其 创建 路 由 规 则 。 比如, 设置 为zuul.ignored-services=*的时候,Zuul将对所有的服务都不自动创建路由规则(此时就需要逐条添加路由映射规则)。
2.2.4 自定义路由映射规则
在构建微服务系统进行业务逻辑开发的时候, 为了兼容外部不同版本的客户端程序(尽量不强迫用户升级客户端), 一般都会采用开闭原则来进行设计与开发。 这使得系统在迭代过程中, 有时候会需要我们为一组互相配合的微服务定义一个版本标识来方便管理它们的版本关系,根据这个标识我们可以很容易地知道这些服务需要一起启动并配合使用。比如可以 采 用 类似 这样 的 命 名: userservice-v1 、 userservice-v2 、orderservice-v1、 orderservice-v2。
默认情况下,Zuul自动为服务创建的路由表达式会采用服务名作为前缀, 比如针对上面的userservice-v1和userservice-v2,它会产生/userservice-vl 和/userservice-v2两个路径表达式来映射,但是这样生
成出来的表达式规则较为单一 ,不利于通过路径规则来进行管理。 通常的做法是为这些不同版本的微 服 务 应 用 生 成以版本代号作 为 路 由 前 缀 定 义 的 路 由 规 则 , 比如/vl/userservice/ 。这时候, 通过这样具有版本号前缀的 URL 路径, 我们就可以很容易地通过路径表达式来归类和管理这些具有版本信息的微服务。
针对上面所述的需求,如果我们的各个微服务应用都遵循了类似userservice-v1这样的命名规则, 通过-分隔的规范来定义服务名和服务版本标识 的话,那么,我们可以使用Zuul中自定义服务与路由映射关系的功能,来实现为符合上述规则的微服务自动化地创建类似/vl/userservice/** 的路由匹配规则。 实现步骤非常简单, 只需在 API 网关程序中, 增加如下Bean即可:
@Beanpublic PatternServiceRouteMapper serviceRouteMapper() {return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)","${version}/${name}");}
PatternServiceRouteMapper对象可以让开发者通过正则表达式来自定义服务与路由映射的生成关系。 其中构造函数的第一个参数是用来匹配服务名称是否符合该自定义规则的正则表达式, 而第二个参数则是定义根据服务名中定义的内容转换出的路径表达式规则。当开发者在 API 网关中定义了PatternServiceRouteMapper 实现之后,只要符合第一个参数定义规则的服务名, 都会优先使用该实现构建出的路径表达式,如果没有匹配上的服务则还是会使用默认的路由映射规则,即采用完整服务名作为前缀的路径表达式。
2.2.5 路径匹配
在Zuul中, 路由匹配的路径表达式采用了Ant风格定义。Ant风格的路径表达式使用起来非常简单, 它一共有下面这三种通配符:
通配符 | 说明 |
---|---|
? | 匹配任意单个字符 |
* | 匹配任意单个字符 |
** | 匹配任意数址的字符, 支待多级目录 |
示例:
URL路径 | 说明 |
---|---|
/user-service/? | 可以匹 配/user-service/之后拼接一个任意字符的路径, 比如/user-service/a、 /user-service/b、 /user-service/c |
/ user-service/* | 可以匹 配 /user-service/ 之后 拼 接任意字 符的路径, 比如/user-service/a 、 /user-service/aaa 、 /user-service/bbb。 但是它无法匹配/user-service/a/b |
/ user-service/** | 可以匹 配/user-service/*包含的内容之外, 还可以匹 配形如/user-service/a/b的多级目录路径 |
- URL路径被多个路由规则匹配上怎么办?
使用通配符的时候,经常会碰到这样的问题: 一个 URL 路径可能会被多个不同路由的表达式匹配上。 比如, 有这样一 个场景, 我们在系统建设的一 开始实现了user-service 服务, 并且配置了如下路由规则:
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceId=user-service
随着版本的迭代, 我们对 user-service 服务做了 一 些功能拆分, 将原属于user-service 服务的某些功能拆分到了另外一个全新的服务 user-service-ext 中去,而这些拆分的外部调用 URL 路径希望能够符合规则/user-service/ext/**, 这个时候我们需要就在配置文件中增加 一个路由规则, 完整配置示例:
zuul.routes. user-service.path=/user-service/**
zuul.routes.user-service.serviceId=user-service
zuul.routes.user-service-ext.path=/user-service/ext/**
zuul.routes.user-service-ext.serviceId=user-service-ext
调用 user-service-ex七服 务的 URL 路径实 际上会同时被 /user-service/**和/user-service/ext/**两个表达式所匹配。 在逻辑上, API 网关服务需要优先选择/user-service/ext/**路由,然后再匹配/user-service/**路由才能实现上述需求。但是如果使用上面的配置方式,实际上是无法保证这样的路由优先顺序的。
由于properties的配置内容无法保证有序,所以当出现这种情况的时候, 为了保证路由的优先顺序, 我们需要使用YAML文件来配置, 以实现有序的路由规则。示例:
zuul:routes:user-service-ext:path: /user-service/ext/**serviceId: user-service-extuser-service:path: /user-service/**serviceId: user-service
- 忽略表达式
Zuul提供 了 一 个忽 略表达 式参 数zuul.ignored-patterns。该参数可以用来设置不希望被 API 网关进行路由的 URL 表达式。
比如不希望/hello 接口被路由,可以这样设置:
zuul.ignored-patterns=/**/hello/**
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=hello-service
此时,通 过网关来访间 hello-service 的 /hello 接口 http://localhost:5555/api-a/hello 。虽然该访问路 径完全符 合 path 参数 定 义 的/api-a/**规则,但是由于该路径符合 zuul.ignored-patterns 参数定义的规则,所以不会被正确路由。
zuul.ignored-patterns参数在使用时还需要注意它的范围并不是对某个路由, 而是对所有路由。 所以在设置的时候需要全面考虑 URL 规则, 防止忽略了不该被忽略的 URL 路径。
2.2.6 路由前缀
为了方便全局地为路由规则增加前缀信息,Zuul提供了zuul.prefix参数来进行设置。比如,希望为网关上的路由规则都增加/api 前缀,那么我们可以在配置文件中增加配置:zuul.prefix=/api。另外, 对于代理前缀会默认从路径中移除,我们可以通过设置zuul.stripPrefix=false 来关闭该移除代理前缀的动作,也可以通过 zuul.routes.<route>.strip-prefix=true
来对指定路由关闭移除代理前缀的动作。
zuul.routes.api-a.path=/api/a/**
zuul.routes.api-a.serviceId=hello-servicezuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=hello-servicezuul.routes.api-c.path=/ccc/**
zuul.routes.api-c.serviceId=hello-service
这里配置了三个路由关系: /ap订a/** 、 /api-b/** 、 /eccl**, 这三个路径规则都将被路由到 hello-service 服务上去。当我们没有设置 zuul.prefix=/api 的时候,一切运作正常。 但是在增加了 zuul.prefix=/api 配置之后, 会得到下面这样的路由关系:
从日志信息中我们可以看到,以/api 开头的路由规则解析除了两个看似就有问题的映射URL, 我们可以通过该输出的URL来访问, 实际是路由不到正确的服务接口。 只有非/api 开头的路由规则/eccl**能够正常路由,所以务必避免让路由表达式的起始字符串与 zuul.prefix 参数相同。
2.2.7 Cookie与头信息
默认情况下, Spring Cloud Zuul在请求路由时, 会过滤掉HTTP请求头信息中的 一些敏感信息, 防止它们被传递到下游的外部服 务器。 默认的敏感 头信 息 通 过zuul.sensitiveHeaders参数定义,包括Cookie、Se七-Cookie、Authorization三个属性。所以, 我们在开发Web项目时常用的Cookie在 SpringCloud Zuul网关中默认是不会传递的, 这就会引发一个常见的问题: 如果我们要将使用了Spring Security、 Shiro
等安全框架构建的Web应用通过SpringCloud Zuul构建的网关来进行路由时,由千Cook迳信息无法传递, 我们的Web应用将无法实现登录和鉴权。
此时可以通过指定路由的参数来配置,2种方法示例:
#方法一:对指定路由开启自定义敏感头
zuul.routes.<router>.customSensitiveHeaders=true
#方法二:将指定路由的敏感头设置为空
zuul.routes.<router>.sensitiveHeaders=
这两种方法, 仅对指定的Web应用开启对敏感信息的传递,影响范围小, 不至于引起其他服务的信息泄露问题。
- 重定向问题
在解决了Cookie问题之后, 我们已经能够通过网关来访问并登录到我们的Web 应用了。 但是 这个时候又会发现另外一个问题: 虽然可以通过网关访问登录页面并发起登录请求, 但是登录成功之后, 我们跳转到的页面URL却是 具体Web应用实例的地址, 而不是通过网关的路由地址。 这个问题非常严重, 因为使用API网关的一个重要原因就是要将网关作为统一入口, 从而不暴露所有的内部服务细节。
引起问题的大致原因是由于SpringSecurity或Shiro在登录完成之后,通过重定向的方式跳转到登录后的页面, 此时登录后的请求结果状态码为302, 请求响应头信息中的 Location指向了具体的服务实例地址, 而请求头信息中的Host也指向 了具体的服务实例 IP地址和端口。 所以, 该问题的根本原因在于Spring Cloud Zuul在路由请求时,并没有将最初的Host信息设置正确。
针对 这个问题,目前在 spring-cloud-netflix-core-1.2.x版本的 Zuul中增加了一个参数配置,能够使得网关在进行路由转发前为请求设置Host头信息,以标识最初的服务端请求地址。 具体配置:
zuul.addHostHeader=true
2.2.8 Hystrix和Ribbon支持
spring-cloud-starter-zuul自身就包含了对 spring-cloud-starter-hystrix 和spring-cloud-starter-ribbon模块的依赖, 所以 Zuul天生就拥有线程隔离和断路器的自我保护功能, 以及 对服务调用的客户端负载均衡功能。 但是,当使用path与url的映射关系来配置路由规则的时候, 对于路由转发的请求不会采用HystrixCommand来包装, 所以 这类路由请求没有线程隔离和断路器的保护,并且也不会有负载均衡的能力。因此,我们在使用Zuul的时候尽量使用path和serviceId的组合来进行配置, 这样不仅可以保证API网关的健壮和稳定,也能用到Ribbon的客户端负载均衡功能。
在使用Zuul搭建API网关的时候,可以通过Hystrix和伈bbon的参数来调整路由请求的各种超时时间等配置。
hystrix.command.default.execution.isolation.thread.timeoutinMilliseconds
: 该参 数可以用来 设 置API 网关中路由 转 发 请 求的HystrixCommand 执行超时时间, 单位为毫秒。 当路由转发请求的命令执行时间超过该配置值之后,Hystrix会将该执行命令标记为TIMEOUT并抛出异常,Zuul会对该异常进行处理并返回如下JSON信息给外部调用方:
{"timestamp": 1481350975323,"status": 500,"error": "Internal Server Error","exception": "com.netflix.zuul.exception.ZuulException","message": "TIMEOUT"
}
ribbon.ConnectTimeout
: 该参数用来设置路由转发请求的时候, 创建请求连接的超时时间。当ribbon.ConnectTimeout的配置值小于hystrix.command.default.execution.isolation.thread.timeoutlnMilliseconds 配置值的时候, 若出现路由请求出现连接超时, 会自动进行重试路由请求, 如果重试依然失败,Zuul会返回如下JSON信息:
{"timestamp": 1481352582852,"status": 500,"error": "Internal Server Error","exception": "com.netflix.zuul.exception.ZuulException","message": "NUMBEROF RETRIES NEXTSERVER EXCEEDED"
}
如果 ribbon.ConnectTimeout的配置值大于hystrix.command.default.execution.isolation.thread.timeout工nMilliseconds配置值的时候,当出现路由请求连接超时时, 由于此时对于路由转发的请求命令已经超时, 所以不会进行重试路由请求, 而是直接按请求命令超时处理, 返回TIMEOUT的错误信息。
ribbon.ReadTimeout
: 该参数用来设置路由转发请求的超时时间。 它的处理与ribbon.ConnectTimeout类似, 只是它的超时是对请求连接建立之后的处理时间。 当ribbon.ReadTimeout的配置值小于 hystrix.command.default.execution.isolation.thread.timeoutinMilliseconds配置值的时候,若路由请求的处理时间超过该配置值且依赖服务的请求还未响应的时候, 会自动进行重 试 路 由 请 求 。 如果 重 试后依然没有获 得请 求 响 应 ,Zuul会返 回NUMBEROF_RETRIES_NEXTSERVER_EXCEEDED 错误。如果ribbon.ReadTimeout
的配 置 值大于 hystrix.command.defaul七.execution.isolation.thread.timeoutinMilliseconds配置值,若路由请求的处理时间超过该配置值且依赖服务的请求还未响应时,不会进行重试路由请求,而是直接按请求命令超时处理,返回TIMEOUT 的错误信息。
在使用Zuul的服务路由时,如果路由转发请求发生超时(连接超时或处理超时),只要超时时间的设置小于Hystrix的命令超时时间,那么它就会自动发起重试。 但是在有些情况下,我们可能需要关闭该 重试机制,那么可以通过下面的两个参数来进行设置:
zuul.retryable=false
zuul.routes.<route>.retryable=false
zuul.retryable用来全局关闭重试机制,而zuul.routes.<route>.retryable =false
则是指定路由关闭重试机制。
2.3 过滤器详解
Zuul的路由功能在运行时,它的路由映射和请求转发都是由几个不同的过滤器完成的。 其中,路由映射主要通过pre类型的过滤器完成,它将请求路径与配置的路由规则进行匹配,以找到需要转发的目标地址;而请求转发的部分则是由route类型的过滤器来完成,对pre类型过滤器获得的路由地址进行转发。 所以,过滤器可以说是Zuul实现API网关功能最为核心的部件,每一个进入Zuul的HTTP请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端。
Zuul中实现的过滤器必须包含4个基本特征: 过滤类型、 执行顺序、执行条件、 具体操作。 这其实就是ZuulFilter接口中定义的4个方法:
String filterType();
int filterOrder();
boolean shouldFilter();
Object run();
filterType
: 该函数需要返回 一个字符串来代表过滤器的类型, 而这个类型就是在HTTP请求过程中定义的各个阶段。在 Zuul 中默认定义了 4 种不同生命周期的过滤器类型:
pre:可以在请求被路由之前调用;
routing:在路由请求时被调用;
post:在routing和error过滤器之后被调用;
error:处理请求,发生错误时被调用。
filterOrder
: 通过 int 值来定义过滤器的执行顺序, 数值越小优先级越高。
shouldFilter
: 返回一个 boolean 值来判断该过滤器是否要执行。 我们可以通过此方法来指定过滤器的有效范围。
run方法
: 过滤器的具体逻辑。 在该函数中, 我们可以实现自定义的过滤逻辑, 来确定是否要拦截当前的请求, 不对其进行后续的路由, 或是在请求路由返回结果之后,对处理结果做一 些加工等。
2.3.1 请求的生命周期
Zuul中4种不同类型的过滤器,他们覆盖了一个外部HTTP请求到达API网关,直到返回请求结果的全部生命周期。
当外部HTTP请求到达API网关服务的时候,首先他会进入第一个阶段pre,在这里它会被pre类型的过滤器进行处理,该类型过滤器的主要目的是在进行请求路由之前做一些前置贾工,比如请求的校验等。
在完成pre类型的过滤器处理之后,请求进入第二个阶段routing,也就是之前说的路由请求转发阶段,请求将会被routing类型过滤器处理。这里的具体处理内容就是将外部请求转发到具体服务实例上去的过程,当服务实例将请求结果都返回之后,routing阶段完成,请求进入第三个阶段post。
此时请求将会被post类型的过滤器处理,这些过滤器在处理的时候不仅可以获取到请求信息,还能获取到服务实例的返回信息,所以在post类型的过滤器中,我们可以对处理结果进行一些加工或转换等内容。
还有一个特殊的阶段error,该阶段只有上述三个阶段发生异常的时候才会触发,但是它最后流向还是post类型的过滤器,因为它需要通过post过滤器将最终结果返回给请求客户端。
2.3.2 核心过滤器
在默认启用 的过滤器中包含三种不同生命周期的过滤器, 这些过滤器都非常重要。
- 默认启用的pre过滤器
ServletDetectionFilter:执行顺序为-3,是最先被执行的过滤器。
Servlet30WrapperFilter:执行顺序为-2,是第二个执行的过滤器。
FromBodyWrapperFilter:执行顺序为-1,是第三个执行的过滤器。
DebugFilter:执行顺序为1,是第四个执行的过滤器。
PreDecorationFilter:执行顺序为5,是pre阶段最后被执行的过滤器。 - 默认启用的route过滤器
RibbonRoutingFilter:执行顺序为10,是route阶段第一个执行的过滤器。该过滤器只对请求上下文中存在serviceId参数的请求进行处理,即只对通过serviceId配置路由规则的请求生效。
SimpleHostRoutingFilter:执行顺序为100,是route阶段第二个执行的过滤器。该过滤器只对请求上下文中存在routeHost参数的请求进行处理,即只对通过url配置路由规则的请求生效。
SendForwardFilter:执行顺序为500,是route阶段第三个执行的过滤器。该过滤器只对请求上下文中存在forward.to参数的请求进行处理,即用来处理路由规则中forward本地跳转配置。 - 默认启用的post过滤器
SendErrorFilter:执行顺序为0,是post阶段第一个执行的过滤器。给过滤器仅在请求上下文中包含error.status_code参数(由之前执行的过滤器设置的错误编码)并且还没有被该过滤器处理过的时候执行。
SendResponseFilter:执行吮吸是1000,是post阶段最后执行的过滤器。该过滤器会检查请求上下文中是否包含请求响应相关的头信息、响应数据流或是响应体,只有在包含他们其中一个时执行处理逻辑。该过滤器的处理逻辑就是利用请求上下文的响应信息来组织需要发送回客户端的响应内容。
2.3.3 异常处理
对自定义过滤器中处理异常的两种基本方法:一种是通过在各个阶段的过滤器中增加try-catch块,实现过滤器内部的异常处理;另一种是利用error类型过滤器的生命周期特性,集中处理pre、route、post阶段抛出的异常信息。
第2种方法:由于在请求生命周期的 pre、route 、 post 三个阶段中有异常抛出的时候都会进入 error 阶段的处理, 所以可以通过创建一个 error 类型的过滤器来捕获这些异常信息,并根据这些异常信息在请求上下文中注入需要返回给客户端的错误描述。
error类型过滤器示例:
public class ErrorFilter extends ZuulFilter {Logger log= LoggerFactory.getLogger(ErrorFilter.class);@Overridepublic String filterType() {return "error";}@Overridepublic int filterOrder() {return 10;}@Overridepublic boolean shouldFilter() {return true;}@Overridepublic Object run() {RequestContext ctx= RequestContext.getCurrentContext();Throwable throwable= ctx.getThrowable();log.error("this is a ErrorFilter : {) ", throwable.getCause().getMessage());ctx.set("error.status_code", H七tpServletResponse.SC_INTERNAL_SERVER—ERROR);Ctx.set("error.exception", throwable.getCause());return null;}
}
2.3.4 禁用过滤器
不过是核心过滤器还是自定义过滤器,只要在API网关应用中为它们创建了实例,默认情况下,它们都是默认启用状态的。
通过重写过滤器的shouldFilter逻辑, 让它返回false,就可以达到禁用该过滤器的目的。
实际上, 在Zuul中特别提供了一个参数来禁用指定的 过滤器, 该参数的配置格式:
zuul.<SimpleClassName>.<filterType>.disable=true
<SimpleClassName>
代表过滤器的类名。filterType即pre/routing/post/error。示例:
zuul.AccessFilter.pre.disable=true
2.3.5 用过滤器限流
比如用令牌桶方式来实现限流。
令牌桶限流:有个桶,如果里面没有满那么就会以一定 固定的速率 会往里面放令牌,一个请求过来首先要从桶中获取令牌,如果没有获取到,那么这个请求就拒绝,如果获取到那么就放行。
用Zuul的前置过滤器来实现一下令牌桶限流:
@Component
@Slf4j
public class RouteFilter extends ZuulFilter {// 定义一个令牌桶,每秒产生2个令牌,即每秒最多处理2个请求private static final RateLimiter RATE_LIMITER = RateLimiter.create(2);@Overridepublic String filterType() {return FilterConstants.PRE_TYPE;}@Overridepublic int filterOrder() {return -5;}@Overridepublic Object run() throws ZuulException {log.info("放行");return null;}@Overridepublic boolean shouldFilter() {RequestContext context = RequestContext.getCurrentContext();if(!RATE_LIMITER.tryAcquire()) {log.warn("访问量超载");// 指定当前请求未通过过滤context.setSendZuulResponse(false);// 向客户端返回响应码429,请求数量过多context.setResponseStatusCode(429);return false;}return true;}
}
三、动态配置Apollo
在Spring Cloud框架中,有Spring Cloud Config可以实现动态配置的功能。不过Apollo也很好用,因此此处介绍一下APollo的使用。
Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。
Apollo 的基础模型:
用户在配置中心对配置进行修改并发布;
配置中心通知Apollo客户端有配置更新;
Apollo客户端从配置中心拉取最新的配置、更新本地配置并通知到应用。
Apollo 客户端会把从服务端获取到的配置在本地文件系统缓存一份,用于在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置,不影响应用正常运行。
3.1 Apollo的四个维度
Apollo支持4个维度管理Key-Value格式的配置:application (应用)、environment (环境)、cluster (集群)、namespace (命名空间)。
- application
Apollo 客户端在运行时需要知道当前应用是谁,从而可以根据不同的应用来获取对应应用的配置。
每个应用都需要有唯一的身份标识,可以在代码中配置 app.id 参数来标识当前应用,Apollo 会根据此指来辨别当前应用。 - environment
在实际开发中,我们的应用经常要部署在不同的环境中,一般情况下分为开发、测试、生产等等不同环境,不同环境中的配置也是不同的,在 Apollo 中默认提供了四种环境:
FAT(Feature Acceptance Test):功能测试环境
UAT(User Acceptance Test):集成测试环境
DEV(Develop):开发环境
PRO(Produce):生产环境
在程序中如果想指定使用哪个环境,可以配置变量 env 的值为对应环境名称即可。
- cluster
一个应用下不同实例的分组,比如典型的可以按地区划分,把上海机房的应用实例分为一个集群,把北京机房的应用实例分为另一个集群。对不同的集群,同一个配置可以有不一样的值。 - namespace
一个应用中不同配置的分组,可以简单地把 namespace 类比为不同的配置文件,不同类型的配置存放在不同的文件中。
Namespace 分为两种权限,分别为:
public(公共的): public权限的 Namespace,能被任何应用获取。
private(私有的): 只能被所属的应用获取到。一个应用尝试获取其它应用 private 的 Namespace,Apollo 会报 404 异常。
Namespace 分为三种类型:
私有类型: 私有类型的 Namespace 具有 private 权限。例如 application Namespace 为私有类型。
公共类型: 公共类型的 Namespace 具有 public 权限。公共类型的N amespace 相当于游离于应用之外的配置,且通过 Namespace 的名称去标识公共 Namespace,所以公共的 Namespace 的名称必须全局唯一。
关联类型(继承类型): 关联类型又可称为继承类型,关联类型具有 private 权限。关联类型的 Namespace 继承于公共类型的 Namespace,将里面的配置全部继承,并且可以用于覆盖公共 Namespace 的某些配置。
3.2 Apollo的使用
3.2.1 页面操作
比如要新增一个配置参数。示例:
创建完成后可以看到配置管理节目新增了一条配置:
点击“发布”后就可以在Web应用中使用该参数。
3.2.2 Web中使用
- 1、引入相关jar
示例:
<dependency><groupId>com.ctrip.framework.apollo</groupId><artifactId>apollo-client</artifactId><version>1.4.0</version></dependency>
- 2、在配置文件中添加Apollo配置的相关参数
Apollo参数的含义:
apollo.meta
:Apollo 配置中心地址。
apollo.cluster
: 指定使用某个集群下的配置。
apollo.bootstrap.enabled
: 是否开启 Apollo。
apollo.bootstrap.namespaces
: 指定使用哪个 Namespace 的配置,默认 application。
apollo.cacheDir=/opt/data/some-cache-dir
: 为了防止配置中心无法连接等问题,Apollo 会自动将配置本地缓存一份。
apollo.autoUpdateInjectedSpringProperties
: Spring应用通常会使用 Placeholder 来注入配置,如${someKey:someDefaultValue},冒号前面的是 key,冒号后面的是默认值。如果想关闭 placeholder 在运行时自动更新功能,可以设置为 false。
apollo.bootstrap.eagerLoad.enabled
: 将 Apollo 加载提到初始化日志系统之前,如果设置为 false,那么将打印出 Apollo 的日志信息,但是由于打印 Apollo 日志信息需要日志先启动,启动后无法对日志配置进行修改,所以 Apollo 不能管理应用的日志配置,如果设置为 true,那么 Apollo 可以管理日志的配置,但是不能打印出 Apollo 的日志信息。
Apollo参数配置示例:
#应用配置
server:port: 8080
spring:application:name: apollo-demo#Apollo 配置
app:id: apollo-test #应用ID
apollo:cacheDir: /opt/data/ #配置本地配置缓存目录cluster: default #指定使用哪个集群的配置meta: http://192.168.2.11:30002 #DEV环境配置中心地址autoUpdateInjectedSpringProperties: true #是否开启 Spring 参数自动更新bootstrap: enabled: true #是否开启 Apollonamespaces: application #设置 NamespaceeagerLoad:enabled: false #将 Apollo 加载提到初始化日志系统之前
- 3、在Java代码中使用Apollo上配置的属性
使用@Value主机即可获取。示例:
@Value("${test:默认值}")private String test;
四、Spring Cloud相关问题
4.1 SpringBoot和SpringCloud的区别
SpringBoot专注于快速方便的开发单个个体微服务。
SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务。
SpringBoot可以离开SpringCloud独立使用开发项目, 但是SpringCloud离不开SpringBoot ,属于依赖的关系。
SpringBoot专注于快速、方便的开发单个微服务个体,SpringCloud关注全局的服务治理框架。
4.2 Spring Cloud断路器的作用
当一个服务调用另一个服务由于网络原因或自身原因出现问题,调用者就会等待被调用者的响应 当更多的服务请求到这些资源导致更多的请求等待,发生连锁效应(雪崩效应)。
断路器有不同的状态:
完全打开状态
:一段时间内,达到一定的次数无法调用,并且多次监测没有恢复的迹象,断路器完全打开,那么下次请求就不会请求到该服务。
半开
:短时间内有恢复迹象,断路器会将部分请求发给该服务,正常调用时断路器关闭。
关闭
:当服务一直处于正常状态,能正常调用。
4.3 什么是Spring Cloud Gateway
Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用。
使用了一个RouteLocatorBuilder的bean去创建路由,除了创建路由RouteLocatorBuilder可以让你添加各种predicates和filters,predicates断言的意思,顾名思义就是根据具体的请求的规则,由具体的route去处理,filters是各种过滤器,用来对请求做各种判断和修改。
4.4 Spring Cloud如何实现服务的注册
- 服务发布时,指定对应的服务名,将服务注册到注册中心(eureka zookeeper)。
- 注册中心加@EnableEurekaServer,服务用@EnableDiscoveryClient,然后用Ribbon或Feign进行服务直接的调用发现。
4.5 REST和RPC对比
1、RPC主要的缺陷是服务提供方和调用方式之间的依赖太强,需要对每一个微服务进行接口的定义,并通过持续继承发布,严格版本控制才不会出现冲突。
2、REST是轻量级的接口,服务的提供和调用不存在代码之间的耦合,只需要一个约定进行规范。
4.6 说说RPC的实现原理
首先需要有处理网络连接通讯的模块,负责连接建立、管理和消息的传输。其次需要有编解码的模块,因为网络通讯都是传输的字节码,需要将我们使用的对象序列化和反序列化。剩下的就是客户端和服务器端的部分,服务器端暴露要开放的服务接口,客户调用服务接口的一个代理实现,这个代理实现负责收集数据、编码并传输给服务器然后等待结果返回。
4.7 接口限流方案
- 限制总并发数(比如数据库连接池、线程池)。
- 限制瞬时并发数(如 nginx 的 limit_conn 模块,用来限制 瞬时并发连接数)。
- 限制时间窗口内的平均速率(如 Guava 的 RateLimiter 、 nginx 的 limit_req 模块,限制每秒的平均速率)。
- 限制远程接口调用速率。
- 限制MQ的消费速率。
- 可以根据网络连接数、网络流量、 CPU 或 内存负载 等来限流。