作者:京东物流 王北永 姚再毅 李振

1 背景

目前,ducc实现了实时近乎所有配置动态生效的场景,但是配置是否实时生效,不能直观展示每个机器上jvm内对象对应的参数是否已变更为准确的值,大部分时候需要查看日志确认是否生效。

2 技术依赖

1)Jsf:京东RPC框架,用作机器之间的通讯工具

2)redis/redisson:redis,用作配置信息的存储

3)ZK/Curator: Zookeeper,用作配置信息的存储 和redis二选一

3)clover:定时任务集群,用作任务延迟或周期性执行

3 实现原理

1)接入方:

各个接入系统通过接入管理模块获取token,并指定所在系统发布的的服务器ip,用作后续的ip鉴权。当系统启动时,自动在各个系统生成接口提供方,并注册到JSF注册中心。别名需各个系统唯一不重复。鉴权为统一服务端做IP鉴权。

2)统一配置服务端:

提供按不同接入方、不同系统、不同环境的配置界面。业务人员可设定自动生效时间或者立即生效时间。如果是立刻生效,则通过JSF广播或者指定机器生效配置。如果是定时生效,则新增定时器并指定生效规则,达到时间后触发广播通知。

整个接入方和统一配置服务端的架构如下图

4 实现步骤

1)重写JSF类ConsumerConfig类方法refer,将其中的轮训调用客户端改为广播调用客户端BroadCastClient。

this.client= new BroadCastClient(this);
this.proxyInvoker = new ClientProxyInvoker(this);
ProtocolFactory.check(Constants.ProtocolType.valueOf(this.getProtocol()), Constants.CodecType.valueOf(this.getSerialization()));
this.proxyIns = (T) ProxyFactory.buildProxy(this.getProxy(), this.getProxyClass(), this.proxyInvoker);

2)广播调用客户端方法分别获取当前注册中心有心跳的服务提供者和已失去连接的机器列表。对统一配置来讲,要么同时失败,要么同时成功,判断如果存在不正常的服务提供方,则不同步。只有全部提供方存在才可以开始广播配置信息

 ConcurrentHashMap<Provider, ClientTransport>  concurrentHashMap = this.connectionHolder.getAliveConnections();
ConcurrentHashMap<Provider, ClientTransportConfig> deadConcurrentHashMap = this.connectionHolder.getDeadConnections();
if(deadConcurrentHashMap!=null && deadConcurrentHashMap.size()>0){
log.warn("当前别名{}存在不正常服务提供方数量{},请关注!",msg.getAlias(),deadConcurrentHashMap.size());
throw new RpcException(String.format("当前别名%s存在不正常服务提供方数量%s,请关注!",msg.getAlias(),deadConcurrentHashMap.size()));
}
if(concurrentHashMap.isEmpty()){
log.info("当前别名{}不存在正常服务提供方",msg.getAlias());
throw new RpcException(String.format("当前别名%s不存在正常服务提供方",msg.getAlias()));
}
Iterator aliveConnections = concurrentHashMap.entrySet().iterator();
log.info("当前别名{}存在正常服务提供方数量{}",msg.getAlias(),concurrentHashMap.size());
while (aliveConnections.hasNext()) {
Entry<Provider, ClientTransport> entry = (Entry) aliveConnections.next();
Provider provider = (Provider) entry.getKey();
log.info("当前连接ip={}、port={}、datacenterCode={}",provider.getIp(),provider.getPort(),provider.getDatacenterCode());
ClientTransport connection = (ClientTransport) entry.getValue();
if (connection != null && connection.isOpen()) {
try {
result = super.sendMsg0(new Connection(provider, connection), msg);
} catch (RpcException rpc) {
exception = rpc;
log.warn(rpc.getMessage(), rpc);
} catch (Throwable e) {
exception = new RpcException(e.getMessage(), e);
log.warn(e.getMessage(), e);
}
}
}

3)服务配置端,当业务人员配置及时生效或者任务达到时,则根据配置,生成服务调用方,通过统一刷新接口将配置同步刷新到对应的接入系统中,如下图为操作界面,当增删改查时,会将属性增量同步。

服务端在上面操作增删改时,通过以下方式获取服务调用方

 public static ExcuteAction createJsfConsumer(String alias, String token) {
RegistryConfig jsfRegistry = new RegistryConfig();
jsfRegistry.setIndex("i.jsf.jd.com");
BroadCastConsumerConfig consumerConfig = new BroadCastConsumerConfig<>();
Map<String, String> parameters = new HashMap<>();
parameters.put(".token",token);
consumerConfig.setParameters(parameters);
consumerConfig.setInterfaceId(RefreshRemoteService.class.getName());
consumerConfig.setRegistry(jsfRegistry);
consumerConfig.setProtocol("jsf");
consumerConfig.setAlias(alias);
consumerConfig.setRetries(2);
return new ExcuteAction(consumerConfig);
}

