Spring Cloud Gateway動態路由實現

閱讀文本大概需要3分鐘。

0x01: Gateway上線部署分析

當你的網關程序開發完成之後,需要部署到生產環境,這個時候你的程序不能是單點運行的,肯定是多節點啓動(獨立部署或者docker等容器部署),防止單節點故障導致整個服務不能訪問,網關是對客戶端的入口與出口,在生產運行中極爲重要,哪怕是簡單的重啓也會導致部分請求的丟失。

網關的路由配置這個時候就是一個大問題,是代碼裏面編寫還是配置文件配置?他們都有一個致命的缺點,當有新的程序需要接入到網關進行路由或者有服務需要下線時候需要修改代碼或者配置,然後重啓整個網關程序,導致其他正常的服務路由受到了影響。各個網關是否都進行了配置更新?又如何查看當前有哪些配置呢?

Spring Boot Admin對Gateway的支持

Spring Boot Admin是一個管理和監控Spring Boot應用程序的開源軟件。它與應用中的Spring Boot Actuator的無縫對接,提供了方便的管理界面直接管理應用程序,支持客戶端直連模式與註冊中心配置模式。本文暫不介紹Spring Boot Admin的相關配置,會在其他的文檔中單獨講解。
Spring Boot Admin很好的支持了Gateway,可以直接在管理界面中查看相關的路由配置,添加或者刪除。

路由列表

添加路由

爲什麼Spring Boot Admin程序中能有這些功能,是因爲Gateway提供了相應的Actuator Endpoint接口來管理路由配置,那又爲什麼不用呢?下面一步一步分析

Gateway提供的Actuator接口

接口列表


官方默認提供了這些接口進行網關的管理,例如獲取所有的路由:
GET
http://ip:port/actuator/gateway/routes

問題分析

在Spring Boot Admin的管理平臺中刪除路由,會發現刪除失敗,添加的成功後路由配置又是存放到了哪裏呢?配置文件?
如果添加的路由配置不能夠落地,就會在網關重啓之後丟失,這樣明顯沒法實現穩定的動態路由。

Spring Gateway Actuator源碼分析

在GatewayControllerEndpoint類中,定義了相關的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這個對象,他是一個RouteDefinitionWriter接口的對象,RouteDefinitionWriter唯一的繼承是RouteDefinitionRepository類,RouteDefinitionRepository唯一的繼承是InMemoryRouteDefinitionRepository,在InMemoryRouteDefinitionRepository中有這樣的一段代碼

//創建了一個以路由id爲key的路由存儲Map
private final Map<String, RouteDefinition> routes = synchronizedMap(
            new LinkedHashMap<String, RouteDefinition>());

在GatewayAutoConfiguration自動配置類中,有這樣的一段代碼,就是routeDefinitionWriter的申明。

@Bean
@ConditionalOnMissingBean(RouteDefinitionRepository.class)
public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() {
    return new InMemoryRouteDefinitionRepository();
}

原來我們通過actuator接口在新增或者刪除路由配置的時候,都是對routeDefinitionWriter對象中的routes這個Map進行操作。
爲什麼我們能看到在配置文件中配置的路由,但是又刪除不了呢?如果你仔細的閱讀源碼,你會發現/actuator/gateway/routes這個接口獲取的是routeDefinitionLocator中的路由配置,routeDefinitionLocator的類型是CompositeRouteDefinitionLocator,並且他的邏輯是把其他的所有RouteDefinitionLocator類型的都包含進去了,讀取接口/actuator/gateway/routes時,你獲取的是整個系統的全部路由配置。

routes中的routeDefinitionLocator

RouteDefinitionLocator子類

@Bean
@Primary
public RouteDefinitionLocator routeDefinitionLocator(
        List<RouteDefinitionLocator> routeDefinitionLocators) {
    return new CompositeRouteDefinitionLocator(
            Flux.fromIterable(routeDefinitionLocators));
}

根據上面的分析,我們現在有幾個問題需要處理
1、增加的路由配置是保存在內存中的,我們沒有辦法保存它
2、刪除只能刪除通過接口增加的路由配置,配置文件中定義的不能刪除

自定義路由配置存儲

我們需要自定義自己的路由存儲,統一管理,全部路由配置都放在一起,除了一個默認的路由用於最後的默認攔截(其他路由斷言匹配不上的統一走默認的格式返回)

你可以將你的路由配置放到數據庫、mongo、redis等等你方便的地方,這裏我以文件系統爲例介紹如何自定義路由配置存儲。

@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();
    }

    /**
     * 監聽事件刷新配置
     */
    @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("路由配置已加載,加載條數:{}", 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);
    }
}

這裏我們對於save與delete的操作都返回了拒絕的操作,因爲我們的路由配置是統一的管理,同一份配置對應的是n個Gateway節點,增刪需要額外的統一操作,對於路由的獲取根據Event事件加載,這是因爲修改了路由配置並不是需要立即發佈到運行環境中,可能還需要在某一個測試節點上驗證過後在統一的進行上線。

新增的Actuator Endpoint,刷新路由的時候,先加載路由配置到內存中,然後再使用RefreshRoutesEvent事件刷新內存中路由配置。

@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的轉換

[
  {
    "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"
  }
]

其實路由的定義就是RouteDefine對象的創建,根據json反序列化成一個對象即可
id 路由配置的id名字
uri 跳轉的地址,lb://表示基於服務註冊的負載均衡
order 路由的順序,越小越先匹配
predicates 斷言列表,比如根據post並且path是什麼開頭
filters 過濾器列表,匹配後需要做的一些操作,比如增加一個請求頭字段

_genkey_0這個name很奇怪,是因爲官方在定義各種各樣的PredicateFactory時,有些PredicateFactory並沒有字段名稱

genkey名字生成

其實這個算是官方的不規範

線上的推薦方案

路由配置已經統一的進行管理了,可能你放到穩妥的數據庫中,你必須得有一個完善的管理界面來管理路由配置,並且支持一鍵發佈到所有節點,在這之前你還需要讀取發佈到一臺測試機驗證所有的路由配置都是ok的,路由的配置存儲應該加入版本控制。

來源:https://www.jianshu.com/p/8f007bcf36ea

往期精彩

01 漫談發版哪些事,好課程推薦

02 Linux的常用最危險的命令

03 互聯網支付系統整體架構詳解

04 優秀的Java程序員必須瞭解的GC哪些

05 IT大企業有哪些病,別被這些病毀了自己?

關注我每天進步一點點

你點的在看,我都當成了喜歡

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章