Netflix ZuulとDisconfを組み合わせた動的ルーティングリフレッシュ

14395 ワード

会社はDisconfを使用して分散構成を管理し、Disconfはzookeeperを使用して構成ストレージを提供し、通知インタフェースを予約して構成変更を受信する.
1.Disconf更新通知エントリの構成
DisconfにはIDisconfUpdatePipelineインタフェースが用意されており、このインタフェースはDisconfコンソールに保存されているプロファイルが変更されたときにリアルタイムでメッセージを得ることができる.コードは以下の通りである.
@Slf4j
@Service
@Scope("singleton")
public class CustomDisconfReloadUpdate implements IDisconfUpdatePipeline {

    /**
     * Store changed configuration file's path
     */
    private static final ThreadLocal newConfigFilePath = new ThreadLocal<>();

    @Autowired
    ApplicationEventPublisher publisher;

    @Autowired
    RouteLocator routeLocator;

    /**
     *   zuul refresh  
     */
    public void refreshRoute() {
        RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
        publisher.publishEvent(routesRefreshedEvent);
    }

    @Override
    public void reloadDisconfFile(String key, String filePath) throws Exception {
        log.info("------Got reload event from disconf------");
        log.info("Change key: {}, filePath: {}", key, filePath);
        newConfigFilePath.set(filePath);
        refreshRoute();
    }

    @Override
    public void reloadDisconfItem(String key, Object content) throws Exception {
        log.info("------Got reload event from disconf with item------");
        log.info("Change key: {}, content: {}", key, content);
    }

