Nacos/Sentinel/Seata核心源码剖析
Nacos/Sentinel/Seata核心源码剖析
1 Nacos源码剖析
Nacos源码有很多值得我们学习的地方,为了深入理解Nacos,我们剖析源码,分析如下2个知识点:
1:Nacos对注册中心的访问原理
2:Nacos注册服务处理流程
我们接下来对Nacos源码做一个深度剖析,首先搭建Nacos源码环境,源码环境搭建起来比较轻松,几乎不会报什么错误,我们这里就不去演示源码环境搭建了。
客户端与注册中心服务端的交互,主要集中在服务注册、服务下线、服务发现、订阅某个服务,其实使用最多的就是服务注册和服务发现,下面我会从源码的角度分析一下这四个功能。
在Nacos源码中 nacos-example 中 com.alibaba.nacos.example.NamingExample 类分别演示了这4个功能的操作,我们可以把它当做入口,代码如下
public class NamingExample {
public static void main(String[] args) throws NacosException {
Properties properties = new Properties();
properties.setProperty("serverAddr", System.getProperty("serverAddr"));
properties.setProperty("namespace", System.getProperty("namespace"));
NamingService naming = NamingFactory.createNamingService(properties);
naming.registerInstance("nacos.test.3", "11.11.11.11", 8888, "DEFAULT");
naming.registerInstance("nacos.test.3", "2.2.2.2", 9999, "DEFAULT");
System.out.println(naming.getAllInstances("nacos.test.3"));
naming.deregisterInstance("nacos.test.3", "2.2.2.2", 9999, "DEFAULT");
System.out.println(naming.getAllInstances("nacos.test.3"));
Executor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("test-thread");
return thread;
}
});
naming.subscribe("nacos.test.3", new AbstractEventListener() {
//EventListener onEvent is sync to handle, If process too low in onEvent, maybe block other onEvent callback.
//So you can override getExecutor() to async handle event.
@Override
public Executor getExecutor() {
return executor;
}
@Override
public void onEvent(Event event) {
System.out.println(((NamingEvent) event).getServiceName());
System.out.println(((NamingEvent) event).getInstances());
}
});
}
}
1.1 客户端工作流程
我们先来看一下客户端是如何实现服务注册、服务发现、服务下线操作、服务订阅操作的。
1.1.1 服务注册
我们沿着案例中的服务注册方法调用找到 nacos-api 中的NamingService.registerInstance() 并找到它的实现类和方法com.alibaba.nacos.client.naming.NacosNamingService ,代码如下:
@Override
public void registerInstance(String serviceName, String ip, int port, String clusterName) throws NacosException {
registerInstance(serviceName, Constants.DEFAULT_GROUP, ip, port, clusterName);
}
@Override
public void registerInstance(String serviceName, String groupName, String ip, int port, String clusterName)
throws NacosException {
Instance instance = new Instance();
instance.setIp(ip);
instance.setPort(port);
instance.setWeight(1.0);
instance.setClusterName(clusterName);
registerInstance(serviceName, groupName, instance);
}
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
NamingUtils.checkInstanceIsLegal(instance);
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
//该字段表示注册的实例是否是临时实例还是持久化实例。
// 如果是临时实例,则不会在 Nacos 服务端持久化存储,需要通过上报心跳的方式进行包活,
// 如果一段时间内没有上报心跳,则会被 Nacos 服务端摘除。
if (instance.isEphemeral()) {
//为注册服务设置一个定时任务获取心跳信息,默认为5s汇报一次
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
//注册到服务端
serverProxy.registerService(groupedServiceName, groupName, instance);
}
注册主要做了两件事,第一件事:为注册的服务设置一个定时任务,定时拉去服务信息。 第二件事:将服务注册到服务端。
1:启动一个定时任务,定时拉取服务信息,时间间隔为5s,如果拉下来服务正常,不做处理,如果不正常,
重新注册
2:发送http请求给注册中心服务端,调用服务注册接口,注册服务
上面代码我们可以看到定时任务添加,但并未完全看到远程请求, serverProxy.registerService()方法如下,会先封装请求参数,接下来调用 reqApi() 而 reqApi() 最后会调用 callServer() ,代码如下:
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
instance);
//封装Http请求参数
final Map<String, String> params = new HashMap<String, String>(16);
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, serviceName);
params.put(CommonParams.GROUP_NAME, groupName);
params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
params.put("ip", instance.getIp());
params.put("port", String.valueOf(instance.getPort()));
params.put("weight", String.valueOf(instance.getWeight()));
params.put("enable", String.valueOf(instance.isEnabled()));
params.put("healthy", String.valueOf(instance.isHealthy()));
params.put("ephemeral", String.valueOf(instance.isEphemeral()));
params.put("metadata", JacksonUtils.toJson(instance.getMetadata()));
//执行Http请求
reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
}
public String callServer(String api, Map<String, String> params, Map<String, String> body, String curServer,
String method) throws NacosException {
long start = System.currentTimeMillis();
long end = 0;
injectSecurityInfo(params);
//封装请求头部
Header header = builderHeader();
//请求是Http还是Https协议
String url;
if (curServer.startsWith(UtilAndComs.HTTPS) || curServer.startsWith(UtilAndComs.HTTP)) {
url = curServer + api;
} else {
if (!IPUtil.containsPort(curServer)) {
curServer = curServer + IPUtil.IP_PORT_SPLITER + serverPort;
}
url = NamingHttpClientManager.getInstance().getPrefix() + curServer + api;
}
try {
//执行远程请求,并获取结果集
HttpRestResult<String> restResult = nacosRestTemplate
.exchangeForm(url, header, Query.newInstance().initParams(params), body, method, String.class);
end = System.currentTimeMillis();
MetricsMonitor.getNamingRequestMonitor(method, url, String.valueOf(restResult.getCode()))
.observe(end - start);
//结果集解析
if (restResult.ok()) {
return restResult.getData();
}
if (HttpStatus.SC_NOT_MODIFIED == restResult.getCode()) {
return StringUtils.EMPTY;
}
throw new NacosException(restResult.getCode(), restResult.getMessage());
} catch (Exception e) {
NAMING_LOGGER.error("[NA] failed to request", e);
throw new NacosException(NacosException.SERVER_ERROR, e);
}
}
执行远程Http请求的对象是 NacosRestTemplate ,该对象就是封装了普通的Http请求,大家可以自己查阅一下。
1.1.2 服务发现
我们沿着案例中的服务发现方法调用找到 nacos-api 中的NamingService.getAllInstances() 并找到它的实现类和方法com.alibaba.nacos.client.naming.NacosNamingService.getAllInstances() ,代码如下:
@Override
public List<Instance> getAllInstances(String serviceName, String groupName, List<String> clusters,
boolean subscribe) throws NacosException {
ServiceInfo serviceInfo;
/*默认true->获取服务实例*/
if (subscribe) {
//从本地缓存中获取,如果本地缓存不存在从服务端拉取
//本地缓存会存储在HostReactor.serviceInfoMap中,它是一个Map对象
serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
StringUtils.join(clusters, ","));
} else {
serviceInfo = hostReactor
.getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName),
StringUtils.join(clusters, ","));
}
List<Instance> list;
if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {
return new ArrayList<Instance>();
}
return list;
}
上面的代码调用了 hostReactor.getServiceInfo() 方法,该方法会先调用 getServiceInfo0() 方法从本地缓存获取数据,缓存没有数据,就构建实例更新到Nacos,并从Nacos中获取最新数据,getServiceInfo0() 方法源码如下:
public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
String key = ServiceInfo.getKey(serviceName, clusters);
if (failoverReactor.isFailoverSwitch()) {
return failoverReactor.getService(key);
}
/*1。先从本地缓存中获取服务对象,因为启动是第一次进来,所以缓存站不存在*/
ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
if (null == serviceObj) {
/*构建服务实例*/
serviceObj = new ServiceInfo(serviceName, clusters);
/*将服务实例存放到缓存中*/
serviceInfoMap.put(serviceObj.getKey(), serviceObj);
/*更新nacos-上的服务*/
updatingMap.put(serviceName, new Object());
/*主动获取,并且更新到缓存本地,以及已过期的服务更新等*/
updateServiceNow(serviceName, clusters);
updatingMap.remove(serviceName);
} else if (updatingMap.containsKey(serviceName)) {
if (UPDATE_HOLD_INTERVAL > 0) {
// hold a moment waiting for update finish
synchronized (serviceObj) {
try {
serviceObj.wait(UPDATE_HOLD_INTERVAL);
} catch (InterruptedException e) {
NAMING_LOGGER
.error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
}
}
}
}
/*2.开启定时任务*/
scheduleUpdateIfAbsent(serviceName, clusters);
return serviceInfoMap.get(serviceObj.getKey());
}
updateServiceNow(serviceName, clusters); 主从从远程服务器获取更新数据,最终会调用updateService() 方法,在该方法中完成远程请求和数据处理,源码如下:
public void updateService(String serviceName, String clusters) throws NacosException {
/*获取本地缓存列表中所存在的服务*/
ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
try {
/*获取服务以及提供者端口信息,端口等*/
String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);
if (StringUtils.isNotEmpty(result)) {
/*对结果进行处理*/
processServiceJson(result);
}
} finally {
if (oldService != null) {
synchronized (oldService) {
oldService.notifyAll();
}
}
}
}
1.1.3 服务下线
我们沿着案例中的服务下线方法调用找到 nacos-api 中的 NamingService.deregisterInstance() 并找到它的实现类和方法 NacosNamingService.deregisterInstance() ,代码如下:
@Override
public void deregisterInstance(String serviceName, String groupName, String ip, int port, String clusterName)
throws NacosException {
Instance instance = new Instance();
instance.setIp(ip);
instance.setPort(port);
instance.setClusterName(clusterName);
//服务下线操作
deregisterInstance(serviceName, groupName, instance);
}
@Override
public void deregisterInstance(String serviceName, String groupName, Instance instance) throws NacosException {
if (instance.isEphemeral()) {
//移除心跳信息监测的定时任务
beatReactor.removeBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), instance.getIp(),
instance.getPort());
}
//发送远程请求执行服务下线销毁操作
serverProxy.deregisterService(NamingUtils.getGroupedName(serviceName, groupName), instance);
}
服务下线方法比较简单,和服务注册做的事情正好相反,也做了两件事,第一件事:不再进行心跳检测。 第二件事:请求服务端服务下线接口。
1.1.4 服务订阅
我们可以查看订阅服务的案例,会先创建一个线程池,接下来会把线程池封装到监听器中,而监听器中可以监听指定实例信息,代码如下:
Executor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("test-thread");
return thread;
}
});
naming.subscribe("nacos.test.3", new AbstractEventListener() {
//EventListener onEvent is sync to handle, If process too low in onEvent, maybe block other onEvent callback.
//So you can override getExecutor() to async handle event.
@Override
public Executor getExecutor() {
return executor;
}
//读取监听到的服务实例
@Override
public void onEvent(Event event) {
System.out.println(((NamingEvent) event).getServiceName());
System.out.println(((NamingEvent) event).getInstances());
}
});
我们沿着案例中的服务订阅方法调用找到 nacos-api 中的NamingService.subscribe() 并找到它的实现类和方法NacosNamingService.deregisterInstance() ,代码如下:
public void subscribe(String serviceName, String clusters, EventListener eventListener) {
//注册监听
notifier.registerListener(serviceName, clusters, eventListener);
//获取并更新服务实例
getServiceInfo(serviceName, clusters);
}
此时会注册监听,注册监听,注册监听就是将当前的监听对象信息注入到listenerMap集合中,在监听对象的指定方法onEvent中可以读取实例信息,代码如下:
public void registerListener(String serviceName, String clusters, EventListener listener) {
String key = ServiceInfo.getKey(serviceName, clusters);
ConcurrentHashSet<EventListener> eventListeners = listenerMap.get(key);
if (eventListeners == null) {
synchronized (lock) {
eventListeners = listenerMap.get(key);
if (eventListeners == null) {
eventListeners = new ConcurrentHashSet<EventListener>();
listenerMap.put(key, eventListeners);
}
}
}
//将当前监听对象放入到集合中,在监听对象的onEvent中可以读出对应的实例对象
eventListeners.add(listener);
}
1.2 服务端工作流程
注册中心服务端的主要功能包括,接收客户端的服务注册,服务发现,服务下线的功能,但是除了这些和客户端的交互之外,服务端还要做一些更重要的事情,就是我们常常会在分布式系统中听到的AP和CP,作为一个集群,nacos即实现了AP也实现了CP,其中AP使用的自己实现的Distro协议,而CP是采用raft协议实现的,这个过程中牵涉到心跳、选主等操作。
我们来学习一下注册中心服务端接收客户端服务注册的功能。
1.2.1 注册处理
我们先来学习一下Nacos的工具类 WebUtils ,该工具类在 nacos-core 工程下,该工具类是用于处理请求参数转化的,里面提供了2个常被用到的方法 required() 和 optional() :
required方法通过参数名key,解析HttpServletRequest请求中的参数,并转码为UTF-8编码。
optional方法在required方法的基础上增加了默认值,如果获取不到,则返回默认值。
代码如下:
public class WebUtils {
/**
* get target value from parameterMap, if not found will throw {@link IllegalArgumentException}.
* required方法通过参数名key,解析HttpServletRequest请求中的参数,并转码为UTF-8编码。
* @param req {@link HttpServletRequest}
* @param key key
* @return value
*/
public static String required(final HttpServletRequest req, final String key) {
String value = req.getParameter(key);
if (StringUtils.isEmpty(value)) {
throw new IllegalArgumentException("Param '" + key + "' is required.");
}
String encoding = req.getParameter("encoding");
return resolveValue(value, encoding);
}
/**
* get target value from parameterMap, if not found will return default value.
* optional方法在required方法的基础上增加了默认值,如果获取不到,则返回默认值。
* @param req {@link HttpServletRequest}
* @param key key
* @param defaultValue default value
* @return value
*/
public static String optional(final HttpServletRequest req, final String key, final String defaultValue) {
if (!req.getParameterMap().containsKey(key) || req.getParameterMap().get(key)[0] == null) {
return defaultValue;
}
String value = req.getParameter(key);
if (StringUtils.isBlank(value)) {
return defaultValue;
}
String encoding = req.getParameter("encoding");
return resolveValue(value, encoding);
}
nacos server-client 使用了 http 协议来交互,那么在 server 端必定提供了 http 接口的入口,并且在 core 模块看到其依赖了 spring boot starter ,所以它的http接口由集成了Spring的web服务器支持,简单地说就是像我们平时写的业务服务一样,有controller层和service层。
以OpenAPI作为入口来学习,我们找到 /nacos/v1/ns/instance 服务注册接口,在 nacos-naming 工程中我们可以看到 InstanceController 正是我们要找的对象,如下图:
处理服务注册,我们直接找对应的POST方法即可,代码如下:
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
//获取namespaceid,该参数是可选参数
final String namespaceId = WebUtils
.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
//获取服务名字
final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
//校验服务的名字,服务的名字格式为groupName@@serviceName
NamingUtils.checkServiceNameFormat(serviceName);
//创建实例
final Instance instance = parseInstance(request);
//注册服务
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
如上图,该方法主要用于接收客户端注册信息,并且会校验参数是否存在问题,如果不存在问题就创建服务的实例,服务实例创建后将服务实例注册到Nacos中,注册的方法如下:
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
//判断本地缓存中是否存在该命名空间,如果不存在就创建,之后判断该命名空间下是否
//存在该服务,如果不存在就创建空的服务
//如果实例为空,则创建实例,并且会将创建的实例存入到serviceMap集合中
createEmptyService(namespaceId, serviceName, instance.isEphemeral());
//从serviceMap集合中获取创建的实例
Service service = getService(namespaceId, serviceName);
if (service == null) {
throw new NacosException(NacosException.INVALID_PARAM,
"service not found, namespace: " + namespaceId + ", service: " + serviceName);
}
//服务注册,这一步才会把服务的实例信息和服务绑定起来
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
注册的方法中会先创建该实例对象,创建前先检查本地缓存是否存在该实例对象,如果不存在就创建,最后注册该服务,并且该服务会和实例信息捆绑到一起,并将信息同步到磁盘,数据同步到磁盘就涉及到数据一致性了,我们接下来讲解Nacos的数据一致性。
1.2.2 一致性算法Distro协议介绍
Distro是阿里巴巴的私有协议,目前流行的Nacos服务管理框架就采用了Distro协议。Distro 协议被定位为 临时数据的一致性协议:该类型协议, 不需要把数据存储到磁盘或者数据库,因为临时数据通常和服务器保持一个session会话, 该会话只要存在,数据就不会丢失 。
Distro 协议保证写必须永远是成功的,即使可能会发生网络分区。当网络恢复时,把各数据分片的数据进行合并。
Distro 协议具有以下特点:
1:专门为了注册中心而创造出的协议;
2:客户端与服务端有两个重要的交互,服务注册与心跳发送;
3:客户端以服务为维度向服务端注册,注册后每隔一段时间向服务端发送一次心跳,心跳包需要带上注册服务的全部信息,在客户端看来,服务端节点对等,所以请求的节点是随机的;
4:客户端请求失败则换一个节点重新发送请求;
5:服务端节点都存储所有数据,但每个节点只负责其中一部分服务,在接收到客户端的“写”(注册、心跳、下线等)请求后,服务端节点判断请求的服务是否为自己负责,如果是,则处理,否则交由负责的节点处理;
6:每个服务端节点主动发送健康检查到其他节点,响应的节点被该节点视为健康节点;
7:服务端在接收到客户端的服务心跳后,如果该服务不存在,则将该心跳请求当做注册请求来处理;
8:服务端如果长时间未收到客户端心跳,则下线该服务;
9:负责的节点在接收到服务注册、服务心跳等写请求后将数据写入后即返回,后台异步地将数据同步给其他节点;
10:节点在收到读请求后直接从本机获取后返回,无论数据是否为最新。
1.2.3 Distro寻址-单机模式
Distro协议服务端节点发现使用寻址机制来实现服务端节点的管理。在Nacos中,寻址模式有三种:
单机模式(StandaloneMemberLookup)
文件模式(FileConfigMemberLookup)
服务器模式(AddressServerMemberLookup)
1.2.3.1 单机模式
在 com.alibaba.nacos.core.cluster.lookup.LookupFactory 中有创建寻址方式,可以创建集群启动方式、单机启动方式,不同启动方式就决定了不同寻址模式,如果是集群启动,
public static MemberLookup createLookUp(ServerMemberManager memberManager) throws NacosException {
//集群方式启动
if (!EnvUtil.getStandaloneMode()) {
String lookupType = EnvUtil.getProperty(LOOKUP_MODE_TYPE);
//由参数中传入的寻址方式得到LookupType对象
LookupType type = chooseLookup(lookupType);
//选择寻址方式
LOOK_UP = find(type);
//设置当前寻址方式
currentLookupType = type;
} else {
//单机启动
LOOK_UP = new StandaloneMemberLookup();
}
LOOK_UP.injectMemberManager(memberManager);
Loggers.CLUSTER.info("Current addressing mode selection : {}", LOOK_UP.getClass().getSimpleName());
return LOOK_UP;
}
private static MemberLookup find(LookupType type) {
//文件寻址模式,也就是配置cluster.conf配置文件将多个节点串联起来,
// 通过配置文件寻找其他节点,以达到和其他节点通信的目的
if (LookupType.FILE_CONFIG.equals(type)) {
LOOK_UP = new FileConfigMemberLookup();
return LOOK_UP;
}
//服务器模式
if (LookupType.ADDRESS_SERVER.equals(type)) {
LOOK_UP = new AddressServerMemberLookup();
return LOOK_UP;
}
// unpossible to run here
throw new IllegalArgumentException();
}
单节点寻址模式会直接创建 StandaloneMemberLookup 对象,而文件寻址模式会创建FileConfigMemberLookup 对象,服务器寻址模式会创建 AddressServerMemberLookup ;
1.2.3.2 文件寻址模式
文件寻址模式主要在创建集群的时候,通过 cluster.conf 来配置集群,程序可以通过监听cluster.conf 文件变化实现动态管理节点, FileConfigMemberLookup 源码如下
public class FileConfigMemberLookup extends AbstractMemberLookup {
//创建文件监听器
private FileWatcher watcher = new FileWatcher() {
//文件发生变更事件
@Override
public void onChange(FileChangeEvent event) {
readClusterConfFromDisk();
}
//检查context是否包含cluster.conf
@Override
public boolean interest(String context) {
return StringUtils.contains(context, "cluster.conf");
}
};
@Override
public void start() throws NacosException {
if (start.compareAndSet(false, true)) {
readClusterConfFromDisk();
// Use the inotify mechanism to monitor file changes and automatically
// trigger the reading of cluster.conf
// 使用inotify机制来监视文件更改,并自动触发对cluster.conf的读取
try {
WatchFileCenter.registerWatcher(EnvUtil.getConfPath(), watcher);
} catch (Throwable e) {
Loggers.CLUSTER.error("An exception occurred in the launch file monitor : {}", e.getMessage());
}
}
}
@Override
public void destroy() throws NacosException {
WatchFileCenter.deregisterWatcher(EnvUtil.getConfPath(), watcher);
}
private void readClusterConfFromDisk() {
Collection<Member> tmpMembers = new ArrayList<>();
try {
List<String> tmp = EnvUtil.readClusterConf();
tmpMembers = MemberUtil.readServerConf(tmp);
} catch (Throwable e) {
Loggers.CLUSTER
.error("nacos-XXXX [serverlist] failed to get serverlist from disk!, error : {}", e.getMessage());
}
afterLookup(tmpMembers);
}
}
1.2.3.3 服务器寻址模式
使用地址服务器存储节点信息,会创建 AddressServerMemberLookup ,服务端定时拉取信息进行管理;
public class AddressServerMemberLookup extends AbstractMemberLookup {
private final GenericType<RestResult<String>> genericType = new GenericType<RestResult<String>>() {
};
public String domainName;
public String addressPort;
public String addressUrl;
public String envIdUrl;
public String addressServerUrl;
private volatile boolean isAddressServerHealth = true;
private int addressServerFailCount = 0;
private int maxFailCount = 12;
private final NacosRestTemplate restTemplate = HttpClientBeanHolder.getNacosRestTemplate(Loggers.CORE);
private volatile boolean shutdown = false;
@Override
public void start() throws NacosException {
if (start.compareAndSet(false, true)) {
this.maxFailCount = Integer.parseInt(EnvUtil.getProperty("maxHealthCheckFailCount", "12"));
initAddressSys();
run();
}
}
private void initAddressSys() {
String envDomainName = System.getenv("address_server_domain");
if (StringUtils.isBlank(envDomainName)) {
domainName = EnvUtil.getProperty("address.server.domain", "jmenv.tbsite.net");
} else {
domainName = envDomainName;
}
String envAddressPort = System.getenv("address_server_port");
if (StringUtils.isBlank(envAddressPort)) {
addressPort = EnvUtil.getProperty("address.server.port", "8080");
} else {
addressPort = envAddressPort;
}
String envAddressUrl = System.getenv("address_server_url");
if (StringUtils.isBlank(envAddressUrl)) {
addressUrl = EnvUtil.getProperty("address.server.url", EnvUtil.getContextPath() + "/" + "serverlist");
} else {
addressUrl = envAddressUrl;
}
addressServerUrl = "http://" + domainName + ":" + addressPort + addressUrl;
envIdUrl = "http://" + domainName + ":" + addressPort + "/env";
Loggers.CORE.info("ServerListService address-server port:" + addressPort);
Loggers.CORE.info("ADDRESS_SERVER_URL:" + addressServerUrl);
}
@SuppressWarnings("PMD.UndefineMagicConstantRule")
private void run() throws NacosException {
// With the address server, you need to perform a synchronous member node pull at startup
// Repeat three times, successfully jump out
boolean success = false;
Throwable ex = null;
int maxRetry = EnvUtil.getProperty("nacos.core.address-server.retry", Integer.class, 5);
for (int i = 0; i < maxRetry; i++) {
try {
//拉取集群节点信息
syncFromAddressUrl();
success = true;
break;
} catch (Throwable e) {
ex = e;
Loggers.CLUSTER.error("[serverlist] exception, error : {}", ExceptionUtil.getAllExceptionMsg(ex));
}
}
if (!success) {
throw new NacosException(NacosException.SERVER_ERROR, ex);
}
GlobalExecutor.scheduleByCommon(new AddressServerSyncTask(), 5_000L);
}
@Override
public void destroy() throws NacosException {
shutdown = true;
}
@Override
public Map<String, Object> info() {
Map<String, Object> info = new HashMap<>(4);
info.put("addressServerHealth", isAddressServerHealth);
info.put("addressServerUrl", addressServerUrl);
info.put("envIdUrl", envIdUrl);
info.put("addressServerFailCount", addressServerFailCount);
return info;
}
private void syncFromAddressUrl() throws Exception {
RestResult<String> result = restTemplate
.get(addressServerUrl, Header.EMPTY, Query.EMPTY, genericType.getType());
if (result.ok()) {
isAddressServerHealth = true;
Reader reader = new StringReader(result.getData());
try {
afterLookup(MemberUtil.readServerConf(EnvUtil.analyzeClusterConf(reader)));
} catch (Throwable e) {
Loggers.CLUSTER.error("[serverlist] exception for analyzeClusterConf, error : {}",
ExceptionUtil.getAllExceptionMsg(e));
}
addressServerFailCount = 0;
} else {
addressServerFailCount++;
if (addressServerFailCount >= maxFailCount) {
isAddressServerHealth = false;
}
Loggers.CLUSTER.error("[serverlist] failed to get serverlist, error code {}", result.getCode());
}
}
// 定时任务
class AddressServerSyncTask implements Runnable {
@Override
public void run() {
if (shutdown) {
return;
}
try {
//拉取服务列表
syncFromAddressUrl();
} catch (Throwable ex) {
addressServerFailCount++;
if (addressServerFailCount >= maxFailCount) {
isAddressServerHealth = false;
}
Loggers.CLUSTER.error("[serverlist] exception, error : {}", ExceptionUtil.getAllExceptionMsg(ex));
} finally {
GlobalExecutor.scheduleByCommon(this, 5_000L);
}
}
}
}
1.2.4 数据同步
Nacos数据同步分为全量同步和增量同步,所谓全量同步就是初始化数据一次性同步,而增量同步是指有数据增加的时候,只同步增加的数据。
1.2.4.1 全量同步
全量同步流程比较复杂,流程如上图:
1:启动一个定时任务线程DistroLoadDataTask加载数据,调用load()方法加载数据
2:调用loadAllDataSnapshotFromRemote()方法从远程机器同步所有的数据
3:从namingProxy代理获取所有的数据data
4:构造http请求,调用httpGet方法从指定的server获取数据
5:从获取的结果result中获取数据bytes
6:处理数据processData
7:从data反序列化出datumMap
8:把数据存储到dataStore,也就是本地缓存dataMap
9:监听器不包括key,就创建一个空的service,并且绑定监听器
10:监听器listener执行成功后,就更新data store
任务启动
在 com.alibaba.nacos.core.distributed.distro.DistroProtocol 的构造函数中调用startDistroTask() 方法,该方法会执行 startVerifyTask() 和 startLoadTask() ,我们重点关注startLoadTask() ,该方法代码如下:
private void startDistroTask() {
if (EnvUtil.getStandaloneMode()) {
isInitialized = true;
return;
}
//启动startVerifyTask,做数据同步校验
startVerifyTask();
//启动DistroLoadDataTask,批量加载数据
startLoadTask();
}
//启动DistroLoadDataTask
private void startLoadTask() {
//处理状态回调对象
DistroCallback loadCallback = new DistroCallback() {
//处理成功
@Override
public void onSuccess() {
isInitialized = true;
}
//处理失败
@Override
public void onFailed(Throwable throwable) {
isInitialized = false;
}
};
//执行DistroLoadDataTask,是一个多线程
GlobalExecutor.submitLoadDataTask(
new DistroLoadDataTask(memberManager, distroComponentHolder, distroConfig, loadCallback));
}
//数据校验
private void startVerifyTask() {
GlobalExecutor.schedulePartitionDataTimedSync(new DistroVerifyTask(memberManager, distroComponentHolder),
distroConfig.getVerifyIntervalMillis());
}
数据如何执行加载
上面方法会调用 DistroLoadDataTask 对象,而该对象其实是个线程,因此会执行它的run方法,run方法会调用load()方法实现数据全量加载,代码如下:
public DistroLoadDataTask(ServerMemberManager memberManager, DistroComponentHolder distroComponentHolder,
DistroConfig distroConfig, DistroCallback loadCallback) {
this.memberManager = memberManager;
this.distroComponentHolder = distroComponentHolder;
this.distroConfig = distroConfig;
this.loadCallback = loadCallback;
loadCompletedMap = new HashMap<>(1);
}
@Override
public void run() {
try {
//加载数据
load();
if (!checkCompleted()) {
GlobalExecutor.submitLoadDataTask(this, distroConfig.getLoadDataRetryDelayMillis());
} else {
loadCallback.onSuccess();
Loggers.DISTRO.info("[DISTRO-INIT] load snapshot data success");
}
} catch (Exception e) {
loadCallback.onFailed(e);
Loggers.DISTRO.error("[DISTRO-INIT] load snapshot data failed. ", e);
}
}
//加载数据,并同步
private void load() throws Exception {
while (memberManager.allMembersWithoutSelf().isEmpty()) {
Loggers.DISTRO.info("[DISTRO-INIT] waiting server list init...");
TimeUnit.SECONDS.sleep(1);
}
while (distroComponentHolder.getDataStorageTypes().isEmpty()) {
Loggers.DISTRO.info("[DISTRO-INIT] waiting distro data storage register...");
TimeUnit.SECONDS.sleep(1);
}
//同步数据
for (String each : distroComponentHolder.getDataStorageTypes()) {
if (!loadCompletedMap.containsKey(each) || !loadCompletedMap.get(each)) {
//从远程机器上同步所有数据
loadCompletedMap.put(each, loadAllDataSnapshotFromRemote(each));
}
}
}
数据同步
数据同步会通过Http请求从远程服务器获取数据,并同步到当前服务的缓存中,执行流程如下:
1:loadAllDataSnapshotFromRemote()从远程加载所有数据,并处理同步到本机
2:transportAgent.getDatumSnapshot()远程加载数据,通过Http请求执行远程加载
3:dataProcessor.processSnapshot()处理数据同步到本地
数据处理完整逻辑代码如下: loadAllDataSnapshotFromRemote() 方法
private boolean loadAllDataSnapshotFromRemote(String resourceType) {
DistroTransportAgent transportAgent = distroComponentHolder.findTransportAgent(resourceType);
DistroDataProcessor dataProcessor = distroComponentHolder.findDataProcessor(resourceType);
if (null == transportAgent || null == dataProcessor) {
Loggers.DISTRO.warn("[DISTRO-INIT] Can't find component for type {}, transportAgent: {}, dataProcessor: {}",
resourceType, transportAgent, dataProcessor);
return false;
}
//遍历集群成员节点,不包括自己
for (Member each : memberManager.allMembersWithoutSelf()) {
try {
Loggers.DISTRO.info("[DISTRO-INIT] load snapshot {} from {}", resourceType, each.getAddress());
//从远程节点加载数据,调用http请求接口: distro/datums;
DistroData distroData = transportAgent.getDatumSnapshot(each.getAddress());
//处理数据
boolean result = dataProcessor.processSnapshot(distroData);
Loggers.DISTRO
.info("[DISTRO-INIT] load snapshot {} from {} result: {}", resourceType, each.getAddress(),
result);
if (result) {
return true;
}
} catch (Exception e) {
Loggers.DISTRO.error("[DISTRO-INIT] load snapshot {} from {} failed.", resourceType, each.getAddress(), e);
}
}
return false;
}
远程加载数据代码如下: transportAgent.getDatumSnapshot() 方法
//从namingProxy代理获取所有的数据data,从获取的结果result中获取数据bytes;
@Override
public DistroData getDatumSnapshot(String targetServer) {
try {
//从namingProxy代理获取所有的数据data,从获取的结果result中获取数据bytes;
byte[] allDatum = NamingProxy.getAllData(targetServer);
//将数据封装成DistroData
return new DistroData(new DistroKey("snapshot", KeyBuilder.INSTANCE_LIST_KEY_PREFIX), allDatum);
} catch (Exception e) {
throw new DistroException(String.format("Get snapshot from %s failed.", targetServer), e);
}
}
/**
* Get all datum from target server.
* NamingProxy.getAllData
* 执行HttpGet请求,并获取返回数据
* @param server target server address
* @return all datum byte array
* @throws Exception exception
*/
public static byte[] getAllData(String server) throws Exception {
//参数封装
Map<String, String> params = new HashMap<>(8);
//组装URL,并执行HttpGet请求,获取结果集
RestResult<String> result = HttpClient.httpGet(
"http://" + server + EnvUtil.getContextPath() + UtilsAndCommons.NACOS_NAMING_CONTEXT + ALL_DATA_GET_URL,
new ArrayList<>(), params);
//返回数据
if (result.ok()) {
return result.getData().getBytes();
}
throw new IOException("failed to req API: " + "http://" + server + EnvUtil.getContextPath()
+ UtilsAndCommons.NACOS_NAMING_CONTEXT + ALL_DATA_GET_URL + ". code: " + result.getCode() + " msg: "
+ result.getMessage());
}
处理数据同步到本地代码如下: dataProcessor.processSnapshot()
public boolean processSnapshot(DistroData distroData) {
try {
return processData(distroData.getContent());
} catch (Exception e) {
return false;
}
}
private boolean processData(byte[] data) throws Exception {
//从data反序列化出datumMap
if (data.length > 0) {
Map<String, Datum<Instances>> datumMap = serializer.deserializeMap(data, Instances.class);
// 把数据存储到dataStore,也就是本地缓存dataMap
for (Map.Entry<String, Datum<Instances>> entry : datumMap.entrySet()) {
dataStore.put(entry.getKey(), entry.getValue());
//监听器不包括key,就创建一个空的service,并且绑定监听器
if (!listeners.containsKey(entry.getKey())) {
// pretty sure the service not exist:
if (switchDomain.isDefaultInstanceEphemeral()) {
// create empty service
//创建一个空的service
Loggers.DISTRO.info("creating service {}", entry.getKey());
Service service = new Service();
String serviceName = KeyBuilder.getServiceName(entry.getKey());
String namespaceId = KeyBuilder.getNamespace(entry.getKey());
service.setName(serviceName);
service.setNamespaceId(namespaceId);
service.setGroupName(Constants.DEFAULT_GROUP);
// now validate the service. if failed, exception will be thrown
service.setLastModifiedMillis(System.currentTimeMillis());
service.recalculateChecksum();
// The Listener corresponding to the key value must not be empty
// 与键值对应的监听器不能为空,这里的监听器类型是 ServiceManager
RecordListener listener = listeners.get(KeyBuilder.SERVICE_META_KEY_PREFIX).peek();
if (Objects.isNull(listener)) {
return false;
}
//为空的绑定监听器
listener.onChange(KeyBuilder.buildServiceMetaKey(namespaceId, serviceName), service);
}
}
}
//循环所有datumMap
for (Map.Entry<String, Datum<Instances>> entry : datumMap.entrySet()) {
if (!listeners.containsKey(entry.getKey())) {
// Should not happen:
Loggers.DISTRO.warn("listener of {} not found.", entry.getKey());
continue;
}
try {
//执行监听器的onChange监听方法
for (RecordListener listener : listeners.get(entry.getKey())) {
listener.onChange(entry.getKey(), entry.getValue().value);
}
} catch (Exception e) {
Loggers.DISTRO.error("[NACOS-DISTRO] error while execute listener of key: {}", entry.getKey(), e);
continue;
}
// Update data store if listener executed successfully:
// 监听器listener执行成功后,就更新dataStore
dataStore.put(entry.getKey(), entry.getValue());
}
}
return true;
}
到此实现数据全量同步,其实全量同步最终封装的协议还是Http。
1.2.4.2 增量同步
新增数据使用异步广播同步:
1:DistroProtocol 使用 sync() 方法接收增量数据
2:向其他节点发布广播任务 调用 distroTaskEngineHolder 发布延迟任务
3:调用 DistroDelayTaskProcessor.process() 方法进行任务投递:将延迟任务转换为异步变更任务
4:执行变更任务 DistroSyncChangeTask.run() 方法:向指定节点发送消息
调用 DistroHttpAgent.syncData() 方法发送数据
调用 NamingProxy.syncData() 方法发送数据
5:异常任务调用 handleFailedTask() 方法进行处理
调用 DistroFailedTaskHandler 处理失败任务
调用 DistroHttpCombinedKeyTaskFailedHandler 将失败任务重新投递成延迟任务。
增量数据入口
我们回到服务注册,服务注册的 InstanceController.register() 就是数据入口,它会调用
ServiceManager.registerInstance() ,执行数据同步的时候,调用 addInstance() ,在该方法中会执行 DistroConsistencyServiceImpl.put() ,该方法是增量同步的入口,会调用
distroProtocol.sync() 方法,代码如下:
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
final String namespaceId = WebUtils
.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
NamingUtils.checkServiceNameFormat(serviceName);
final Instance instance = parseInstance(request);
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
createEmptyService(namespaceId, serviceName, instance.isEphemeral());
Service service = getService(namespaceId, serviceName);
if (service == null) {
throw new NacosException(NacosException.INVALID_PARAM,
"service not found, namespace: " + namespaceId + ", service: " + serviceName);
}
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
throws NacosException {
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
Service service = getService(namespaceId, serviceName);
synchronized (service) {
List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
Instances instances = new Instances();
instances.setInstanceList(instanceList);
consistencyService.put(key, instances);
}
}
put
@Override
public void put(String key, Record value) throws NacosException {
//将数据存入到dataStore中
onPut(key, value);
//使用distroProtocol同步数据
distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE,
globalConfig.getTaskDispatchPeriod() / 2);
}
sync() 方法会执行任务发布,代码如下:
public void sync(DistroKey distroKey, DataOperation action, long delay) {
//向除了自己外的所有节点广播
for (Member each : memberManager.allMembersWithoutSelf()) {
DistroKey distroKeyWithTarget = new DistroKey(distroKey.getResourceKey(), distroKey.getResourceType(),
each.getAddress());
DistroDelayTask distroDelayTask = new DistroDelayTask(distroKeyWithTarget, action, delay);
//从distroTaskEngineHolder获取延时执行引擎,并将distroDelayTask任务添加进来
//执行延时任务发布
distroTaskEngineHolder.getDelayTaskExecuteEngine().addTask(distroKeyWithTarget, distroDelayTask);
if (Loggers.DISTRO.isDebugEnabled()) {
Loggers.DISTRO.debug("[DISTRO-SCHEDULE] {} to {}", distroKey, each.getAddress());
}
}
}
增量同步操作
延迟任务对象我们可以从 DistroTaskEngineHolder 构造函数中得知是DistroDelayTaskProcessor ,代码如下:
@Component
public class DistroProtocol {
private final ServerMemberManager memberManager;
private final DistroComponentHolder distroComponentHolder;
private final DistroTaskEngineHolder distroTaskEngineHolder;
private final DistroConfig distroConfig;
private volatile boolean isInitialized = false;
public DistroProtocol(ServerMemberManager memberManager, DistroComponentHolder distroComponentHolder,
DistroTaskEngineHolder distroTaskEngineHolder, DistroConfig distroConfig) {
this.memberManager = memberManager;
this.distroComponentHolder = distroComponentHolder;
this.distroTaskEngineHolder = distroTaskEngineHolder;
this.distroConfig = distroConfig;
startDistroTask();
}
//构造函数指定任务处理器
public DistroTaskEngineHolder(DistroComponentHolder distroComponentHolder) {
DistroDelayTaskProcessor defaultDelayTaskProcessor = new DistroDelayTaskProcessor(this, distroComponentHolder);
//指定任务处理器defaultDelayTaskProcessor
delayTaskExecuteEngine.setDefaultTaskProcessor(defaultDelayTaskProcessor);
}
它延迟执行的时候会执行 process 方法,该方法正是执行数据同步的地方,它会执行
DistroSyncChangeTask任务,代码如下:
//任务处理过程
@Override
public boolean process(NacosTask task) {
if (!(task instanceof DistroDelayTask)) {
return true;
}
DistroDelayTask distroDelayTask = (DistroDelayTask) task;
DistroKey distroKey = distroDelayTask.getDistroKey();
if (DataOperation.CHANGE.equals(distroDelayTask.getAction())) {
//将延迟任务变更成异步任务,异步任务对象是一个线程
DistroSyncChangeTask syncChangeTask = new DistroSyncChangeTask(distroKey, distroComponentHolder);
//将任务添加到NacosExecuteTaskExecuteEngine中,并执行
distroTaskEngineHolder.getExecuteWorkersManager().addTask(distroKey, syncChangeTask);
return true;
}
return false;
}
DistroSyncChangeTask 实质上是任务的开始,它自身是一个线程,所以会执行它的run方法,而run方法这是数据同步操作,代码如下:
@Override
public void run() {
Loggers.DISTRO.info("[DISTRO-START] {}", toString());
try {
//获取本地缓存数据
String type = getDistroKey().getResourceType();
DistroData distroData = distroComponentHolder.findDataStorage(type).getDistroData(getDistroKey());
distroData.setType(DataOperation.CHANGE);
//向其他节点同步数据
boolean result = distroComponentHolder.findTransportAgent(type).syncData(distroData, getDistroKey().getTargetServer());
if (!result) {
handleFailedTask();
}
Loggers.DISTRO.info("[DISTRO-END] {} result: {}", toString(), result);
} catch (Exception e) {
Loggers.DISTRO.warn("[DISTRO] Sync data change failed.", e);
handleFailedTask();
}
}
数据同步会执行调用 syncData ,该方法其实就是通过Http协议将数据发送到其他节点实现数据同步,代码如下:
@Override
public boolean syncData(DistroData data, String targetServer) {
if (!memberManager.hasMember(targetServer)) {
return true;
}
//获取数据字节数组
byte[] dataContent = data.getContent();
//通过Http协议同步数据
return NamingProxy.syncData(dataContent, data.getDistroKey().getTargetServer());
}
2 Sentinel Dashboard数据持久化
Sentinel 的理念是开发者只需要关注资源的定义,当资源定义成功后可以动态增加各种流控降级规则。 Sentinel 提供两种方式修改规则:
- 通过 API 直接修改 ( loadRules )
- 通过 DataSource 适配不同数据源修改
手动通过 API 修改比较直观,可以通过以下几个 API 修改不同的规则:
FlowRuleManager.loadRules(List<FlowRule> rules); // 修改流控规则
DegradeRuleManager.loadRules(List<DegradeRule> rules); // 修改降级规则
手动修改规则(硬编码方式)一般仅用于测试和演示,生产上一般通过动态规则源的方式来动态管理规则。
2.1 动态配置原理
loadRules() 方法只接受内存态的规则对象,但更多时候规则存储在文件、数据库或者配置中心当中。 DataSource 接口给我们提供了对接任意配置源的能力。相比直接通过 API 修改规则,实现DataSource 接口是更加可靠的做法。
我们推荐通过控制台设置规则后将规则推送到统一的规则中心,客户端实现 ReadableDataSource 接口端监听规则中心实时获取变更,流程如下:
DataSource 扩展常见的实现方式有:
拉模式:客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件,甚至是 VCS 等。这样做的方式是简单,缺点是无法及时获取变更;
推模式:规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、 Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。
Sentinel 目前支持以下数据源扩展:
Pull-based: 动态文件数据源、Consul, Eureka
Push-based: ZooKeeper, Redis, Nacos, Apollo, etcd
2.2 Sentinel+Nacos数据持久化
我们要想实现Sentinel+Nacos数据持久化,需要下载Sentinel控制台源码,关于源码下载我们这里就不再重复了。
在Sentinel Dashboard中配置规则之后重启应用就会丢失,所以实际生产环境中需要配置规则的持久化实现,Sentinel提供多种不同的数据源来持久化规则配置,包括file,redis、nacos、zk。
这就需要涉及到Sentinel Dashboard的规则管理及推送功能:集中管理和推送规则。 sentinel-core提供 API 和扩展接口来接收信息。开发者需要根据自己的环境,选取一个可靠的推送规则方式;同时,规则最好在控制台中集中管理。
我们采用Push模式,即Sentinel-Dashboard统一管理配置,然后将规则统一推送到Nacos并持久化(生成配置文件),最后客户端监听Nacos(这一部了解使用过Nacos的话应该很熟,采用ConfigService.getConfg()方法获取配置文件),下发配置生成Rule。
这张图的意思我们解释说明一下:
1:Sentinel Dashboard界面配置流控规则---发布/推送--->Nacos生成配置文件并持久化;
2:通过Nacos配置文件修改流控规则---拉取--->Sentinel Dashboard界面显示最新的流控规则。
在Nacos控制台上修改流控制,虽然可以同步到Sentinel Dashboard,但是Nacos此时应该作为一个流控规则的持久化平台,所以正常操作过程应该是开发者在Sentinel Dashboard上修改流控规则后同步到Nacos,遗憾的是目前Sentinel Dashboard不支持该功能。
如果公司没有统一在Sentinel Dashboard或Nacos中二选一进行配置,而是一会在Sentinel Dashboard配置,一会在Nacos配置。那么就会出现很严重的问题(流控规则达不到预期,配置数据不一致),所以推荐使用Sentinel Dashboard统一界面进行配置管理流控规则。
我们接下来基于Sentinel1.8.1开始改造Sentinel Dashboard,使他能结合Nacos实现数据持久化。
2.2.1 Dashboard改造分析
Sentinel Dashboard的流控规则下的所有操作,都会调用Sentinel-Dashboard源码中的
FlowControllerV1类,这个类中包含流控规则本地化的CRUD操作;
在com.alibaba.csp.sentinel.dashboard.controller.v2包下存在一个FlowControllerV2;类,这个类同样提供流控规则的CURD,与V1不同的是,它可以实现指定数据源的规则拉取和发布。
上面代码就是 FlowControllerV2 部分代码,分别实现了拉取规则和推送规则:
1:DynamicRuleProvider:动态规则的拉取,从指定数据源中获取控制后在Sentinel Dashboard中展示。
2:DynamicRulePublisher:动态规则发布,将在Sentinel Dashboard中修改的规则同步到指定数据源中。
我们只需要扩展这两个类,然后集成Nacos来实现Sentinel Dashboard规则同步。
2.2.2 页面改造
在目录resources/app/scripts/directives/sidebar找到sidebar.html,里面有关于V1版本的请求入口:
<li ui-sref-active="active" ng-if="!entry.isGateway">
<a ui-sref="dashboard.flowV1({app: entry.app})">
<i class="glyphicon glyphicon-filter"></i> 流控规则</a>
</li>
对应的JS(app.js)请求如下,可以看到请求就是V1版本的Controller,那么之后的改造需要重新对应V2版本的Controller
.state('dashboard.flowV1', {
templateUrl: 'app/views/flow_v1.html',
url: '/flow/:app',
controller: 'FlowControllerV1',
resolve: {
loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) {
return $ocLazyLoad.load({
name: 'sentinelDashboardApp',
files: [
'app/scripts/controllers/flow_v1.js',
]
});
}]
}
})
2.2.3 Nacos配置
在源码中虽然官方提供了test示例(即test目录)下关于Nacos等持久化示例,但是具体的实现还需要一些细节,比如在Sentinel Dashboard配置Nacos的serverAddr、namespace、groupId,并且通过Nacos获取配置文件获取服务列表等。
我们可以打开 NacosConfig 源码, NacosConfig 中ConfigFactory.createConfigService(“localhost”) 并没有实现创建具体的 nacos config service ,而是默认 localhost ,application.properties文件中也没有Nacos的相关配置,这些都需要我们额外配置, NacosConfig 代码如下:
@EnableConfigurationProperties(NacosPropertiesConfiguration.class)
@Configuration
public class NacosConfig {
@Bean
public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() {
return s -> JSON.parseArray(s, FlowRuleEntity.class);
}
@Bean
public ConfigService nacosConfigService(NacosPropertiesConfiguration nacosPropertiesConfiguration) throws Exception {
return ConfigFactory.createConfigService(properties);
}
}
如果我们需要把数据存储到Nacos,在 NacosConfigutils 已经指定了默认的流控规则配置文件的groupId 等,如果需要指定的话这里也需要修改
public final class NacosConfigUtil {
//Nacos中对应的GroupID
public static final String GROUP_ID = "SENTINEL_GROUP";
//文件后半部分
public static final String FLOW_DATA_ID_POSTFIX = "-flow-rules";
public static final String PARAM_FLOW_DATA_ID_POSTFIX = "-param-rules";
public static final String CLUSTER_MAP_DATA_ID_POSTFIX = "-cluster-map";
/**
* cc for `cluster-client`
*/
public static final String CLIENT_CONFIG_DATA_ID_POSTFIX = "-cc-config";
/**
* cs for `cluster-server`
*/
public static final String SERVER_TRANSPORT_CONFIG_DATA_ID_POSTFIX = "-cs-transport-config";
public static final String SERVER_FLOW_CONFIG_DATA_ID_POSTFIX = "-cs-flow-config";
public static final String SERVER_NAMESPACE_SET_DATA_ID_POSTFIX = "-cs-namespace-set";
private NacosConfigUtil() {}
}
.我们在 application.properties 中配置Nacos:
spring.cloud.sentinel.datasource.flow.nacos.server-addr=nacos:8848
spring.cloud.sentinel.datasource.flow.nacos.data-id=${spring.application.name}-flow-rules
spring.cloud.sentinel.datasource.flow.nacos.group-id=SENTINEL_GROUP
spring.cloud.sentinel.datasource.flow.nacos.data-type=json
spring.cloud.sentinel.datasource.flow.nacos.rule-type=flow
2.3.4 Dashboard持久化改造
我们接下来开始改造Dashboard源码,官方提供的Nacos持久化用例都是在test目录下,所以scope需要去除test,需要sentinel-datasource-nacos包的支持。之后将修改好的源码放在源码主目录下,而不是继续在test目录下。
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<!--<scope>test</scope>-->
</dependency>
找到resources/app/scripts/directives/sidebar/sidebar.html文件修改,修改flowV1为flow,去掉V1,这样的话会调用FlowControllerV2接口
修改前:
<li ui-sref-active="active" ng-if="!entry.isGateway">
<a ui-sref="dashboard.flowV1({app: entry.app})">
<i class="glyphicon glyphicon-filter"></i> 流控规则</a>
</li>
修改后:
<li ui-sref-active="active" ng-if="!entry.isGateway">
<a ui-sref="dashboard.flow({app: entry.app})">
<i class="glyphicon glyphicon-filter"></i> 流控规则 V1</a>
</li>
这样就可以通过js跳转至FlowControllerV2了,app.js代码如下:
.state('dashboard.flow', {
templateUrl: 'app/views/flow_v2.html',
url: '/v2/flow/:app',
controller: 'FlowControllerV2',
resolve: {
loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) {
return $ocLazyLoad.load({
name: 'sentinelDashboardApp',
files: [
'app/scripts/controllers/flow_v2.js',
]
});
}]
}
})
2.3.5 Nacos配置创建
我们采用官方的约束,即 默认 Nacos 适配的 dataId 和 groupId 约定,所以不需要修改
NacosConfigUtil.java了,配置如下:
groupId: SENTINEL_GROUP
流控规则 dataId: {appName}-flow-rules,比如应用名为 appA,则 dataId 为 appA-flow-rules
我们在 application.properties 中配置Nacos服务信息:
# nacos config server
sentinel.nacos.serverAddr=nacos:8848
sentinel.nacos.namespace=
sentinel.nacos.group-id=SENTINEL_GROUP
接下来创建读取 nacos 配置的 NacosPropertiesConfiguration 文件并且 application.properties指定配置
ConfigurationProperties(prefix = "sentinel.nacos")
public class NacosPropertiesConfiguration {
private String serverAddr;
private String dataId;
private String groupId = "SENTINEL_GROUP"; // 默认分组
private String namespace;
public String getServerAddr() {
return serverAddr;
}
public void setServerAddr(String serverAddr) {
this.serverAddr = serverAddr;
}
public String getDataId() {
return dataId;
}
public void setDataId(String dataId) {
this.dataId = dataId;
}
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
}
2.3.6 改造源码
2.3.6.1 改造NacosConfig
我们最后改造NacosConfig,让NacosConfig做两件事:
1) 注入Convert转换器,将FlowRuleEntity转化成FlowRule,以及反向转化
2) 注入Nacos配置服务ConfigService
@EnableConfigurationProperties(NacosPropertiesConfiguration.class)
@Configuration
public class NacosConfig {
@Bean
public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() {
return s -> JSON.parseArray(s, FlowRuleEntity.class);
}
@Bean
public ConfigService nacosConfigService(NacosPropertiesConfiguration nacosPropertiesConfiguration) throws Exception {
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, nacosPropertiesConfiguration.getServerAddr());
properties.put(PropertyKeyConst.NAMESPACE, nacosPropertiesConfiguration.getNamespace());
return ConfigFactory.createConfigService(properties);
// return ConfigFactory.createConfigService("localhost");
}
}
2.3.6.2 动态获取流控规则
动态实现从Nacos配置中心获取流控规则需要重写FlowRuleNacosProvider与FlowRuleNacosPublisher类。
1)重写FlowRuleNacosProvider类
//1)通过ConfigService的getConfig()方法从Nacos Config Server读取指定配置信息
//2)通过转为converter转化为FlowRule规则
@Component("flowRuleNacosProvider")
public class FlowRuleNacosProvider implements DynamicRuleProvider<List<FlowRuleEntity>> {
@Autowired
private ConfigService configService;
@Autowired
private Converter<String, List<FlowRuleEntity>> converter;
@Override
public List<FlowRuleEntity> getRules(String appName) throws Exception {
String rules = configService.getConfig(appName + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
NacosConfigUtil.GROUP_ID, 3000);
if (StringUtil.isEmpty(rules)) {
return new ArrayList<>();
}
return converter.convert(rules);
}
}
2)重写FlowRuleNacosPublisher类:
@Service("flowRuleNacosPublisher")
public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {
public static final Logger log = LoggerFactory.getLogger(FlowRuleNacosPublisher.class);
@Autowired
private ConfigService configService;
@Autowired
private Converter<List<FlowRuleEntity>, String> converter;
/**
* 通过configService的publishConfig()方法将rules发布到nacos
* @param app app name
* @param rules list of rules to push
* @throws Exception
*/
@Override
public void publish(String app, List<FlowRuleEntity> rules) throws Exception {
AssertUtil.notEmpty(app, "app name cannot be empty");
if (rules == null) {
return;
}
log.info("sentinel dashboard push rules: {}", rules);
configService.publishConfig(app + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
NacosConfigUtil.GROUP_ID, converter.convert(rules));
}
}
3)替换默认对象
修改FlowControllerV2类,使用@Qulifier将上面配置的两个类注入进来
2.3.6.3 数据持久化测试
我们接下来将程序打包,如下图:
打包好程序后,再将程序运行起来:
java -Dserver.port=8858 -Dcsp.sentinel.dashboard.server=localhost:8858 -
Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
运行起来后,我们可以发现在Nacos中多了一个服务,如下图:
我们随意增加一个流控规则,如下图:
点击新增之后,我们可以发现SENTILE_GROUP组下多了一个文件,sentinel-flow-rule文件,效果如下:
3 Seata事务控制源码剖析
我们前面说过,Seata通过代理数据源实现了将业务数据操作和日志数据操作绑定到同一个事务中了,实现流程如上图,我们接下来分析一下源码。源码学习我们带着下面这3个问题去学习:
1:SQL 解析与执行。
2:全局”事务”的注册,提交与回滚。
3:undo 日志的生成与使用。
3.1 全局事务操作判断
对SQL按照不同的SQL类型进行执行,并保存beforeImage以及afterImage镜像文件到 undo 数据结构中用于事务回滚,这是Seata代理数据源的一大功能,我们看看它的源码是如何实现的。
SQL解析的入口是 BaseTransactionalExecutor ,它是一个抽象类,实现了Executor 接口中的execute 方法,在该方法中会判断是否需要全局事务,如果需要全局事务会给每个操作分配一个xid。它还定义了doExecute() 抽象方法,具体执行由子类AbstractDMLBaseExecutor 实现。源码如下:
/***
* 事务操作判断
*/
@Override
public T execute(Object... args) throws Throwable {
//1. 如果为Global 事务,则将XId 绑定在上下文中
if (RootContext.inGlobalTransaction()) {
String xid = RootContext.getXID();
statementProxy.getConnectionProxy().bind(xid);
}
//2. 是否需要获取全局锁,进行不同设置
statementProxy.getConnectionProxy().setGlobalLockRequire(RootContext.requireGlobalLock());
return doExecute(args);
}
/**
* Do execute object.
* 定义抽象方法,由子类自行实现。
* @param args the args
* @return the object
* @throws Throwable the throwable
*/
protected abstract T doExecute(Object... args) throws Throwable;
3.2 操作日志镜像保
AbstractDMLBaseExecutor 类继承自 BaseTransactionalExecutor 抽象。实现了父类的 doExecute()方法,并定义了多个抽象方法。代码如下所示:
@Override
public T doExecute(Object... args) throws Throwable {
AbstractConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
// 是否为自动提交
if (connectionProxy.getAutoCommit()) {
return executeAutoCommitTrue(args);
} else {
//非自动提交
return executeAutoCommitFalse(args);
}
}
protected T executeAutoCommitFalse(Object[] args) throws Exception {
if (!JdbcConstants.MYSQL.equalsIgnoreCase(getDbType()) && getTableMeta().getPrimaryKeyOnlyName().size() > 1)
{
throw new NotSupportYetException("multi pk only support mysql!");
}
//保存操作前数据镜像
TableRecords beforeImage = beforeImage();
//执行操作
T result = statementCallback.execute(statementProxy.getTargetStatement(), args);
//保存操作后镜像
TableRecords afterImage = afterImage(beforeImage);
//将前后镜像保存到undolog中
prepareUndoLog(beforeImage, afterImage);
return result;
}
/**
* Before image table records.
* 业务SQL 执行前的镜像 (用于事务回滚)
* @return the table records
* @throws SQLException the sql exception
*/
protected abstract TableRecords beforeImage() throws SQLException;
/**
* After image table records.
* 业务SQL 执行后的镜像。(用于事务回滚)
* @param beforeImage the before image
* @return the table records
* @throws SQLException the sql exception
*/
protected abstract TableRecords afterImage(TableRecords beforeImage) throws SQLException;
3.3 事务操作
事务提交也是Seata中的核心逻辑,在 ConnectionProxy 类中对核心逻辑进行了实现,在 doCommit 中实现了事务控制逻辑,代码如下:
@Override
public void commit() throws SQLException {
try {
LOCK_RETRY_POLICY.execute(() -> {
doCommit();
return null;
});
} catch (SQLException e) {
if (targetConnection != null && !getAutoCommit()) {
rollback();
}
throw e;
} catch (Exception e) {
throw new SQLException(e);
}
}
/**
* 事务提交
* @throws SQLException
*/
private void doCommit() throws SQLException {
// 如果是Global事务,则执行 processGlobalTransactionCommit方法
if (context.inGlobalTransaction()) {
processGlobalTransactionCommit();
} else if (context.isGlobalLockRequire()) {
// 是否需要获取全局锁
processLocalCommitWithGlobalLocks();
} else {
//本地事务直接 commit
targetConnection.commit();
}
}
/****
* 事务提交
* @throws SQLException
*/
private void processGlobalTransactionCommit() throws SQLException {
try {
//1. 向 server 端进行分支事务注册
register();
} catch (TransactionException e) {
recognizeLockKeyConflictException(e, context.buildLockKeys());
}
try {
//2. 如果有 UndoLog 日志,则将其新增到 undo_log 表中
UndoLogManagerFactory.getUndoLogManager(this.getDbType()).flushUndoLogs(this);
//3. 执行本地事务
targetConnection.commit();
} catch (Throwable ex) {
LOGGER.error("process connectionProxy commit error: {}", ex.getMessage(), ex);
//4. 如果异常,则向 server 端 报告未提交完成。
report(false);
throw new SQLException(ex);
}
if (IS_REPORT_SUCCESS_ENABLE) {
//5. 向 server 端,报告提交完成。
report(true);
}
//6. 并将ccontext.reset()掉
context.reset();
}
/**
* 事务回滚
* @throws SQLException
*/
@Override
public void rollback() throws SQLException {
//1. 本地事务进行回滚
targetConnection.rollback();
//2. 如果是全局事务,且进行过分支注册,则向server报告未提交完成。
if (context.inGlobalTransaction() && context.isBranchRegistered()) {
report(false);
}
context.reset();
}
总结
Nacous
服务启动
修改application.properties
Nacos服务:console->com.alibaba.nacos.Nacos.main()
服务注册
服务注册
nacos-client->NacosNamingService
客户端:nacos-client
服务注册:
NacosNamingService.registerInstance()
注册服务->NamingProxy.registerService()
添加心跳检测->BeatReactor.addBeatInfo()
服务注册源代码->registerInstance()
流程源码
心跳定时任务添加源码
服务注册源码
NamingProxy.registerService()
NamingProxy.reqApi()
NamingProxy.callServer()
NacosRestTemplate.exchangeForm()
NacosRestTemplate.execute()
Http服务注册:/v1/ns/instance
NacosRestTemplate
exchangeForm()
execute()->DefaultHttpClientRequest.execute()
HttpClient:InternalHttpClient.execute()
HttpClient的主要功能
实现了所有 HTTP 的方法(GET、POST、PUT、HEAD、DELETE、HEAD、OPTIONS 等)
支持 HTTPS 协议
支持代理服务器(Nginx等)等
支持自动(跳转)转向
服务端:nacos-naming
InstanceController.register()
服务订阅
服务订阅->NIO
入口:NamingFactory.createNamingService(properties);
构造函数:NacosNamingService.NacosNamingService()
NacosNamingService.init()
new HostReactor()->创建订阅->new PushReceiver(this)
服务订阅:PushReceiver->Runnable线程->缓存serviceInfoMap
run()基于NIO循环获取数据包
udpSocket.receive(packet):获取数据包
hostReactor.processServiceJson(pushPacket.data):处理数据包
serviceInfoMap.put():服务列表信息存储到serviceInfoMap中
udpSocket.send():ack确认
服务发现
服务发现
服务发现调用方法:NacosNamingService.getAllInstances()
没开启服务订阅:远程获取 /v1/ns/instance/list
服务端:nacos-naming->InstanceController.list()
客户端
HostReactor.getServiceInfoDirectlyFromServer()
NamingProxy.queryList()
NamingProxy.reqApi()
开启服务订阅:本地缓存serviceInfoMap获取
HostReactor.getServiceInfo()
服务注销
服务注销:
源代码
NamingProxy.deregisterService()
NamingProxy.reqApi()
NamingProxy.callServer()
NacosRestTemplate.exchangeForm()
增量数据同步
增量数据同步
任务添加
DistroProtocol.sync():同步数据添加定时任务,1秒后执行
添加至->NacosDelayTaskExecuteEngine.tasks
同步任务启动:DistroTaskEngineHolder
构造函数DistroTaskEngineHolder()
创建任务执行引擎:NacosDelayTaskExecuteEngine
源代码
多线程创建:ProcessRunnable
线程:NacosDelayTaskExecuteEngine()
线程创建代码
任务执行:ProcessRunnable.run()
run()->NacosDelayTaskExecuteEngine.processTasks()
NacosDelayTaskExecuteEngine.processTasks()
源代码
从tasks获取任务
执行任务
process(NacosTask task)->添加同步任务->DistroSyncChangeTask
创建任务源码
添加任务源码
任务添加流程
任务添加到队列中
多线程任务执行:TaskExecuteWorker->InnerWorker(线程)
创建构造函数
任务执行InnerWorker.run()
数据同步任务执行
数据同步执行:DistroSyncChangeTask.run()
同步请求:http
设置定时任务对象:DistroDelayTaskProcessor
Distro协议:数据一致性(服务数据并不会持久化:DataStore)
Distro协议:数据一致性(服务数据并不会持久化:DataStore)
Distro特点
专门为了注册中心而创造出的协议
客户端与服务端有两个重要的交互:服务注册与心跳发送
客户端以服务为维度向服务端注册,注册后每隔一段时间向服务端发送一次心跳,心跳包需要带上注册服务的全部信息,在客户端看来,服务端节点对等,所以请求的节点是随机的
客户端请求失败则换一个节点重新发送请求
服务端节点都存储所有数据,但每个节点只负责其中一部分服务,在接收到客户端的“写”(注册、心跳、下线等)请求后,服务端节点判断请求的服务是否为自己负责,如果是,则处理,否则交由负责的节点处理
每个服务端节点主动发送健康检查到其他节点,响应的节点被该节点视为健康节点
服务端在接收到客户端的服务心跳后,如果该服务不存在,则将该心跳请求当做注册请求来处理
服务端如果长时间未收到客户端心跳,则下线该服务
负责的节点在接收到服务注册、服务心跳等写请求后将数据写入后即返回,后台异步地将数据同步给其他节点
节点在收到读请求后直接从本机获取后返回,无论数据是否为最新
Distro寻址模式->LookupFactory.createLookUp()
Distro寻址模式-单机寻址-StandaloneMemberLookup
只需要获取自己服务的地址
Distro寻址模式-文件寻址->FileConfigMemberLookup->cluster.conf
服务器寻址模式->AddressServerMemberLookup.initAddressSys()
数据批量同步
DistroLoadDataTask->线程->run()->load()
循环所有节点,从远程获取数据->loadAllDataSnapshotFromRemote(each)
获取所有数据:DistroHttpAgent.getDatumSnapshot()
->NamingProxy.getAllData()->/v1/ns/distro/datums
同步到本地:DataStore
创建监听器同步数据
Sentinel1.8.0配置持久化->Nacos
配置持久化
依赖管理
拷贝测试文件
src/test/java/com/alibaba/csp/sentinel/dashboard/rule/nacos
拷贝至
src/main/java/com/alibaba/csp/sentinel/dashboard/rule
Nacos配置
application.properties中添加nacos配置
配置代码
Nacos配置导入:修改com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConfig
流控规则注入bean->替换成Nacos操作对象FlowControllerV2
流控管理页面修改
src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.html
dashboard.flowV1({app: entry.app})
修改为
dashboard.flow({app: entry.app})
改前:56行
改后:56行
流控规则添加
添加流程
Nacos效果
持久化的配置详情
新增修改
配置后缀修改:NacosConfigUtil
规则推送-System
SystemRuleNacosProvider
SystemRuleNacosPublisher
规则转换器:在NacosConfig中添加规则转换器
配置修改控制器:SystemController
效果
服务端配置
服务端配置
引入依赖配置
修改配置文件
Seata源码
事务操作流程
业务操作入口:BaseTransactionalExecutor
execute()
AbstractDMLBaseExecutor.doExecute()
ConnectionProxy.processGlobalTransactionCommit()
数据回滚
DataSourceManager.branchRollback()
AbstractUndoLogManager.undo()