SpringCloudGateway整合ribbon实现路由

young 682 2022-06-24

近期某个团队的项目交接我所在的项目组,且临近上线,便未对该项目进行改造,故该项目未接入到注册中心。上线后发现忘记购买LB,导致gateway无法进行转发(测试环境单机部署,未及时发现问题)。故考虑扩展gateway的功能以支持该场景。

参考:https://www.jianshu.com/p/cdf63185b0c3

SpringCloud版本:Hoxton.SR9

引入spring-cloud-starter-netflix-ribbon依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

ribbon配置

my-load-balanced-service: # 建议和项目一致
  ribbon:
    listOfServers: http://localhost:9000, http://localhost:9001, http://localhost:9002 # 应用的地址
    NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList # ServerList实现
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule	# 负载均衡算法
    NFLoadBalancerPingClassName: com.xxx.gateway.common.config.ribbon.CheckServerAlive # 心跳检查
   

因项目中引入了注册中心,所以这里需要使用ConfigurationBasedServerList来接受listOfServers,否则在配置中获取不到应用的地址

心跳检查

心跳检查是为了防止由到下线的应用上而作的检查,项目中都引入了actuator

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

所以考虑服务上线检查都访问/actuator/health

但是有几个问题需要解决:

  • 一部分项目带有servletContextPath,一部分项目没有
  • 项目的端口和actuator的端口可能不是同一个
  • 有的项目可能没有开启actuator,但是有自己的健康检查接口

于是考虑增加一些自定义配置来进行处理

定义健康检查接口

public interface CheckServerUp {
    boolean checkServerUp(RestTemplate restTemplate, ObjectMapper objectMapper, String serverName, String healthCheckUrl);
}

定义默认实现

访问/actuator/health,判断status字段是否为UP

@Slf4j
public class DefaultCheckServerUpImpl implements CheckServerUp {
    private static final String STATUS_FIELD = "status";
    private static final String HEALTH_STATUS = "UP";

    @Override
    public boolean checkServerUp(RestTemplate restTemplate, ObjectMapper objectMapper, String serverName,
                                 String healthCheckUrl) {
        String healthResult = null;
        try {
            healthResult = restTemplate.getForObject(healthCheckUrl, String.class);
        } catch (RestClientException e) {
            log.warn(String.format("health check server[%s] url:%s error", serverName, healthCheckUrl), e);
            return false;
        }
        try {
            JsonNode jsonNode = objectMapper.readTree(healthResult);
            if (jsonNode.has(STATUS_FIELD)) {
                JsonNode status = jsonNode.get(STATUS_FIELD);
                String statusValue = status.asText();
                boolean alive = HEALTH_STATUS.equals(statusValue);
                if (!alive) {
                    log.warn("health check server[{}] url:{} , result:{}", serverName, healthCheckUrl, healthResult);
                }
                return alive;
            }
            return false;
        } catch (JsonProcessingException e) {
            log.error("解析健康检查json异常", e);
            return false;
        }
    }
}

自定义配置类

@Configuration
@ConfigurationProperties(prefix = "ribbon-server-alive")
@Data
public class RibbonServerAliveConfiguration {
    public static final String DEFAULT_CHECK_SERVER_UP_CLASS_NAME = DefaultCheckServerUpImpl.class.getName();
    public static final Class<DefaultCheckServerUpImpl> DEFAULT_CHECK_SERVER_UP_CLASS = DefaultCheckServerUpImpl.class;
    public static final CheckServerUp DEFAULT_CHECK_SERVER_UP_IMPL = new DefaultCheckServerUpImpl();
    private static final Config DEFAULT_CONFIG = new Config();
    private Map<String, Config> config;

    /**
     * 根据服务名获取配置,如果没有配置,则返回默认配置
     */
    public Config getConfigWithServerName(String serverName) {
        return config.getOrDefault(serverName, DEFAULT_CONFIG);
    }

    @Data
    public static class Config {
        /**
         * 服务上线检测的URI
         */
        private String checkServerUpUri = "/actuator/health";
        /**
         * 服务的ServletContextPath
         */
        private String servletContextPath = "";
        /**
         * 服务上线检测的访问的端口
         */
        private List<String> checkServerUpPorts = Collections.emptyList();
        /**
         * 服务上线检测的处理类
         */
        private String checkServerUpClassName = DEFAULT_CHECK_SERVER_UP_CLASS_NAME;
    }

}