通过以上的配置的客户端,调用服务提供方方法refreshRemoteService#refresh,将配置信息进行同步到各个接入系统

public void call(Map<String,Object> propertiesValue){
try{
RefreshRemoteService refreshRemoteService = (RefreshRemoteService)consumerConfig.refer();
if(refreshRemoteService!=null){
refreshRemoteService.refresh(propertiesValue);
}
}catch (Exception e){
log.error(e.getMessage());
throw new EasyConfigException(e);
}finally {
consumerConfig.unrefer(); ;
}
}

4)接入方启动时,需要根据自己配置,将存在redis或者zk的配置一次加载到实例变量中。并注册刷新接口到JSF注册中心。

其中注册刷新接口到JSF注册中心代码如下

 @Bean(name = "refreshPorpertiesService")
public ProviderConfig createJsfProvider() throws Exception {
ProviderConfig providerConfig = new ProviderConfig();
providerConfig.setId("refreshPorpertiesService");
providerConfig.setInterfaceId(RefreshRemoteService.class.getName());
providerConfig.setRef(new RefreshRemoteServiceDelage(applicationContext));
providerConfig.setTimeout(30000);
providerConfig.setAlias(EasyConfigure.getAppCode()+EasyConfigure.getEnv());
providerConfig.setServer(serverConfig);
providerConfig.setRegistry(jsfRegistry);
providerConfig.setParameter("token", MD5Util.md5(EasyConfigure.getAppCode()));
providerConfig.export();
return providerConfig;
}

其中

RefreshRemoteServiceDelage类提供刷新接口的实际逻辑如下,需判断当前实例是jdk动态代理还是cglib代理

判断逻辑如下

 if(AopUtils.isJdkDynamicProxy(object)) {
object= AopUtil.getJdkDynamicProxyTargetObject(object);
} else if(AopUtils.isCglibProxy(object)){ //cglib
object= AopUtil.getCglibProxyTargetObject(object);
}

实例对象变量值根据自定义的参数转换方式转换后赋值实例变量

if(autoValue.convert()!=null && !autoValue.getClass().isInterface()){
if(!autoValue.convert().newInstance().getInClassType().isAssignableFrom(newVal.getClass()) ){
continue;
}
newVal = autoValue.convert().newInstance().convert(newVal);
if(newVal!=null){
if(!autoValue.convert().newInstance().getOutClassType().isAssignableFrom(newVal.getClass()) ){
continue;
}
field.setAccessible(true);
Object value = ReflectionUtils.getField(field,object);
log.info("change properties{} for object {} before value {}",field.getName(),object.getClass().getName(),value);
ReflectionUtils.setField(field,object,newVal);
log.info("change properties{} for object {} after value {}",field.getName(),object.getClass().getName(),newVal);
}
}

5 实践

1)pom引入

<dependency>
<groupId>com.jdl</groupId>
<artifactId>easyconfig</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

2)配置存储配置(比如redis方式)

refresh:
config:
appCode: zdzq-worker-appcode
redisUrl: redis://:@127.0.0.1

3)类全局变量需要实时刷新配置,需在类统一指定注解PropertiryChangeListener,实例变量需要增加注解AutoValue并指定数据格式转换器

@PropertiryChangeListener
public class ChanceServiceImpl implement ChanceService{
@AutoValue(convert = DateConvert.class,alias = "config-id")
private Date signDate;
@AutoValue(convert = SpmKaApply Convert.class,alias = "config-id")
private SpmKaApply spmKaApply;
}

以上convert方法自定义,支持各种复杂配置对象,举例数据转换为List如下

public  class Convert2 implements Convert<Map<String, String>, Set<String>> {
public Convert2(){}
@Override
public Set<String> convert(Map<String, String> siteInfoMap) {
....你的对象值转换
}
@Override
public Class<?> getInClassType() {
return Map.class;
}
@Override
public Class<?> getOutClassType() {
return Set.class;
}

接入应用服务启动后,可访问/refreshUI 可查看应用在集群中为自动配置的实例,并显示当前实例中变量值参数。key为实例变量名。

6 总结

1、支持jdk动态代理的实例对象和cglib代理对象的参数动态配置

2、支持定时刷新配置

3、直接查看和验证应用集群中实例变量是否一致

一种面向业务配置基于JSF广播定时生效的工具的更多相关文章

  1. 两种交换机配置模式,以配置基于端口划分的VLAN为例

    关于交换机的配置模式,大体上可以分为两类:其一以CISCO交换机为代表的配置模式,其二以Huawei.H3C交换机为代表的配置模式.其实这两种配置模式并没有本质的不同,只是配置的命令名称和配置方式存在 ...

