微服務工程中,基礎元件應用

語言: CN / TW / HK

一、閘道器服務

1、閘道器模式

閘道器作為架構的最外層服務,用來統一攔截各個埠的請求,識別請求合法性,攔截異常動作,並提供路由和負載能力,保護業務服務;這種策略與外觀模式異曲同工。

閘道器服務和門面類服務有部分的邏輯相似,閘道器服務的攔截側重處理通用的策略和路由負載,而不同的門面聚合服務側重場景分類,例如常見的幾種門面服務:

  • Facade:服務產品開放的埠請求,例如Web,App,小程式等;
  • Admin:通常服務於內部的管理系統,例如Crm,BI報表,控制檯等;
  • Third:聚合第三方的對接服務,例如簡訊,風控,動作埋點等;

不同的門面服務中,也會存在特定的攔截策略,如果把Facade、Admin、Third等校驗都整合在閘道器中,很顯然會加重閘道器服務的負擔,不利於架構的穩定。

2、Gateway元件

如果微服務架構接觸較早的話,初期閘道器中常採用的是Zuul元件,後來SpringCloud才釋出Gateway元件,是當前常用選型。

  • 請求攔截:閘道器作為API請求的開放入口,完成請求的攔截、識別校驗等是基礎能力;
  • 定製策略:除常規身份識別,根據服務場景設計相應的攔截邏輯,儘量攔截異常請求;
  • 服務路由:請求通過攔截後轉發到具體的業務服務,這裡存在兩個核心動作路由和負載;

作為微服務架構中常用的選型元件,下面從使用細節中詳細分析Gateway閘道器的使用方式,與其他元件的對接流程和模式。

Nacos作為微服務的註冊和配置中心,已經是當下常用的元件,並且Nacos也提供Gateway元件的整合案例,首先就是把閘道器服務註冊到Nacos:

spring:
  cloud:
    nacos:
      config:
        server-addr: nacos-port
      discovery:
        server-addr: nacos-port

Nacos管理閘道器服務註冊和相關配置檔案,在專案中通常與nacos共用一套MySQL庫,用來管理閘道器的服務路由資料,是當下比較常見的解決方案。

3、閘道器攔截

GlobalFilter:閘道器中的全域性過濾器,攔截經過閘道器的所有請求,經過相應的校驗策略,判斷請求是否需要執行:

@Order(-1)
@Component
public class GatewayFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        return chain.filter(exchange);
    }
}

通常在閘道器中會執行一些必要共性攔截,例如:IP黑白名單,Token身份令牌,在請求中獲取對應引數,執行相關服務的校驗方法即可。

4、動態路由

4.1 路由定義

在服務路由的實現上,存在複雜的邏輯和策略,來適配各種場景;路由的概念如何定義,可以查閱Gateway元件的原始碼RouteDefinition物件,結構涉及到幾個核心的屬性:路由、斷言、過濾、元資料:

  • Route路由:由ID、轉發Uri、斷言、過濾、元資料組成;
  • Predicate斷言:判斷請求和路由是否匹配;
  • Filter過濾:可以對請求動作進行修改,例如引數;
  • Metadata元資料:裝載路由服務的元資訊;
@Validated
public class RouteDefinition {
    private String id;
    @NotNull
    private URI uri;
    @NotEmpty
    @Valid
    private List<PredicateDefinition> predicates = new ArrayList<>();
    @Valid
    private List<FilterDefinition> filters = new ArrayList<>();
    private Map<String, Object> metadata = new HashMap<>();
}

通常把這些路由放在nacos庫的config_route資料表中,圍繞上述路由物件的結構,管理對應的表資料即可:

關於轉發的目標uri也有不同的配置,這裡選擇lb://服務註冊名的模式,即把請求負載均衡分配到路由對應的服務節點,補充說明一下Nacos元件內部採用Ribbon負載演算法,可以參考相關的文件。

4.2 管理路由

在路由的管理上有兩個核心介面:Locator載入Writer增刪,並且還提供了聚合的Repository介面:

public interface RouteDefinitionLocator {
    // 獲取路由列表
    Flux<RouteDefinition> getRouteDefinitions();
}
public interface RouteDefinitionWriter {
    // 儲存路由
    Mono<Void> save(Mono<RouteDefinition> route);
    // 刪除路由
    Mono<Void> delete(Mono<String> routeId);
}
public interface RouteDefinitionRepository extends RouteDefinitionLocator, RouteDefinitionWriter{}

這樣通過定義路由管理元件,實現上述聚合介面,完成路由資料從資料表載入到應用的過程:

@Component
public class RouteFactory implements RouteDefinitionRepository {
    @Resource
    private RouteService routeService ;

    // 載入全部路由
    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        return Flux.fromIterable(routeService.getRouteDefinitions());
    }
}

RouteService則是路由管理的服務類,管理配置資料以及實體物件與路由定義物件的轉換:

@Service
public class RouteServiceImpl implements RouteService {
    // 資料庫路由實體,轉換為閘道器路由的定義物件
    private RouteDefinition buildRoute (ConfigRoute configRoute){
        RouteDefinition routeDefinition = new RouteDefinition () ;
        // 基礎
        routeDefinition.setId(configRoute.getRouteId());
        routeDefinition.setOrder(configRoute.getOrders());
        routeDefinition.setUri(URI.create(configRoute.getUri()));
        // 斷言
        routeDefinition.setPredicates(JSONUtil.parseArray(configRoute.getPredicates()).toList(PredicateDefinition.class));
        // 過濾
        routeDefinition.setFilters(JSONUtil.parseArray(configRoute.getFilters()).toList(FilterDefinition.class));
        return routeDefinition ;
    }
}

