springCloud — 中级篇(1)
本系列笔记涉及到的代码在GitHub上,地址:https://github.com/zsllsz/cloud
本文涉及知识点:
-
服务降级熔断之hystrix;
-
服务网关之gateway;
欢迎大家关注我的公众号 javawebkf,目前正在慢慢地将简书文章搬到公众号,以后简书和公众号文章将同步更新,且简书上的付费文章在公众号上将免费。
一、服务降级&熔断&限流之Hystrix(豪猪)
1、微服务面临的问题:
将一个个的业务拆分出来,独立成一个个服务,降低了系统的耦合度,但是也面临了一些问题。比如实现业务场景一需要调用服务A,A又要调用B,B还要调用C……,这叫做“扇出”,就是各个微服务相互调用,像一把折扇一样。如果B挂了,那就导致整条链路不能用了,就出现了服务雪崩的情况。为了解决这种问题,就需要一种名为“服务降级和熔断”的办法。
2、Hystrix是什么?
Hystrix就是服务降级和熔断的一种落地实现。可以保证当某个服务超时、异常的情况下,不会导致整体服务雪崩,避免级联故障,提高分布式系统的弹性。它会在当某个服务超时、异常的时候,返回一个符合预期格式、可处理的备选响应给调用者,从而保证了服务的可用。
3、Hystrix能干嘛:
- 服务降级:fallback,比如我们写代码的时候,有下面这种情况:
if () {
} else if () {
} else {
}
这里最后的else就是服务降级,就是给一个兜底的解决方案。 会发生服务降级的情况:程序异常、响应超时、服务熔断触发降级、线程池/信号量打满也会导致服务降级。
-
服务熔断:break,就像保险丝熔断一样,当服务达到了所能承受的最大访问量后,就拉闸断电,调用服务降级给用户返回友好的提示。
-
服务限流:flowlimit,在秒杀等高并发的场景下,严禁一窝蜂地访问系统,而是排队访问,一秒钟N个,有序进行。
4、cloud-provider-hystrix-payment8001项目搭建:
新建一个名为cloud-provider-hystrix-payment8001的module。
- pom.xml:
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
- application.yml:
server:
port: 8001
spring:
application:
name: cloud-provider-hystrix-payment
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka #eureka server
- 启动类:
@SpringBootApplication
@EnableEurekaClient
public class PaymentHystrixMain8001 {
public static void main(String[] args) throws Exception {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
}
- service:
@Service
public class PaymentService {
public String paymentOk(Integer id) {
return " thats ok";
}
public String paymentError(Integer id) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "somethind wrong";
}
}
这里一个方法模拟正常情况,一个方法模拟超时的情况。然后在controller中调这两个方法。
- controller:
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Autowired
private PaymentService paymentService;
@Value("${server.port}")
private String port;
@GetMapping("/ok/{id}")
public String paymentOk(@PathVariable("id") Integer id) {
return paymentService.paymentOk(id) + "/r/n" + port;
}
@GetMapping("/error/{id}")
public String paymentError(@PathVariable("id") Integer id) {
return paymentService.paymentError(id) + "/r/n" + port;
}
}
启动7001的eureka server和这个8001,访问从controller中的两个接口,测试一下。在非高并发的情况下,两个接口都可以正常返回数据,只不过paymentOk可以立即响应,而paymentError就要等待5秒才会响应。但是,如果搞个jmeter对paymentError接口进行压测,20000个线程并发去请求paymentError,然后你再用浏览器访问paymentOk,发现paymentOk也被拖慢了。如果线程再多一点儿,可能就会访问不了了。可明明是paymentError接口响应慢,paymentOk是不应该受影响的。所以可以用hystrix对服务进行降级。下面先新建一个consumer调用这个payment服务,然后再在这个consumer中对服务进行降级。
5、cloud-consumer-feign-hystrix-order80项目搭建:
新建名为cloud-consumer-feign-hystrix-order80的module。
- pom.xml:
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
org.springframework.cloud
spring-cloud-starter-openfeign
- yml:
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka
- 主启动类:
@SpringBootApplication
@EnableFeignClients
public class OrderHystrixMain80 {
public static void main(String[] args) throws Exception {
SpringApplication.run(OrderHystrixMain80.class, args);
}
}
- service:
@Service
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
public interface PaymentHystrixService {
@GetMapping("payment/ok/{id}")
public String paymentOk(@PathVariable("id") Integer id);
@GetMapping("payment/error/{id}")
public String paymentError(@PathVariable("id") Integer id);
}
- controller:
@RestController
@RequestMapping("/order")
public class OrderHystrixController {
@Autowired
private PaymentHystrixService phService;
@GetMapping("/hystrix/ok/{id}")
public String paymentOk(@PathVariable("id") Integer id) {
return phService.paymentOk(id);
}
@GetMapping("/hystrix/error/{id}")
public String paymentError(@PathVariable("id") Integer id) {
return phService.paymentError(id);
}
}
启动后,通过浏览器访问http://localhost/order/hystrix/ok/1,发现是立即响应的,访问http://localhost/order/hystrix/error/1发现报500了,控制台报错 Read timed out。这是因为openfeign默认超时时间是1秒,而error接口又设置了线程睡5秒。可以在order80的配置文件中配置openfeign超时时间,设置大于5秒,就可以正常访问。
ribbon:
# 建立连接后从服务器获取可用资源的时间
ReadTimeout: 6000
# 建立连接所有的时间
ConnectTimeout: 6000
这是没有并发的情况,再使用jmeter进行压测,那么这两个接口都得凉凉了。
6、如何解决上面的问题?
服务提供方payment8001:
- 如果8001超时,不能让order80一直转圈圈等待,要进行payment8001服务降级
- 如果8001挂掉了,也不能让order80一直等待,要进行payment8001服务降级
服务调用方order80:
- 如果payment8001没问题,order80自己出故障了,那么order80要进行服务降级
7、payment8001服务降级配置:
payment8001的paymentError方法,线程睡了5秒,所以至少5秒才会有响应。假设我们认为这个方法正常是3秒就能响应完的,超过3秒就要进行服务降级。那么我们就设置超时时间峰值为3秒,超过了3秒就要有兜底的方法。
- 首先在启动类上加上@EnableCircuitBreaker注解激活hystrix功能;
- 然后在paymentError方法上加上@HystrixCommand注解,并且配置超时调用的方法,如下:
@HystrixCommand(fallbackMethod = "paymentError_default", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
public String paymentError(Integer id) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "somethind wrong";
}
public String paymentError_default(Integer id) {
return "这是兜底的方法";
}
这段代码的意思就是,paymentError方法至少要5秒才会响应,但是我加上了hystrix的注解,设置了最大响应时长为3秒,超过3秒,那就走兜底的方法。所以现在启动payment8001访问paymentError方法,应该会打出"这是兜底的方法"这句话。
说明hystrix配置成功了,并且除了超时,如果paymentError报异常了,比如写一个int x = 10 / 0
,也会走兜底的方法。
8、消费端order80的服务降级:
消费端服务降级也一样的配置,在主启动类上加@EnableCircuitBreaker注解,然后controller写成下面这样:
@GetMapping("/hystrix/error/{id}")
@HystrixCommand(fallbackMethod = "paymentError_default", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
public String paymentError(@PathVariable("id") Integer id) {
return phService.paymentError(id);
}
public String paymentError_default(@PathVariable("id")Integer id) {
return "这是消费端兜底的方法";
}
配置超过3秒就走兜底的方法。而服务端payment8001对应的方法设置了线程睡5秒,超过了3秒才会有响应,所以这里会走兜底的方法。注意ribbon默认超时时间是1秒,所以如果修改ribbon超时时间的话,即使payment8001中设置线程睡2秒,order80中hystrix配置超时时间3秒,也会走兜底的方法,因为是否超时优先取的是ribbon配置的。
9、全局降级配置:
上面针对需要服务降级的两个方法加上了hystrix相关配置,如果还有大量的方法也需要服务降级,在每个方法上都加上这么一段注解,不太方便,所以就出现了全局降级配置。比如要在order80上做全局服务降级配置,只需要将controller改成下面这样即可:
@RestController
@RequestMapping("/order")
@DefaultProperties(defaultFallback = "defaultMethod", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
public class OrderHystrixController {
@Autowired
private PaymentHystrixService phService;
@GetMapping("/hystrix/ok/{id}")
@HystrixCommand
public String paymentOk(@PathVariable("id") Integer id) {
return phService.paymentOk(id);
}
@GetMapping("/hystrix/error/{id}")
@HystrixCommand(fallbackMethod = "paymentError_default", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
public String paymentError(@PathVariable("id") Integer id) {
return phService.paymentError(id);
}
public String paymentError_default(@PathVariable("id")Integer id) {
return "这是消费端兜底的方法";
}
public String defaultMethod() {
return "我是全局默认的服务降级配置";
}
}
首先在类上加注解,并配置超时时间,然后需要用全局降级的方法上加@HystrixCommand
注解。这里用paymentOk方法来演示,payment8001端的paymentOk方法也设置线程睡4秒钟,paymentOk方法应该会打印出“我是全局默认的服务降级配置”这句话。
然后将payment8001的paymentOk方法改成睡2秒钟,那么就会正常返回:
然后paymentError方法,因为在方法上配置了hystrix相关配置,所以还是走方法对应的配置,而不是走全局。
但是以上做法还是不够完美,因为order80调用payment8001,所有接口都是通过openfeign去调用的,擒贼先擒王,所以最好的做法就是在openfeign调用payment8001的接口中添加实现去做降级。具体做法如下:
- 修改yml:
feign:
hystrix:
enabled: true
- order80的service改成这样:
@Service
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentHystrixServiceImpl.class)
public interface PaymentHystrixService {
@GetMapping("payment/ok/{id}")
public String paymentOk(@PathVariable("id") Integer id);
@GetMapping("payment/error/{id}")
public String paymentError(@PathVariable("id") Integer id);
}
- 新建service的实现:
@Component
public class PaymentHystrixServiceImpl implements PaymentHystrixService{
@Override
public String paymentOk(Integer id) {
return "paymentOk-default-return";
}
@Override
public String paymentError(Integer id) {
return "paymentError-default-return";
}
}
- 最后把controller中hystrix相关注解都去掉。
这样就搞定了,当访问8001发生异常、或者8001压根儿就没启动时,就会执行serviceImpl里面的方法。
10、服务熔断:
当微服务调用链路的某个服务不可用了或者响应时间太长了,会对服务进行降级,快速返回友好的响应信息,当检测到该节点正常了之后,又会恢复调用。即一开始是close状态,检测到异常就变成open状态,然后再变成half open状态,恢复一部分调用,如果没问题再变成close。下面就演示服务熔断。
- 首先在payment8001的service中加如下代码:
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback", commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"), // 是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), // 请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"), // 失败率达到多少后跳闸
// 10次请求有6次是失败的,就进行服务熔断,10秒后会变为half open状态,看看能否请求成功,如果成功就变成close状态
})
public String paymentCircuitBreaker(Integer id) {
if (id < 0) {
throw new RuntimeException("id不能为负数");
}
return "调用成功";
}
public String paymentCircuitBreaker_fallback(Integer id) {
return "id不能为负数";
}
这几个注解的意思就是在10次访问中,如果失败了6次,那就熔断,返回“id不能为负数这句话”,这个时候,即使你传的id是正数,也会返回这句话,因为这个时候服务熔断了。10000 milliseconds后,会变成半开状态,试着放行一个请求,如果此时你请求的id是正数,那么就返回“调用成功”,并且解除熔断。
- 在controller中新增如下方法:
@GetMapping("/breaker/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
return paymentService.paymentCircuitBreaker(id);
}
可以访问该接口,先传入负数id,然后满足10次有6次失败的情况下,再将id变为正数传入,发现也是返回“id不能为负数”这句话。过一段时间后才恢复正常,说明服务熔断起作用了。
11、hystrix的图形化界面:
- 新建一个cloud-consumer-hystrix-dashboard9001的module
- pom.xml:
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-netflix-hystrix-dashboard
- 主启动类:
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashBoardMain9001 {
public static void main(String[] args) throws Exception {
SpringApplication.run(HystrixDashBoardMain9001.class, args);
}
}
- 如果要监控8001,那么需要在8001中配置如下bean:
@Configuration
public class DashBoardConfig {
@Bean
public ServletRegistrationBean getBean() {
HystrixMetricsStreamServlet bean = new HystrixMetricsStreamServlet();
ServletRegistrationBean register = new ServletRegistrationBean(bean);
register.setLoadOnStartup(1);
register.addUrlMappings("/hystrix.stream");
register.setName("HystrixMetricsStreamServlet");
return register;
}
}
-
然后访问 localhost:9001/hystrix,在页面中填写如下信息:
填好后回车,然后再去访问8001,那么8001的访问信息就会出现在dashboard中了。
二、服务网关之spring gateway
1、服务网关是来干嘛的?
一般来说,用户访问首先是到nginx,nginx会做负载均衡,然后并不是直接访问各位微服务应用,而是先到gateway。gateway可以做路由转发、权限校验、流量控制等。功能类似的框架还有zuul和zuul2,zuul停更了,zuul2还没完善出来,所以spring自己搞了一套gateway。
2、gateway核心概念:
- 路由Route:简单的理解为url,如果断言为true就匹配该url
- 断言Predicate:路由转发的判断条件
- 过滤Filter:过滤器
3、gateway工作流程:
核心逻辑是路由转发 + 执行过滤链。客户端向gateway发请求,然后在gateway handler mapping 找到相匹配的路由,将请求发送到gateway web handler,handler再通过过滤链将请求发送到微服务中,然后返回。
4、代码演示:
- 新建名为cloud-gateway-gateway9527的module;
- pom.xml:
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-gateway
- yml:
server:
port: 9527
spring:
application:
name: cloud-gateway
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka
instance:
instance-id: cloud-gateway
prefer-ip-address: true
- 主启动:
@SpringBootApplication
@EnableEurekaClient
public class GatewayMain9527 {
public static void main(String[] args) throws Exception {
SpringApplication.run(GatewayMain9527.class, args);
}
}
8001的controller中有两个接口,现在我们可以直接通过接口路由访问到,但是我不想暴露8001端口,想通过9527端口去访问,做法如下:
- 将9527的yml改成下面这样:
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
- id: payment_route # id,要求唯一
uri: http://localhost:8001 #提供服务的路由地址
predicates:
- Path=/payment/ok/** # 路径匹配的进行路由
- id: payment_route # id,要求唯一
uri: http://localhost:8001 #提供服务的路由地址
predicates:
- Path=/payment/error/** # 路径匹配的进行路由
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka
instance:
instance-id: cloud-gateway
prefer-ip-address: true
现在依次启动7001的eureka、8001的payment和9527的gateway,然后访问呢localhost:9527/payment/ok/1,发现可以访问成功,说明路由转发成功。
这是配置网关的第一种方式,还有硬编码的方式,如下:
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
RouteLocatorBuilder.Builder routes = builder.routes();
// 意思就是访问localhost:9527/domestic将会转发到https://tuijian.hao123.com/domestic中去
routes.route("path_id", r -> r.path("/domestic").uri("https://tuijian.hao123.com/domestic")).build();
return routes.build();
}
}
现在访问localhost:9527/domestic,结果如下:
5、动态路由:
提供payment服务的有8001和8002,但是上面那样配置只写了8001,现在要实现通过微服务名称来做动态路由。即在配置那里不写死8001或8002,而是写8001和8002的微服务名称,这样就可以实现动态路由。
- 首先确保启动了8001和8002;
- 改yml:
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能
routes:
- id: payment_route1 # id,要求唯一
#uri: http://localhost:8001 #提供服务的路由地址
uri: lb://CLOUD-PAYMENT-SERVICE #微服务名称
predicates:
- Path=/payment/** # 路径匹配的进行路由
- id: payment_route2 # id,要求唯一
#uri: http://localhost:8001 #提供服务的路由地址
uri: lb://CLOUD-PAYMENT-SERVICE #微服务名称
predicates:
- Path=/payment/** # 路径匹配的进行路由
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka
instance:
instance-id: cloud-gateway
prefer-ip-address: true
就这样就行了,然后访问http://localhost:9527/payment/1,就可以成功访问到8001和8002,并且会进行负载均衡,一次8001,一次8002,轮着来。
6、常用断言:
predicates:
- Path=/payment/** # 路径匹配的进行路由
- After=2020-05-04T20:47:11.281+08:00[Asia/Shanghai] # 配置的规则要在2020年5月4日20点20分后才生效,有after就有before和between
- Cookie=username,test # 必须带cookie访问,cookie名叫username,值为test,值也可以写正则表达式
- Header=X-Request-Id,/d+ # 必须带请求头访问,请求头名为X-Request-Id,值要满足 /d+ 这个正则表达式
- Host=**.somehost.com,**.otherhost.com #请求域名必须匹配这两个,即请求时添加header,名为host,值匹配这两个即可
- Method=GET #必须是get请求
- Query=id,/d+ #必须有名为id的参数且值匹配 /d+ 这个正则
7、常用filter:
filter生命周期有两种,一种是在业务逻辑之前,一种是在业务逻辑之后;种类也是两种,一种gatewayFilter,一种globalFilter。用法和predicates一样,比如:
filters:
- AddRequestParameter=X-Request-Id,1024 # 请求必须带名为X-Request-Id的请求头,值为1024
8、自定义过滤器:
可以实现全局日志记录,统一网关鉴权等功能。
- 新建自定义过滤器类:
@Component
@Slf4j
public class MyFilter implements GlobalFilter, Ordered{
/**
* 表示这个过滤器的优先级,数字越小优先级越高
*/
@Override
public int getOrder() {
return 0;
}
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("=================进去自定义全局过滤器=============");
// 获取id参数
String id = exchange.getRequest().getQueryParams().getFirst("id");
if (!"1".equals(id)) {
log.error("================= id不存在==================");
// 设置返回码
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
}
也就是说,如果没有id这个参数或者id参数值不为1的,都会被拦截。实际生产中可以用这个来验证请求是否携带token。
共有 0 条评论