辅助类

@Configuration
public class HealthCheckSupportConfig {

    @Bean
    @ConditionalOnMissingBean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

    @Bean
    @ConditionalOnMissingBean
    public ObjectMapper objectMapper(){
        return new ObjectMapper();
    }
}
@Component
public class BeanFactory implements ApplicationContextAware {
    private static final Logger log = LoggerFactory.getLogger(BeanFactory.class);
    private static ApplicationContext applicationContext;

    public BeanFactory() {
    }

    public synchronized void setApplicationContext(ApplicationContext context) {
        applicationContext = context;
    }

    public static Object getBean(String beanName) {
        Object object = null;
        try {
            object = applicationContext.getBean(beanName);
        } catch (NoSuchBeanDefinitionException exception) {
            throw new RuntimeException("获取spring对象发生异常",exception);
        }

        return object;
    }

    public static <T> T getBean(Class<T> clazz) {
        Object object = null;

        try {
            object = applicationContext.getBean(clazz);
        } catch (NoSuchBeanDefinitionException var3) {
          throw new RuntimeException("获取spring对象发生异常",exception);
        }
        return object;
    }
}

自定义配置

ribbon-health:
    config:
        my-load-balanced-service:         
            healthCheckPort: [9000,8888,7777,6666] # 自定义配置,服务上线检查端口,如果不配置,则使用listOfServer中的端口,如果配置一个,则全部server使用该端口,否则按listOfServers的顺序配置对应端口
            servletContextPath: /hahaha # 自定义配置,服务上线检查的servletContextPath,代码里只能拿到HostPort信息,所以需要的场景需要额外配置
            healthCheckUri: /actuator/health #自定义配置,服务上线检查的uri,默认为/actuator/health
            healthCheckClass: # 自定义配置,服务上线检查处理类,不填写走默认值

实现心跳检查

@Slf4j
public class CheckServerAlive extends AbstractLoadBalancerPing {
    /**
     * 应用名称
     */
    private String serverName;
    /**
     * 服务上线检查端口
     */
    private List<String> checkServerUpPorts;
    /**
     * 服务列表
     */
    private String[] servers;
    /**
     * 服务ServletContextPath
     */
    private String servletContextPath;
    /**
     * 服务上线检测检查uri
     */
    private String checkServerUpUri;

    private IClientConfig iClientConfig;
    /**
     * 服务上线检测的类
     */
    private Class<?> checkServerUpClass = null;
    /**
     * 服务上线检测的实例
     */
    private CheckServerUp checkServerUp = null;
    // 配置类
    private RibbonServerAliveConfiguration ribbonServerAliveConfiguration;

    private RestTemplate restTemplate;

