终于整完了, 看了两天,中间还穿插各种面试和会议. 看了我的英语水平...或者说google的英译汉能力着实可以的.
看完之后有几个感受.
gateway本身分成三个组件
routes: 路由, 也是最小的颗粒组件
predicates: 断言, 就是满足什么样的条件
filter: 过滤器, 里面可以对请求做一些处理
application.yml
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- Cookie=mycookie,mycookievalue
filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
这就是一个最简单的标准的一个路由里面有自己的
id(唯一标识),
predicates用来标记哪些请求进来,
filter代表着请求进来以后你要做什么,
这里面他提供了大批量的类库;
包括, 时间, cookie, url, 参数, header, 基本你能想到的东西都可以放到predicates和filter里面.他也比较希望你用它的类库.
从1~6 其实他就是在各种介绍他的类库.....不过我说实话.....太多了,整的我都不想用了,不是侥幸, 纯粹习惯问题.
这幅图要从上往下读: 请求进来, 进来以后经过两个handle,然后经过filter逐层的返回, 应该用的是责任链, 这个我还没有细看,后续去解读源码时候看看.
Example 59. ExampleConfiguration.java
@Bean
public GlobalFilter customFilter() {
return new CustomGlobalFilter();
}
public class CustomGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("custom global filter");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -1;
}
}
我喜欢这个:全局filter, 里面我们可以随意的写代码,各种搞事情,比如jwt.
这时候有个问题了,如果多个filter他究竟是咋执行的
7.10. Marking An Exchange As Routed
网关路由ServerWebExchange之后,通过将gatewayAlreadyRouted添加到交换属性来将交换标记为“已路由”。 将请求标记为已路由后,其他路由筛选器将不会再次路由请求,实质上会跳过该过滤器。 您可以使用多种便捷方法将交换标记为已路由,或者检查交换是否已路由。
-
ServerWebExchangeUtils.isAlreadyRouted
takes aServerWebExchange
object and checks if it has been “routed”. -
ServerWebExchangeUtils.setAlreadyRouted
takes aServerWebExchange
object and marks it as “routed”
也就是是说一个路由以后他就关闭了.其他的就不执行了,注意这里说的并不包括全局的,因为我创建了两个globalFilter都是执行的.那globalFilter呢,他的执行顺序是按照order执行的, 然后每个请求都必须执行.
我目前呢写了两个globalFilter, 一个是用来打印日志的, 另外一个是用来做jwt鉴权的.还有就是直接加一个@Component标签就行,不用非得声明一个@Bean,他的意思大概是希望你写的更加显式一点. 这个在
17. Developer Guide 导读更加明显.
我们看一下他的建议
17.1. Writing Custom Route Predicate Factories 编写自定义路由工厂
MyRoutePredicateFactory.java
public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<HeaderRoutePredicateFactory.Config> {
public MyRoutePredicateFactory() {
super(Config.class);
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
// grab configuration from Config object
return exchange -> {
//grab the request
ServerHttpRequest request = exchange.getRequest();
//take information from the request to see if it
//matches configuration.
return matches(config, request);
};
}
public static class Config {
//Put the configuration properties for your filter here
}
}
17.2. Writing Custom GatewayFilter Factories
To write a GatewayFilter
, you must implement GatewayFilterFactory
. You can extend an abstract class called AbstractGatewayFilterFactory
. The following examples show how to do so:
Example 76. PreGatewayFilterFactory.java
public class PreGatewayFilterFactory extends AbstractGatewayFilterFactory<PreGatewayFilterFactory.Config> {
public PreGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
// grab configuration from Config object
return (exchange, chain) -> {
//If you want to build a "pre" filter you need to manipulate the
//request before calling chain.filter
ServerHttpRequest.Builder builder = exchange.getRequest().mutate();
//use builder to manipulate the request
return chain.filter(exchange.mutate().request(builder.build()).build());
};
}
public static class Config {
//Put the configuration properties for your filter here
}
}
PostGatewayFilterFactory.java
public class PostGatewayFilterFactory extends AbstractGatewayFilterFactory<PostGatewayFilterFactory.Config> {
public PostGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
// grab configuration from Config object
return (exchange, chain) -> {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
//Manipulate the response in some way
}));
};
}
public static class Config {
//Put the configuration properties for your filter here
}
}
他把rotes,prediscates 和filter给分开了.而且可以prefilter和postFilter
下面我们结合我们立下的flag, 看看怎么搞重定向
现在我想把所有/action/*****的请求都变成/business
Example 8. application.yml
spring:
cloud:
gateway:
routes:
- id: path_route
uri: https://example.org
predicates:
- Path=/red/{segment},/blue/{segment}
巴巴的说了好久了,停了好几天.因为在忙别的业务.在这里说一下最终解决方案
cloud:
gateway:
locator:
enabled: true
default-filters:
- AddResponseHeader=X-Response-Default-Foo, Default-Bar
# 服务自动发现,取第一个截取词匹配consul
discovery:
locator:
lower-case-service-id: true
enabled: true #开启根据微服务名称自动转发
filters:
- StripPrefix=1
routes:
- id: action
uri: lb://business
predicates:
- Path= /action/**
没错,就是加了这么一个路由,这里加了一个断言predicates: 所有 /action/ 的请求 都被转发到注册中心的 business服务上.
比如http://gateway.com/action/abcServer 会被转译为 http://gateway.com/business/action/abcServer
-----------------------------------------------------
上边解决了路由转发,我们在这里在从头梳理一下我的需求
1.我有一个gateway, 一个consul 和一个business的主项目
2.我有一堆历史的债务有个叫做static的项目需要被整合到business里面,原因是里面就只有两个接口,一个2B同事搞得,个人感觉就是拿公司的服务玩.
3.我有个php的老项目api.com,原来有公网域名,现在需要整合进geteway,我需要把原有域名也可以正常通过gateway能访问到服务
4.我需要做一个负载均衡实现灰度
-------------------------------------
1.业务合并,上边已经解决了
2.现在我们做负载均衡和灰度
那么我们首先设置一个负载均衡的flag:假设我有两台服务, (ip分别是83,84),我这里是本地用端口91和92代替.91和92都已经注册到了consul(注册中心)上面.
- 实现所有的请求20%在91上, 80%在92上
- 实现header中所有请求studentId=123的学员请求都在92上
- 实现header中所有请求按照studentId分群,将20%的比例固定的分配到91上,其他在92上
- 实现header中url=abc的请求按照studentId分群,将20%的比例固定的分配到91上,其他在92上
回过来我们梳理一下无非就是按照url,studentId两个维度将流量分配给不同的服务. 好了现在我们需求有了,开始做个设计,
这个明显的是一个策略的模式,而且策略间应该是有优先级的.比如一个我们策略一是按照studentId%100<20在91,上策略2是studentId=123的在91上, 1,2明显是冲突的. 所以涉及到了优先级.
这个策略明显是不定长度的, 意思就是说比如url=abc的,后面还有url=bcd的,而且以后还可能有其他的项目.
最后初步设计这里采用责任链的方式,因为最后我们可以很明确的抽象出来几种规则有几个共性, 入参是studentId和url,回参是服务地址.
话不多说直接上代码:
-----------------------------兜兜转转,写完这个代码半个月了才想起来博客还没收尾--------------------------
首先yml的配置
#负载均衡 mybalance: open : true #灰度 grayscale: - order: -129 id: ver等于2.0.0的queryPort请求,投射到9091,ver不等于2.0.0的queryPort分发到其他服 ip: 192.168.0.225:9091 ver: '=2.0.0' url: '/statistics/testServer/queryPort' exclusive : 'trueUrl' - order: -128 id: ver>=2.4.0的所有请求,投射到9091,ver小于2.4.0分发到其他服 ip: 192.168.0.225:9092 ver: '>2.4.0' url: '*' exclusive : 'trueVer' - order: -127 id: studentId=123的,url=queryPort 投射到9091,其他请求随机投放 ip: 192.168.0.225:9091 studentId: 123 url: '/statistics/testServer/queryPort2' exclusive : trueUrl - order: -125 id: studentId=123的,url=queryPort 投射到9091,其他请求随机投放 ip: 192.168.0.225:9091 studentId: 123 url: '*' exclusive : '*'
其次我们看看这个策略的解析类
@Api("灰度策略") public class Grayscale { @ApiParam("优先级,值越小优先级越高,当出现了高优先级的负载均衡以后低优先级的就不再执行") private Integer order; @ApiParam("策略的唯一标识") private String id; @ApiParam("策略的ip标识,正则 consul中的address ip或者ip+端口") private String ip; @ApiParam("header中studentId,正则,如果是*就代表所有") private String studentId; @ApiParam("请求的url,正则,如果是*就代表所有") private String url; @ApiParam("强制,true如果找不到对应ip就抛出异常,false:如果找不到对应的ip就随机返回一台 ") private boolean enforce=false; @ApiParam("权重0~100 当ip 有内容的时候本字段不生效") private Integer weight; @ApiParam("排他 如果之前的条件没有完全命中,那么就会执行exclusive过滤, 比如 trueStu 代表如果stu判断是true就会从服务列表摘除, falseStu代表如果stu是false就从列表摘除 ") private String exclusive; @ApiParam("版本号") private String ver; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } public String getStudentId() { return studentId; } public void setStudentId(String studentId) { this.studentId = studentId; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public boolean isEnforce() { return enforce; } public void setEnforce(boolean enforce) { this.enforce = enforce; } public Integer getWeight() { return weight; } public void setWeight(Integer weight) { this.weight = weight; } public Integer getOrder() { return order; } public void setOrder(Integer order) { this.order = order; } public String getExclusive() { return exclusive; } public void setExclusive(String exclusive) { this.exclusive = exclusive; } public String getVer() { return ver; } public void setVer(String ver) { this.ver = ver; }
}
@Component @ConditionalOnProperty( matchIfMissing = true ,prefix = "mybalance",name="open",havingValue = "true" ) @Api("负载均衡") @ConfigurationProperties(prefix = "mybalance") public class MyBalanceEntity { @ApiParam("灰度") private List<Grayscale> grayscale; @Constructor @ApiParam("所有策略") public void sort() { if (grayscale != null && grayscale.size() > 1) { grayscale.sort((g1, g2) -> { if (g2.getOrder() > g1.getOrder()) return 1; if (g2.getOrder() < g1.getOrder()) return -1; return 0; }); } } public List<Grayscale> getGrayscale() { return grayscale; } public void setGrayscale(List<Grayscale> grayscale) { this.grayscale = grayscale; } }
----------------------------------------这个可以看出来策略就是先按照id升序然后相同的按照前后顺序
/** * https://blog.csdn.net/zhou1124/article/details/103773835 */ @Api("负载均衡,先执行MyLoadBalancerClientFilter,再执行MyLoadBalanceRule") @Component public class MyLoadBalancerClientFilter extends LoadBalancerClientFilter { public static ThreadLocal<ServerWebExchange> exchange = new ThreadLocal<>(); private static Logger log = LoggerFactory.getLogger(DaishuCloudGatewayApplication.class); @Autowired private MyBalanceEntity myBalanceEntity; public MyLoadBalancerClientFilter(LoadBalancerClient loadBalancer, LoadBalancerProperties properties) { super(loadBalancer, properties); } @Override protected ServiceInstance choose(ServerWebExchange exchange) { //如果没有任何策略就使用 if(myBalanceEntity==null||myBalanceEntity.getGrayscale()==null||myBalanceEntity.getGrayscale().size()==0){ return super.choose(exchange); } //这里可以拿到web请求的上下文,可以从header中取出来自己定义的数据。 MyLoadBalancerClientFilter.exchange.set(exchange); //获得真实的请求路径lb://statistics/testServer/queryPort URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR); HttpHeaders httpHeaders = exchange.getRequest().getHeaders(); BalanceDto balanceDto=new BalanceDto(httpHeaders,uri); balanceDto.setHttpHeaders(httpHeaders); balanceDto.setUri(uri); log.info("步骤1"); //如果在已有的ThreadLocal中没有连接 if (MyLoadBalanceRule.originHost.get() == null) { //获得所属ip List<String> originHostHeader = httpHeaders.get(MyLoadBalanceRule.originHostHeader); if (originHostHeader == null || originHostHeader.size() == 0) { String host = exchange.getRequest().getURI().getHost(); //设置请求头 exchange.getRequest().mutate().header(MyLoadBalanceRule.originHostHeader, host).build(); //设置本机地址 MyLoadBalanceRule.originHost.set(host); } else { MyLoadBalanceRule.originHost.set(originHostHeader.get(0)); } } //开始路由 if (this.loadBalancer instanceof RibbonLoadBalancerClient) { RibbonLoadBalancerClient client = (RibbonLoadBalancerClient) this.loadBalancer; String serviceId = ((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost(); //这里使用userId做为选择服务实例的key, 调用的是MyLoadBalanceRule的choose, balanceDto 就是那边接收到的key return client.choose(serviceId, balanceDto); } return super.choose(exchange); } }
@Component @Api("路由规则") public class MyLoadBalanceRule extends BestAvailableRule { private static Logger log = LoggerFactory.getLogger(DaishuCloudGatewayApplication.class); @Autowired @Qualifier("grayscaleBalance") private Balance grayscaleBalance; public static ThreadLocal<String> originHost=new ThreadLocal<>(); public static String originHostHeader="originHost"; @Autowired private MyBalanceEntity myBalanceEntity; public Server choose(ILoadBalancer lb, Object key) { //log.info("步骤2"+key); if (lb == null) { log.error("MyLoadBalanceRule Exception no load balancer"); return null; } if(myBalanceEntity==null||myBalanceEntity.getGrayscale()==null||myBalanceEntity.getGrayscale().size()==0){ return grayscaleBalance.loadRandomServer(lb.getReachableServers()); } BalanceDto balanceDto=(BalanceDto) key; //consul 上的注册192.168.0.225:9091 192.168.0.225:9092 consul中服务对应的address项目 List<Server> reachableServers = lb.getReachableServers(); if(reachableServers==null ||reachableServers.size()==0){ log.error("MyLoadBalanceRule Exception 没有可用的服务"); return null; } balanceDto.setReachableServers(reachableServers); BalanceContext balanceContext=new BalanceContext(balanceDto); //进行负载均衡 grayscaleBalance.chooseServer(balanceContext); //log.info("balanceContext:" + JSONObject.toJSONString(balanceContext)); return balanceContext.getServer(); } @Override public Server choose(Object key) { return choose(getLoadBalancer(), key); } @Override public void initWithNiwsConfig(IClientConfig clientConfig) { // TODO Auto-generated method stub } }
//这两个类是负载均衡
先执行
MyLoadBalancerClientFilter
在执行
MyLoadBalanceRule
@Api("负载均衡的抽象规范,暂时没有其他用途") public abstract class Balance { private static Logger log = LoggerFactory.getLogger(DaishuCloudGatewayApplication.class); @ApiParam("返回一台服务") public abstract void chooseServer(BalanceContext balanceContext); @ApiParam("随机返回一台服务") public abstract Server loadRandomServer(List<Server> serverList); @ApiParam("随机返回一台服务") protected void loadRandomServer(final @ApiParam("入参") BalanceContext balanceContext) { Server server= loadRandomServer(new ArrayList(balanceContext.getBalanceDto().getReachableServerMap().values())); balanceContext.setServer(server); balanceContext.setPolicyId("loadRandomServer"); } @ApiParam("根据策略加载服务到context") protected void loadServer(final @ApiParam("灰度策略") Grayscale grayscale, final @ApiParam("入参") BalanceContext balanceContext) { if (Collections.isEmpty(balanceContext.getBalanceDto().getReachableServerMap())) { throw new DsException(10009); } MatchMap matchMap = new MatchMap(); //判断是否命中 effectiveStu(grayscale, balanceContext, matchMap); effectiveUrl(grayscale, balanceContext, matchMap); effectiveVer(grayscale, balanceContext, matchMap); //命中ip if (matchMap.isAllMatch()) { //构建context buildContextServer(grayscale, balanceContext); } else { //移除server exclusiveContextServer(grayscale, balanceContext, matchMap); } } @ApiParam("移除sever") private void exclusiveContextServer(Grayscale grayscale, BalanceContext balanceContext, @ApiParam("匹配结果") MatchMap matchMap) { //如果只剩下一台服务并且是一个有效的移除策略才开始移除 if (balanceContext.getBalanceDto().getReachableServerMap().size() > 1 && DsStringUtil.isNotEmpty(grayscale.getExclusive()) && !grayscale.getExclusive().equals("*")) { String[] exp = grayscale.getExclusive().split(","); for (String str : exp) { switch (str) { case "falseVer": if (!matchMap.isVerMatch()) balanceContext.getBalanceDto().getReachableServerMap().remove(grayscale.getIp()); break; case "trueVer": if (matchMap.isVerMatch()) balanceContext.getBalanceDto().getReachableServerMap().remove(grayscale.getIp()); break; case "trueUrl": if (matchMap.isUrlMatch()) balanceContext.getBalanceDto().getReachableServerMap().remove(grayscale.getIp()); break; case "falseUrl": if (!matchMap.isUrlMatch()) balanceContext.getBalanceDto().getReachableServerMap().remove(grayscale.getIp()); break; case "trueStu": if (matchMap.isStuMatch()) balanceContext.getBalanceDto().getReachableServerMap().remove(grayscale.getIp()); break; case "falseStu": if (!matchMap.isStuMatch()) balanceContext.getBalanceDto().getReachableServerMap().remove(grayscale.getIp()); break; default: } } } } @ApiParam("构建context") private void buildContextServer(Grayscale grayscale, BalanceContext balanceContext) { if (balanceContext.getBalanceDto().getReachableServerMap().get(grayscale.getIp()) != null) { balanceContext.setOrder(grayscale.getOrder()); balanceContext.setPolicyId(grayscale.getId()); balanceContext.setServer(balanceContext.getBalanceDto().getReachableServerMap().get(grayscale.getIp())); } else { if (grayscale.isEnforce()) { log.info("策略:" + grayscale.getId() + "没有找到服务==" + grayscale.getIp() + "强制执行失败"); throw new DsException(10010, "策略id:" + grayscale.getId()); } else { log.info("策略:" + grayscale.getId() + "没有找到服务==" + grayscale.getIp() + "跳过策略"); } } } @ApiParam("匹配版本") private void effectiveVer(Grayscale grayscale, BalanceContext balanceContext, MatchMap matchMap) { //如果策略有但是header没有就不通过 if (effective(grayscale.getVer()) && DsStringUtil.isEmpty(balanceContext.getBalanceDto().getHttpHeaders().getFirst("ver"))) { matchMap.setAllMatch(false); matchMap.setVerMatch(false); return; } if (effective(grayscale.getVer())) { DsHeader dsHeader = new DsHeader(balanceContext.getBalanceDto().getHttpHeaders()); String ver = grayscale.getVer().replaceAll(">", "").replaceAll("=", "").replaceAll("<", ""); //判断版本情况 header内容小于输入version版本返回-1 0 等于 header内容大于version返回1 Integer i = dsHeader.afterVer(ver); //结果是小于 if (i == -1 && !grayscale.getVer().startsWith("<")) { matchMap.setAllMatch(false); matchMap.setVerMatch(false); } //结果是大于 if (i == 1 && !grayscale.getVer().startsWith(">")) { matchMap.setAllMatch(false); matchMap.setVerMatch(false); } //如果结果相等但是判断条件是不等于,或者不包含= if (i == 0 && (grayscale.getVer().startsWith("!=") || !grayscale.getVer().contains("="))) { matchMap.setAllMatch(false); matchMap.setVerMatch(false); } } } @ApiParam("判断地址匹配结果") private void effectiveUrl(Grayscale grayscale, BalanceContext balanceContext, MatchMap matchMap) { //如果需要检测url if (effective(grayscale.getUrl()) && !match(grayscale.getUrl(), balanceContext.getBalanceDto().getUri().toString())) { matchMap.setAllMatch(false); matchMap.setUrlMatch(false); } } @ApiParam("判断学生匹配结果") private void effectiveStu(Grayscale grayscale, BalanceContext balanceContext, MatchMap matchMap) { //如果策略有但是header没有 if (effective(grayscale.getStudentId()) && DsStringUtil.isEmpty(balanceContext.getBalanceDto().getStudentId())) { matchMap.setAllMatch(false); matchMap.setStuMatch(false); return; } if (effective(grayscale.getStudentId()) && !match(grayscale.getStudentId(), balanceContext.getBalanceDto().getStudentId().toString())) { matchMap.setAllMatch(false); matchMap.setStuMatch(false); } } @ApiParam("是否是有效字段") private boolean effective(String str) { if (DsStringUtil.isEmpty(str) || str.trim().equals("*")) { return false; } return true; } @ApiParam("正则表达式是否匹配") private boolean match(@ApiParam("正则表达式") String exp, @ApiParam("内容") String str) { // 忽略大小写的写法 Pattern pattern = Pattern.compile(exp, Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(str.replaceAll("lb://","/")); boolean rs = matcher.matches(); return rs; } }
@Api("灰度负载均衡") @Component public class GrayscaleBalance extends Balance { @Autowired private MyBalanceEntity myBalanceEntity; @Override public void chooseServer(BalanceContext balanceContext) { if (myBalanceEntity.getGrayscale() != null && myBalanceEntity.getGrayscale().size() > 0) { for (Grayscale grayscale : myBalanceEntity.getGrayscale()) { //如果还没有产生有效策略 if (balanceContext.getServer() == null) { //根据策略加载服务到context super.loadServer(grayscale, balanceContext); } else { //因为本身就已经进行过 break; } } //如果所有的都执行完了还没有拿到有效的策略 if (balanceContext.getServer() == null) { super.loadRandomServer(balanceContext); } } } @ApiParam("随机返回一台服务") @Override public Server loadRandomServer(List<Server> serverList) { Random random = new Random(); int index = random.nextInt(serverList.size()); return serverList.get(index); } }
//这两个类是实际的逻辑
这里面用了继承的方式倒不是说必须的,主要是考虑以后万一有扩展
通过这些配置就可以实现负载均衡的灰度, 其实如果需求不这么复杂的话还是建议用自带的断言和filter,或者自带的ribbon.可读性更好,性能也高.而且权重什么的也不用自己去做.主要还是看需求吧.