    /**
     * Checkout configFilePath and remove the ThreadLocal value content
     * @return
     */
    public static String getConfigFilePath() {
        String path = newConfigFilePath.get();
        newConfigFilePath.remove();
        return path;
    }
}
  • 1.ThreadLocalを使用して、変更するプロファイルをローカルのパスに保存し、スレッドの後続の実行時に
  • を使用して読み込む.
  • 2.RoutesRefreshedEventイベント機構を使用してZuulのルーティングテーブル
  • をリフレッシュする.
    2.カスタムRouteLocatorによるルーティングテーブルのリフレッシュ機能
    ZuulはデフォルトでSimpleRouteLocatorをルーティング発見エントリとして使用しているが、動的リフレッシュはサポートされていない.ZuulはRefreshableRouteLocatorインタフェースを予約して動的ルーティングテーブルのリフレッシュをサポートし、以下はカスタムクラスがルーティングリフレッシュ機能を実現することである.
    @Slf4j
    public class CustomZuulRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
    
        private ZuulProperties properties;
        private SpringClientFactory springClientFactory;
    
        public CustomZuulRouteLocator(String servletPath, ZuulProperties properties, SpringClientFactory springClientFactory) {
            super(servletPath, properties);
            this.properties = properties;
            this.springClientFactory = springClientFactory;
        }
    
        @Override
        public void refresh() {
            doRefresh();
        }
    
        @Override
        protected Map locateRoutes() {
            LinkedHashMap routesMap = new LinkedHashMap<>();
            // This will load the old routes which exist in cache
            routesMap.putAll(super.locateRoutes());
            try {
                // If server can load routes from file which means the file changed, using the new config routes
                Map newRouteMap = loadRoutesFromDisconf();
                if(newRouteMap.size() > 0) {
                    log.info("New config services list: {}", Arrays.toString(newRouteMap.keySet().toArray()));
                    routesMap.clear();
                    routesMap.putAll(newRouteMap);
                }
            } catch (Exception e) {
                // For every exception, do not break the gateway working
                log.error(e.getMessage(), e);
            }
            LinkedHashMap values = new LinkedHashMap<>();
            for (Map.Entry entry : routesMap.entrySet()) {
                String path = entry.getKey();
                // Prepend with slash if not already present.
                if (!path.startsWith("/")) {
                    path = "/" + path;
                }
                if (StringUtils.hasText(this.properties.getPrefix())) {
                    path = this.properties.getPrefix() + path;
                    if (!path.startsWith("/")) {
                        path = "/" + path;
                    }
                }
                values.put(path, entry.getValue());
            }
            return values;
        }
    
        /**
         * Read the new config file and reload new route and service config
         * @return
         */
        private Map loadRoutesFromDisconf() {
            log.info("----load configuration----");
            Map latestRoutes = new LinkedHashMap<>(16);
            String configFilePath = CustomDisconfReloadUpdate.getConfigFilePath();
            if(configFilePath != null) {
                String newConfigContent = readFileContent(configFilePath);
                ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
                try {
                    Map configResult = objectMapper.readValue(newConfigContent, Map.class);
                    // Store all serviceId with ribbon configuration
                    List allServiceIds = new ArrayList<>();
                    // This Node used to deal with Zuul route configuration
                    if(configResult.containsKey("zuul")) {
                        Map zuulConfig = (Map) configResult.get("zuul");
                        if(zuulConfig.containsKey("routes")) {
                            Map routes = (Map) zuulConfig.get("routes");
                            if(routes != null) {
                                for(Map.Entry tempRoute : routes.entrySet()) {
                                    String id = tempRoute.getKey();
                                    Map routeDetail = (Map) tempRoute.getValue();
                                    ZuulProperties.ZuulRoute zuulRoute = generateZuulRoute(id, routeDetail);
                                    // Got list with config serviceId
                                    if(zuulRoute.getServiceId() != null) {
                                        allServiceIds.add(zuulRoute.getServiceId());
                                    }
                                    latestRoutes.put(zuulRoute.getPath(), zuulRoute);
                                }
                            }
                        }
                    }
                    // deal with all serviceIds and read properties from yaml configuraiton
                    if(allServiceIds.size() > 0) {
                        allServiceIds.forEach(temp -> {
                            // Exist this serviceId
                            if(configResult.containsKey(temp)) {
                                Map serviceRibbonConfig = (Map) configResult.get(temp);
                                if(serviceRibbonConfig.containsKey("ribbon")) {
                                    Map ribbonConfig = (Map) serviceRibbonConfig.get("ribbon");
                                    if(ribbonConfig.containsKey("listOfServers")) {
                                        String listOfServers = (String) ribbonConfig.get("listOfServers");
                                        String ruleClass = (String) ribbonConfig.get("NFLoadBalancerRuleClassName");
                                        String[] serverList = listOfServers.split(",");
                                        dealLoadBalanceConfig(temp, ruleClass, Arrays.asList(serverList));
                                    }
                                }
                            }
                        });
                    }
                } catch (IOException e) {
                    log.error(e.getMessage(), e);
                }
            }
            return latestRoutes;
        }
    
        /**
         * Change loadbalancer's serverList configuration
         */
        private void dealLoadBalanceConfig(String serviceId, String newRuleClassName, List servers) {
            DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) springClientFactory.getLoadBalancer(serviceId);
            if(loadBalancer != null) {
                // 1.Reset listOfServer content if listOfServers changed
                List newServerList = new ArrayList<>();
                servers.forEach(temp -> {
                    // remove the empty characters
                    temp = temp.trim();
                    newServerList.add(temp);
                });
                List oldServerList = loadBalancer.getServerListImpl().getUpdatedListOfServers();
                // Judge the listOfServers changed or not
                boolean serversChanged = false;
                if(oldServerList != null) {
                    if(oldServerList.size() != newServerList.size()) {
                        serversChanged = true;
                    } else {
                        for(Server temp : oldServerList) {
                            if(!newServerList.contains(temp.getId())) {
                                serversChanged = true;
                                break;
                            }
                        }
                    }
                } else {
                    serversChanged = true;
                }
                // listOfServers has changed
                if(serversChanged) {
                    log.info("ServiceId: {} has changed listOfServers, new: {}", serviceId, Arrays.toString(servers.toArray()));
                    loadBalancer.setServerListImpl(new ServerList() {
                        @Override
                        public List getInitialListOfServers() {
                            return null;
                        }
    
                        @Override
                        public List getUpdatedListOfServers() {
                            List newServerConfigList = new ArrayList<>();
                            newServerList.forEach(temp -> {
                                Server server = new Server(temp);
                                newServerConfigList.add(server);
                            });
                            // Using the new config listOfServers
                            return newServerConfigList;
                        }
                    });
                }
    
                // Reset loadBalancer rule
                if(loadBalancer.getRule() != null) {
                    String existRuleClassName = loadBalancer.getRule().getClass().getName();
                    if(!newRuleClassName.equals(existRuleClassName)) {
                        log.info("ServiceId: {}, Old rule class: {}, New rule class: {}", serviceId, existRuleClassName, newRuleClassName);
                        initializeLoadBalancerWithNewRule(newRuleClassName, loadBalancer);
                    }
                } else {
                    initializeLoadBalancerWithNewRule(newRuleClassName, loadBalancer);
                    log.info("ServiceId: {}, Old rule class: Null, Need rule class: {}", serviceId, newRuleClassName);
                }
            }
        }
    
        /**
         * Change loadBalancer's rule
         * @param ruleClassName
         * @param loadBalancer
         */
        private void initializeLoadBalancerWithNewRule(String ruleClassName, DynamicServerListLoadBalancer loadBalancer) {
            try {
                // Create specify class instance
                Class clazz = Class.forName(ruleClassName);
                Constructor constructor = clazz.getConstructor();
                IRule rule = (IRule) constructor.newInstance();
                // Bind loadBalancer with this Rule
                rule.setLoadBalancer(loadBalancer);
                loadBalancer.setRule(rule);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        }
    
        /**
         * Generate zuul route object from configuration
         * @param id
         * @param routeDetail
         * @return
         */
        private ZuulProperties.ZuulRoute generateZuulRoute(String id, Map routeDetail) {
            ZuulProperties.ZuulRoute route = new ZuulProperties.ZuulRoute();
            route.setId(id);
            if(routeDetail.containsKey("path")) {
                route.setPath((String) routeDetail.get("path"));
            }
            if(routeDetail.containsKey("serviceId")) {
                route.setServiceId((String) routeDetail.get("serviceId"));
            }
            if(routeDetail.containsKey("url")) {
                route.setUrl((String) routeDetail.get("url"));
            }
            if(routeDetail.containsKey("stripPrefix")) {
                route.setStripPrefix((Boolean) routeDetail.get("stripPrefix"));
            }
            return route;
        }
    
        /**
         * Read config file from local file system
         * @param configFile
         * @return
         */
        private String readFileContent(String configFile) {
            try {
                FileInputStream inputStream = new FileInputStream(new File(configFile));
                String content = IOUtils.toString(inputStream, "UTF-8");
                return content;
            } catch (IOException e) {
                log.error(e.getMessage(), e);
            }
            return null;
        }
    }
    
  • 1.第1のステップで発行するrefreshEventは、最終的にはrefresh()メソッドのdoRefresh()メソッドを呼び出し、doRefresh()は親インプリメンテーションでlocateRoutes()を呼び出して新しいルーティングテーブル
  • を取得する.
  • 2.弊社ではyamlプロファイルを使用しておりますので、解析プロファイルデータ比較ルーティングルールが変更されたかどうかを確認する必要があります.ここではjackson-dataformat-yaml解析
  • を使用しています.
  • 3.当社はRibbonを使用してクライアントの負荷等化を行うため、ルーティングテーブルが変化する場合、負荷等化のルールも変化する可能性があり、この部分の動的リフレッシュはDynamicServerListLoadBalancerを修正することによって
  • を完了する必要がある.
    3.デフォルトのSimpleRouteLocatorを置き換える
    上記の2つのステップが完了すると、Zuulの動的ルーティング機能は基本的にサポートされ、システムのデフォルトのSimpleRouteLocatorを私たちの実装クラスに置き換える必要があります.Bean注釈を使用してカスタムクラスを有効にするだけでいいです.
      @Bean
      public CustomZuulRouteLocator routeLocator() {
            CustomZuulRouteLocator customZuulRouteLocator = new CustomZuulRouteLocator(serverProperties.getServlet().getServletPrefix(), zuulProperties, springClientFactory);
            return customZuulRouteLocator;
        }