提示:源码为nacos-1.4.1版本。
文章目录
前言
听说nacos异步任务+内存队列来支持高并发。那么是不是呢?我们从源码来验证一下。
源码分析:
提示:注意看源码里的注释,都是关键代码。其余代码由于篇幅原因,省略。如有错误,请留言。
上一篇讲了客户端注册是发送v1/ns/instance的POST请求,那么我们寻找这个controller。
程序入口com.alibaba.nacos.naming.controllers.InstanceController#register
/**
* 服务端注册实例
* Register new instance.
*
* @param request http request
* @return 'ok' if success
* @throws Exception any error during register
*/
@CanDistro
@PostMapping //=v1/ns/instance
@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);
//组装instance
final Instance instance = parseInstance(request);
//注册instance
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
追进registerInstance方法查看
/**
* Register an instance to a service in AP mode.
*
* <p>This method creates service or cluster silently if they don't exist.
*
* @param namespaceId id of namespace
* @param serviceName service name
* @param instance instance to register
* @throws Exception any error occurred in the process
*/
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
//组建空的Service
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);
}
创建和初始化Service
点进createEmptyService方法,进去追踪com.alibaba.nacos.naming.core.ServiceManager#createServiceIfAbsent
/**
* Create service if not exist.
*
* @param namespaceId namespace
* @param serviceName service name
* @param local whether create service by local
* @param cluster cluster
* @throws NacosException nacos exception
*/
public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)throws NacosException {
//第一次为空
Service service = getService(namespaceId, serviceName);
if (service == null) {
Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
service = new Service();
service.setName(serviceName);
service.setNamespaceId(namespaceId);
service.setGroupName(NamingUtils.getGroupName(serviceName));
// now validate the service. if failed, exception will be thrown
service.setLastModifiedMillis(System.currentTimeMillis());
service.recalculateChecksum();
if (cluster != null) {
cluster.setService(service);
service.getClusterMap().put(cluster.getName(), cluster);
}
service.validate();
//初始化
putServiceAndInit(service);
if (!local) {
addOrReplaceService(service);
}
}
}
putServiceAndInit方法
private void putServiceAndInit(Service service) throws NacosException {
//构建空的服务列表
putService(service);
//客户端心跳检查
service.init();
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson());
}
点进putService方法
/**
* Put service into manager.
*
* @param service service
*/
public void putService(Service service) {
if (!serviceMap.containsKey(service.getNamespaceId())) {
synchronized (putServiceLock) {
if (!serviceMap.containsKey(service.getNamespaceId())) {
//构建NamespaceId为key
serviceMap.put(service.getNamespaceId(), new ConcurrentSkipListMap<>());
}
}
}
//构建服务列表
serviceMap.get(service.getNamespaceId()).put(service.getName(), service);
}
这里介绍serviceMap,就是存放实例的注册表。结构为Map(namespace, Map(group::serviceName, Service)).
/**
* Nacos服务注册表
*/
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
这里Service构建完成。继续执行service.init();
/**
* Init service.
*/
public void init() {
//客户端心跳检查定时任务
HealthCheckReactor.scheduleCheck(clientBeatCheckTask);
for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
entry.getValue().setService(this);
entry.getValue().init();
}
}
点进scheduleCheck方法,发现就是延时任务,这里具体逻辑后续会出。
public static void scheduleCheck(ClientBeatCheckTask task) {
//延时任务,延时5秒,每5秒执行一次
futureMap.putIfAbsent(task.taskKey(), GlobalExecutor.scheduleNamingHealth(task, 5000, 5000, TimeUnit.MILLISECONDS));
}
到这里service部分完成。
添加实例
我们回到最开始的方法,查看addInstance方法
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
...省略
//添加实例
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
这里需要关注一下buildInstanceListKey方法,这个方法构建了一个key。key=com.alibaba.nacos.naming.iplist.ephemeral.dev##DEFAULT_GROUP@@stock-service
简单来说,就是包含ephemeral的一个字符串,用来判断是临时实例还是永久实例。并且还跟nacos的AP CP模式相关。后续在展开。
/**
* Add instance to service.
*
* @param namespaceId namespace
* @param serviceName service name
* @param ephemeral whether instance is ephemeral
* @param ips instances
* @throws NacosException nacos exception
*/
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
throws NacosException {
// ephemeral 默认true。ephemeral ture,临时的。
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
Service service = getService(namespaceId, serviceName);
synchronized (service) {
//返回instance集合
List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
Instances instances = new Instances();
instances.setInstanceList(instanceList);
//继续追踪
consistencyService.put(key, instances);
}
}
进入com.alibaba.nacos.naming.consistency.DelegateConsistencyServiceImpl#put方法
@Override
public void put(String key, Record value) throws NacosException {
//根据key,判断是临时实例,还是持久实例。
//DistroConsistencyServiceImpl.put
mapConsistencyService(key).put(key, value);
}
//根据key,返回ephemeralConsistencyService(AP)
private ConsistencyService mapConsistencyService(String key) {
return KeyBuilder.matchEphemeralKey(key) ? ephemeralConsistencyService : persistentConsistencyService;
}
进入com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl#put方法
@Override
public void put(String key, Record value) throws NacosException {
//关键代码 将instance放到dataStore
onPut(key, value);
distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE,globalConfig.getTaskDispatchPeriod() / 2);
}
/**
* Put a new record.
*
* @param key key of record
* @param value record
*/
public void onPut(String key, Record value) {
if (KeyBuilder.matchEphemeralInstanceListKey(key)) {
Datum<Instances> datum = new Datum<>();
datum.value = (Instances) value;
datum.key = key;
datum.timestamp.incrementAndGet();
dataStore.put(key, datum);
}
if (!listeners.containsKey(key)) {
return;
}
//加入到阻塞队列
notifier.addTask(key, DataOperation.CHANGE);
}
addTask方法
public class Notifier implements Runnable {
private ConcurrentHashMap<String, String> services = new ConcurrentHashMap<>(10 * 1024);
//阻塞队列 1mb
private BlockingQueue<Pair<String, DataOperation>> tasks = new ArrayBlockingQueue<>(1024 * 1024);
/**
* Add new notify task to queue.
*
* @param datumKey data key
* @param action action for data
*/
public void addTask(String datumKey, DataOperation action) {
if (services.containsKey(datumKey) && action == DataOperation.CHANGE) {
return;
}
if (action == DataOperation.CHANGE) {
services.put(datumKey, StringUtils.EMPTY);
}
//封装为Pair,加入队列
tasks.offer(Pair.with(datumKey, action));
}
@Override
public void run() {
Loggers.DISTRO.info("distro notifier started");
for (; ; ) {
try {
//从阻塞队列取出
//死循环,阻塞队列内没有内容,tasks.take会阻塞住,不会占用cpu
Pair<String, DataOperation> pair = tasks.take();
//处理
handle(pair);
} catch (Throwable e) {
Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
}
}
}
那么问题来了,将instance加入到队列后,什么时候从队列中取出呢?
从上面Notifier看出,是一个线程,那么看run方法实现即可。handle(pair);方法即处理pair。
/**
* spring容器初始化后,执行
*/
@PostConstruct
public void init() {
//实现异步注册
GlobalExecutor.submitDistroNotifyTask(notifier);
}
private void handle(Pair<String, DataOperation> pair) {
...省略
for (RecordListener listener : listeners.get(datumKey)) {
count++;
try {
if (action == DataOperation.CHANGE) {
//dataStore.get(datumKey).value就是instance
//servce.onChange
listener.onChange(datumKey, dataStore.get(datumKey).value);
continue;
}
if (action == DataOperation.DELETE) {
listener.onDelete(datumKey);
continue;
}
} catch (Throwable e) {
Loggers.DISTRO.error("[NACOS-DISTRO] error while notifying listener of key: {}", datumKey, e);
}
}
...省略
}
进入com.alibaba.nacos.naming.core.Service#onChange方法
省略其他代码,进入最关键的updateIps方法
@Override
public void onChange(String key, Instances value) throws Exception {
...省略
//真正的注册逻辑
updateIPs(value.getInstanceList(), KeyBuilder.matchEphemeralInstanceListKey(key));
recalculateChecksum();
}
/**
* Update instances.
*
* @param instances instances
* @param ephemeral whether is ephemeral instance
*/
public void updateIPs(Collection<Instance> instances, boolean ephemeral) {
Map<String, List<Instance>> ipMap = new HashMap<>(clusterMap.size());
...省略
for (Map.Entry<String, List<Instance>> entry : ipMap.entrySet()) {
//make every ip mine
List<Instance> entryIPs = entry.getValue();
//updateIps 真正的注册逻辑
clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
}
...省略
}
从clusterMap中取出cluster。调用cluster.updateIps方法。传入的是Instance集合。
/**
* Update instance list.
*
* @param ips instance list
* @param ephemeral whether these instances are ephemeral
*/
public void updateIps(List<Instance> ips, boolean ephemeral) {
//ephemeralInstances真正的注册表,是不操作的。这里创建了oldIpMap,是注册表的副本,进行增删改,最后在赋值给 ephemeralInstances
Set<Instance> toUpdateInstances = ephemeral ? ephemeralInstances : persistentInstances;
HashMap<String, Instance> oldIpMap = new HashMap<>(toUpdateInstances.size());
for (Instance ip : toUpdateInstances) {
oldIpMap.put(ip.getDatumKey(), ip);
}
...省略
toUpdateInstances = new HashSet<>(ips);
if (ephemeral) {
//存Instance的集合,是注册列表map的一部分
ephemeralInstances = toUpdateInstances;
} else {
persistentInstances = toUpdateInstances;
}
}
查看ephemeralInstances是什么?ephemeralInstances是存放所有注册实例的。
//instance注册表set
@JsonIgnore
private Set<Instance> ephemeralInstances = new HashSet<>();
总结
现在我们可以印证异步注册+内存队列的说法了。
具体将要注册instance实例放入阻塞队列BlockingQueue。
//阻塞队列 1mb
private BlockingQueue<Pair<String, DataOperation>> tasks = new ArrayBlockingQueue<>(1024 * 1024);
然后采用异步线程,从队列中拿取进行handle(pair)处理,最终放入到
//存放instance
Set<Instance> ephemeralInstances = new HashSet<>();
服务注册表的高并发读写
问题1:我们关注一下注册表的结构,是Set集合。那么在高并发情况下,它的读写是怎么做的呢?加锁?
从源码来看,根据ephemeral,将真正的注册表ephemeralInstances,赋值给toUpdateInstances(副本)。下面的操作都是操作toUpdateInstances。最后将其替换真正的注册表ephemeralInstances。
所有nacos针对高并发读写问题,采用copy on write设计,读写分离。采用修改一个副本,然后替换真正的注册表。
/**
* Update instance list.
*
* @param ips instance list
* @param ephemeral whether these instances are ephemeral
*/
public void updateIps(List<Instance> ips, boolean ephemeral) {
//ephemeralInstances真正的注册表,是不操作的。这里创建了oldIpMap,是注册表的副本,进行增删改,最后在赋值给 ephemeralInstances
Set<Instance> toUpdateInstances = ephemeral ? ephemeralInstances : persistentInstances;
HashMap<String, Instance> oldIpMap = new HashMap<>(toUpdateInstances.size());
for (Instance ip : toUpdateInstances) {
oldIpMap.put(ip.getDatumKey(), ip);
}
...省略
toUpdateInstances = new HashSet<>(ips);
if (ephemeral) {
//存Instance的集合,是注册列表map的一部分
ephemeralInstances = toUpdateInstances;
} else {
persistentInstances = toUpdateInstances;
}
}
问题2:用副本是否会有延时问题呢?客户端拉取时,正好还在复制,不就读取的是老的注册表吗?
是的,会有这个问题。但是为了高可用,牺牲一点一致性。在读取时,可能会延迟1s得到新的注册表。
问题3:如果有上千个注册实例,复制副本是否会大量占用内存?
从clusterMap.get(entry.getKey())可以看出是取的对应的cluter的集合。只复制cluster粒度的Set集合。
//updateIps 真正的注册逻辑
clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
创作不易,如果对您有所帮助,请点赞关注!