文章出處

微服務架構體系中,通常一個業務系統會有很多的微服務,比如:OrderService、ProductService、UserService...,為了讓調用更簡單,一般會在這些服務前端再封裝一層,類似下面這樣:

前面這一層俗稱為“網關層”,其存在意義在于,將"1對N"問題 轉換成了"1對1”問題,同時在請求到達真正的微服務之前,可以做一些預處理,比如:來源合法性檢測,權限校驗,反爬蟲之類...

傳統方式下,最土的辦法,網關層可以人肉封裝,類似以下示例代碼:

LoginResult login(...){
//TODO 預處理... return userService.login();//調用用戶服務的登錄方法 } Product queryProduct(...){
//TODO 預處理... return productService.queryProduct();//調用產品服務的查詢方法 } Order submitOrder(...){
//TODO 預處理... return orderService.submitOrder();//調用訂單服務的查詢方法 }

這樣做,當然能跑起來,但是維護量大,以后各個微服務增加了新方法,都需要在網關層手動增加相應的方法封裝,而spring cloud 中的zuul很好的解決了這一問題,示意圖如下:

Zuul做為網關層,自身也是一個微服務,跟其它服務Service-1,Service-2, ... Service-N一樣,都注冊在eureka server上,可以相互發現,zuul能感知到哪些服務在線,同時通過配置路由規則(后面會給出示例),可以將請求自動轉發到指定的后端微服務上,對于一些公用的預處理(比如:權限認證,token合法性校驗,灰度驗證時部分流量引導之類),可以放在所謂的過濾器(ZuulFilter)里處理,這樣后端服務以后新增了服務,zuul層幾乎不用修改。

使用步驟:

一、添加zuul依賴的jar包

compile 'org.springframework.cloud:spring-cloud-starter-zuul'

 

二、application.yml里配置路由

zuul:
  routes:
    api-a:
      path: /api-user/**
      service-id: service-provider
      sensitive-headers:
    api-b:
      path: /api-order/**
      service-id: service-consumer 

解釋一下:上面這段配置表示,/api-user/開頭的url請求,將轉發到service-provider這個微服務上,/api-order/開頭的url請求,將轉發到service-consumer這個微服務上。

 

三、熔斷處理

如果網關后面的微服務掛了,zuul還允許定義一個fallback類,用于熔斷處理,參考下面的代碼:

package com.cnblogs.yjmyzz.spring.cloud.study.gateway;

import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
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.Charset;

/**
 * Created by yangjunming on 2017/7/14.
 */
@Component
public class ServiceConsumerFallbackProvider implements ZuulFallbackProvider {

    @Override
    public String getRoute() {
        return "service-consumer";
    }

    @Override
    public ClientHttpResponse fallbackResponse() {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return this.getStatusCode().value();
            }

            @Override
            public String getStatusText() throws IOException {
                return this.getStatusCode().getReasonPhrase();
            }

            @Override
            public void close() {

            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream("Service-Consumer不可用".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                MediaType mt = new MediaType("application", "json", Charset.forName("UTF-8"));
                headers.setContentType(mt);
                return headers;
            }
        };
    }
}

開發人員只要在getRoute這個方法里指定要處理的微服務實例,然后重寫fallbackResponse即可。

此時,如果觀察/health端點,也可以看到hystrix處于融斷開啟狀態

 

四、ZuulFilter過濾器

過濾器是一個很有用的機制,下面分幾種經典場景演示下:

4.1、token校驗/安全認證

網關直接暴露在公網上時,終端要調用某個服務,通常會把登錄后的token傳過來,網關層對token進行有效性驗證,如果token無效(或沒傳token),提示重新登錄或直接拒絕。另外,網關后面的微服務,如果設置了spring security中的basic Auth(即:不允許匿名訪問,必須提供用戶名、密碼),也可以在Filter中處理。參考下面的代碼:

package com.cnblogs.yjmyzz.spring.cloud.study.gateway;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * Created by yangjunming on 2017/7/13.
 */
@Component
public class AccessFilter extends ZuulFilter {

    private static Logger logger = LoggerFactory.getLogger(AccessFilter.class);

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        Object token = request.getParameter("token");

        //校驗token
        if (token == null) {
            logger.info("token為空,禁止訪問!");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            return null;
        } else {
            //TODO 根據token獲取相應的登錄信息,進行校驗(略)
        }

        //添加Basic Auth認證信息
        ctx.addZuulRequestHeader("Authorization", "Basic " + getBase64Credentials("app01", "*****"));

        return null;
    }

    private String getBase64Credentials(String username, String password) {
        String plainCreds = username + ":" + password;
        byte[] plainCredsBytes = plainCreds.getBytes();
        byte[] base64CredsBytes = Base64.encodeBase64(plainCredsBytes);
        return new String(base64CredsBytes);
    }
}

Filter一共有4種類型,其常量值在org.springframework.cloud.netflix.zuul.filters.support.FilterConstants 中定義

	// Zuul Filter TYPE constants -----------------------------------

	/**
	 * {@link ZuulFilter#filterType()} error type.
	 */
	String ERROR_TYPE = "error";

	/**
	 * {@link ZuulFilter#filterType()} post type.
	 */
	String POST_TYPE = "post";

	/**
	 * {@link ZuulFilter#filterType()} pre type.
	 */
	String PRE_TYPE = "pre";

	/**
	 * {@link ZuulFilter#filterType()} route type.
	 */
	String ROUTE_TYPE = "route";  