  2. mysql主丛之基于binlog的不停业务配置主从

    一 环境准备 主:192.168.132.121 从:192.168.132.122 主的数据库上面已经有数据,而且还在不断的写入 mysql> select * from darren.tes ...

  3. 一种M2M业务的架构及实现M2M业务的方法

    http://www.cnblogs.com/coryxie/p/3849764.html 技术领域 [0001] 本发明涉及通信技术领域,尤其涉及一种M2M业务的架构及实现M2M业务的方法. 背景技 ...

  4. 业务配置开发平台qMISPlat 2.0 产品介绍

    qMISPlat是什么 qMISPlat(业务配置开发平台)是一套基于.net core 2.0.跨平台的,面向开发人员和具有一定技术水平的业务人员使用的业务配置开发平台.基于此平台您只需通过配置和少 ...

  5. 一种面向云服务的UCON多义务访问控制方法及系统

    )设置每一云服务的义务项:建立每一云服务所包含的义务图:2)根据用户所请求的云服务查找该云服务的所有强制义务图和可选义务图,并提取该用户对该云服务的历史完成情况:3)对每一强制义务图,监控其每一义务项 ...

  6. Django的几种缓存的配置

    1.缓存的简介 在动态网站中,用户所有的请求,服务器都会去数据库中进行相应的增,删,查,改,渲染模板,执行业务逻辑,最后生成用户看到的页面. 当一个网站的用户访问量很大的时候,每一次的的后台操作,都会 ...

  7. Nginx配置基于多域名、端口、IP的虚拟主机

    原文:https://www.cnblogs.com/ssgeek/p/9220922.html ------------------------------- Nginx配置基于多域名.端口.IP的 ...

  8. 关于ActiveMQ的几种集群配置

    ActiveMQ的几种集群配置. Queue consumer clusters 此集群让多个消费者同时消费一个队列,若某个消费者出问题无法消费信息,则未消费掉的消息将被发给其他正常的消费者,结构图如 ...

  9. ActiveMQ的几种集群配置

    ActiveMQ是一款功能强大的消息服务器,它支持许多种开发语言,例如Java, C, C++, C#等等.企业级消息服务器无论对服务器稳定性还是速度,要求都很高,而ActiveMQ的分布式集群则能很 ...

  10. linux apache虚拟主机配置(基于ip,端口,域名)

    配置环境: linux版本:Centos6.4 httpd版本: [root@centos64Study init.d]# pwd/etc/init.d[root@centos64Study init ...

随机推荐

  1. go-zero docker-compose搭建课件服务(四):生成Dockerfile

    0.转载 go-zero docker-compose 搭建课件服务(四):生成Dockerfile并在docker-compose中启动 0.1源码地址 https://github.com/liu ...

  2. Visual Studio(VS)修改C语言scanf等报错

    1.在程序最前面加:#define_CRT_SECURE_NO_DEPRECATE 2.按照vs规定进行修改,例如把scanf改为scanf_s: 3.在"项目" -> &q ...

  3. 手把手教你从安装CentOS7.4镜像开始,搭建IoT视频监控系统

    摘要:在CentOS7.4服务器版本的环境下安装nginx服务器.配置文件服务器.流媒体服务器. 本文分享自华为云社区<华为云ECS服务器安装CentOS7.4镜像,部署GINX服务器.搭建物联 ...

  4. Druid SQL注入防御模块技术浅析

    官方参考: https://www.bookstack.cn/read/Druid/ffdd9118e6208531.md 前置知识 什么是Druid? Druid是一个高效的数据查询系统,主要解决的 ...

  5. Django系列---理论一

    教程:http://c.biancheng.net/django/ 特点 集成 ORM 组件:Django 的 Model 层自带数据库 ORM 组件,为操作不同类型的数据库提供了统一的方式. URL ...

  6. 什么是CLR?

    CLR翻译为"公共语言运行库",全称是Common Language Runtime,它是一个可以由多种语言使用的"运行时",CLR的核心功能如内存管理,程序集 ...

  7. tcp网络交互的理解 以及代码实现

    服务端 import socketserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)server.bind(("127.0. ...

  8. 用最少的代码模拟gRPC四种消息交换模式

    我们知道,建立在HTTP2/3之上的gRPC具有四种基本的通信模式或者消息交换模式(MEP: Message Exchange Pattern),即Unary.Server Stream.Client ...

  9. React综合使用联系

    index.js import React from 'react' import ReactDOM from 'react-dom' import CartSimple from './CartSi ...

  10. <五>模板的完全特例化和非完全特例化

    模板作为C++泛型编程的基础十分重要,其使得一份代码能用于处理多种数据类型.而有些时候,我们会希望对一些特定的数据类型执行不同的代码,这时就需要使用模板特例化(template specializat ...