通過上述流程即可將路由資訊持久化儲存在資料庫,如果服務節點很少,也可以直接在nacos的配置檔案中管理。

4.3 匹配模式

Predicate斷言其實就是一個匹配規則的設定,查閱PredicateDefinition相關原始碼可知,有Host、Path、Time等多種模式,通過上述資料可知本文采用的是路徑匹配,

由於採用路徑匹配的方式,會把/服務名拼接在uri起始位置,所以在配置過濾規則的時候設定StripPrefix去掉該路由標識。

請求進入閘道器之後,首先進入全域性攔截器執行校驗策略,檢驗通過之後匹配相關路由的服務,匹配成功之後進行過濾操作,最終將請求轉發到相應的服務,這就是請求在閘道器中要執行的核心流程。

二、註冊與配置

Nacos在整個微服務體系中,提供服務註冊與配置管理兩個核心能力,通常在程式碼工程中只保留核心的bootstrap配置檔案即可,可以極大簡化工程中的配置並且提高相關資料的安全性。

1、服務註冊

Nacos支援基於DNS和基於RPC的服務發現,並提供對服務的實時健康檢查,阻止向非健康的主機或服務例項傳送請求:

在服務的註冊列表中可以檢視註冊資訊,例項數健康數等,並且可以刪除註冊服務;在詳情中可以檢視具體例項資訊,可以對服務例項進行動態上下線和相關配置編輯。

2、配置檔案

通常會採用namespace名稱空間的管理,隔離開不同的服務的註冊與配置,可以在nacos庫中tenant_info檢視,一般會存在如下幾個分類:

這是最常見的名稱空間,gateway閘道器比較獨立,seate事務的配置比較複雜,serve業務服務具有大量的公共配置,通常採用如下的策略:

  • application.yml:所有服務的公共配置,例如mybatis;
  • dev||pro.yml:環境隔離配置,不同環境設定不同的中介軟體地址;
  • serve.yml:服務的個性化配置,比如連線的庫,引數等;
spring:
  application:
    name: facade
  profiles:
    active: dev,facade
  cloud:
    nacos:
      config:
        prefix: application
        file-extension: yml
        server-addr: nacos-port
      discovery:
        server-addr: nacos-port

服務通過上述profiles引數會識別和載入對應的配置檔案,在這種管理模式中,環境的差異只在dev||pro.yml配置檔案中維護即可,其他配置會相對穩定許多。

三、服務間呼叫

1、Feign元件

Feign元件是宣告式、模板化的HTTP客戶端,可以讓服務之間的呼叫變得更簡單優雅,通常將服務提供Feign介面在獨立的程式碼包中管理,方便被其他服務依賴使用:

/**
 * 指定服務名稱
 */
@FeignClient(name = "account")
@Component
public interface FeignService {
    /**
     * 服務的API介面
     */
    @RequestMapping(value = "/user/profile/{paramId}", method = RequestMethod.GET)
    Rep<Entity> getById(@PathVariable("paramId") String paramId);
}
public class Rep<T> {
    // 響應編碼
    private int code;
    // 語義描述
    private String msg;
    // 返回資料
    private T data;
}

通常會把Feign介面的響應格式做包裝,實現返參結構統一管理,有利於呼叫端的識別,這裡就涉及到泛型資料的處理問題。

2、響應解碼

通過繼承ResponseEntityDecoder類,實現自定義的Feign介面響應資料處理,例如返參風格,資料轉換等:

/**
 * 配置解碼
 */
@Configuration
public class FeignConfig {
    @Bean
    @Primary
    public Decoder feignDecoder(ObjectFactory<HttpMessageConverters> feignHttpConverter) {
        return new OptionalDecoder(
                new FeignDecode(new SpringDecoder(feignHttpConverter)));
    }
}
/**
 * 定義解碼
 */
public class FeignDecode extends ResponseEntityDecoder {
    public FeignDecode(Decoder decoder) {
        super(decoder);
    }
    @Override
    public Object decode(Response response, Type type) {
        if (!type.getTypeName().startsWith(Rep.class.getName())) {
            throw new RuntimeException("響應格式異常");
        }
        try {
            return super.decode(response, type);
        } catch (IOException e) {
            throw new RuntimeException(e.getMessage());
        }
    }
}

3、請求解析

採用註解方式就輕鬆實現服務間的通訊請求,查閱Feign元件的原始碼可以理解封裝邏輯,其內在是把介面呼叫轉換成HTTP請求:

  • FeignClientBuilder:不採用註解的方式直接構建Feign請求的客戶端,該類有助於理解@FeignClient註解原理;
  • FeignClientsRegistrar:即專案中採用@FeignClient註解方式,該API中描述了註解的解析方式和服務請求的構建邏輯;

微服務工程的架構是一項複雜和持續的過程,其中涉及到的元件也十分繁雜,本文只是選取Gateway、Nacos、Feign三個基礎元件做簡單的總結,在其邏輯的理解上需要圍繞該元件的核心功能和專案使用的API作為切入點,時常查閱原始碼和官方文件。

四、參考原始碼

應用倉庫:
https://gitee.com/cicadasmile/butte-flyer-parent

元件封裝:
https://gitee.com/cicadasmile/butte-frame-parent