    private ObjectMapper objectMapper;

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
        this.iClientConfig = iClientConfig;
        this.serverName = iClientConfig.getClientName().trim();
        ribbonServerAliveConfiguration = BeanFactory.getBean(RibbonServerAliveConfiguration.class);
        restTemplate = BeanFactory.getBean(RestTemplate.class);
        objectMapper = BeanFactory.getBean(ObjectMapper.class);
    }

    @Override
    public boolean isAlive(Server server) {
        // 检查server列表是否为空
        if (isListOfServersEmpty()) {
            return false;
        }
        // 初始化参数
        init();
        // 拼接健康检查url
        String healthCheckUrl = fillHealthCheckUrl(server);
        boolean alive = checkServerUp.checkServerUp(restTemplate, objectMapper, serverName, healthCheckUrl);
        if (!alive) {
            log.warn("health check server[{}] url:{} status not UP ", serverName, healthCheckUrl);
        }
        server.setAlive(alive);
        return alive;
    }

    /**
     * 判断服务列表是否为空
     */
    private boolean isListOfServersEmpty() {
        String listOfServers = iClientConfig.get(CommonClientConfigKey.ListOfServers);
        if (StringUtils.isBlank(listOfServers)) {
            return true;
        }
        servers = StringUtils.split(listOfServers, ",");
        return servers.length == 0;
    }

    /**
     * 初始化参数
     */
    private void init() {
        RibbonServerAliveConfiguration.Config config =
                ribbonServerAliveConfiguration.getConfigWithServerName(serverName);
        servletContextPath = config.getServletContextPath();
        checkServerUpUri = config.getCheckServerUpUri();
        checkServerUpPorts = config.getCheckServerUpPorts();
        checkAndInitCheckServerUpClass(config);
    }

    /**
     * 拼接检查url
     * 例如:http://localhost:9000/hahaha/actuator/health
     */
    private String fillHealthCheckUrl(Server server) {
        Integer checkServerUpPort = getCheckServerUpPort(server);
        // 拼接检查url
        String checkServerUpUrl =
                server.getScheme() + "://" + (server.getHost() + ":" + checkServerUpPort + "/" + servletContextPath +
                        "/" + checkServerUpUri).replaceAll("/+", "/");
        log.info("server:{},checkServerUpUrl:{}", serverName, checkServerUpUrl);
        return checkServerUpUrl;
    }

    /**
     * 获取检查服务端口
     */
    private Integer getCheckServerUpPort(Server server) {
        if (CollectionUtil.isEmpty(checkServerUpPorts)) {
            return server.getPort();
        }
        // 如果只配置了1个
        if (checkServerUpPorts.size() == 1) {
            return Integer.valueOf(checkServerUpPorts.get(0).trim());
        } else {
            String hostPort = server.getHostPort();
            // 配置了多个
            for (int i = 0; i < servers.length; i++) {
                // 匹配顺序
                if (servers[i].contains(hostPort)) {
                    return Integer.valueOf(checkServerUpPorts.get(i).trim());
                }
            }
        }
        return server.getPort();
    }

    /**
     * 检查并且初始化健康检查实现类
     */
    private void checkAndInitCheckServerUpClass(RibbonServerAliveConfiguration.Config config) {
        String checkServerUpClassName = config.getCheckServerUpClassName();
        if (StringUtils.isBlank(checkServerUpClassName)) {
            checkServerUpClassName = RibbonServerAliveConfiguration.DEFAULT_CHECK_SERVER_UP_CLASS_NAME;
        }
        if (checkServerUpClass != null) {
            if (!StringUtils.equals(checkServerUpClass.getName(), checkServerUpClassName)) {
                initCheckServerUpClass(checkServerUpClassName);
            }
        } else {
            initCheckServerUpClass(checkServerUpClassName);
        }
    }

    /**
     * 初始化检查实例
     */
    private void initCheckServerUpClass(String checkServerUpClassName) {
        // 如果是默认的,直接取配置的单例
        if (StringUtils.equals(checkServerUpClassName,
                RibbonServerAliveConfiguration.DEFAULT_CHECK_SERVER_UP_CLASS_NAME)) {
            checkServerUpClass = RibbonServerAliveConfiguration.DEFAULT_CHECK_SERVER_UP_CLASS;
            checkServerUp = RibbonServerAliveConfiguration.DEFAULT_CHECK_SERVER_UP_IMPL;
            return;
        }
        Class<?> clazz = null;
        try {
            clazz = Class.forName(checkServerUpClassName);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        Object bean;
        try {
            // 从spring容器获取
            bean = BeanFactory.getBean(clazz);
        } catch (Exception e) {
            try {
                // Spring容器没有,则利用反射初始化
                bean = clazz.newInstance();
            } catch (InstantiationException | IllegalAccessException ex) {
                throw new RuntimeException(ex);
            }
        }
        if (bean instanceof CheckServerUp) {
            this.checkServerUp = (CheckServerUp) bean;
            this.checkServerUpClass = clazz;
        } else {
            log.error("检查配置类不是CheckServerUp的子类,采用默认处理");
            checkServerUpClass = RibbonServerAliveConfiguration.DEFAULT_CHECK_SERVER_UP_CLASS;
            checkServerUp = RibbonServerAliveConfiguration.DEFAULT_CHECK_SERVER_UP_IMPL;
        }
    }
}

配置路由:

我的项目中采用了Nacos作为配置中心,动态路由采用的json格式,所以对应配置为

{
    "id": "my-load-balanced-service",
    "order": 0,
    "predicates": [{
        "args": {
            "pattern": "/hahaha/**"
        },
        "name": "Path"
    }],
    
    "uri": "lb://my-load-balanced-service"
}

如果采用yaml的话

spring:
  cloud:
    gateway:
      routes:
        - id: my-load-balanced-service
          predicates:
            - Path=/hahaha/**
          uri: lb://my-load-balanced-service

启动gateway,然后访问我本地起的项目http://localhost:8089/hahaha/test

gateway-ribbon-1

可以看到,9000端口的健康检查已经通过了,8888端口的项目我没有启动,所以失败。