相关:SpringCloud_从分布式到微服务的架构演变
本篇无代码,只说WHAT不说HOW。
引:网约车
网约车出现以前,人们出门叫车只能叫出租车。一些私家车想做出租却没有资格,被称为黑车。而很多人想要约车,但是无奈出租车太少,不方便。私家车很多却不敢拦,而且满大街的车,谁知道哪个才是愿意载人的。一个想要,一个愿意给,就是缺少引子,缺乏管理啊。
此时滴滴这样的网约车平台出现了,所有想载客的私家车全部到滴滴注册,记录你的车型(服务类型),身份信息(联系方式)。这样提供服务的私家车,在滴滴那里都能找到,一目了然。
此时要叫车的人,只需要打开APP,输入你的目的地,选择车型(服务类型),滴滴自动安排一个符合需求的车到你面前,为你服务,完美!
Eureka就好比是滴滴,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。
同时,服务提供方与Eureka之间通过“心跳”
机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。
Eureka实现了服务的自动注册、自动发现、状态监控。
Eureka架构图
Eureka:就是服务注册中心(可以是一个集群),对外暴露地址,提供服务注册和发现功能,即可接受服务提供者或消费者的注册,可自动发现可用的服务提供者。
服务提供者:提供服务的应用,可以是SpringBoot应用,也可以是其它任意技术实现,只要对外提供的是Rest风格服务即可。启动后向Eureka注册自己信息(地址,提供什么服务)
服务消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新
服务同步
多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。
服务续约
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew);
有两个重要参数可以修改服务续约的行为:
eureka:
instance:
lease-expiration-duration-in-seconds: 90 #服务确认失效时间,默认值90秒
lease-renewal-interval-in-seconds: 30 #服务续约(renew)的间隔,默认为30秒
也就是说,默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。
但是在开发时,这个值有点太长了,经常我们关掉一个服务,会发现Eureka依然认为服务在活着。所以我们在开发阶段可以适当调小。
服务提供者失效剔除
对90秒无心跳服务的处理不是立即执行的。
有些时候,我们的服务提供方并不一定会正常下线,可能因为内存溢出、网络故障等原因导致服务无法正常工作。
Eureka Server需要将这样的服务剔除出服务列表。因此它会开启一个定时任务,每隔60秒对所有已经确认失效的服务(90秒无心跳发送)进行剔除。
可以通过eureka.server.eviction-interval-timer-in-ms
参数对其进行修改,单位是毫秒,生产环境不要修改。
服务消费者_获取服务列表
当服务消费者启动时,会检测eureka.client.fetch-registry=true
参数的值,默认true,如果为true,则会拉取Eureka Server服务的列表只读备份,然后缓存在本地。并且每隔30秒
会重新获取并更新数据。我们可以通过下面的参数来修改:
eureka:
client:
registry-fetch-interval-seconds: 5
生产环境中,我们不需要修改这个值。
但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点。
服务提供者下线
当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求之后,将该服务置为下线状态。
自我保护
当一个服务未按时(90秒内)进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%,如果低于 85%,Eureka Server 会将这些实例保护起来,让这些实例不会过期,但是在保护期内如果服务刚好这个服务提供者非正常下线了,此时服务消费者就会拿到一个无效的服务实例,此时会调用失败,对于这个问题需要服务消费者端要有一些容错机制,如重试,断路器等。
我们在生产环境下很容易满足心跳失败比例在 15 分钟之内低于 85%,这个时候就会触发 Eureka 的保护机制,一旦开启了保护机制,则服务注册中心维护的服务实例就不是那么准确了,此时我们可以使用eureka.server.enable-self-preservation=false来关闭保护机制,这样可以确保注册中心中不可用的实例被及时的剔除
但是实际环境中,我们往往会开启很多个服务提供者的集群。此时服务消费者获取的服务列表中就会有多个服务,到底该访问哪一个呢?
这是Ribbon要解决的问题。
在没有Ribbon的情况,需要我们自己去实现负载均衡算法。
不过Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。它为我们提供了不同的负载均衡算法,根据需要进行选择即可。
Ribbon在Netflix发布的负载均衡器,有助于控制HTTP和TCP客户端(服务消费者)的行为。
为Ribbon配置服务提供者地址列表后,Ribbon就可基于某种负载均衡算法,自动帮助服务消费者去请求。
Ribbon提供轮询、随机等算法可供我们选择,当然也可为Ribbon自定义负载均衡算法。
Ribbon默认的负载均衡策略是简单的轮询。
Ribbon这边建议是追一下源码。有机会出实战篇的时候我再说。
Hystrix,英文意思是豪猪,全身是刺,看起来就不好惹,是一种保护机制。
Hystrix也是Netflix公司的一款组件。
Hystix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。
雪崩问题
微服务中,服务间调用关系错综复杂,一个请求,可能需要调用多个微服务接口才能实现,会形成非常复杂的调用链路:
如图,一次业务请求,需要调用A、P、H、I四个服务,这四个服务又可能调用其它服务。
如果此时,某个服务出现异常:
例如微服务I发生异常,请求阻塞,用户不会得到响应,则tomcat的这个线程不会释放,于是越来越多的用户请求到来,越来越多的线程会阻塞:
服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成雪崩效应。
Redis的缓存雪崩问题是由于key大面积同一时间过期,导致大量请求落在数据库上。
这就好比,一个汽车生产线,生产不同的汽车,需要使用不同的零件,如果某个零件因为种种原因无法使用,那么就会造成整台车无法装配,陷入等待零件的状态,直到零件到位,才能继续组装。 此时如果有很多个车型都需要这个零件,那么整个工厂都将陷入等待的状态,导致所有生产都陷入瘫痪。一个零件的波及范围不断扩大,也即雪崩效应。
Hystix解决雪崩问题的手段有两个:
线程隔离
Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。
用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理,什么是服务降级?
服务降级:优先保证核心服务,非核心服务不可用或弱可用。
用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息)
服务降级的逻辑需要自己编写。
如:
@GetMapping
@ResponseBody
//用来声明一个降级逻辑的方法
@HystrixCommand(fallbackMethod = "queryUserByIdFallBack")
public String queryUserById(@RequestParam("id") Long id) {
String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
return user;
}
//降级处理逻辑,返回友好提示信息
public String queryUserByIdFallBack(Long id){
return "请求繁忙,请稍后再试!";
}
服务降级虽然会导致请求失败,但是不会导致阻塞。
要注意,因为熔断的降级逻辑方法必须跟正常逻辑方法保证:相同的参数列表和返回值声明。
失败逻辑中返回User对象没有太大意义,一般会返回友好提示。所以我们把queryById的方法改造为返回String,反正也是Json数据。这样失败逻辑中返回一个错误说明,会比较方便。
默认FallBack
我们刚才把fallback写在了某个业务方法上,其实我们可以把Fallback配置加在类上,实现默认fallback:
@Controller
@RequestMapping("consumer/user")
@DefaultProperties(defaultFallback = "fallBackMethod") // 指定一个类的全局熔断方法
public class UserController {
@Autowired
private RestTemplate restTemplate;
@GetMapping
@ResponseBody
@HystrixCommand // 标记该方法需要熔断
public String queryUserById(@RequestParam("id") Long id) {
String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
return user;
}
/**
* 熔断方法
* 返回值要和被熔断的方法的返回值一致
* 熔断方法不需要参数
* @return
*/
public String fallBackMethod(){
return "请求繁忙,请稍后再试!";
}
}
请求超时时间
Hystix的默认请求超时时长为1,我们可以通过配置修改这个值:
通过hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
来设置Hystrix超时时间。
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000 # 设置hystrix的超时时间为6000ms
服务熔断
服务调用方可以自己判断某些服务反应慢或存在大量超时的情况,根据情况能够主动熔断、防止系统被拖垮。
Hystrix甚至能做到当服务提供者恢复后进行自动重连。
通过断路的方式,可以把后续请求直接拒绝,一段时间后允许部分请求通过,如果调用成功则恢复电路闭合状态、否则继续断开。
熔断状态机3个状态:
熔断配置参数:
# 触发熔断的最小请求次数,默认20
circuitBreaker.requestVolumeThreshold=10
# 休眠时长,默认是5000毫秒
circuitBreaker.sleepWindowInMilliseconds=10000
# 触发熔断的失败请求最小占比,默认50%
circuitBreaker.errorThresholdPercentage=50
在之前,我们是通过
String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
进行服务调用
Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。
Feign是Netflix开发的声明式、模板化的HTTP客户端,其灵感来自Retrofit、JAXRS-2.0以及WebSocket。
Feign可帮助我们更加便捷、优雅地调用HTTP API
SpringCloud对Feigh进行了增强,使Feigh支持了SpringMVC注解,并整合了Ribbon和Eureka。
Feign自动集成了Ribbon负载均衡过的RestTemplate。
Feign默认也有对Hystrix的集成:
只不过,默认情况下是关闭的。我们需要通过下面的参数来开启:
feign:
hystrix:
enabled: true # 开启Feign的熔断功能
Feign调用实例-微信小程序登录:
@FeignClient(name = "wechat", url = "https://api.weixin.qq.com")
public interface AuthClient {
/**
* 获取用户的session_key和open_id
* */
@RequestMapping(value = "/sns/jscode2session", method = RequestMethod.GET)
String getUserFromWeChat(
@RequestParam("appid") String appid,
@RequestParam("secret") String secret,
@RequestParam("js_code") String js_code,
@RequestParam("grant_type") String grant_type
);
}
但是,Feign中的Fallback配置不像hystrix中那样简单了…
通过前面的学习,使用Spring Cloud实现微服务的架构基本成型,大致是这样的:
我们使用Spring Cloud Netflix中的Eureka实现了服务注册中心以及服务注册与发现;而服务间通过Ribbon或Feign实现服务的消费以及均衡负载。
为了使得服务集群更为健壮,使用Hystrix的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延。
在该架构中,我们的服务集群包含:内部服务Service A和Service B,他们都会注册与订阅服务至Eureka Server。
而Open Service是一个对外的服务,通过均衡负载公开至外部调用方。
我们把焦点聚集在对外服务这块,直接暴露我们的对外服务地址,这样的实现是否合理,或者是否有更好的实现方式呢?
答案当然是不合理的,这样做会:
破坏了服务无状态原则。
Web服务的状态指的是Client与Server进行交互操作时所留下来的公共信息(工作流、用户状态信息等数据)。这些信息可以被指定在不同的作用域中,如:page、request、session、application或全局作用域,一般由Server中的Session来保存这些信息。
在基于状态的Web服务中,Client与Server交互的信息(如:用户登录状态)会保存在Server的Session中。再这样的前提下,Client中的用户请求只能被保存在有此用户相关状态信息的Server上,这也就意味着在基于状态的Web系统中的Server无法对用户请求进行负载均衡(一个Client请求只能由一个指定的Server处理)。
同时这也会导致另外一个容错性的问题,如果指定的Server在Client的用户发出请求的过程中宕机,那么此用户最近的所有交互操作将无法被转移至别的Server上,即此请求将无效化。
无状态:
在无状态的Web服务中,每一个Web请求都必须是独立的,请求之间是完全分离的。Server没有保存Client的状态信息,所以Client发送的请求必须包含有能够让服务器理解请求的全部信息。
为了保证对外服务的安全性,我们需要实现对服务访问的权限控制,而开放服务的权限控制机制将会贯穿并污染整个开放服务的业务逻辑,这会带来的最直接问题是,破坏了服务集群中REST API无状态的特点。
无法直接复用既有接口。
当我们需要对一个即有的集群内访问接口实现外部服务访问时,我们不得不通过在原有接口上增加校验逻辑,或增加一个代理调用来实现权限控制,无法直接复用原有的接口。
为了解决上面这些问题,我们需要将权限控制这样的东西从我们的服务单元中抽离出去,而最适合这些逻辑的地方就是处于对外访问最前端的地方。
服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由
、均衡负载
功能之外,它还具备了权限控制
等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。
Zuul加入后的架构:
不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。
Zuul过滤器
Zuul作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作我们往往是通过Zuul提供的过滤器来实现的。
ZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法:
public abstract ZuulFilter implements IZuulFilter{
//返回字符串,代表过滤器的类型。包含以下4种:
//pre:请求在被路由之前执行
//route:在路由请求时调用
//post:在route和errror过滤器之后调用
//error:处理请求时发生错误调用
abstract public String filterType();
//通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。
abstract public int filterOrder();
// 来自IZuulFilter
//判断该过滤器是否需要执行。返回true执行,返回false不执行。
boolean shouldFilter();
//过滤器的具体业务逻辑
Object run() throws ZuulException;// IZuulFilter
}
过滤器执行生命周期
这张是Zuul官网提供的请求生命周期图,清晰的表现了一个请求在各个过滤器的执行顺序。
正常流程:
异常流程:
使用场景