一、閘道器服務
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: 127.0.0.1:8848
discovery:
server-addr: 127.0.0.1:8848
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: 127.0.0.1:8848
discovery:
server-addr: 127.0.0.1:8848
服務通過上述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