概述
SpringCloud是微服务架构下的一站式解决方案,主要提供了以下功能:
- 服务注册与服务发现;
- 负载均衡;
- 熔断降级;
- 调用链追踪;
- 网关;
- 分布式配置;
在线资源
Spring Cloud官网 | |
Spring Cloud中文网 | |
SpringCloud中国社区 | |
Netfilx Github | |
SpringCloutDemo项目 | source-reading/spring_cloud_demo at master · echo20222022/source-reading · GitHub |
Eureka
简介
Eureka是Netflix开发的一个用于服务注册与发现的组件,被Spring团队集成后形成了Spring Cloud Eureka,成为Spring Cloud微服务解决方案的一个组件。
架构
CAP定理
CAP定理说的是在一个分布式系统中,不可能同时满足一致性、可用性和分区容错性,Eureka作为一个分布式注册中心它满足了AP,而像Zookeeper这种配置中心满足的是CP,所以说Eureka的性能和可用性更好,但是可能会出现集群节点内数据不一致的情况,而Zookeeper的性能相对比较差,但他能够满足数据一致性。
DiscoveryClient
DiscoveryClient是Eureka的客户端,可以通过Spring自动注入的方式添加到应用系统中,从而拿到Eureka中服务相关的信息。
自我保护机制
默认情况下,接入Eureka的业务系统会定时向Eureka Server发送心跳续约消息(默认30s),如果某个服务在3个心跳周期内没有上报续约消息,就会被Eureka剔除服务列表,但有时候是由于网络抖动导致的心跳续约失败,所以Eureka引入了自我保护机制来最大化的保证服务的可用性,即如果Eureka判定接收到的续约消息少于某个阈值(默认85%),就开启自我宝珠机制,在自我保护机制下Eureka内的服务列表不会被剔除,只能查询和新增,这样避免了由于网络抖动造成上游服务调用不到下游服务的严重异常,保证了可用性。
eureka:
server:
# 打开/关闭自我保护机制,默认开启
enable-self-preservation: false
# 指定自我保护机制的开启阈值,默认85%
renewal-percent-threshold: 0.75
# 设置server端剔除不可用服务的时间窗,单位毫秒, 默认30s
eviction-interval-timer-in-ms: 4000
关闭服务
- kill -9 非平滑关机
- curl -X POST http://localhost:8081/actuator/shutdown 非平滑关机
- curl -X POST -H "Content-Type:application/json" http://localhost:8081/actuator/service-registry -d '{"status": "DOWN"}' 从注册中心平滑下线,不关闭服务
高可用
集群环境
- 启动多个eureka节点;
- eureka.service-url.defaultZone 包含所有节点的schema;
异地多活
为了能够实现服务的高可用以及就近访问,Eureka引入了Region和Zone的概念,实际上就相当于是给某个服务打上了两个标签,在进行服务间调用的时候,优先匹配同一个Region&Zone下的服务,Eureka一般情况下在不同Region之间不进行数据复制,而在同一个Region内的不同Region之间进行数据复制,就行Eureka架构中表示的那样:
其中三台Eureka Server属于同一个Region(即us-east),但是不同的Zone(1c,1d,1e),但在实际生产环境中,图中的每一个Eureka Server实际上又是一个Eureka集群,比如向构建一个如下的系统,配置见:
https://github.com/echo20222022/source-reading/blob/master/spring_cloud_demo/cloud-eureka/region-zone.config
Ribbon
Ribbon是Netflix开发的一个面向服务名的HttpRest调用工具,内部提供了负载均衡的能力,需要Eureka的支持来获取服务实例列表。
负载均衡策略
RoundRobinRule | 轮询策略,是默认的负载均衡策略 |
RandomRule | 随机策略 |
RetryRule | 重试策略,先按照轮序策略进行选择,如果获取失败,则在指定的时间内重试,默认为500ms |
BestAvaliableRule | 最佳可用策略,选择并发量最小的provider |
AvaliabilityFilteringRule | 可用过滤策略 |
ZoneAvoidanceRule | zone回避策略,根据provider提供的zone及provider的可用性进行选择 |
WeightedResponseRule | 权重响应时间策略,根据每个provider的平均响应时间计算权重,响应时间约快权重越大。刚启动时采用轮询策略,逐渐切换到权重响应时间策略。 |
- 通过配置文件修改
service-name:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
- 通过配置类修改
@Bean
public IRule loadbalance() {
return new RandomRule();
}
- 自定义负载均衡策略
实现IRule接口,重写相关方法,并通过上面两种方式进行配置
配置
核心配置
cloud-fd:
ribbon:
#度超时时间
ReadTimeout: 5000
#连接超时时间
ConnectTimeout: 5000
#是否开启重试
OkToRetryOnAllOperations: true
#总的重试次数
MaxAutoRetriesNextServer: 2
#在单个实例上重试的次数
MaxAutoRetries: 1
#负载均衡策略
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
日志配置
logging:
level:
com:
cloud:
fd:
client:
CloudApiFeign: debug
Feign
Feign是一个能够把接口调用转换成RestHttp调用的工具,一般是Feign配合Ribbon一块使用,实现一个面向接口调用切带有负载均衡能力的跨服务调用,但也可以单独使用。
使用方式
- 定义API接口;
- 通过@EnableFeignClients开启对Fein的支持;
- 实现API接口,配置@FeignClient;
- 将@FeignClient接口注入到业务类中;
@FeignClient
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
//名称,对应服务id
@AliasFor("name")
String value() default "";
/** @deprecated */
@Deprecated
String serviceId() default "";
//在spring 中的id
String contextId() default "";
@AliasFor("value")
String name() default "";
String qualifier() default "";
//在不引入eureka时,请求资源对应的url
String url() default "";
boolean decode404() default false;
//外部配置类
Class<?>[] configuration() default {};
//降级类
Class<?> fallback() default void.class;
//降级工厂类
Class<?> fallbackFactory() default void.class;
//路径前缀
String path() default "";
boolean primary() default true;
}
内容压缩
feign:
compression:
request:
#请求压缩是否开启
enable: true
#指定mime类型
mime-types: ["",""],
#指定请求体的大小
min-request-size: 2048
response:
enable: true
Hystrix
Hystrix是Netflix开发的一个具有熔断降级功能的组件,它的实现方式就是在被调放的基础上包装了一个熔断器,当触发熔断条件时就会自动断开对下游的访问,实现降级返回,这样在分布式系统中就保证了异常不会在整个系统中传导,最终导致整个服务出现雪崩效应。
原理
触发熔断降级的四个场景:
- 熔断器开启;
- 线程池资源占满;
- 调用异常;
- 调用超时;
隔离策略
-
线程隔离,适合大部分场景,能够对调用进行精准控制,但有额外的开销;
-
信号量隔离,适合调用量特别大但响应时间特别快的服务 ;
使用方式
- @HystrixCommand
- 基于配置
降级方式
- 方法级别的服务降级(配合@HystrixCommand)
- 类级别的服务降级(配合Feign)
高级配置
Configuration · Netflix/Hystrix Wiki · GitHub
execution.isolation.strategy | 隔离策略,THREAD/SEMAPHORE | THREAD |
execution.isolation.thread.timeoutInMilliseconds | 线程隔离策略下的超时时间 | 1000ms |
execution.timeout.enabled | 是否开启超时机制 | true |
execution.isolation.thread.interruptOnTimeout | 发生超时时是否中断对下游服务的调用 | true |
execution.isolation.thread.interruptOnCancel | 发生取消时是否中断对下游服务的调用 | false |
execution.isolation.semaphore.maxConcurrentRequests | 信号量隔离策略下最大的信号量数量 | 10 |
fallback.isolation.semaphore.maxConcurrentRequests | 信号量隔离策略下执行fallback的最大并发数,超过的不会执行fallback,直接拒绝 | 10 |
fallback.enabled | 是否启动fallback机制 | true |
circuitBreaker.requestVolumeThreshold | 熔断器滑动窗口中的最小请求数量 | 20 |
circuitBreaker.sleepWindowInMilliseconds | 熔断器开始后经过多长时间开始释放探测请求 | 5000ms |
circuitBreaker.errorThresholdPercentage | 当错误请求的数量达到多少百分比是开启熔断器 | 50 |
circuitBreaker.forceOpen | 是否强制开启熔断器 | false |
circuitBreaker.forceClosed | 是否强制关闭熔断器 | false |
监控仪表
- Hystrix GUI
- Turbin
trubin: http://localhost:9000/hystrix
监控地址:http://localhost:10000/turbine.stream?cluster=default
Zuul
Zuul是SpringCloud微服务解决方案中的网关组件,核心功能是路由和过滤,路由就是将请求转发到后端对应的服务上,过滤就是对请求进行逻辑过滤,比如认证和授权等,基于过滤的功能,还可以实现诸如限流和灰度发布能功能,同时也包含了其他一些高级功能,比如负载均衡、异常降级、请求头过滤等。
路由配置
路由配置的方式有三种:
- ①不依赖于注册中心;
#①不依赖于注册中心的配置
zuul:
routes:
cloud-fd:
path: /fd/**
serviceId: cloud-fd
#如果是单节点的服务,则不用配置listOfServers
cloud-robot:
path: /robot/**
url: "http://localhost:8085"
cloud-fd:
ribbon:
listOfServers: "http://localhost:8081,http://localhost:8082"
- ②标准配置;
#②标准配置
zuul:
routes:
cloud-fd:
path: /fd/**
serviceId: cloud-fd
cloud-robot:
path: /robot/**
serviceId: cloud-robot
#向下游转发时,是否移除前传,在当前例子中,如果不移除就会出现404
strip-prefix: true
- ③简化配置;
#③简写
zuul:
routes:
cloud-fd: /fd/**
cloud-robot: /robot/**
# 通配符规则
# /** 可以匹配0或多级路径
# /* 可以匹配1级路径
# /? 可以匹配1级路径,且只能包含一个字符
#ignore-patterns: /**/list/**
配置URL前缀
有时候需要在url中增加统一的前缀来规范url的格式,比如在一个API系统中如果要在url中增加/api前缀,就可以通过以下配置实现:
zuul:
routers:
prefix: /cloud
#向下游转发时是否移除前缀,正常需要移除,因为下游服务无法识别这个统一前缀
stripPrefix: true
忽略服务名
默认情况下,可以通过服务名直接路由到下游服务,比如下面的配置:
zuul:
routes:
cloud-fd:
path: /fd/**
serviceId: cloud-fd
prefix: /cloud
也可以通过/cloud/cloud-fd/xxx/xxx来访问下游服务,但对外暴露服务名称是存在安全风险的,所以需要将通过服务名路由的能力禁用掉:
zuul:
#屏蔽掉通过服务名称访问
ignored-services: "*"
过滤请求头
有时候一些敏感的请求头不需要传递到下游服务中,比如认证、授权的一些请求头,就可以通过下面的配置过滤掉:
zuul:
sensitive-headers: token,Cookie
ProxyRequestHelper.java
public boolean isIncludedHeader(String headerName) {
String name = headerName.toLowerCase();
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.containsKey(IGNORED_HEADERS)) {
Object object = ctx.get(IGNORED_HEADERS);
if (object instanceof Collection && ((Collection<?>) object).contains(name)) {
return false;
}
}
switch (name) {
case "host":
if (addHostHeader) {
return true;
}
case "connection":
case "content-length":
case "server":
case "transfer-encoding":
case "x-application-context":
return false;
default:
return true;
}
}
传递Host
Host header是客户端在访问服务端的时候制定的需要访问的服务端地址,由于网关是统一入口,所以Host一般都是统一的域名,对下游来说没有什么用,所以不会传递下去,如果想传递到下游服务则可以通过如下配置实现:
zuul:
add-host-header: true
Hystrix&Ribbon配置
hystrix:
command:
#默认的超时时间
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 5000
#某个指定的服务的超时时间
cloud-robot:
execution:
isolation:
thread:
timeoutInMilliseconds: 5000
#ribbon指定服务的超时配置
cloud-robot:
ribbon:
ReadTimeout: 5000
ConnectTimeout: 5000
cloud-fd:
ribbon:
ReadTimeout: 5000
ConnectTimeout: 5000
OkToRetryOnAllOperations: true
MaxAutoRetriesNextServer: 2
MaxAutoRetries: 1
#ribbon的全局配置
ribbon:
ReadTimeout: 5000
ConnectTimeout: 5000
过滤器
过滤器是zuul中的核心组件,zuul的很多功能都是基于过滤器实现的。
逻辑抽象
#过滤器类型 pre/routing/post/error
String filterType();
#过滤器执行顺序,数值越小优先级越高
int filterOrder();
#是否应该执行该filter
boolean shouleFilter();
#过滤器的执行逻辑
Object run();
生命周期
内置过滤器
过滤器名称 | 类型 | 描述 | 顺序 |
ServletDetectionFilter | pre | 检测当前请求是通过Spring的DispatcherServlet处理运行的还是ZuulServlet来处理运行的 | -3 |
Servlet30WrapperFilter | 将HttpServletRequest包装成Servlet30RequestWrapper | -2 | |
DebugFilter | 根据配置参数zuul.debug.request和请求中的debug参数来决定是否执行过滤器中的操作 | 1 | |
PreDecorationFilter | 5 | ||
RibbonRoutingFilter | routing | 通过service id来将请求路由到下游服务 | 10 |
SimpleHostRoutingFilter | 通过HttpClient的方式将请求路由到下游服务 | 100 | |
SendForwardFilter | 500 | ||
SendErrorFilter | post | 1 | |
SendResponseFilter | 1000 |
ServletDetectionFilter
该过滤器的执行顺序为-3,默认情况下最先被执行,主要作用是判断当前请求的入口源头,然后向RequestContext中设置标识,供后续过滤器使用。
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if (!(request instanceof HttpServletRequestWrapper)
&& isDispatcherServletRequest(request)) {
ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, true);
}
else {
ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, false);
}
return null;
}
默认情况下是通过DispatcherServlet和ZuulController来间接的将请求转发到ZuulServlet中(ZuulController继承了SpringMVC的ServletWrappingController,即是一个SpringMVC的处理器),但Zuul实际上也将ZuulServlet注册到了Servlet容器中,只不过需要通过特殊的路径进行访问,默认时/zuul/**,可以通过zuul.servlet-path参数进行修改。
Servlet30WrapperFilter
该过滤器的执行顺序为-3,默认情况下被第二个执行,将HttpServletRequest包装成Servlet30RequestWrapper,即能够支持Servlet 3.0规范。
DebugFilter
该过滤器的执行顺序为1,主要作用是在Zuul的上下文中设置debug标识,这里涉及到两个参数,zuul.debug.request=true/false和zuul.debug.parameter=key,zuul.debug.parameter的优先级要高于zuul.debug.request,当设置了zuul.debug.parameter并且在请求参数中key对应的值=true时,或没有配置zuul.debug.parameter但配置了zuul.debug.request=true时,都会开启debug模式,即向RequestContext中设置两个参数:debugRouting和debugRequest,在后续的Filter中可以通过下面的API来获取:
//DebugFilter
@Override
public boolean shouldFilter() {
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
if ("true".equals(request.getParameter(DEBUG_PARAMETER.get()))) {
return true;
}
return ROUTING_DEBUG.get();
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
ctx.setDebugRouting(true);
ctx.setDebugRequest(true);
return null;
}
//CustomFilter
RequestContext ctx = RequestContext.getCurrentContext();
boolean debugRouting = ctx.debugRouting();
boolean debugRequest = ctx.debugRequest();
这样当线上环境出现问题需要紧急处理的时候,就可以通过参数来控制某些debug日志的输出,但这仅限于Zuul的内部。
PreDecoreationFilter
该过滤器的执行顺序5,他的主要作用是通过解析url及路由配置,将一些路由信息设置到RequestContext中,同时还有其他一些参数,比如敏感Header、重试、Host请求头等。
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
final String requestURI = this.urlPathHelper
.getPathWithinApplication(ctx.getRequest());
Route route = this.routeLocator.getMatchingRoute(requestURI);
if (route != null) {
//cloud-fd 服务名称 或 http://
String location = route.getLocation();
if (location != null) {
ctx.put(REQUEST_URI_KEY, route.getPath());
ctx.put(PROXY_KEY, route.getId());
if (!route.isCustomSensitiveHeaders()) {
this.proxyRequestHelper.addIgnoredHeaders(
this.properties.getSensitiveHeaders().toArray(new String[0]));
}
else {
this.proxyRequestHelper.addIgnoredHeaders(
route.getSensitiveHeaders().toArray(new String[0]));
}
if (route.getRetryable() != null) {
ctx.put(RETRYABLE_KEY, route.getRetryable());
}
//如果localtion是以http开头的,就是这事RouteHost,后面会利用SimpleHostRoutingFilter进行处理
if (location.startsWith(HTTP_SCHEME + ":")
|| location.startsWith(HTTPS_SCHEME + ":")) {
ctx.setRouteHost(getUrl(location));
ctx.addOriginResponseHeader(SERVICE_HEADER, location);
}
else if (location.startsWith(FORWARD_LOCATION_PREFIX)) {
//处理forward
ctx.set(FORWARD_TO_KEY,
StringUtils.cleanPath(
location.substring(FORWARD_LOCATION_PREFIX.length())
+ route.getPath()));
ctx.setRouteHost(null);
return null;
}
else {
//如果是服务ID,就设置Service id key,后面会用RibbonRoutingFilter进行处理
// set serviceId for use in filters.route.RibbonRequest
ctx.set(SERVICE_ID_KEY, location);
ctx.setRouteHost(null);
ctx.addOriginResponseHeader(SERVICE_ID_HEADER, location);
}
if (this.properties.isAddProxyHeaders()) {
addProxyHeaders(ctx, route);
String xforwardedfor = ctx.getRequest()
.getHeader(X_FORWARDED_FOR_HEADER);
String remoteAddr = ctx.getRequest().getRemoteAddr();
if (xforwardedfor == null) {
xforwardedfor = remoteAddr;
}
else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates
xforwardedfor += ", " + remoteAddr;
}
ctx.addZuulRequestHeader(X_FORWARDED_FOR_HEADER, xforwardedfor);
}
//这里处理add-host-header
if (this.properties.isAddHostHeader()) {
ctx.addZuulRequestHeader(HttpHeaders.HOST,
toHostHeader(ctx.getRequest()));
}
}
}
else {
log.warn("No route found for uri: " + requestURI);
String forwardURI = getForwardUri(requestURI);
ctx.set(FORWARD_TO_KEY, forwardURI);
}
return null;
}
RibbionRoutingFilter
该过滤器的执行顺序10,内部是通过service id将请求路由到下游服务的逻辑。
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
//这里的ServiceID就是PreDecoreationFilter根据配置信息设置到上下文的
return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
&& ctx.sendZuulResponse());
}
SimpleHostRoutingFilter
该过滤器的执行顺序100,如果不依赖于注册中心进行请求转发,那就会利用这个Filter使用HttpClient向下游转发请求,使用这种方式进行请求转发时,会涉及到HttpClient底层线程池的设置。
SendForwardFilter
RequestContext ctx = RequestContext.getCurrentContext();
//key = forward.to
String path = (String) ctx.get(FORWARD_TO_KEY);
RequestDispatcher dispatcher = ctx.getRequest().getRequestDispatcher(path);
if (dispatcher != null) {
ctx.set(SEND_FORWARD_FILTER_RAN, true);
if (!ctx.getResponse().isCommitted()) {
dispatcher.forward(ctx.getRequest(), ctx.getResponse());
ctx.getResponse().flushBuffer();
}
}
SendErrorFilter
该过滤器的执行顺序1,是post阶段第一个执行的过滤器,该过滤器仅在上下人中包含error.status_code参数并且没有被改过滤器处理过的时候执行,具体就是利用上下文中的错误信息组装成一个forward到api网关的/error错误端点的请求来产生错误响应。
SendResponseFilter
该过滤器的执行顺序1000,是post阶段最后要给执行的过滤器,用于把响应结果发送给客户端。
网关限流
令牌桶算法
漏桶算法
简单实现
@Component
public class RateLimitFilter extends ZuulFilter {
//每秒钟产生2个令牌
private static final RateLimiter limiter = RateLimiter.create(1);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return -10;
}
@Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
//尝试获取令牌,如果获取失败,则直接返回结果
if (!limiter.tryAcquire()) {
System.out.println("限流检测未通过...");
//不向后传播了
context.setSendZuulResponse(false);
context.setResponseStatusCode(429);
context.setResponseDataStream(new ByteArrayInputStream("rate limit error".getBytes(StandardCharsets.UTF_8)));
return false;
}
return true;
}
@Override
public Object run() throws ZuulException {
System.out.println("限流通过...");
return null;
}
}
多维限流
<dependency>
<groupId>com.marcosbarbero.cloud</groupId>
<artifactId>spring-cloud-zuul-ratelimit</artifactId>
<version>2.0.5.RELEASE</version>
</dependency>
zuul:
ratelimit:
enabled: true #开启多维度限流
#在一个单位时间窗内 通过该zuul的用户数量、ip数量、及url数量 都不能超过3个
default-policy: #设置限流策略
quota: 1 #指定限流的时间窗数量
refresh-interval: 3 #时间窗大小,单位是秒
limit: 3 #在指定的单位时间窗内 启用限流的 限定值
type: origin
# user #cloud-gateway:fd:anonymous request.getRemoteUser();
# url cloud-gateway:fd:/hello
# origin cloud-gateway:fd:0:0:0:0:0:0:0:1
# origin,url #指定限流维度
总结
SpringCloud是一个微服务架构解决方案,提供了服务注册与服务发现、负载均衡、熔断降级、面向接口的Http调用、网关、监控、调用链追踪等功能,大部分功能是通过集成第三方开源项目的方式实现的(主要以Netflix为主),但SpringCloud存在两个比较大的问题:
- 功能分散,组件众多,学习成功很高;
- 有很多功能都耦合在了业务系统中,会影响代码的管理,对业务代码有一定的侵入性;
针对上面这两个问题,很多框架都做出了调整,比如dubbo 3.0对Service Mesh架构进行了支持,Spring Cloud Tencent提供了更加通用的管控平台北极星,将很多功能封装并统一管理等,但由于Spring Cloud是Spring官方提供了完整解决方案,目前还是有很大一部分项目在使用。
在代码实现层面,几乎所有的组件都是基于SpringBoot的自动装配功能实现初始化的,通过自定义Configuration/spring.factories/Marker Bean/标记注解的方式自动注册对应的组件。