ribbon 配置 动态更新_Ribbon的应用
1 背景19年笔者一直在跟进一个NFC相关的项目(这个会在以后的文章中详细介绍),年关临近,项目即将产品化,许多第三方提供的能力、服务都准备验收,其中有一个关键的供应商的验收出了些问题。具体来说,该供应商的交付物是web应用,已docker容器的方式部署在我公司的服务器上,由于不满足公司容器集群的相关要求,无法纳入k8s统一管理,于是成为了“野生”docker,关于此类docker的运维部署方案却
1 背景
19年笔者一直在跟进一个NFC相关的项目(这个会在以后的文章中详细介绍),年关临近,项目即将产品化,许多第三方提供的能力、服务都准备验收,其中有一个关键的供应商的验收出了些问题。具体来说,该供应商的交付物是web应用,已docker容器的方式部署在我公司的服务器上,由于不满足公司容器集群的相关要求,无法纳入k8s统一管理,于是成为了“野生”docker,关于此类docker的运维部署方案却着实难住了我。
难点有很多,这次我们只说高可用这一件事,简单来讲高可用就是可侦测,可切换,可恢复三个大方向。落实到编程技术方面就是拨测、负载均衡和重试(这可能是最简陋的实现了),接下来说说我是怎么思考和实现这三方面的。
2 方案
两个方案:
- 服务端负载均衡,创建几个docker,前置一个nginx;或者搞一个docker-swarm
- 客户端负载均衡,ribbon,eureka这一套,访问一个自定义的域名,由域名指向不同的server
先说方案1,对于服务调用方,也就是我,这是最省心的方案了。代码修改几乎为0,运维监控也完全不需要我操心,相当于所有的权限责任都划归运维团队就好了。但是,由于docker不能纳入集群管理,运维团队对野生docker投入的资源能有多少呢?而且这样做客户端丧失了个性化配置的主动权。另一方面,本良部署几个docker就可以的事,现在还要swarm,nginx增加了组件也就增加了运维成本和监控点。本着自己的服务自己负责的原则,决定用客户端负载均衡
3 实现
客服端负载均衡,笔者公司用的spring-cloud这一套架构,从前往后看,将服务注册到euraka,这需要服务端开发,所以不行,其余两个还可以试一下。于是决定用ribbon。
ribbon的基本用法google一下随处可见,原理也很清晰,对请求做拦截,实现不同的负载均衡算法(说句题外话,很多spring的架构都逃不开动态代理,反射)具体可以看这篇文章。下面说一下思路,配置自定义ribbon,先看看默认的实现是否符合要求,如果符合直接用(自己实现的肯定没有源码nb,我还是很有自知之明的),不符合的自己写个简单的
@Configuration
@RibbonClient(name = "test-server", configuration = CustomizeRibbonConfig.class)
public class CustomizeRibbonClient {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
// 用okhttpclient做访问,简单实现
OkHttp3ClientHttpRequestFactory factory = new OkHttp3ClientHttpRequestFactory(OkHttpBuilderFactory.getTrusClient());
return new RestTemplate(factory);
}
}
当扫描到@LoadBalanced时,会默认生成一个DynamicServerListLoadBalancer,这个是核心配置,ribbon的配置会注册到DynamicServerListLoadBalancer上。ribbon的自定义配置是从@RibbonClient(name ="test-server", configuration = CustomizeRibbonConfig.class)注解开始的,这里的name就是你访问的域名,当然也支持配制的,CustomizeRibbonConfig 这个类就是各个功能组件的配置类,下面的restTemplate就是访问的客户端对象,此处可以配置用OkHttp3Client,ribbon是支持OkHttp的。
注意,很多博客会说CustomizeRibbonConfig这个类不能被spring扫描到,否则会覆盖全局配置,其实想想,既然是做自定义ribbon配置,肯定要有自定义的配置项啊,如果怕用@ComponentScan避免加载的方式影响项目其他模块的加载,CustomizeRibbonConfig类上可以不写@Configuration。
继续来看,当启动扫描到@RibbonClient注解时,它会创建spring子上下文,寻找如下七个对象:
- IClientConfig 自定义ribbon的所有配置项,默认读取yml文件中@RibbonClient(name ="test-server") name的value对应的配置,
- IRule 定义负载均衡策略
- IPing 定义如何ping目标服务实例来判断是否存活,默认返回true,不处理
- ServerList定义如何获取服务实例列表. 有两个默认实现,一个是基于配置文件,一个基于eureka
- ServerListFilter用来使用期望的特征过滤静态配置动态获得的候选服务实例列表
- ServerListUpdater 用来定义服务器列表的更新方式,默认有定时读取的和注册监听eureka事件的
如果没找到就会默认创建,其中超时、重试等功能可以通过IClientConfig 配置,通过 IPing 可以实现一些简单的拨测,如下:
@Bean
public IPing getIPing(){
return server -> {
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(server.getHost(), server.getPort()), Config.getPingConnectionTimeoutMs());
} catch (IOException e) {
log.warn("Failed to connect to {} in zone {} via {}", server.getId(), server.getZone(), server.getHostPort());
return false;
}
return true;
};
}
下面介绍下serverList的更新机制
我们先看ServerList接口
public interface ServerList<T extends Server> {
public List<T> getInitialListOfServers();
/**
* Return updated list of servers. This is called say every 30 secs
* (configurable) by the Loadbalancer's Ping cycle
*
*/
public List<T> getUpdatedListOfServers();
}
嗯,没什么说的,再看ServerListUpdater
public interface ServerListUpdater {
public interface UpdateAction {
void doUpdate();
}
/**
* start the serverList updater with the given update action
* This call should be idempotent.
*
* @param updateAction
*/
void start(UpdateAction updateAction);
/**
* stop the serverList updater. This call should be idempotent
*/
void stop();
// continue ...
}
可以看到真正的更新动作在UpdateAction 中,而更新的trigger在start,我们看看ribbon的默认实现:
//LoadBalancer默认实现
public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer{
protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
@Override
public void doUpdate() {
updateListOfServers();
}
};
@VisibleForTesting
public void updateListOfServers() {
List<T> servers = new ArrayList<T>();
if (serverListImpl != null) {
// 调用serverList的update接口
servers = serverListImpl.getUpdatedListOfServers();
LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
if (filter != null) {
servers = filter.getFilteredListOfServers(servers);
LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
}
}
updateAllServerList(servers);
}
}
// ServerListUpdater 默认实现
public class PollingServerListUpdater implements ServerListUpdater {
@Override
public synchronized void start(final UpdateAction updateAction) {
if (isActive.compareAndSet(false, true)) {
final Runnable wrapperRunnable = new Runnable() {
@Override
public void run() {
if (!isActive.get()) {
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
return;
}
try {
updateAction.doUpdate();
lastUpdated = System.currentTimeMillis();
} catch (Exception e) {
logger.warn("Failed one update cycle", e);
}
}
};
// 系统启动定时任务,默认30s更新一次
scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
wrapperRunnable,
initialDelayMs,
refreshIntervalMs,
TimeUnit.MILLISECONDS
);
} else {
logger.info("Already active, no-op");
}
}
}
可以看到,默认30s读一次serverList,其实这里如果觉得缺少实时性,可以基于事件修改的,现在的互联网中台都有配置中心,可以监听配置中心的配置更新事件,来动态更新serverList这里给一个示例:
@Slf4j
public class CustomizeServerListUpdater implements ServerListUpdater, ServerListUpdater.UpdateAction {
private ConfigChangeListener listener;
private UpdateAction updateAction;
private final AtomicBoolean isActive = new AtomicBoolean(false);
private volatile long lastUpdated = System.currentTimeMillis();
public CustomizeServerListUpdater (ConfigChangeListener listener) {
this.listener = listener;
}
// 装饰器
@Override
public void doUpdate(){
if (isActive.get() && updateAction!=null){
updateAction.doUpdate();
}
}
@Override
public void start(UpdateAction updateAction) {
if (isActive.compareAndSet(false, true)) {
try {
this.updateAction = updateAction;
// 初始化操作先执行一次
listener.init(this);
updateAction.doUpdate();
lastUpdated = System.currentTimeMillis();
} catch (Exception e) {
log.warn("Failed one update cycle", e);
}
}
else {
log.info("Already active, no-op");
}
}
@Override
public void stop() {
if (isActive.compareAndSet(true, false)) {
this.updateAction = null;
this.listener.destroy();
}
else {
log.info("Not active, no-op");
}
}
@Override
public String getLastUpdate() {
return new Date(lastUpdated).toString();
}
@Override
public long getDurationSinceLastUpdateMs() {
return System.currentTimeMillis() - lastUpdated;
}
@Override
public int getNumberMissedCycles() {
return 0;
}
@Override
public int getCoreThreads() {
return 0;
}
}
// 各位可以依据各自公司的架构来实现哈
@Configuration
public class ConfigChangeListener implements ConfigChangeListener {
private ServerListUpdater.UpdateAction updater;
private boolean isActive = false;
@Override
public void onChange(ConfigChangeEvent changeEvent) {
if (isActive && changeEvent.changedKeys().stream()
.anyMatch(changedKey->changedKey.startsWith(Config.PREFIX))){
updater.doUpdate();
}
}
public void init(ServerListUpdater.UpdateAction updater){
this.updater = updater;
this.isActive = updater!=null;
}
public void destroy(){
this.updater = null;
this.isActive = false;
}
}
这样就可以了,再说几点注意事项哈,监听配置变化时要注意,切面是在配置修改前还是后,还有哈,ribbon默认是懒加载的,不访问接口,连IPing都不执行,所以最好改成起动加载。
4 思考
现在saas流行,不知道其他公司如何应对这种野生docker,欢迎留言哈。
最后啰嗦一点,我觉得网上的博客真是多啊,但脱离应用场景就讲代码有点耍流氓,我想写一些和实际应用结合的东西。但是笔者就是一普通程序员,能力有限,希望批评指正!
更多推荐
所有评论(0)