參考:https://blog.csdn.net/u014091123/article/details/75433656
https://blog.csdn.net/u013815546/article/details/68944039
Zuul是Netflix開源的微服務閘道器,他的核心是一系列的過濾器,通過這些過濾器我們可以輕鬆的實現服務的訪問認證、限流、路由、負載、熔斷等功能。
基於對已有專案程式碼零侵入的需求,本文沒有將zuul閘道器專案註冊到eureka中心,而是將zuul與springboot結合作為一個獨立的專案進行請求轉發,因此本專案是非spring cloud架構。
開始編寫zuul閘道器專案
首先,新建一個spring boot專案。加入zuul依賴,開啟@EnableZuulProxy註解。
pom.xml
1 <dependency> 2 <groupId>org.springframework.cloud</groupId> 3 <artifactId>spring-cloud-starter-zuul</artifactId> 4 <version>1.4.4.RELEASE</version> 5 </dependency>
application.properties
1 server.port=8090 2 eureka.client.enable=false 3 zuul.ribbon.eager-load.enabled=true 4 5 zuul.SendErrorFilter.post.disable=true
由於後續會使用到動態路由,所以這裡我們並不需要在application.properties中做閘道器地址轉發對映。
SpringBootZuulApplication.java
1 package com.syher.zuul; 2 3 import com.google.common.util.concurrent.ThreadFactoryBuilder; 4 import com.syher.zuul.core.zuul.router.PropertiesRouter; 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.boot.CommandLineRunner; 7 import org.springframework.boot.SpringApplication; 8 import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 9 import org.springframework.cloud.netflix.zuul.EnableZuulProxy; 10 import org.springframework.cloud.netflix.zuul.RoutesRefreshedEvent; 11 import org.springframework.cloud.netflix.zuul.filters.RouteLocator; 12 import org.springframework.context.ApplicationEventPublisher; 13 import org.springframework.context.annotation.ComponentScan; 14 15 import java.io.File; 16 import java.util.concurrent.Executors; 17 import java.util.concurrent.ScheduledExecutorService; 18 import java.util.concurrent.TimeUnit; 19 20 /** 21 * @author braska 22 * @date 2018/06/25. 23 **/ 24 @EnableAutoConfiguration 25 @EnableZuulProxy 26 @ComponentScan(basePackages = { 27 "com.syher.zuul.core", 28 "com.syher.zuul.service" 29 }) 30 public class SpringBootZuulApplication implements CommandLineRunner { 31 @Autowired 32 ApplicationEventPublisher publisher; 33 @Autowired 34 RouteLocator routeLocator; 35 36 private ScheduledExecutorService executor; 37 private Long lastModified = 0L; 38 private boolean instance = true; 39 40 public static void main(String[] args) { 41 SpringApplication.run(SpringBootZuulApplication.class, args); 42 } 43 44 @Override 45 public void run(String... args) throws Exception { 46 executor = Executors.newSingleThreadScheduledExecutor( 47 new ThreadFactoryBuilder().setNameFormat("properties read.").build() 48 ); 49 executor.scheduleWithFixedDelay(() -> publish(), 0, 1, TimeUnit.SECONDS); 50 } 51 52 private void publish() { 53 if (isPropertiesModified()) { 54 publisher.publishEvent(new RoutesRefreshedEvent(routeLocator)); 55 } 56 } 57 58 private boolean isPropertiesModified() { 59 File file = new File(this.getClass().getClassLoader().getResource(PropertiesRouter.PROPERTIES_FILE).getPath()); 60 if (instance) { 61 instance = false; 62 return false; 63 } 64 if (file.lastModified() > lastModified) { 65 lastModified = file.lastModified(); 66 return true; 67 } 68 return false; 69 } 70 }
一、自定義過濾器
自定義zuul過濾器比較簡單。我們先講過濾器。
zuul過濾器分為pre、route、post、error四種型別。作用我就不詳細講了,網上資料一大把。本文主要寫路由前的過濾,即pre型別。
要自定義一個過濾器,只需要要繼承ZuulFilter,然後指定過濾型別、過濾順序、是否執行這個過濾器、過濾內容就OK了。
為了便於擴充套件,這裡用到了模板模式。
AbstractZuulFilter.java
1 package com.syher.zuul.core.zuul.filter; 2 3 import com.netflix.zuul.ZuulFilter; 4 import com.netflix.zuul.context.RequestContext; 5 import com.syher.zuul.core.zuul.ContantValue; 6 7 /** 8 * @author braska 9 * @date 2018/06/29. 10 **/ 11 public abstract class AbstractZuulFilter extends ZuulFilter { 12 13 protected RequestContext context; 14 15 @Override 16 public boolean shouldFilter() { 17 RequestContext ctx = RequestContext.getCurrentContext(); 18 return (boolean) (ctx.getOrDefault(ContantValue.NEXT_FILTER, true)); 19 } 20 21 @Override 22 public Object run() { 23 context = RequestContext.getCurrentContext(); 24 return doRun(); 25 } 26 27 public abstract Object doRun(); 28 29 public Object fail(Integer code, String message) { 30 context.set(ContantValue.NEXT_FILTER, false); 31 context.setSendZuulResponse(false); 32 context.getResponse().setContentType("text/html;charset=UTF-8"); 33 context.setResponseStatusCode(code); 34 context.setResponseBody(String.format("{\"result\":\"%s!\"}", message)); 35 return null; 36 } 37 38 public Object success() { 39 context.set(ContantValue.NEXT_FILTER, true); 40 return null; 41 } 42 }
定義preFilter的抽象類,繼承AbstractZuulFilter。指定pre型別,之後所有的pre過濾器都可以繼承這個抽象類。
AbstractPreZuulFilter.java
1 package com.syher.zuul.core.zuul.filter.pre; 2 3 import com.syher.zuul.core.zuul.FilterType; 4 import com.syher.zuul.core.zuul.filter.AbstractZuulFilter; 5 6 /** 7 * @author braska 8 * @date 2018/06/29. 9 **/ 10 public abstract class AbstractPreZuulFilter extends AbstractZuulFilter { 11 @Override 12 public String filterType() { 13 return FilterType.pre.name(); 14 } 15 }
接著編寫具體一個具體的過濾器,比如限流。
RateLimiterFilter.java
1 package com.syher.zuul.core.zuul.filter.pre; 2 3 import com.google.common.util.concurrent.RateLimiter; 4 import com.syher.zuul.core.zuul.FilterOrder; 5 import org.slf4j.Logger; 6 import org.slf4j.LoggerFactory; 7 8 import javax.servlet.http.HttpServletRequest; 9 10 /** 11 * @author braska 12 * @date 2018/06/29. 13 **/ 14 public class RateLimiterFilter extends AbstractPreZuulFilter { 15 16 private static final Logger LOGGER = LoggerFactory.getLogger(RateLimiterFilter.class); 17 18 /** 19 * 每秒允許處理的量是50 20 */ 21 RateLimiter rateLimiter = RateLimiter.create(50); 22 23 @Override 24 public int filterOrder() { 25 return FilterOrder.RATE_LIMITER_ORDER; 26 } 27 28 @Override 29 public Object doRun() { 30 HttpServletRequest request = context.getRequest(); 31 String url = request.getRequestURI(); 32 if (rateLimiter.tryAcquire()) { 33 return success(); 34 } else { 35 LOGGER.info("rate limit:{}", url); 36 return fail(401, String.format("rate limit:{}", url)); 37 } 38 } 39 }
其他型別的過濾器也一樣。建立不同的抽象類,比如AbstractPostZuulFilter,指定filterType,然後具體的postFilter只要繼承該抽象類即可。
最後,將過濾器託管給spring。
ZuulConfigure.java
1 package com.syher.zuul.core.config; 2 3 import com.netflix.loadbalancer.IRule; 4 import com.netflix.zuul.ZuulFilter; 5 import com.syher.zuul.core.ribbon.ServerLoadBalancerRule; 6 import com.syher.zuul.core.zuul.filter.pre.RateLimiterFilter; 7 import com.syher.zuul.core.zuul.filter.pre.TokenAccessFilter; 8 import com.syher.zuul.core.zuul.filter.pre.UserRightFilter; 9 import com.syher.zuul.core.zuul.router.PropertiesRouter; 10 import org.springframework.beans.factory.annotation.Autowired; 11 import org.springframework.boot.autoconfigure.web.ServerProperties; 12 import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; 13 import org.springframework.context.annotation.Bean; 14 import org.springframework.context.annotation.Configuration; 15 16 /** 17 * @author braska 18 * @date 2018/07/05. 19 **/ 20 @Configuration 21 public class ZuulConfigure { 22 23 @Autowired 24 ZuulProperties zuulProperties; 25 @Autowired 26 ServerProperties server; 27 28 /** 29 * 動態路由 30 * @return 31 */ 32 @Bean 33 public PropertiesRouter propertiesRouter() { 34 return new PropertiesRouter(this.server.getServletPrefix(), this.zuulProperties); 35 } 36 37 /** 38 * 動態負載 39 * @return 40 */ 41 @Bean 42 public IRule loadBalance() { 43 return new ServerLoadBalancerRule(); 44 } 45 46 /** 47 * 自定義過濾器 48 * @return 49 */ 50 @Bean 51 public ZuulFilter rateLimiterFilter() { 52 return new RateLimiterFilter(); 53 } 54 }
二、動態路由
接著寫動態路由。動態路由需要配置可持久化且能動態重新整理。
zuul預設使用的路由是SimpleRouteLocator,不具備動態重新整理的效果。DiscoveryClientRouteLocator具備重新整理功能,但是需要已有的專案將服務註冊到eureka,這不符合已有專案程式碼零侵入的需求所以排除。那麼還有個辦法就是自定義路由然後實現RefreshableRouteLocator類。
部分程式碼如下:
AbstractDynamicRouter.java
1 package com.syher.zuul.core.zuul.router; 2 3 import com.syher.zuul.core.zuul.entity.BasicRoute; 4 import org.apache.commons.lang.StringUtils; 5 import org.slf4j.Logger; 6 import org.slf4j.LoggerFactory; 7 import org.springframework.beans.BeanUtils; 8 import org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator; 9 import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator; 10 import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; 11 12 import java.util.LinkedHashMap; 13 import java.util.List; 14 import java.util.Map; 15 16 /** 17 * @author braska 18 * @date 2018/07/02. 19 **/ 20 public abstract class AbstractDynamicRouter extends SimpleRouteLocator implements RefreshableRouteLocator { 21 22 private static final Logger LOGGER = LoggerFactory.getLogger(AbstractDynamicRouter.class); 23 24 public AbstractDynamicRouter(String servletPath, ZuulProperties properties) { 25 super(servletPath, properties); 26 } 27 28 @Override 29 public void refresh() { 30 doRefresh(); 31 } 32 33 @Override 34 protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() { 35 LinkedHashMap<String, ZuulProperties.ZuulRoute> routes = new LinkedHashMap<String, ZuulProperties.ZuulRoute>(); 36 routes.putAll(super.locateRoutes()); 37 38 List<BasicRoute> results = readRoutes(); 39 40 for (BasicRoute result : results) { 41 if (StringUtils.isEmpty(result.getPath()) ) { 42 continue; 43 } 44 ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute(); 45 try { 46 BeanUtils.copyProperties(result, zuulRoute); 47 } catch (Exception e) { 48 LOGGER.error("=============load zuul route info from db with error==============", e); 49 } 50 routes.put(zuulRoute.getPath(), zuulRoute); 51 } 52 return routes; 53 } 54 55 /** 56 * 讀取路由資訊 57 * @return 58 */ 59 protected abstract List<BasicRoute> readRoutes(); 60 }
由於本人比較懶。不想每次寫個demo都要重新配置一大堆資料庫資訊。所以本文很多資料比如路由資訊、比如負載策略。要麼寫在文字里面,要麼直接java程式碼構造。
本demo的路由資訊就是從properties裡面讀取。嗯,繼承AbstractDynamicRouter即可。
PropertiesRouter.java
1 package com.syher.zuul.core.zuul.router; 2 3 import com.google.common.collect.Lists; 4 import com.google.common.util.concurrent.ThreadFactoryBuilder; 5 import com.syher.zuul.common.Context; 6 import com.syher.zuul.core.zuul.entity.BasicRoute; 7 import org.apache.commons.lang.StringUtils; 8 import org.slf4j.Logger; 9 import org.slf4j.LoggerFactory; 10 import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; 11 12 import java.io.File; 13 import java.io.IOException; 14 import java.util.HashMap; 15 import java.util.List; 16 import java.util.Map; 17 import java.util.Properties; 18 import java.util.concurrent.Executors; 19 import java.util.concurrent.ScheduledExecutorService; 20 import java.util.stream.Collectors; 21 22 /** 23 * @author braska 24 * @date 2018/07/02. 25 **/ 26 public class PropertiesRouter extends AbstractDynamicRouter { 27 28 private static final Logger LOGGER = LoggerFactory.getLogger(PropertiesRouter.class); 29 public static final String PROPERTIES_FILE = "router.properties"; 30 private static final String ZUUL_ROUTER_PREFIX = "zuul.routes"; 31 32 33 public PropertiesRouter(String servletPath, ZuulProperties properties) { 34 super(servletPath, properties); 35 } 36 37 @Override 38 protected List<BasicRoute> readRoutes() { 39 List<BasicRoute> list = Lists.newArrayListWithExpectedSize(3); 40 try { 41 Properties prop = new Properties(); 42 prop.load( 43 this.getClass().getClassLoader().getResourceAsStream(PROPERTIES_FILE) 44 ); 45 46 Context context = new Context(new HashMap<>((Map) prop)); 47 Map<String, String> data = context.getSubProperties(ZUUL_ROUTER_PREFIX); 48 List<String> ids = data.keySet().stream().map(s -> s.substring(0, s.indexOf("."))).distinct().collect(Collectors.toList()); 49 ids.stream().forEach(id -> { 50 Map<String, String> router = context.getSubProperties(String.join(".", ZUUL_ROUTER_PREFIX, id)); 51 52 String path = router.get("path"); 53 path = path.startsWith("/") ? path : "/" + path; 54 55 String serviceId = router.getOrDefault("serviceId", null); 56 String url = router.getOrDefault("url", null); 57 58 BasicRoute basicRoute = new BasicRoute(); 59 basicRoute.setId(id); 60 basicRoute.setPath(path); 61 basicRoute.setUrl(router.getOrDefault("url", null)); 62 basicRoute.setServiceId((StringUtils.isBlank(url) && StringUtils.isBlank(serviceId)) ? id : serviceId); 63 basicRoute.setRetryable(Boolean.parseBoolean(router.getOrDefault("retry-able", "false"))); 64 basicRoute.setStripPrefix(Boolean.parseBoolean(router.getOrDefault("strip-prefix", "false"))); 65 list.add(basicRoute); 66 }); 67 } catch (IOException e) { 68 LOGGER.info("error to read " + PROPERTIES_FILE + " :{}", e); 69 } 70 return list; 71 } 72 }
既然是動態路由實時重新整理,那肯定需要一個定時器定時監控properties檔案。所以我在啟動類SpringBootZuulApplication加了個定時器監控properties是否發生過變更(之前有疑問的現在可以解惑了)。一旦檔案被修改過就重新發布一下, 然後會觸發routeLocator的refresh方法。
1 public void publish() { 2 if (isPropertiesModified()) { 3 publisher.publishEvent(new RoutesRefreshedEvent(routeLocator)); 4 } 5 }
當然,如果是從資料庫或者其他地方比如redis讀取就不需要用到定時器,只要在增刪改的時候直接publish就好了。
最後,記得PropertiesRouter類交由spring託管(在ZuulConfigure類中配置bean)。
router.properties檔案:
1 zuul.routes.dashboard.path=/** 2 zuul.routes.dashboard.strip-prefix=true 3 4 ##不使用動態負載需指定url 5 ##zuul.routes.dashboard.url=http://localhost:9000/ 6 ##zuul服務部署後,動態增加閘道器對映,無需重啟即可實時路由到新的閘道器 7 ##zuul.routes.baidu.path=/**
三、動態負載
負載也算比較簡單,複雜點的是寫負載演算法。
動態負載主要分兩個步驟:
1、根據閘道器專案配置的host和port去資料庫(我是java直接造的資料)查詢負載策略,比如輪詢、比如隨機、比如iphash等等。
2、根據策略結合每臺伺服器分配的權重選出合適的服務。
實現動態負載需要自定義rule類然後繼承AbstractLoadBalancerRule類。
首先看負載策略的選擇:
ServerLoadBalancerRule.java
1 package com.syher.zuul.core.ribbon; 2 3 import com.google.common.base.Preconditions; 4 import com.netflix.client.config.IClientConfig; 5 import com.netflix.loadbalancer.AbstractLoadBalancerRule; 6 import com.netflix.loadbalancer.ILoadBalancer; 7 import com.netflix.loadbalancer.Server; 8 import com.syher.zuul.common.util.SystemUtil; 9 import com.syher.zuul.core.ribbon.balancer.LoadBalancer; 10 import com.syher.zuul.core.ribbon.balancer.RandomLoadBalancer; 11 import com.syher.zuul.core.ribbon.balancer.RoundLoadBalancer; 12 import com.syher.zuul.entity.GatewayAddress; 13 import com.syher.zuul.service.GatewayService; 14 import org.apache.commons.lang.StringUtils; 15 import org.slf4j.Logger; 16 import org.slf4j.LoggerFactory; 17 import org.springframework.beans.factory.annotation.Autowired; 18 import org.springframework.beans.factory.annotation.Value; 19 20 /** 21 * @author braska 22 * @date 2018/07/05. 23 **/ 24 public class ServerLoadBalancerRule extends AbstractLoadBalancerRule { 25 26 private static final Logger LOGGER = LoggerFactory.getLogger(ServerLoadBalancerRule.class); 27 28 @Value("${server.host:127.0.0.1}") 29 private String host; 30 @Value("${server.port:8080}") 31 private Integer port; 32 33 @Autowired 34 private GatewayService gatewayService; 35 36 @Override 37 public void initWithNiwsConfig(IClientConfig iClientConfig) { 38 } 39 40 @Override 41 public Server choose(Object key) { 42 return getServer(getLoadBalancer(), key); 43 } 44 45 private Server getServer(ILoadBalancer loadBalancer, Object key) { 46 if (StringUtils.isBlank(host)) { 47 host = SystemUtil.ipList().get(0); 48 } 49 //Preconditions.checkArgument(host != null, "server.host must be specify."); 50 //Preconditions.checkArgument(port != null, "server.port must be specify."); 51 52 GatewayAddress address = gatewayService.getByHostAndPort(host, port); 53 if (address == null) { //這裡的邏輯可以改,找不到閘道器配置資訊可以指定預設的負載策略 54 LOGGER.error(String.format("must be config a gateway info for the server[%s:%s].", host, String.valueOf(port))); 55 return null; 56 } 57 58 LoadBalancer balancer = LoadBalancerFactory.build(address.getFkStrategyId()); 59 60 return balancer.chooseServer(loadBalancer); 61 } 62 63 static class LoadBalancerFactory { 64 65 public static LoadBalancer build(String strategy) { 66 GatewayAddress.StrategyType type = GatewayAddress.StrategyType.of(strategy); 67 switch (type) { 68 case ROUND: 69 return new RoundLoadBalancer(); 70 case RANDOM: 71 return new RandomLoadBalancer(); 72 default: 73 return null; 74 } 75 } 76 } 77 }
然後是負載演算法介面程式碼。
LoadBalancer.java
1 package com.syher.zuul.core.ribbon.balancer; 2 3 import com.netflix.loadbalancer.ILoadBalancer; 4 import com.netflix.loadbalancer.Server; 5 6 /** 7 * @author braska 8 * @date 2018/07/06. 9 **/ 10 public interface LoadBalancer { 11 12 /** 13 * choose a loadBalancer 14 * @param loadBalancer 15 * @return 16 */ 17 Server chooseServer(ILoadBalancer loadBalancer); 18 }
定義抽象類,實現LoadBalancer介面
AbstractLoadBalancer.java
1 package com.syher.zuul.core.ribbon.balancer; 2 3 import com.netflix.loadbalancer.ILoadBalancer; 4 import com.netflix.loadbalancer.Server; 5 import com.syher.zuul.core.SpringContext; 6 import com.syher.zuul.service.ServerService; 7 import org.slf4j.Logger; 8 import org.slf4j.LoggerFactory; 9 10 /** 11 * @author braska 12 * @date 2018/07/06. 13 **/ 14 public abstract class AbstractLoadBalancer implements LoadBalancer { 15 private static final Logger LOGGER = LoggerFactory.getLogger(AbstractLoadBalancer.class); 16 protected ServerService serverService; 17 18 @Override 19 public Server chooseServer(ILoadBalancer loadBalancer) { 20 this.serverService = SpringContext.getBean(ServerService.class); 21 Server server = choose(loadBalancer); 22 if (server != null) { 23 LOGGER.info(String.format("the server[%s:%s] has been select.", server.getHost(), server.getPort())); 24 } else { 25 LOGGER.error("could not find any server."); 26 } 27 return server; 28 } 29 30 public abstract Server choose(ILoadBalancer loadBalancer); 31 }
輪詢負載演算法
RoundLoadBalancer.java
1 package com.syher.zuul.core.ribbon.balancer; 2 3 import com.netflix.loadbalancer.ILoadBalancer; 4 import com.netflix.loadbalancer.Server; 5 import com.syher.zuul.common.Constant; 6 import com.syher.zuul.core.GlobalCache; 7 import com.syher.zuul.core.ribbon.LoadBalancerRuleUtil; 8 import com.syher.zuul.entity.ServerAddress; 9 10 import java.util.List; 11 12 /** 13 * 權重輪詢 14 * 首次使用取最大權重的伺服器。而後通過權重的不斷遞減,尋找適合的伺服器。 15 * @author braska 16 * @date 2018/07/06. 17 **/ 18 public class RoundLoadBalancer extends AbstractLoadBalancer { 19 20 private Integer currentServer; 21 private Integer currentWeight; 22 private Integer maxWeight; 23 private Integer gcdWeight; 24 25 @Override 26 public Server choose(ILoadBalancer loadBalancer) { 27 List<ServerAddress> addressList = serverService.getAvailableServer(); 28 if (addressList != null && !addressList.isEmpty()) { 29 maxWeight = LoadBalancerRuleUtil.getMaxWeightForServers(addressList); 30 gcdWeight = LoadBalancerRuleUtil.getGCDForServers(addressList); 31 currentServer = Integer.parseInt(GlobalCache.instance().getOrDefault(Constant.CURRENT_SERVER_KEY, -1).toString()); 32 currentWeight = Integer.parseInt(GlobalCache.instance().getOrDefault(Constant.CURRENT_WEIGHT_KEY, 0).toString()); 33 34 Integer serverCount = addressList.size(); 35 36 if (1 == serverCount) { 37 return new Server(addressList.get(0).getHost(), addressList.get(0).getPort()); 38 } else { 39 while (true) { 40 currentServer = (currentServer + 1) % serverCount; 41 if (currentServer == 0) { 42 currentWeight = currentWeight - gcdWeight; 43 if (currentWeight <= 0) { 44 currentWeight = maxWeight; 45 if (currentWeight == 0) { 46 GlobalCache.instance().put(Constant.CURRENT_SERVER_KEY, currentServer); 47 GlobalCache.instance().put(Constant.CURRENT_WEIGHT_KEY, currentWeight); 48 Thread.yield(); 49 return null; 50 } 51 } 52 } 53 54 ServerAddress address = addressList.get(currentServer); 55 if (address.getWeight() >= currentWeight) { 56 GlobalCache.instance().put(Constant.CURRENT_SERVER_KEY, currentServer); 57 GlobalCache.instance().put(Constant.CURRENT_WEIGHT_KEY, currentWeight); 58 return new Server(address.getHost(), address.getPort()); 59 } 60 } 61 } 62 63 } 64 return null; 65 } 66 }
最後,ServerLoadBalancerRule交由spring託管。
至此,springboot+zuul實現自定義過濾器、動態路由、動態負載就都完成了。
原始碼:https://github.com/rxiu/study-on-road/tree/master/trickle-gateway