Gateway上線部署分析
當(dāng)你的網(wǎng)關(guān)程序開發(fā)完成之后沮趣,需要部署到生產(chǎn)環(huán)境,這個(gè)時(shí)候你的程序不能是單點(diǎn)運(yùn)行的塔猾,肯定是多節(jié)點(diǎn)啟動(dòng)(獨(dú)立部署或者docker等容器部署)耻瑟,防止單節(jié)點(diǎn)故障導(dǎo)致整個(gè)服務(wù)不能訪問(wèn),網(wǎng)關(guān)是對(duì)客戶端的入口與出口厦取,在生產(chǎn)運(yùn)行中極為重要潮太,哪怕是簡(jiǎn)單的重啟也會(huì)導(dǎo)致部分請(qǐng)求的丟失。
網(wǎng)關(guān)的路由配置這個(gè)時(shí)候就是一個(gè)大問(wèn)題虾攻,是代碼里面編寫還是配置文件配置铡买?他們都有一個(gè)致命的缺點(diǎn),當(dāng)有新的程序需要接入到網(wǎng)關(guān)進(jìn)行路由或者有服務(wù)需要下線時(shí)候需要修改代碼或者配置霎箍,然后重啟整個(gè)網(wǎng)關(guān)程序奇钞,導(dǎo)致其他正常的服務(wù)路由受到了影響。各個(gè)網(wǎng)關(guān)是否都進(jìn)行了配置更新漂坏?又如何查看當(dāng)前有哪些配置呢景埃?
Spring Boot Admin對(duì)Gateway的支持
Spring Boot Admin是一個(gè)管理和監(jiān)控Spring Boot應(yīng)用程序的開源軟件媒至。它與應(yīng)用中的Spring Boot Actuator的無(wú)縫對(duì)接,提供了方便的管理界面直接管理應(yīng)用程序谷徙,支持客戶端直連模式與注冊(cè)中心配置模式拒啰。本文暫不介紹Spring Boot Admin的相關(guān)配置,會(huì)在其他的文檔中單獨(dú)講解完慧。
Spring Boot Admin很好的支持了Gateway谋旦,可以直接在管理界面中查看相關(guān)的路由配置,添加或者刪除屈尼。
為什么Spring Boot Admin程序中能有這些功能册着,是因?yàn)镚ateway提供了相應(yīng)的Actuator Endpoint接口來(lái)管理路由配置,那又為什么不用呢脾歧?下面一步一步分析
Gateway提供的Actuator接口
官方默認(rèn)提供了這些接口進(jìn)行網(wǎng)關(guān)的管理甲捏,例如獲取所有的路由:
GET http://ip:port/actuator/gateway/routes
問(wèn)題分析
在Spring Boot Admin的管理平臺(tái)中刪除路由,會(huì)發(fā)現(xiàn)刪除失敗鞭执,添加的成功后路由配置又是存放到了哪里呢司顿?配置文件?
如果添加的路由配置不能夠落地蚕冬,就會(huì)在網(wǎng)關(guān)重啟之后丟失免猾,這樣明顯沒法實(shí)現(xiàn)穩(wěn)定的動(dòng)態(tài)路由。
Spring Gateway Actuator源碼分析
在GatewayControllerEndpoint類中囤热,定義了相關(guān)的api猎提,比如新增或者刪除
@PostMapping("/routes/{id}")
@SuppressWarnings("unchecked")
public Mono<ResponseEntity<Void>> save(@PathVariable String id,
@RequestBody Mono<RouteDefinition> route) {
return this.routeDefinitionWriter.save(route.map(r -> {
r.setId(id);
log.debug("Saving route: " + route);
return r;
})).then(Mono.defer(() -> Mono
.just(ResponseEntity.created(URI.create("/routes/" + id)).build())));
}
@DeleteMapping("/routes/{id}")
public Mono<ResponseEntity<Object>> delete(@PathVariable String id) {
return this.routeDefinitionWriter.delete(Mono.just(id))
.then(Mono.defer(() -> Mono.just(ResponseEntity.ok().build())))
.onErrorResume(t -> t instanceof NotFoundException,
t -> Mono.just(ResponseEntity.notFound().build()));
}
這里面的核心是routeDefinitionWriter這個(gè)對(duì)象,他是一個(gè)RouteDefinitionWriter接口的對(duì)象旁蔼,RouteDefinitionWriter唯一的繼承是RouteDefinitionRepository類锨苏,RouteDefinitionRepository唯一的繼承是InMemoryRouteDefinitionRepository,在InMemoryRouteDefinitionRepository中有這樣的一段代碼
//創(chuàng)建了一個(gè)以路由id為key的路由存儲(chǔ)Map
private final Map<String, RouteDefinition> routes = synchronizedMap(
new LinkedHashMap<String, RouteDefinition>());
在GatewayAutoConfiguration自動(dòng)配置類中棺聊,有這樣的一段代碼伞租,就是routeDefinitionWriter的申明。
@Bean
@ConditionalOnMissingBean(RouteDefinitionRepository.class)
public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() {
return new InMemoryRouteDefinitionRepository();
}
原來(lái)我們通過(guò)actuator接口在新增或者刪除路由配置的時(shí)候限佩,都是對(duì)routeDefinitionWriter對(duì)象中的routes這個(gè)Map進(jìn)行操作葵诈。
為什么我們能看到在配置文件中配置的路由,但是又刪除不了呢祟同?如果你仔細(xì)的閱讀源碼作喘,你會(huì)發(fā)現(xiàn)/actuator/gateway/routes這個(gè)接口獲取的是routeDefinitionLocator中的路由配置,routeDefinitionLocator的類型是CompositeRouteDefinitionLocator晕城,并且他的邏輯是把其他的所有RouteDefinitionLocator類型的都包含進(jìn)去了泞坦,讀取接口/actuator/gateway/routes時(shí),你獲取的是整個(gè)系統(tǒng)的全部路由配置砖顷。
@Bean
@Primary
public RouteDefinitionLocator routeDefinitionLocator(
List<RouteDefinitionLocator> routeDefinitionLocators) {
return new CompositeRouteDefinitionLocator(
Flux.fromIterable(routeDefinitionLocators));
}
根據(jù)上面的分析贰锁,我們現(xiàn)在有幾個(gè)問(wèn)題需要處理
1赃梧、增加的路由配置是保存在內(nèi)存中的,我們沒有辦法保存它
2豌熄、刪除只能刪除通過(guò)接口增加的路由配置授嘀,配置文件中定義的不能刪除
自定義路由配置存儲(chǔ)
我們需要自定義自己的路由存儲(chǔ),統(tǒng)一管理房轿,全部路由配置都放在一起粤攒,除了一個(gè)默認(rèn)的路由用于最后的默認(rèn)攔截(其他路由斷言匹配不上的統(tǒng)一走默認(rèn)的格式返回)
你可以將你的路由配置放到數(shù)據(jù)庫(kù)所森、mongo囱持、redis等等你方便的地方,這里我已文件系統(tǒng)為例介紹如何自定義路由配置存儲(chǔ)焕济。
@Component
public class FileRouteDefinitionRepository implements RouteDefinitionRepository, ApplicationEventPublisherAware {
private static final Logger LOGGER = LoggerFactory.getLogger(FileRouteDefinitionRepository.class);
private ApplicationEventPublisher publisher;
private List<RouteDefinition> routeDefinitionList = new ArrayList<>();
@Value("${gateway.route.config.file}")
private String file;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@PostConstruct
public void init() {
load();
}
/**
* 監(jiān)聽事件刷新配置
*/
@EventListener
public void listenEvent(RouteConfigRefreshEvent event) {
load();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
}
/**
* 加載
*/
private void load() {
try {
String jsonStr = Files.lines(Paths.get(file)).collect(Collectors.joining());
routeDefinitionList = JSON.parseArray(jsonStr, RouteDefinition.class);
LOGGER.info("路由配置已加載,加載條數(shù):{}", routeDefinitionList.size());
} catch (Exception e) {
LOGGER.error("從文件加載路由配置異常", e);
}
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return Mono.defer(() -> Mono.error(new NotFoundException("Unsupported operation")));
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return Mono.defer(() -> Mono.error(new NotFoundException("Unsupported operation")));
}
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return Flux.fromIterable(routeDefinitionList);
}
}
這里我們對(duì)于save與delete的操作都返回了拒絕的操作纷妆,因?yàn)槲覀兊穆酚膳渲檬墙y(tǒng)一的管理,同一份配置對(duì)應(yīng)的是n個(gè)Gateway節(jié)點(diǎn)晴弃,增刪需要額外的統(tǒng)一操作掩幢,對(duì)于路由的獲取根據(jù)Event事件加載,這是因?yàn)樾薷牧寺酚膳渲貌⒉皇切枰⒓窗l(fā)布到運(yùn)行環(huán)境中上鞠,可能還需要在某一個(gè)測(cè)試節(jié)點(diǎn)上驗(yàn)證過(guò)后在統(tǒng)一的進(jìn)行上線际邻。
新增的Actuator Endpoint,刷新路由的時(shí)候芍阎,先加載路由配置到內(nèi)存中世曾,然后再使用RefreshRoutesEvent事件刷新內(nèi)存中路由配置。
@Component
@RestControllerEndpoint(id = "demoGateway")
public class CustomGatewayControllerEndpoint implements ApplicationEventPublisherAware {
private ApplicationEventPublisher publisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@PostMapping("/refreshRouteConfig")
public Mono<Void> refreshRoutes() {
this.publisher.publishEvent(new RouteConfigRefreshEvent(this));
return Mono.empty();
}
}
官方文檔配置與json的轉(zhuǎn)換
[
{
"filters": [
{
"args": {
"name": "hystrix",
"fallbackUri": "forward:/hystrix"
},
"name": "Hystrix"
},
{
"args": {},
"name": "RateLimit"
}
],
"id": "DEMO_API_ROUTE",
"order": 1,
"predicates": [
{
"args": {
"_genkey_0": "/demoApi/**"
},
"name": "Path"
}
],
"uri": "lb://demo-api"
}
]
其實(shí)路由的定義就是RouteDefine對(duì)象的創(chuàng)建谴咸,根據(jù)json反序列化成一個(gè)對(duì)象即可
id 路由配置的id名字
uri 跳轉(zhuǎn)的地址轮听,lb://表示基于服務(wù)注冊(cè)的負(fù)載均衡
order 路由的順序,越小越先匹配
predicates 斷言列表岭佳,比如根據(jù)post并且path是什么開頭
filters 過(guò)濾器列表血巍,匹配后需要做的一些操作,比如增加一個(gè)請(qǐng)求頭字段
_genkey_0這個(gè)name很奇怪珊随,是因?yàn)楣俜皆诙x各種各樣的PredicateFactory時(shí)述寡,有些PredicateFactory并沒有字段名稱
其實(shí)這個(gè)算是官方的不規(guī)范
線上的推薦方案
路由配置已經(jīng)統(tǒng)一的進(jìn)行管理了,可能你放到穩(wěn)妥的數(shù)據(jù)庫(kù)中叶洞,你必須得有一個(gè)完善的管理界面來(lái)管理路由配置鲫凶,并且支持一鍵發(fā)布到所有節(jié)點(diǎn),在這之前你還需要讀取發(fā)布到一臺(tái)測(cè)試機(jī)驗(yàn)證所有的路由配置都是ok的京办,路由的配置存儲(chǔ)應(yīng)該加入版本控制掀序。