Spring Cloud Zuul
网关是介于客户端(外部调用方比如app,h5)和微服务的中间层。
Zuul是Netflix开源的微服务网关,核心是一系列过滤器。这些过滤器可以完成以下功能。
- 是所有微服务入口,进行分发。
- 身份认证与安全。识别合法的请求,拦截不合法的请求。
- 监控。在入口处监控,更全面。
- 动态路由。动态将请求分发到不同的后端集群。
- 压力测试。可以逐渐增加对后端服务的流量,进行测试。
- 负载均衡。也是用ribbon。
- 限流(望京超市)。比如我每秒只要1000次,1001次就不让访问了。
- 服务熔断
zuul默认集成了:ribbon和hystrix。
基本使用
1 2 3 4 5 6 7 8
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency>
|
1 2 3 4 5 6 7
| @EnableZuulProxy @SpringBootApplication public class ZuulApplication { public static void main(String[] args) { SpringApplication.run(ApplicationTest.class, args); } }
|
1 2 3
| eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ spring.application.name=zuulserver server.port=80
|
通过http://localhost:80/consumer/alive
就能够达到负载均衡Consumer
微服务的作用,其中consumer
是注册eureka的服务名,alive
是Controller
的接口。
相关配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
consumer.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
management.endpoints.web.exposure.include=* management.endpoint.health.show-details=always management.endpoint.health.enabled=true management.endpoint.routes.enabled=true
zuul.routes.consumer=/xxoo/**
zuul.routes.xx.path=/xx/** zuul.routes.xx.url=http://mashibing.com
zuul.ignored-services=consumer
zuul.prefix=/api/v1
zuul.strip-prefix=false
|
相关配置
1 2 3 4 5 6 7 8 9 10 11 12 13
| management: endpoints: web: exposure: include: "*" endpoint: health: show-details: ALWAYS enabled: true routes: enabled: true
|
1 2 3 4
| api-driver: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
|
路由转发
1 2 3 4
| zuul: routes: api-driver: /zuul-api-driver/**
|
1 2 3 4 5 6
| zuul: routes: custom-zuul-name: path: /zuul-custom-name/** url: http://localhost:9002/
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| zuul: routes: custom-zuul-name: path: /zuul-custom-name/** service-id: no-eureka-api-driver no-eureka-api-driver: ribbon: listOfServers: localhost:9003,localhost:9002 ribbon: eureka: enabled: false
|
1 2 3 4 5 6 7
| zuul: routes: custom-zuul-name: path: /zuul-custom-name/** service-id: api-driver
|
1 2 3 4 5 6
| #4.自己转发自己 zuul: routes: xxx: path: /a-forward/** url: forward:/myController
|
1 2 3 4 5 6
| zuul: routes: api-driver: /zuul-api-driver/** ignored-services: - api-driver
|
1 2 3 4 5
| zuul: prefix: /api strip-prefix: true
|
三个问题
1 2 3 4
| Zuul的三个问题: 1.Token不向后传 (配置sentinel) 2.老项目改造中路由问题 3.动态路由
|
1 2 3 4 5
| zuul: sensitive-headers: token
|
过滤器
Zuul的大部分功能都是有过滤器实现的。4种过滤器
1 2 3 4 5 6 7 8 9 10 11
| PRE: 在请求被路由之前调用,可利用这种过滤器实现身份验证。选择微服务,记录日志、鉴权、限流
ROUTING:在将请求路由到微服务调用,用于构建发送给微服务的请求,并用http client(或者ribbon)请求微服务。 有三种类型的Filter: 1.RibbonRoutingFilter 路由到服务 2.SimpleHostRoutingFilter 路由到URL 3.SendForwardFilter 转发(转向Zuul自己) POST:在调用微服务执行后。可用于添加header,记录日志,将响应发给客户端。
ERROR:在其他阶段发生错误是,走此过滤器。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| try { this.preRoute(); } catch (ZuulException var13) { this.error(var13); this.postRoute(); return; }
try { this.route(); } catch (ZuulException var12) { this.error(var12); this.postRoute(); return; }
try { this.postRoute(); } catch (ZuulException var11) { this.error(var11); }
|
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 32 33 34 35 36 37 38 39 40
| @Component public class MyFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.ROUTE_TYPE; }
@Override public int filterOrder() { return 0; }
@Override public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); if (!ctx.sendZuulResponse()) { return false; } return true; }
@Override public Object run() throws ZuulException { RequestContext currentContext = RequestContext.getCurrentContext(); HttpServletRequest request = currentContext.getRequest();
currentContext.setSendZuulResponse(false); return null; } }
|
路由映射
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 32 33 34 35
| @Component public class MyFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.ROUTE_TYPE; }
@Override public int filterOrder() { return 0; }
@Override public boolean shouldFilter() { return true; }
@Override public Object run() throws ZuulException { RequestContext currentContext = RequestContext.getCurrentContext(); HttpServletRequest request = currentContext.getRequest(); String uri = request.getRequestURI(); if(uri.contains("/aaa")){ currentContext.set(FilterConstants.SERVICE_ID_KEY,"serviceA"); currentContext.set(FilterConstants.REQUEST_URI_KEY,"/test/bbb"); } return null; } }
|
动态路由
1 2 3
| ZUUL404找不到地址,需要排查请求地址的来源: 1.eureka服务 2.zuul的配置文件
|
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 32
| @Component public class MyFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.ROUTE_TYPE; }
@Override public int filterOrder() { return 0; }
@Override public boolean shouldFilter() { return true; }
@Override public Object run() throws ZuulException { RequestContext currentContext = RequestContext.getCurrentContext(); HttpServletRequest request = currentContext.getRequest(); String uri = request.getRequestURI(); if(uri.contains("/aaa")){ currentContext.set(new URI("http://localhost:8003/test/bbb").toURL()); } return null; } }
|
灰度发布
灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。
网关级别
1 2 3 4 5 6
| <dependency> <groupId>io.jmnarloch</groupId> <artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId> <version>2.1.0</version> </dependency>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Component public class GrayFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.ROUTE_TYPE; }
@Override public int filterOrder() { return 0; }
@Override public boolean shouldFilter() { return true; }
@Override public Object run() throws ZuulException { RibbonFilterContextHolder.getCurrentContext().add("version", "v1"); return null; } }
|
1 2 3 4 5
| eureka: instance: metadataMap: version: v1
|
1 2 3 4 5
| 注意事项: - 通过以上配置,启动各服务,可以实现灰度测试和版本测试 - 在网关依据灰度和版本请求标识,Ribbon利用各服务的元信息进行匹配,以实现过滤和负载 - 服务中必须配置相应的请求标识,否则该请求无法负载,将会报错 - 关闭组件,ribbon.filter.metadata.enabled=false #默认true
|
微服务级别
1 2 3 4 5 6
| public class GrayRibbonConfiguration { @Bean public IRule ribbonRule(){ return new GrayRule(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Aspect @Component public class RequestAspect {
@Pointcut("execution(* com.kuro.apipassenger.controller..*Controller*.*(..))") private void anyMehtod(){ }
@Before(value = "anyMehtod()") public void before(JoinPoint joinPoint){
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest(); String version = request.getHeader("version");
if(version.trim().equals("v1")) { RibbonFilterContextHolder.getCurrentContext().add("version","v1"); } } }
|
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 32 33 34 35 36 37 38 39 40
| public class GrayRule extends AbstractLoadBalancerRule {
@Override public void initWithNiwsConfig(IClientConfig iClientConfig) { }
@Override public Server choose(Object key) { return choose(getLoadBalancer(),key); }
public Server choose(ILoadBalancer lb, Object key){ String version = RibbonFilterContextHolder.getCurrentContext().get("version");
List<Server> reachableServers = lb.getReachableServers();
for (int i = 0; i < reachableServers.size(); i++) { Server server = reachableServers.get(i); Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata(); String version1 = metadata.get("version");
if (version.trim().equals(version1)){ return server; } } return null; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class ApiPassengerApplication {
public static void main(String[] args) { SpringApplication.run(ApiPassengerApplication.class, args); }
@LoadBalanced @Bean public RestTemplate restTemplate(){ return new RestTemplate(); }
@GetMapping("/test") public String test(){ System.out.println("api-passenger-test"); return "api-passenger"; } }
|
1 2 3 4 5
| eureka: instance: metadataMap: version: v1
|
容错
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| @Component public class MyFallback implements FallbackProvider{
@Override public String getRoute() { return "*"; }
@Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
if (cause instanceof HystrixTimeoutException) { return response(HttpStatus.GATEWAY_TIMEOUT); } else { return response(HttpStatus.INTERNAL_SERVER_ERROR); }
}
private ClientHttpResponse response(final HttpStatus status) { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.BAD_REQUEST; }
@Override public int getRawStatusCode() throws IOException { return HttpStatus.BAD_REQUEST.value(); }
@Override public String getStatusText() throws IOException { return HttpStatus.BAD_REQUEST.getReasonPhrase(); }
@Override public void close() { }
@Override public InputStream getBody() throws IOException { String msg = "{\"msg\":\"服务故障\"}"; return new ByteArrayInputStream(msg.getBytes()); }
@Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; } }
|
网关限流
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 32 33 34 35 36 37 38
| import com.google.common.util.concurrent.RateLimiter; @Component public class LimitFilter extends ZuulFilter {
@Override public String filterType() { return FilterConstants.PRE_TYPE; }
@Override public int filterOrder() { return -10; }
@Override public boolean shouldFilter() { return true; } private static final RateLimiter RATE_LIMITER = RateLimiter.create(50);
@Override public Object run() throws ZuulException { RequestContext currentContext = RequestContext.getCurrentContext();
if (RATE_LIMITER.tryAcquire()){ System.out.println("通过"); return null; }else { System.out.println("被限流了"); currentContext.setSendZuulResponse(false); currentContext.setResponseStatusCode(HttpStatus.TOO_MANY_REQUESTS.value()); } return null; } }
|
服务限流
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 32 33 34 35
| import com.google.common.util.concurrent.RateLimiter;
@Component public class LimitFilter implements Filter {
private static final RateLimiter RATE_LIMITER = RateLimiter.create(1);
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (RATE_LIMITER.tryAcquire()){ filterChain.doFilter(servletRequest,servletResponse); }else {
servletResponse.setCharacterEncoding("utf-8"); servletResponse.setContentType("text/html; charset=utf-8");
PrintWriter pw = null;
pw = servletResponse.getWriter(); pw.write("限流了"); pw.close(); } }
@Override public void init(FilterConfig filterConfig) throws ServletException { }
@Override public void destroy() { } }
|
Sentinel网关限流
1 2 3 4 5
| <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-annotation-aspectj</artifactId> <version>1.4.1</version> </dependency>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @SpringBootApplication @EnableZuulProxy public class CloudZuulApplication { public static void main(String[] args) { init(); SpringApplication.run(CloudZuulApplication.class, args); }
private static void init() { List<FlowRule> rules = new ArrayList<>(); FlowRule rule = new FlowRule(); rule.setResource("hello"); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setCount(2);
rules.add(rule); FlowRuleManager.loadRules(rules); } }
|
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 32 33 34 35 36 37 38 39
| import com.alibaba.csp.sentinel.Entry; @Component public class SentinelFilter extends ZuulFilter {
@Override public String filterType() { return FilterConstants.PRE_TYPE; }
@Override public int filterOrder() { return -10; }
@Override public boolean shouldFilter() { return true; }
@Override public Object run() throws ZuulException { Entry entry = null; try { entry = SphU.entry("hello"); System.out.println("正常请求"); } catch (Exception e) { System.out.println("阻塞住了"); } finally { if (entry != null) { entry.exit(); } } return null; } }
|
Service限流
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
| @SpringBootApplication @EnableZuulProxy public class CloudZuulApplication { public static void main(String[] args) { init(); SpringApplication.run(CloudZuulApplication.class, args); }
private static void init() { List<FlowRule> rules = new ArrayList<>(); FlowRule rule = new FlowRule(); rule.setResource("hello"); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setCount(2);
rules.add(rule); FlowRuleManager.loadRules(rules); }
@Bean public SentinelResourceAspect sentinelResourceAspect() { return new SentinelResourceAspect(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| @Service public class SentinelService {
@SentinelResource(value = "SentinelService.success", blockHandler = "fail") public String success() { return "正常请求"; }
public String fail(BlockException e) { return "阻塞请求"; } }
|
Spring Cloud Zipkin
sleuth用来收集分布式微服务的调用链。
1 2 3 4
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency>
|
sleuth收集跟踪信息通过http请求发送给zipkin server,zipkin将跟踪信息存储,以及提供RESTful API接口,zipkin ui通过调用api进行数据展示,Zipkin
内部集成了sleuth
。
默认内存存储,可以用mysql,ES等存储。
1 2 3 4
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency>
|
1 2 3 4 5 6 7 8
| spring: zipkin: base-url: http://localhost:9411/ sleuth: sampler: rate: 1
|
官网下载zipkin
的服务端后启动,默认端口就是9411,通过访问http://localhost:9411/
即可。
Spring Cloud Admin
采集日志默认使用logback。
服务端
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-server</artifactId> <version>2.2.1</version> </dependency>
<dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-server-ui</artifactId> </dependency>
|
1 2 3 4 5 6 7 8
| @SpringBootApplication @EnableAdminServer public class AdminApplication {
public static void main(String[] args) { SpringApplication.run(AdminApplication.class, args); } }
|
客户端
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> <version>2.2.1</version> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
|
1 2 3
| management.endpoints.web.exposure.include=* management.endpoint.health.show-details=always spring.boot.admin.client.url=http://localhost:8080
|
邮件通知
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>
|
1 2 3 4 5 6 7 8 9 10 11 12
| spring.mail.host= spring.mail.username= spring.mail.password= spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true
spring.boot.admin.notify.mail.to=
spring.boot.admin.notify.mail.from=
management.health.mail.enabled=false
|