Spring Cloud 入门手册(三)
application.yml
- zuul 路由配置可以省略,缺省以服务 id 作为访问路径
server: port: 3001 spring: application: name: zuul eureka: client: service-url: defaultZone: http://eureka1:2001/eureka, http://eureka2:2002/eureka instance: prefer-ip-address: true # 界面列表中显示的格式也显示ip instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port} zuul: routes: item-service: /item-service/** user-service: /user-service/** order-service: /order-service/**
主程序
添加 @EnableZuulProxy
注解
package cn.tedu.sp06; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; /** * @ClassName Sp06ZuulApplication * @Description * @Author keke * @Time 2021/7/18 20:39 * @Version 1.0 */ @SpringBootApplication @EnableZuulProxy public class Sp06ZuulApplication { public static void main(String[] args) { SpringApplication.run(Sp06ZuulApplication.class, args); } }
启动服务,访问测试
http://eureka1:2001
http://localhost:3001/item-service/35
http://localhost:3001/item-service/decreaseNumber
使用 postman,POST 发送以下格式数据
[ { "id": 1, "name": "abc", "number": 23 }, { "id": 2, "name": "def", "number": 11 } ]
http://localhost:3001/user-service/7
http://localhost:3001/user-service/7/score?score=100
http://localhost:3001/order-service/123abc
http://localhost:3001/order-service/
配置 zuul 开启重试,并配置 ribbon 重试参数
- 需要开启重试,默认不开启
zuul: routes: item-service: /item-service/** user-service: /user-service/** order-service: /order-service/** # 启用重试 retryable: true ribbon: ConnectTimeout: 1000 ReadTimeout: 1000 MaxAutoRetriesNextServer: 1 MaxAutoRetries: 1 # 针对一个指定的后台服务,来配置重试参数 item-service: ribbon: MaxAutoRetries: 0 # 暴露actuator的所有监控数据 management: endpoints: web: exposure: include: "*"
zuul 降级
创建降级类
- getRoute()方法中指定应用降级类的服务 id,星号或 null 值可以通配所有服务
ItemFallback
package cn.tedu.sp06.fallback; import cn.tedu.sp01.web.util.JsonResult; import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.client.ClientHttpResponse; import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; /** * @ClassName ItemFallback * @Description 通过zuul网关,调用后台商品服务失败时,会执行这段降级代码,向客户端返回降级结果 * @Author keke * @Time 2021/7/19 11:25 * @Version 1.0 */ @Component public class ItemFallback implements FallbackProvider { /** * 设置当前降级类,是针对哪个服务的降级类 * - item-service:只针对商品服务降级 * - *: 针对所有服务都应用当前降级类 * - null: 针对所有服务都应用当前降级类 * @return */ @Override public String getRoute() { return "item-service"; } /** * 发送给客户端的降级结果,封装在response对象中 * @param route * @param cause * @return */ @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.INTERNAL_SERVER_ERROR; } @Override public int getRawStatusCode() throws IOException { return HttpStatus.INTERNAL_SERVER_ERROR.value(); } @Override public String getStatusText() throws IOException { return HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(); } /** * 用来关闭流 * ByteArrayInputStream不需要关闭 */ @Override public void close() { } @Override public InputStream getBody() throws IOException { String json = JsonResult.err().code(500) .msg("调用商品服务失败").toString(); return new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); } @Override public HttpHeaders getHeaders() { // Content-Type:application/json;charset=UTF-8 HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add("Content-Type", "application/json;charset=UTF-8"); return httpHeaders; } }; } }
OrderFallback
package cn.tedu.sp06.fallback; import cn.tedu.sp01.web.util.JsonResult; import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.client.ClientHttpResponse; import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; /** * @ClassName OrderFallback * @Description 通过zuul网关,调用后台商品服务失败时,会执行这段降级代码,向客户端返回降级结果 * @Author keke * @Time 2021/7/19 11:25 * @Version 1.0 */ @Component public class OrderFallback implements FallbackProvider { /** * 设置当前降级类,是针对哪个服务的降级类 * - order-service:只针对订单服务降级 * - *: 针对所有服务都应用当前降级类 * - null: 针对所有服务都应用当前降级类 * @return */ @Override public String getRoute() { return "order-service"; } /** * 发送给客户端的降级结果,封装在response对象中 * @param route * @param cause * @return */ @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.INTERNAL_SERVER_ERROR; } @Override public int getRawStatusCode() throws IOException { return HttpStatus.INTERNAL_SERVER_ERROR.value(); } @Override public String getStatusText() throws IOException { return HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(); } /** * 用来关闭流 * ByteArrayInputStream不需要关闭 */ @Override public void close() { } @Override public InputStream getBody() throws IOException { String json = JsonResult.err().code(500) .msg("调用订单服务失败").toString(); return new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); } @Override public HttpHeaders getHeaders() { // Content-Type:application/json;charset=UTF-8 HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add("Content-Type", "application/json;charset=UTF-8"); return httpHeaders; } }; } }
LoginFilter
package cn.tedu.sp06.filter; import cn.tedu.sp01.web.util.JsonResult; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import org.apache.commons.lang.StringUtils; import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; /** * @ClassName LoginFilter * @Description * @Author keke * @Time 2021/7/18 21:19 * @Version 1.0 */ @Component public class LoginFilter extends ZuulFilter { /** * 过滤器类型: pre, routing, post, error * @return */ @Override public String filterType() { return FilterConstants.PRE_TYPE; } /** * 位置顺序号,zuul的前置过滤器默认有5个 * 其中第5个过滤器中,在上下文对象中放入了serviceId * 在后面过滤器中,才能使用serviceId * @return */ @Override public int filterOrder() { return 6; } /** * 判断针对当前请求,是否要执行过滤代码 * @return */ @Override public boolean shouldFilter() { /* 调用item-service,需要检查权限 调用其他服务不判断权限,可以直接访问 */ // 获取当前请求调用的服务id RequestContext currentContext = RequestContext.getCurrentContext(); String serviceId = (String) currentContext.get(FilterConstants.SERVICE_ID_KEY); // 判断id是不是item-service return "item-service".equalsIgnoreCase(serviceId); } /** * 过滤代码 * @return * @throws com.netflix.zuul.exception.ZuulException */ @Override public Object run() throws ZuulException { // 得到request对象 RequestContext currentContext = RequestContext.getCurrentContext(); HttpServletRequest request = currentContext.getRequest(); // 用request接收token参数 String token = request.getParameter("token"); // 如果收不到token,阻止向后台转发,直接向客户端返回响应 if (StringUtils.isBlank(token)) { // 阻止向后台服务转发 currentContext.setSendZuulResponse(false); // 向客户端直接发送响应 String json = JsonResult.err() .code(JsonResult.NOT_LOGIN) .msg("Not login!") .toString(); // http 协议头属性 Content-Type:application/json;charset=UTF-8 currentContext.addZuulRequestHeader("Content-Type", "application/json;charset=UTF-8"); currentContext.setResponseBody(json); } // 在当前zuul版本中,没有使用这个返回值 return null; } }
启动服务,访问测试
- http://localhost:3001/item-service/35
- http://localhost:3001/actuator/hystrix.stream
Hystrix dashboard 断路器仪表盘
Hystrix 对请求的降级和熔断,可以产生监控信息,hystrix dashboard 可以实时的进行监控
新建 maven 项目
配置依赖 pom.xml
配置 application.yml
主程序启用 eureka 服务器
启动,访问测试
新建 maven 项目
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>order-parent</artifactId> <groupId>cn.tedu</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>sp07-hystrix-dashboard</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId> </dependency> </dependencies> </project>
application.yml
server: port: 4001 spring: application: name: hystrix-dashboard # 允许抓取日志的服务器列表 hystrix: dashboard: proxy-stream-allow-list: localhost eureka: client: service-url: defaultZone: http://eureka1:2001/eureka, http://eureka2:2002/eureka instance: prefer-ip-address: true # 界面列表中显示的格式也显示ip instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}
主程序
- 添加
@EnableHystrixDashBoard
package cn.tedu.sp07; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard; /** * @ClassName Sp07HystrixDashboardApplication * @Description * @Author keke * @Time 2021/7/19 14:29 * @Version 1.0 */ @SpringBootApplication @EnableHystrixDashboard public class Sp07HystrixDashboardApplication { public static void main(String[] args) { SpringApplication.run(Sp07HystrixDashboardApplication.class, args); } }
启动服务,访问测试
填入 hystrix 的监控端点,开启监控
- http://localhost:3001/actuator/hystrix.stream
通过 hystrix 访问服务多次,观察监控信息
http://localhost:3001/item-service/35?token=2wasa2f3
http://localhost:3001/user-service/7
http://localhost:3001/user-service/7/score?score=100
http://localhost:3001/order-service/123abc
http://localhost:3001/order-service
hystrix 熔断
整个链路达到一定的阈值,默认情况下,10秒内产生超过20次请求,则符合第一个条件。
满足第一个条件的情况下,如果请求的错误百分比大于阈值,则打开断路器,默认为50%。
hystrix 的逻辑,先判断是否满足第一个条件,再判断第二个条件,如果两个条件都满足,则会开启断路器,断路器打开5秒后,会处于半开状态,会尝试转发请求,如果仍然失败,保持打开状态,如果成功,则关闭断路器
使用 Apache 的并发访问测试工具 ab
http://httpd.apache.org/docs/current/platform/windows.html#down
- 用 ab 工具,以并发50次,来发送20000个请求
ab -n 20000 -c 50 http://localhost:3001/item-service/35?token=2sswbbbbw
- 断路器为 open,所有请求都会短路,直接降级执行 fallback 方法
hystrix 配置
http://github.com/Netflix/Hystrix/wiki/Configuration
- hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
请求超时时间,超时后触发失败降级
hystrix.command.default.circuitBreaker.requestVolumeThreshold
10秒内请求数量,默认20,如果没有达到该数量,即使请求全部失败,也不会触发断路器打开
hystrix.command.default.circuitBreaker.errorThresoldPercentage
失败请求百分比,达到该比例则触发断路器打开
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds
断路器打开多长时间后,再次允许尝试访问(半开),仍失败则继续保持打开状态,如成功访问则关闭断路器,默认5000
hystrix + turbine 集群聚合监控
hystrix dashboard 一次只能监控一个服务实例,使用 turbine 可以汇集监控信息,将聚合后的信息提供给 hystrix dashboard 来集中展示和监控
新建 maven 项目
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>order-parent</artifactId> <groupId>cn.tedu</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>sp08-turbine</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-turbine</artifactId> </dependency> </dependencies> </project>
application.yml
spring: application: name: turbine server: port: 5001 eureka: instance: prefer-ip-address: true # 界面列表中显示的格式也显示ip instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port} client: service-url: defaultZone: http://eureka1:2001/eureka, http://eureka2:2002/eureka # 从zuul的两台服务器聚合hystrix日志 turbine: app-config: zuul cluster-name-expression: new String("default")
主程序
添加 @EnableTurbine 注解
package cn.tedu.sp08; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.turbine.EnableTurbine; /** * @ClassName Sp08TurbineApplication * @Description * @Author keke * @Time 2021/7/19 22:25 * @Version 1.0 */ @EnableTurbine @SpringBootApplication public class Sp08TurbineApplication { public static void main(String[] args) { SpringApplication.run(Sp08TurbineApplication.class, args); } }
启动服务,访问测试
8201服务器产生监控数据:
http://localhost:8201/abc123
http://localhost:8201/
turbine 监控路径
http://localhost:5001/turbine.stream
在 hystrix dashboard 中填入turbine 监控路径,开启监控
http://localhost:4001/hystrix
turbine聚合了order-service两台服务器的hystrix监控信息
config 配置中心
yml 配置文件保存到 git 服务器,例如 github.com或者gitee.com
微服务启动时,从服务器获取配置文件
git 上存放配置文件
新建文件夹,命名为 config
将 sp02,sp03,sp04 三个项目的yml配置文件,复制到 config 文件夹,并改名
item-service-dev.yml
user-service-dev.yml
order-service-dev.yml
最后注释掉三个项目中的 application.yml 文件
禁止配置中心的配置信息覆盖客户端配置
默认配置中心配置优先级高,配置中心配置会覆盖客户端的所有配置,包括命令行的参数配置,这样我们在 item-service 中配置的端口号启动参数会无效
item-service 启动参数
--server.port=8081
--server.port=8082
我们可以设置禁止配置中心的配置将客户端配置覆盖掉
在三个配置文件中添加下面的配置
spring: # 让配置中心的配置,不覆盖项目的本地配置和命令参数 cloud: config: override-none: true
将项目上传到 git
新建 maven 项目
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>order-parent</artifactId> <groupId>cn.tedu</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>sp09-config</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency> </project>
application.yml
server: port: 6001 spring: application: name: config-server # 连接git仓库,在指定目录下找到配置文件 cloud: config: server: git: uri: https://gitee.com/Jasonakeke/CGBVProjects search-paths: codes/springcloud1/config eureka: instance: prefer-ip-address: true # 界面列表中显示的格式也显示ip instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port} client: service-url: defaultZone: http://eureka1:2001/eureka, http://eureka2:2002/eureka management: endpoints: web: exposure: include: bus-refresh
主程序
添加 @EnableConfigServer 注解
package cn.tedu.sp09; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.config.server.EnableConfigServer; /** * @ClassName Sp09ConfigApplication * @Description * @Author keke * @Time 2021/7/20 13:41 * @Version 1.0 */ @EnableConfigServer @SpringBootApplication public class Sp09ConfigApplication { public static void main(String[] args) { SpringApplication.run(Sp09ConfigApplication.class, args); } }
sp02,sp03,sp04 的 pom.xml 添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency>
启动服务,访问测试
先启动 sp05-eureka,再启动 sp09-config,最后启动 sp02-itemservice, sp03-userservice, sp04-orderservice