安全校驗,一般放在請求真正處理之前,所以上面的示例filterType指定為pre,剩下的只要在shouldFilter()、run()方法中重寫自己的邏輯即可。

4.2 動態修改請求參數

zuulFilter可以攔截所有請求參數,并對其進行修改,比如:終端發過來的數據,出于安全要求,可能是經過加密處理的,需要在網關層進行參數解密,再傳遞到后面的服務;再比如:用戶傳過來的token值,需要轉換成userId/userName這些信息,再傳遞到背后的微服務。參考下面的run方法:

    public Object run() {
        try {
            RequestContext context = getCurrentContext();
            InputStream in = (InputStream) context.get("requestEntity");
            if (in == null) {
                in = context.getRequest().getInputStream();
            }
            String body = StreamUtils.copyToString(in, Charset.forName("UTF-8"));
            body = "動態增加一段內容到body中: " + body;
            byte[] bytes = body.getBytes("UTF-8");
            context.setRequest(new HttpServletRequestWrapper(getCurrentContext().getRequest()) {
                @Override
                public ServletInputStream getInputStream() throws IOException {
                    return new ServletInputStreamWrapper(bytes);
                }

                @Override
                public int getContentLength() {
                    return bytes.length;
                }

                @Override
                public long getContentLengthLong() {
                    return bytes.length;
                }
            });
        } catch (IOException e) {
            rethrowRuntimeException(e);
        }
        return null;
    }

更多filter的示例,可以參考官網:https://github.com/spring-cloud-samples/sample-zuul-filters

4.3 灰度發布(Gated Launch/Gray Release) 

大型分布式系統中,灰度發布是保證線上系統安全生產的重要手段,一般的做法為:從集群中指定一臺(或某幾臺)機器,每次做新版本發布前,先只發布這些機器上,先觀察一下是否正常,如果穩定運行后,再發布到其它機器。這種策略(相當于按部分節點來灰度),大多數情況下可以滿足要求,但是有一些特定場景,可能不太適用。

比如:筆者所在的“美味不用等”公司,主要B端用戶為各餐飲品牌的商家,多數情況下,如果新上了一個功能,希望找一些規模較小的餐廳做試點,先看看上線后的運行情況,如果運行良好,再推廣到其它商家。

再比如:后端服務有N多個版本在同時運行,比如V1、V2,現在新加了一個V3版本(這在手機app應用中很常見),希望只有部分升級了app的用戶訪問最新的V3版本服務,其它用戶仍然訪問舊版本,待系統穩定后,再大規模提示用戶升級。

對于這些看上去需求各異的灰度需求,其實本質是一樣的:將請求(根據參數內容+業務規則),將其轉向到特定的灰度機器上。Spring Cloud MicroService中有一個metadata-map(元數據)設置,可以很好的滿足這類需求。

首先要引入一個jar包:(這是github上開源的一個項目ribbon-discovery-filter-spring-cloud-starter

compile 'io.jmnarloch:ribbon-discovery-filter-spring-cloud-starter:2.1.0'

示例如下:

在各個服務的application.yml中設置以下metadata-map

eureka:
  instance:
    metadata-map:
      gated-launch: false

即:所有節點發布后,默認灰度模式為false。然后把特定的灰度機器上的配置,該參數改成true(表明這臺機器是用于灰度驗證的)。

然后在ZuulFilter中參考下面的代碼:

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        Object token = request.getParameter("token");

        //校驗token
        if (token == null) {
            logger.info("token為空,禁止訪問!");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            return null;
        } else {
            //TODO 根據token獲取相應的登錄信息,進行校驗(略)

            //灰度示例
            RibbonFilterContextHolder.clearCurrentContext();
            if (token.equals("1234567890")) {
                RibbonFilterContextHolder.getCurrentContext().add("gated-launch", "true");
            } else {
                RibbonFilterContextHolder.getCurrentContext().add("gated-launch", "false");
            }
        }

        //添加Basic Auth認證信息
        ctx.addZuulRequestHeader("Authorization", "Basic " + getBase64Credentials("app01", "*****"));

        return null;
    }

注意18-23行,這里演示了通過特定的token參數值,將請求引導到gated-lanuch=true的機器上。(注:參考這個原理,大家可以把參數值,換成自己的version-版本號,shopId-商家Id之類)。只要請求參數中的token=1234567890,這次請求就會轉發到灰度節點上。

如果有朋友好奇這是怎么做到的,可以看下io.jmnarloch.spring.cloud.ribbon.predicate.MetadataAwarePredicate 這個類:

    @Override
    protected boolean apply(DiscoveryEnabledServer server) {

        final RibbonFilterContext context = RibbonFilterContextHolder.getCurrentContext();
        final Set<Map.Entry<String, String>> attributes = Collections.unmodifiableSet(context.getAttributes().entrySet());
        final Map<String, String> metadata = server.getInstanceInfo().getMetadata();
        return metadata.entrySet().containsAll(attributes);
    }  

大致原理就是拿上下文中,開發人員設置的屬性 與 服務節點里的metadata-map 進行比較,如果metadata-map中包括開發人員設置的屬性,就返回成功(即:選擇這臺服務器)

示例源碼:https://github.com/yjmyzz/spring-cloud-demo 


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()