zuul是什麼
1. API網關
在微服務架構中,通常會有多個服務提供者。設想一個電商系統,可能會有商品、訂單、支付、用戶等多個類型的服務,而每個類型的服務數量也會隨着整個系統體量的增大也會隨之增長和變更。作爲UI端,在展示頁面時可能需要從多個微服務中聚合數據,而且服務的劃分位置結構可能會有所改變。網關就可以對外暴露聚合API,屏蔽內部微服務的微小變動,保持整個系統的穩定性。
簡單來說,API網關可以提供一個單獨且統一的API入口用於訪問內部一個或多個API。
當然這只是網關衆多功能中的一部分,它還可以做負載均衡,統一鑑權,協議轉換,監控監測等一系列功能。
2. 網關的選擇
現在市場上有很多的網關可供選擇:
- Spring Cloud Zuul:本身基於Netflix開源的微服務網關,可以和Eureka,Ribbon,Hystrix等組件配合使用。
- Kong : 基於OpenResty的 API 網關服務和網關服務管理層。
- Spring Cloud Gateway:是由spring官方基於Spring5.0,Spring Boot2.0,Project Reactor等技術開發的網關,提供了一個構建在Spring - Ecosystem之上的API網關,旨在提供一種簡單而有效的途徑來發送API,並向他們提供交叉關注點,例如:安全性,監控/指標和彈性。目的是爲了替換Spring Cloud Netfilx Zuul的。
在Spring cloud體系中,一般上選擇zuul或者Gateway。當然,也可以綜合自己的業務複雜性,自研一套或者改在一套符合自身業務發展的api網關的,最簡單做法是做個聚合api服務,通過SpringBoot構建對外的api接口,實現統一鑑權、參數校驗、權限控制等功能,說白了就是一個rest服務。
3. zuul
Zuul是Netflix開源的微服務網關,可以和Eureka、Ribbon、Hystrix等組件配合使用,Spring Cloud對Zuul進行了整合與增強,Zuul的主要功能是路由轉發和過濾器。
Zuul是基於JVM的路由器和服務器端負載均衡器。同時,Zuul的規則引擎允許規則和過濾器基本上用任何JVM語言編寫,內置支持Java和Groovy。這個功能,就可以實現動態路由的功能了。當需要添加某個新的對外服務時,一般上不停機更新是通過數據緩存配置或者使用Groovy進行動態路由的添加的。
zuul=路由+過濾器
Zuul的核心一系列的過濾器:
- 身份認證與安全:識別每個資源的驗證要求,並拒絕那些與要求不符的請求。
- 審查與監控:在邊緣位置追蹤有意義的數據和統計結果,從而帶來精確的生產視圖。
- 動態路由:動態地將請求路由到不同的後端集羣。
- 壓力測試:逐漸增加指向集羣的流量,以瞭解性能。
- 負載分配:爲每一種負載類型分配對應容量,並啓用超出限定值的請求。
- 靜態響應處理:在邊緣位置直接建立部分相應,從而避免其轉發到內部集羣。
二、zuul的工作原理
1. 過濾器機制
zuul的核心是一系列的filters, 其作用可以類比Servlet框架的Filter,或者AOP。
zuul把Request route到 用戶處理邏輯 的過程中,這些filter參與一些過濾處理,比如Authentication,Load Shedding等。
Zuul提供了一個框架,可以對過濾器進行動態的加載,編譯,運行。
Zuul的過濾器之間沒有直接的相互通信,他們之間通過一個RequestContext的靜態類來進行數據傳遞的。RequestContext類中有ThreadLocal變量來記錄每個Request所需要傳遞的數據。
Zuul的過濾器是由Groovy寫成,這些過濾器文件被放在Zuul Server上的特定目錄下面,Zuul會定期輪詢這些目錄,修改過的過濾器會動態的加載到Zuul Server中以便過濾請求使用。
2. 過濾器類型
Zuul大部分功能都是通過過濾器來實現的。Zuul中定義了四種標準過濾器類型,這些過濾器類型對應於請求的典型生命週期。
- PRE:在請求路由到目標之前執行。我們可利用這種過濾器實現身份驗證、在集羣中選擇請求的微服務、記錄調試信息等。
- ROUTING:這種過濾器將請求路由到微服務。這種過濾器用於構建發送給微服務的請求,並使用Apache HttpClient或Netfilx Ribbon請求微服務。
- POST:這種過濾器在目標請求路由到微服務以後執行。這種過濾器可用來爲響應添加標準的HTTP Header、收集統計信息和指標、將響應從微服務發送給客戶端等。
- ERROR:在其他階段發生錯誤時執行該過濾器。
內置的特殊過濾器
zuul還提供了一類特殊的過濾器,分別爲:StaticResponseFilter和SurgicalDebugFilter
StaticResponseFilter:StaticResponseFilter允許從Zuul本身生成響應,而不是將請求轉發到源。
SurgicalDebugFilter:SurgicalDebugFilter允許將特定請求路由到分隔的調試集羣或主機。
自定義的過濾器
除了默認的過濾器類型,Zuul還允許我們創建自定義的過濾器類型。
例如,我們可以定製一種STATIC類型的過濾器,直接在Zuul中生成響應,而不將請求轉發到後端的微服務。
3. 過濾器生命週期
Zuul請求的生命週期如圖,該圖詳細描述了各種類型的過濾器的執行順序。
4. 與應用的集成方式
ZuulServlet - 處理請求(調度不同階段的filters,處理異常等)
ZuulServlet類似SpringMvc的DispatcherServlet,所有的Request都要經過ZuulServlet的處理
三個核心的方法preRoute(),route(), postRoute(),zuul對request處理邏輯都在這三個方法裏
ZuulServlet交給ZuulRunner去執行。
由於ZuulServlet是單例,因此ZuulRunner也僅有一個實例。
ZuulRunner直接將執行邏輯交由FilterProcessor處理,FilterProcessor也是單例,其功能就是依據filterType執行filter的處理邏輯
FilterProcessor對filter的處理邏輯:
- 首先根據Type獲取所有輸入該Type的filter,List list。
- 遍歷該list,執行每個filter的處理邏輯,processZuulFilter(ZuulFilter filter)
- RequestContext對每個filter的執行狀況進行記錄,應該留意,此處的執行狀態主要包括其執行時間、以及執行成功或者失敗,如果執行失敗則對異常封裝後拋出。
- 到目前爲止,zuul框架對每個filter的執行結果都沒有太多的處理,它沒有把上一filter的執行結果交由下一個將要執行的filter,僅僅是記錄執行狀態,如果執行失敗拋出異常並終止執行。
ContextLifeCycleFilter - RequestContext 的生命週期管理
ContextLifecycleFilter的核心功能是爲了清除RequestContext; 請求上下文RequestContext通過ThreadLocal存儲,需要在請求完成後刪除該對象。
RequestContext提供了執行filter Pipeline所需要的Context,因爲Servlet是單例多線程,這就要求RequestContext即要線程安全又要Request安全。
context使用ThreadLocal保存,這樣每個worker線程都有一個與其綁定的RequestContext,因爲worker僅能同時處理一個Request,這就保證了Request Context 即是線程安全的由是Request安全的。
三、zuul實踐
1. 自定義Filter
添加一個PreRequestLogFilter:
public class PreRequestLogFilter extends ZuulFilter{
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
System.out.print(String.format("send %s request to %s",request.getMethod(),request.getRequestURL()));
return null;
}
}
修改啓動類GatewayZuulDemoApplication,添加Filter注入:
@Bean
public PreRequestLogFilter preRequestLogFilter(){
return new PreRequestLogFilter();
}
啓動服務並請求服務,觀察控制檯輸出日誌信息。
2. 實現動態路由
動態路由需要達到可持久化配置,動態刷新的效果。不僅要能滿足從spring的配置文件properties加載路由信息,還需要從數據庫加載我們的配置。另外一點是,路由信息在容器啓動時就已經加載進入了內存,我們希望配置完成後,實施發佈,動態刷新內存中的路由信息,達到不停機維護路由信息的效果。
網關項目
啓動類:GatewayApplication.Java
@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
自定義路由定位器:
```java
public class CustomRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator{
public final static Logger logger = LoggerFactory.getLogger(CustomRouteLocator.class);
private JdbcTemplate jdbcTemplate;
private ZuulProperties properties;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate){
this.jdbcTemplate = jdbcTemplate;
}
public CustomRouteLocator(String servletPath, ZuulProperties properties) {
super(servletPath, properties);
this.properties = properties;
logger.info("servletPath:{}",servletPath);
}
//父類已經提供了這個方法,這裏寫出來只是爲了說明這一個方法很重要!!!
// @Override
// protected void doRefresh() {
// super.doRefresh();
// }
@Override
public void refresh() {
doRefresh();
}
@Override
protected Map<String, ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
//從application.properties中加載路由信息
routesMap.putAll(super.locateRoutes());
//從db中加載路由信息
routesMap.putAll(locateRoutesFromDB());
//優化一下配置
LinkedHashMap<String, ZuulRoute> values = new LinkedHashMap<>();
for (Map.Entry<String, ZuulRoute> entry : routesMap.entrySet()) {
String path = entry.getKey();
// Prepend with slash if not already present.
if (!path.startsWith("/")) {
path = "/" + path;
}
if (StringUtils.hasText(this.properties.getPrefix())) {
path = this.properties.getPrefix() + path;
if (!path.startsWith("/")) {
path = "/" + path;
}
}
values.put(path, entry.getValue());
}
return values;
}
private Map<String, ZuulRoute> locateRoutesFromDB(){
Map<String, ZuulRoute> routes = new LinkedHashMap<>();
List<ZuulRouteVO> results = jdbcTemplate.query("select * from gateway_api_define where enabled = true ",new BeanPropertyRowMapper<>(ZuulRouteVO.class));
for (ZuulRouteVO result : results) {
if(org.apache.commons.lang3.StringUtils.isBlank(result.getPath()) || org.apache.commons.lang3.StringUtils.isBlank(result.getUrl()) ){
continue;
}
ZuulRoute zuulRoute = new ZuulRoute();
try {
org.springframework.beans.BeanUtils.copyProperties(result,zuulRoute);
} catch (Exception e) {
logger.error("=============load zuul route info from db with error==============",e);
}
routes.put(zuulRoute.getPath(),zuulRoute);
}
return routes;
}
public static class ZuulRouteVO {
/**
* The ID of the route (the same as its map key by default).
*/
private String id;
/**
* The path (pattern) for the route, e.g. /foo/**.
*/
private String path;
/**
* The service ID (if any) to map to this route. You can specify a physical URL or
* a service, but not both.
*/
private String serviceId;
/**
* A full physical URL to map to the route. An alternative is to use a service ID
* and service discovery to find the physical address.
*/
private String url;
/**
* Flag to determine whether the prefix for this route (the path, minus pattern
* patcher) should be stripped before forwarding.
*/
private boolean stripPrefix = true;
/**
* Flag to indicate that this route should be retryable (if supported). Generally
* retry requires a service ID and ribbon.
*/
private Boolean retryable;
private Boolean enabled;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getServiceId() {
return serviceId;
}
public void setServiceId(String serviceId) {
this.serviceId = serviceId;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public boolean isStripPrefix() {
return stripPrefix;
}
public void setStripPrefix(boolean stripPrefix) {
this.stripPrefix = stripPrefix;
}
public Boolean getRetryable() {
return retryable;
}
public void setRetryable(Boolean retryable) {
this.retryable = retryable;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
}
}
路由定位器配置:
@Configuration
public class CustomZuulConfig {
@Autowired
ZuulProperties zuulProperties;
@Autowired
ServerProperties server;
@Autowired
JdbcTemplate jdbcTemplate;
@Bean
public CustomRouteLocator routeLocator() {
CustomRouteLocator routeLocator = new CustomRouteLocator(this.server.getServletPrefix(),
this.zuulProperties);
routeLocator.setJdbcTemplate(jdbcTemplate);
return routeLocator;
}
}
動態刷新服務:默認的ZuulConfigure已經配置了事件監聽器,我們只需要發送一個事件就可以實現刷新了。
public class RefreshRouteService {
@Autowired
ApplicationEventPublisher publisher;
@Autowired
RouteLocator routeLocator;
public void refreshRoute() {
RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
publisher.publishEvent(routesRefreshedEvent);
}
}
配置文件:application.properties
#不使用註冊中心,會帶來侵入性
ribbon.eureka.enabled=false
#網關端口
server.port=8080
業務項目
啓動類:BookApplication.java
@RestController
@SpringBootApplication
public class BookApplication {
@RequestMapping(value = "/available")
public String available() {
System.out.println("Spring in Action");
return "Spring in Action";
}
@RequestMapping(value = "/checked-out")
public String checkedOut() {
return "Spring Boot in Action";
}
public static void main(String[] args) {
SpringApplication.run(BookApplication.class, args);
}
}
zuul實現轉發、路由源碼解讀
@Configuration
@EnableConfigurationProperties({ZuulProperties.class
})
@ConditionalOnClass(ZuulServlet.class)
@Import(ServerPropertiesAutoConfiguration.class)
public class ZuulConfiguration {
@Autowired
//zuul的配置文件,對應了application.properties中的配置信息
protected ZuulProperties zuulProperties;
@Autowired
protected ServerProperties server;
@Autowired(required = false)
private ErrorController errorController;
@Bean
public HasFeatures zuulFeature() {
return HasFeatures.namedFeature("Zuul (Simple)", ZuulConfiguration.class);
}
//核心類,路由定位器,最最重要
@Bean
@ConditionalOnMissingBean(RouteLocator.class)
public RouteLocator routeLocator() {
//默認配置的實現是SimpleRouteLocator.class
return new SimpleRouteLocator(this.server.getServletPrefix(),
this.zuulProperties);
}
//zuul的控制器,負責處理鏈路調用
@Bean
public ZuulController zuulController() {
return new ZuulController();
}
//MVC HandlerMapping that maps incoming request paths to remote services.
@Bean
public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes,
zuulController());
mapping.setErrorController(this.errorController);
return mapping;
}
//註冊了一個路由刷新監聽器,默認實現是ZuulRefreshListener.class,這個是我們動態路由的關鍵
@Bean
public ApplicationListener<ApplicationEvent> zuulRefreshRoutesListener() {
return new ZuulRefreshListener();
}
@Bean
@ConditionalOnMissingBean(name = "zuulServlet")
public ServletRegistrationBean zuulServlet() {
ServletRegistrationBean servlet = new ServletRegistrationBean(new ZuulServlet(),
this.zuulProperties.getServletPattern());
// The whole point of exposing this servlet is to provide a route that doesn't
// buffer requests.
servlet.addInitParameter("buffer-requests", "false");
return servlet;
}
// pre filters
@Bean
public ServletDetectionFilter servletDetectionFilter() {
return new ServletDetectionFilter();
}
@Bean
public FormBodyWrapperFilter formBodyWrapperFilter() {
return new FormBodyWrapperFilter();
}
@Bean
public DebugFilter debugFilter() {
return new DebugFilter();
}
@Bean
public Servlet30WrapperFilter servlet30WrapperFilter() {
return new Servlet30WrapperFilter();
}
// post filters
@Bean
public SendResponseFilter sendResponseFilter() {
return new SendResponseFilter();
}
@Bean
public SendErrorFilter sendErrorFilter() {
return new SendErrorFilter();
}
@Bean
public SendForwardFilter sendForwardFilter() {
return new SendForwardFilter();
}
@Configuration
protected static class ZuulFilterConfiguration {
@Autowired
private Map<String, ZuulFilter> filters;
@Bean
public ZuulFilterInitializer zuulFilterInitializer() {
return new ZuulFilterInitializer(this.filters);
}
}
//上面提到的路由刷新監聽器
private static class ZuulRefreshListener implements ApplicationListener<ApplicationEvent> {
@Autowired
private ZuulHandlerMapping zuulHandlerMapping;
private HeartbeatMonitor heartbeatMonitor = new HeartbeatMonitor();
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextRefreshedEvent ||
event instanceof RefreshScopeRefreshedEvent || event instanceof RoutesRefreshedEvent) {
//設置爲髒,下一次匹配到路徑時,如果發現爲髒,則會去刷新路由信息
this.zuulHandlerMapping.setDirty(true);
} else if (event instanceof HeartbeatEvent) {
if (this.heartbeatMonitor.update(
((HeartbeatEvent) event).getValue())) {
this.zuulHandlerMapping.setDirty(true);
}
}
}
}
}
public class SimpleRouteLocator implements RouteLocator {
//配置文件中的路由信息配置
private ZuulProperties properties;
//路徑正則配置器,即作用於path:/books/**
private PathMatcher pathMatcher = new AntPathMatcher();
private String dispatcherServletPath = "/";
private String zuulServletPath;
private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>();
public SimpleRouteLocator(String servletPath, ZuulProperties properties) {
this.properties = properties;
if (servletPath != null && StringUtils.hasText(servletPath)) {
this.dispatcherServletPath = servletPath;
}
this.zuulServletPath = properties.getServletPath();
}
//路由定位器和其他組件的交互,是最終把定位的Routes以list的方式提供出去,核心實現
@Override
public List<Route> getRoutes() {
if (this.routes.get() == null) {
this.routes.set(locateRoutes());
}
List<Route> values = new ArrayList<>();
for (String url : this.routes.get().keySet()) {
ZuulRoute route = this.routes.get().get(url);
String path = route.getPath();
values.add(getRoute(route, path));
}
return values;
}
@Override
public Collection<String> getIgnoredPaths() {
return this.properties.getIgnoredPatterns();
}
//這個方法在網關產品中也很重要,可以根據實際路徑匹配到Route來進行業務邏輯的操作,進行一些加工
@Override
public Route getMatchingRoute(final String path) {
if (log.isDebugEnabled()) {
log.debug("Finding route for path: " + path);
}
if (this.routes.get() == null) {
this.routes.set(locateRoutes());
}
if (log.isDebugEnabled()) {
log.debug("servletPath=" + this.dispatcherServletPath);
log.debug("zuulServletPath=" + this.zuulServletPath);
log.debug("RequestUtils.isDispatcherServletRequest()="
+ RequestUtils.isDispatcherServletRequest());
log.debug("RequestUtils.isZuulServletRequest()="
+ RequestUtils.isZuulServletRequest());
}
String adjustedPath = adjustPath(path);
ZuulRoute route = null;
if (!matchesIgnoredPatterns(adjustedPath)) {
for (Entry<String, ZuulRoute> entry : this.routes.get().entrySet()) {
String pattern = entry.getKey();
log.debug("Matching pattern:" + pattern);
if (this.pathMatcher.match(pattern, adjustedPath)) {
route = entry.getValue();
break;
}
}
}
if (log.isDebugEnabled()) {
log.debug("route matched=" + route);
}
return getRoute(route, adjustedPath);
}
private Route getRoute(ZuulRoute route, String path) {
if (route == null) {
return null;
}
String targetPath = path;
String prefix = this.properties.getPrefix();
if (path.startsWith(prefix) && this.properties.isStripPrefix()) {
targetPath = path.substring(prefix.length());
}
if (route.isStripPrefix()) {
int index = route.getPath().indexOf("*") - 1;
if (index > 0) {
String routePrefix = route.getPath().substring(0, index);
targetPath = targetPath.replaceFirst(routePrefix, "");
prefix = prefix + routePrefix;
}
}
Boolean retryable = this.properties.getRetryable();
if (route.getRetryable() != null) {
retryable = route.getRetryable();
}
return new Route(route.getId(), targetPath, route.getLocation(), prefix,
retryable,
route.isCustomSensitiveHeaders() ? route.getSensitiveHeaders() : null);
}
//注意這個類並沒有實現refresh接口,但是卻提供了一個protected級別的方法,旨在讓子類不需要重複維護一個private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>();也可以達到刷新的效果
protected void doRefresh() {
this.routes.set(locateRoutes());
}
//具體就是在這兒定位路由信息的,我們之後從數據庫加載路由信息,主要也是從這兒改寫
/**
* Compute a map of path pattern to route. The default is just a static map from the
* {@link ZuulProperties}, but subclasses can add dynamic calculations.
*/
protected Map<String, ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
for (ZuulRoute route : this.properties.getRoutes().values()) {
routesMap.put(route.getPath(), route);
}
return routesMap;
}
protected boolean matchesIgnoredPatterns(String path) {
for (String pattern : this.properties.getIgnoredPatterns()) {
log.debug("Matching ignored pattern:" + pattern);
if (this.pathMatcher.match(pattern, path)) {
log.debug("Path " + path + " matches ignored pattern " + pattern);
return true;
}
}
return false;
}
private String adjustPath(final String path) {
String adjustedPath = path;
if (RequestUtils.isDispatcherServletRequest()
&& StringUtils.hasText(this.dispatcherServletPath)) {
if (!this.dispatcherServletPath.equals("/")) {
adjustedPath = path.substring(this.dispatcherServletPath.length());
log.debug("Stripped dispatcherServletPath");
}
}
else if (RequestUtils.isZuulServletRequest()) {
if (StringUtils.hasText(this.zuulServletPath)
&& !this.zuulServletPath.equals("/")) {
adjustedPath = path.substring(this.zuulServletPath.length());
log.debug("Stripped zuulServletPath");
}
}
else {
// do nothing
}
log.debug("adjustedPath=" + path);
return adjustedPath;
}
}
四、內置組件
1. RequestContext
zuul的源碼中是這樣解釋RequestContext的:
RequestContext保存了request、response、狀態信息和數據,以供ZuulFilter來訪問和共享。RequestContext在請求期間有效,並且保存在ThreadLocal中。可以通過設置contextClass替換RequestContext的擴展。RequestContext是ConcurrentHashMap的擴展實現。
setSendZuulResponse
ctx.setSendZuulResponse(false)
表示不進行路由。換句話說,如果設置爲false,這個請求最終不會被zuul轉發到後端服務器,但是如果當前Filter後面還存在其他Filter,那麼其他Filter仍然會被調用到,所以一般我們在Filter的shouldFilter方法中,都會通過
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
if(!ctx.sendZuulResponse()){
return false;
}
}
這樣的方法來做判斷,如果這個請求最終被攔截掉,則後面的過濾器邏輯也不需要執行了