近期某个团队的项目交接我所在的项目组,且临近上线,便未对该项目进行改造,故该项目未接入到注册中心。上线后发现忘记购买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
可以看到,9000端口的健康检查已经通过了,8888端口的项目我没有启动,所以失败。