断路由
在微服务架构中,我们将系统拆分成了一个个的服务单元,各单元间通过服务注册与订阅的方式互相依赖。由于每个单元都在不同的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身问题出现调用故障或延迟,而这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会出现因等待出现故障的依赖方响应而形成任务积压,最终导致自身服务的瘫痪。
举个例子,在一个电商网站中,我们可能会将系统拆分成,用户、订单、库存、积分、评论等一系列的服务单元。用户创建一个订单的时候,在调用订单服务创建订单的时候,会向库存服务来请求出货(判断是否有足够库存来出货)。此时若库存服务因网络原因无法被访问到,导致创建订单服务的线程进入等待库存申请服务的响应,在漫长的等待之后用户会因为请求库存失败而得到创建订单失败的结果。如果在高并发情况之下,因这些等待线程在等待库存服务的响应而未能释放,使得后续到来的创建订单请求被阻塞,最终导致订单服务也不可用。
在微服务架构中,存在着那么多的服务单元,若一个单元出现故障,就会因依赖关系形成故障蔓延,最终导致整个系统的瘫痪,这样的架构相较传统架构就更加的不稳定。为了解决这样的问题,因此产生了断路器模式。
断路器本身是一种开关装置,用于在电路上保护线路过载,当线路中有电器发生短路时,“断路器”能够及时的切断故障电路,防止发生过载、发热、甚至起火等严重后果。
在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。
在Spring Cloud中使用了Hystrix 来实现断路器的功能。接着我们使用前面创建的项目分别在服务消费断Ribbn与Feign中使用断路由模式:
Ribbon中使用断路由
Ribbon使用Hystrix需要导入Hystrix 相关的依赖
- 1
- 2
- 3
- 4
- 5
在主类中使用@EnableCircuitBreaker注解开启断路器功能:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
在原先我们封装的Service中添加注解:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
这样,当Ribbon所调用的服务出现异常情况,Ribbon将访问错误的回调而不是服务异常消息。
Feign中使用断路由
Feigh工程不需要引入Hystix,因为Feign中已经依赖了Hystrix,我们可以直接使用:
- 1
- 2
- 3
- 4
- 5
在FeignClient注解中指定回调,这个回调是需要实现该接口的实现类.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
这样当服务不可用时,则会返回null。
服务网关
服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。
如果你熟悉Nginx的反向代理配置,那么Zuul对你来说非常简单。
Zuul也是一个独立的服务,首先需要引入其依赖文件:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
每个服务都有一个独一无二的serviceId,在zuul中引入eureka的目的就是为了直接使用serviceId来代替url.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
这里用了@SpringCloudApplication注解,它整合了@SpringBootApplication、@EnableDiscoveryClient、@EnableCircuitBreaker
同样的在配置文件application.properties中指定Zuul应用的基础信息应用名、端口信息等。
- 1
- 2
上述完成了Zuul的基础配置,但为了使Zuul服务于系统集群充当一个均衡负载器,还需要额外的配置,即配置服务路由:
- 1
- 2
- 3
该配置,定义了,所有到Zuul的中规则为:/api-a-url/**的访问都映射到http://localhost:2222/上,也就是说当我们访问http://localhost:5555/api-a-url/add?a=1&b=2的时候,Zuul会将该请求路由到:http://localhost:2222/add?a=1&b=2上。
刚刚笔者提到,引入了eureka的目的就是为了避免使用URL而使用serviceId。因为服务名与服务实例地址的关系在eureka server中已经存在了,所以只需要将Zuul注册到eureka server上去发现其他服务,我们就可以实现对serviceId的映射。
- 1
- 2
- 3
- 4
- 5
尝试通过服务网关来访问service-A和service-B,根据配置的映射关系,分别访问下面的url
- 1
- 2
- 3
推荐使用serviceId的映射方式,除了对Zuul维护上更加友好之外,serviceId映射方式还支持了断路器,对于服务故障的情况下,可以有效的防止故障蔓延到服务网关上而影响整个系统的对外服务。
服务过滤
同样的,如果熟悉Spring MVC或Struts拦截器过滤器相关的知识,那么Zuul的服务过滤也很容易掌握。在服务网关中定义过滤器只需要继承ZuulFilter抽象类实现其定义的四个抽象函数就可对请求进行拦截与过滤。
比如下面的例子,定义了一个Zuul过滤器,实现了在请求被路由之前检查请求中是否有accessToken参数,若有就进行路由,若没有就拒绝访问,返回401 Unauthorized错误。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
对于上述四个方法做详细解释:
- filterType:返回一个字符串代表过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型,具体如下:pre:可以在请求被路由之前调用,routing:在路由请求时候被调用,post:在routing和error过滤器之后被调用,error:处理请求时发生错误时被调用
- filterOrder:通过int值来定义过滤器的执行顺序
- shouldFilter:返回一个boolean类型来判断该过滤器是否要执行,所以通过此函数可实现过滤器的开关。在上例中,我们直接返回true,所以该过滤器总是生效。
- run:过滤器的具体逻辑。需要注意,这里我们通过ctx.setSendZuulResponse(false)令zuul过滤该请求,不对其进行路由,然后通过ctx.setResponseStatusCode(401)设置了其返回的错误码,当然我们也可以进一步优化我们的返回,比如,通过ctx.setResponseBody(body)对返回body内容进行编辑等。
在实现了自定义过滤器之后,还需要实例化该过滤器才能生效,我们只需要在应用主类中增加如下内容:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
最后,总结一下为什么服务网关是微服务架构的重要部分,是我们必须要去做的原因:
- 不仅仅实现了路由功能来屏蔽诸多服务细节,更实现了服务级别、均衡负载的路由。
- 实现了接口权限校验与微服务业务逻辑的解耦。通过服务网关中的过滤器,在各生命周期中去校验请求的内容,将原本在对外服务层做的校验前移,保证了微服务的无状态性,同时降低了微服务的测试难度,让服务本身更集中关注业务逻辑的处理。
- 实现了断路器,不会因为具体微服务的故障而导致服务网关的阻塞,依然可以对外服务。