openfire的session与路由机制(四)集群简析

注意:源码的研究是基于openfire_src_4_0_2源码版本。

先简单地理解集群的概念。

集群的目的是:让多个实例像一个实例一样运行,这样就可以通过增长实例来增长计算能力。所谓集群就是把一些数据共享或者同步到不同的实例上,这样系统使用同样的算法,相同的程序执行,最终的结果当然应该是相同的。所以一些数据库的主从复制,缓存数据集群都是类似这种解决方法。

4.1  openfire集群总体方案

在openfire中的集群主要包括:集群管理(启动集群、加入和退出集群等)、同步管理(同步数据、状态信息等)、集群计算任务。
我们从CacheFactory类的方法列表中可以看到有太多与集群相关的方法,所以在介绍openfire的集群之前不得要说的就是openfire的缓存机制了,因为openfire的集群是在缓存的基础之上实现的。

4.1.1 缓存与集群

在这里讨论的是openfire自带的存储在本地jVM的缓存机制。我们先来看看类层次设计图:


 
Cache接口
它继承了jdk的Map工具类,它通过key-value键值对方式把相关的对象的存储在内存中,可快速访问。缓存中的value可能实现org.jivesoftware.util.cache.Cacheable接口,它允许通过缓存来更快地确定对象的大小。这些限制使得在变大的缓存不会超过一个指定的字节数,并可选地分布到一个服务器集群。
如果缓存的对象变得特别大的话,那么越不频繁访问的对象将首先被剔除。因为缓存过期会自动发生,缓存不保证一个对象将驻留在缓存多久。
可以指定所有对象的最大生命周期。在这种情况下,超过了最大生命期,对象将从缓存删除,即使他们是经常访问的。如果对象放入应该定期刷新的缓存表示数据, 这个特性就很有用,例如,从数据库中取出的数据。这里所有缓存操作是线程安全的。

 

重要属性或方法名

功能描述

getMaxCacheSize()/

setMaxCacheSize()

返回缓存的字节数最大值。如果缓存生命周期大于最大值,那么最不常使用的缓存项将被删除。如果最大的缓存大小设置为-1,那它没有大小限制。

getMaxLifetime()/

setMaxLifetime()

返回最大的毫秒数,任何对象都可以放进缓存。一旦超过指定的毫秒数,对象将自动从缓存消除掉。如果最大的生命周期被设置为-1,那么对象永远不会过期。

getCacheSize()/

setCacheSize()

返回缓存的字节大小。这个值只是粗略的近似值,实际VM内存使用的缓存可以明显高于这个方法返回的报告值。

getCacheHits()

返回的缓存命中率的数量。每次获取缓存的方法被调用且cache包含请求的对象时,将会发生一个缓存命中。跟踪缓存命中和未命中衡量缓存的效率,命中率越高,效率越高。

GetCacheMisses()

返回的缓存未命中的数量。每次这获取缓存的方法被调用且cache不包含请求的对象时,将会发生一个缓存未命中。跟踪缓存命中和未命中衡量缓存的效率,命中率越高,效率越高。

 

DefaultCache类
它是Cache接口的一个非分集群实现,其中保持了两个链表(LinkedList),lastAccessedList 和ageList。缓存的算法如下:一个HashMap维持快速的对象查找。两个链表维护: lastAccessedList用于缓存对象被访问的顺序管理,ageList用于保持缓存对象最初加入到缓存的顺序。当对象被添加到缓存中,他们首先被CacheObject包装,且维护以下信息:
1. 对象的大小(以字节为单位)。
2. 一个指向该节点的链表,维护秩序的对象访问。保持一个参考节点让我们避免线性扫描的链表。
3.  一个指向该节点的链表,维护对象在缓存的寿命。保持一个参考节点让我们避免线性扫描的链表。
为了从缓存中获取对象,hash查找将被执行,从而获得一个包装了我们寻找的实际对象的CahceObject的引用。获取的这个对象随后被移动到访问链表的前面,同时必要的清理将被执行。如果有需要,缓存删除和过期操作也将被执行。


CacheWrapper类
作为一个缓存实现的代理(包装)。缓存的实现可以动态切换,使用户能够持有CacheWrapper对象的引用, 能潜在地将集群缓存转换到本地的缓存实现,等等。
这个类对于将本地缓存转为集群缓存(即将DefaultCache转为Hazelcast插件实现的缓存类ClusteredCache)至关重要,下面在集群的启动将会再展开描述。


CacheFactory 工厂类
这里用到了java 设计模式中的工厂模式(详细可参考图说设计模式--简单工厂模式)
在Openfire 甚至我们自己写的插件中会用到各种各样的Cache,CacheFactory则提供了一个统一的创建和使用Cache的平台。
这里重点看三个成员: 
Map<String,Cache> caches = new ConcurrentHashMap<String,Cache>();  
Map<String,String> cacheNames = new HashMap<String,String>();  
Map<String,Long> cacheProps = new HashMap<String,Long>();  
第一个用来存储所有创建的Cache
第二个用来存储所有创建的Cache名称
第三个用来存储Cache的属性
整个Factory 中的大部分方法都是围绕这三个成员进行操作的。
我们来关注下如何创建缓存对象:
/**
 * Returns the named cache, creating it as necessary.
 */
@SuppressWarnings("unchecked")
public static synchronized <T extends Cache> T createCache(String name) {
    T cache = (T) caches.get(name);
    if (cache != null) {
        return cache;
    }
    cache = (T) cacheFactoryStrategy.createCache(name);
    log.info("Created cache [" + cacheFactoryStrategy.getClass().getName() 
+ "] for " + name);
    return wrapCache(cache, name);
}
createCache方法直接调用cacheFactoryStrategy 的createCache()方法 来创建Cache。这里涉及到了缓存的创建策略,当前存在本地默认缓存策略(DefaultFactoryStrategy)、集群缓存策略(ClusteredCacheFactory)。
我们先来看看什么事策略设计模式(参考自图说设计模式--策略设计模式):
策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。策略模式是一种对象行为型模式。(参考自:图说设计模式--策略模式)
在以下情况下可以使用策略模式:
1. 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
2. 一个系统需要动态地在几种算法中选择一种。
3. 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
4. 不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法和相关的数据结构,提高算法的保密性与安全性。
没有设置集群的情况下就用DefaultLocalCacheStategy来创建cache,当设置了集群则自动使用ClusteredCacheFactory来创建,由此我们可以根据配置来灵活地切换缓存的生成策略。后面要提到的hazelcast就是通过这个来替换了本地缓存策略的。从接口的设计上来看,openfire的缓存策略也就是为了集群与非集群的实现。
到此,建立了能够理解集群机制的缓存基础了。

4.1.2  集群管理

集群管理者
在openfire中主要是一个类来实现:ClusterManager,在ClusterManager中实现了集群实例的加入、退出管理,因为没有使用主从结构,所以ClusterManager实现了一个无中心管理。因为只要当前实例启用了集群,ClusterManager就会主动的加载集群管理并与其他的集群进行同步。
4.1.1.1  启动
HazelcastPlugin类的方法initializePlugin, 启动了run方法:

  


重点是看ClusterManager的startup方法做了什么:
 
首先要判断是否开启了集群并且当前集群实例未运行时才去启动。
启动主要做了两件事:
1.初始化了事件分发器,用于处理集群的同步。
2. 调用CacheFactory的startClustering来运行集群。


在初始化了事件分发器,即调用initEventDispatcher方法,在这里会注册一个分发线程监听到集群事件,收到事件后会执行joinedCluster或者leftCluster的操作,joinedCluster就是加入到集群中的意思。在joinedCluster时会将本地的缓存容器都转换为集群缓存。集群事件监听器列表会依次触发执行。由此便完成集群的初始化并加入到集群中了。
private static void initEventDispatcher() {
   if (dispatcher == null || !dispatcher.isAlive()) {
     dispatcher = new Thread("ClusterManager events dispatcher") {
         @Override
public void run() {
           // exit thread if/when clustering is disabled
             while (ClusterManager.isClusteringEnabled()) {
                 try {
                     Event event = events.take();
                     EventType eventType = event.getType();
                     if (event.getNodeID() == null) {
                       // Replace standalone caches with clustered caches and migrate data
                       if (eventType == EventType.joined_cluster) {
                          CacheFactory.joinedCluster();
                       } else if (eventType == EventType.left_cluster) {
                          CacheFactory.leftCluster();
                       }
                     }
                     for (ClusterEventListener listener : listeners) {
                         try {switch (eventType) { // Now notify rest of the listeners
                                 case joined_cluster: {
                                     if (event.getNodeID() == null) {
                                         listener.joinedCluster();
                                     }else {
                                         listener.joinedCluster(event.getNodeID());
                                     }   break;
                                 }
                                 case left_cluster: {
                                     if (event.getNodeID() == null) {
                                         listener.leftCluster();
                                     }else {
                                         listener.leftCluster(event.getNodeID());
                                     }   break;
                                 }
                                 case marked_senior_cluster_member: {
                                     listener.markedAsSeniorClusterMember();
                                     break;
                                 }   default:break;
                             }}
                         catch (Exception e) {…} }
                     event.setProcessed(true); // Mark event as processed
                 } catch (Exception e) {…}
      }}};
     dispatcher.setDaemon(true);
     dispatcher.start();
   }
}
将本地的缓存容器都转换为集群缓存时,在上文中提到的的缓存类CacheWrapper包装了单机本地缓存DefaultCache,此时将会把DefaultCahce转为ClusteredCache:
public static synchronized void joinedCluster() {
       cacheFactoryStrategy = clusteredCacheFactoryStrategy;
       // Loop through local caches and switch them to clustered cache (copy content)
       for (Cache cache : getAllCaches()) {
           // skip local-only caches
           if (localOnly.contains(cache.getName())) continue;
           CacheWrapper cacheWrapper = ((CacheWrapper) cache);
           Cache clusteredCache = cacheFactoryStrategy.createCache(cacheWrapper.getName());
           clusteredCache.putAll(cache);
           cacheWrapper.setWrappedCache(clusteredCache);
       }
       clusteringStarting = false;
       clusteringStarted = true;
       log.info("Clustering started; cache migration complete");
   }
这里可以看到会读取所有的缓存容器并一个个的使用Wrapper包装一下,然后用同样的缓存名称去createCache一个新的Cache,这步使用的是切换后的集群缓存策略工厂,也就是说会使用ClusteredCacheFactory去创建新的缓存容器。最后再将cache写入到新的clusteredCache 里,这样就完成了缓存的切换。


在CacheFactory类的startClustering方法中主要是这几个事情:
会使用集群的缓存工厂策略来启动,同时使自己加入到集群中;开启一个线程用于同步缓存的状态:
public static void startClustering() {
   if (isClusteringAvailable()) {
      clusteringStarting = clusteredCacheFactoryStrategy.startCluster();
   }
    if (clusteringStarting) {
        if (statsThread == null) {
            // Start a timing thread with 1 second of accuracy.
            statsThread = new Thread("Cache Stats") {
                private volatile boolean destroyed = false;
                @Override
   public void run() {
                XMPPServer.getInstance().addServerListener(new XMPPServerListener() {…  });
                ClusterManager.addListener(new ClusterEventListener() { … });
                    // Run the timer indefinitely.
                    while (!destroyed && ClusterManager.isClusteringEnabled()) {
                  // Publish cache stats for this cluster node (assuming clustering is
                  // enabled and there are stats to publish).
                        try {
                            cacheFactoryStrategy.updateCacheStats(caches);
                        }
                        catch (Exception e) {log.error(e.getMessage(), e);}
                        try {
                            sleep(10000); // Sleep 10 seconds.
                        }catch (InterruptedException ie) {// Ignore.}
                    }
                    statsThread = null;
                    log.debug("Cache stats thread terminated.");
                }
            };
            statsThread.setDaemon(true);
            statsThread.start();
        }
    }
}

4.1.1.2  关闭
ClusterManager类的方法shutdown()用来关闭集群:
public static synchronized void shutdown() {
    if (isClusteringStarted()) {
        Log.debug("ClusterManager: Shutting down clustered cache service.");
        CacheFactory.stopClustering();
    }
}
此方法在系统正在关闭或集群已经失效的情况下被调用。调用这个方法失败时临时可能会影响集群的执行,系统将不得不做额外的工作来恢复non-clean关闭。如果集群没有被启用,这个方法不会做任何事情。
我们来看下CacheFactory类的stopClustering()方法:
public static void stopClustering() {
    // Stop the cluster
   clusteredCacheFactoryStrategy.stopCluster();
   clusteredCacheFactoryStrategy = null;
    // Set the strategy to local
    cacheFactoryStrategy = localCacheFactoryStrategy;
}

重要的关闭方法是clusteredCacheFactoryStrategy.stopCluster();将在不同的集群插件中实现。


4.1.3  集群同步

首先我们看下哪些情况下需要进行集群间的同步?
大致分为两种情况:
1. Openfire服务器启动集群配置加入集群、退出集群;
2. 集群中的openfire实例的需要同步的数据、状态等有变动;


对于第一种情况,当server加入到集群或退出集群产生的事件,将会通知给每个实现了ClusterEventListener接口的类进行相应的处理和数据、状态的同步:
 
ClusterEventListener接口
集群事件的监听器。使用ClusterManager#addListener(ClusterEventListener)方法添加新的监听器。

权限修饰符

返回类型

重要属性或方法名

功能描述

 

void

joinedCluster()

本通知事件意味着本地JVM现在已是集群的一部分。

这个时候,可以使用XMPPServer#getNodeID()来获获取新节点的值。当作为高级集群成员加入集群时,markedAsSeniorClusterMember()方法的事件紧随着这个事件被触发。

在这个时候,CacheFactory能够拿到着集群的缓存。那意味着修改缓存将会被反应到集群中。

 

void

joinedCluster(byte[] nodeID)

本通知事件意味着本地JVM现在已是集群的一部分。

在这个时候,新节点的CacheFactory能够拿到集群的缓存。那意味着修改本地JVM的缓存将会被反应到集群中和这个指定的新节点。

 

void

leftCluster()

本通知事件意味着本地JVM不在是集群的一部分。当关闭集群或移除提供集群支持的企业插件或者链接到集群失败时,这个事件将被触发。

//todo

那意味着缓存将被重置,因此,需要通过从本地JVM刷新数据来重新构建缓存。

这个时候,CacheFactory能拿到本地的缓存。那意味着对缓存的更改将只会影响本地JVM应用。

 

void

leftCluster(byte[] nodeID)

 


对于第二种情况,在下文hazelcast的实现中将会采用分布式缓存的机制来实现,不需要再业务逻辑中做额外的同步工作。

4.1.4  集群计算

有了集群是不是可以利用集群的优势进行一些并行计算呢? 
在CacheFactory类中有几个方法:doClusterTask、doSynchronousClusterTask,这两个都是overload方法,参数有所不同而已。这几个方法就是用于执行一些计算任务的。就看一下doClusterTask:
public static void doClusterTask(final ClusterTask<?> task) {
cacheFactoryStrategy.doClusterTask(task)
}
这里有个限定就是必须是ClusterTask派生的类才行,看看它的定义:
public interface ClusterTask<V> extends Runnable, Externalizable {
V getResult();
}
主要是为了异步执行和序列化,异步是因为不能阻塞,而序列化当然就是为了能在集群中传送。再看CacheFactory的doClusterTask方法可以发现,它只不过是代理了缓存策略工厂的doClusterTask,具体的实现还是要看集群实现的。

4.2  hazelcast插件集群实现

4.2.1 集群管理

4.2.1.1  启动
在上文openfire的集群启动时,CacheFactory类的startClustering方法里调用了clusteredCacheFactoryStrategy.startCluster(),在这里这条语句即是具体的集群启动实现:
public boolean startCluster() {
    state = State.starting;
    // Set the serialization strategy to use for transmitting objects between node clusters
    serializationStrategy = ExternalizableUtil.getInstance().getStrategy();
    ExternalizableUtil.getInstance().setStrategy(new ClusterExternalizableUtil());
    // Set session locator to use when in a cluster
    XMPPServer.getInstance().setRemoteSessionLocator(new RemoteSessionLocator());
    // Set packet router to use to deliver packets to remote cluster nodes
  XMPPServer.getInstance().getRoutingTable().setRemotePacketRouter(new ClusterPacketRouter());
    ClassLoader oldLoader = null;
    // Store previous class loader (in case we change it)
    oldLoader = Thread.currentThread().getContextClassLoader();
    ClassLoader loader = new ClusterClassLoader();
    Thread.currentThread().setContextClassLoader(loader);
    int retry = 0;
    do {
        try {
         Config config = new ClasspathXmlConfig(HAZELCAST_CONFIG_FILE);
         config.setInstanceName("openfire");
            config.setClassLoader(loader);
         if (JMXManager.isEnabled() && HAZELCAST_JMX_ENABLED) {
           config.setProperty("hazelcast.jmx", "true");
           config.setProperty("hazelcast.jmx.detailed", "true");
         }
       hazelcast = Hazelcast.newHazelcastInstance(config);
         cluster = hazelcast.getCluster();
         // Update the running state of the cluster
         state = cluster != null ? State.started : State.stopped;
         // Set the ID of this cluster node
         XMPPServer.getInstance().setNodeID(NodeID.getInstance(getClusterMemberID()));
         // CacheFactory is now using clustered caches. We can add our listeners.
         clusterListener = new ClusterListener(cluster);
     lifecycleListener = hazelcast.getLifecycleService().addLifecycleListener(clusterListener);
         membershipListener = cluster.addMembershipListener(clusterListener);
         break;
     } catch (Exception e) {
         …
     }
   } while (retry++ < CLUSTER_STARTUP_RETRY_COUNT);
    if (oldLoader != null) {
        // Restore previous class loader
        Thread.currentThread().setContextClassLoader(oldLoader);
    }
    return cluster != null;
}
主要完成几件事情:
1.  设置缓存序列化工具类,ClusterExternalizableUtil。这个是用于集群间数据复制时的序列化工具;
2.  设置远程session定位器,RemoteSessionLocator,因为session不同步,所以它主要是用于多实例间的session读取;
3.  设置远程报文路由器ClusterPacketRouter,这样就可以在集群中发送消息了;
4. 加载Hazelcast的实例设置NodeID,以及设置ClusterListener;
4.2.1.2  关闭
hazelcast对集群关闭的实现是在ClusteredCacheFactory类的stopCluster方法:
public void stopCluster() {
    // Stop the cache services.
    cacheStats = null;
    // Update the running state of the cluster
    state = State.stopped;
    // Stop the cluster
    Hazelcast.shutdownAll();
    cluster = null;
    if (clusterListener != null) {
     // Wait until the server has updated its internal state
     while (!clusterListener.isDone()) {
         try {
             Thread.sleep(100);
         } catch (InterruptedException e) {
             // Ignore
         }
     }
     hazelcast.getLifecycleService().removeLifecycleListener(lifecycleListener);
     cluster.removeMembershipListener(membershipListener);
     lifecycleListener = null;
     membershipListener = null;
     clusterListener = null;
    }
    // Reset the node ID
    XMPPServer.getInstance().setNodeID(null);
    // Reset packet router to use to deliver packets to remote cluster nodes
    XMPPServer.getInstance().getRoutingTable().setRemotePacketRouter(null);
    // Reset the session locator to use
    XMPPServer.getInstance().setRemoteSessionLocator(null);
    // Set the old serialization strategy was using before clustering was loaded
    ExternalizableUtil.getInstance().setStrategy(serializationStrategy);
}
先是停止集群缓存服务,更新集群运行状态为stopped,关闭hazelcast的服务。
然后关闭和清理集群相关的监听器,将服务器实例的nodeID、RemotePacketRouter、RemoteSessionLocator设为null,序列化策略设置为在集群加载以前的序列化策略。
注意:关闭hazelcast服务会触发ClusterListener的方法stateChanged(LifecycleEvent event):
public void stateChanged(LifecycleEvent event) {
   if (event.getState().equals(LifecycleState.SHUTDOWN)) {
      leaveCluster();
   } else if (event.getState().equals(LifecycleState.STARTED)) {
      joinCluster();
   }
}
对于关闭事件会执行leaveCluster方法:
private synchronized void leaveCluster() {
   if (isDone()) { // not a cluster member
      return;
   }
       seniorClusterMember = false;
       // Clean up all traces. This will set all remote sessions as unavailable
       List<NodeID> nodeIDs = new ArrayList<NodeID>(nodeSessions.keySet());
       // Trigger event. Wait until the listeners have processed the event. Caches will be populated
       // again with local content.
       ClusterManager.fireLeftCluster();
       if (!XMPPServer.getInstance().isShuttingDown()) {
           for (NodeID key : nodeIDs) {
           // Clean up directed presences sent from entities hosted in the leaving node to local entities
           // Clean up directed presences sent to entities hosted in the leaving node from local entities
              cleanupDirectedPresences(key);
              cleanupPresences(key);// Clean up no longer valid sessions
           }
         // Remove traces of directed presences sent from local entities to handlers that no longer exist
         // At this point c2s sessions are gone from the routing table so we can identify expired sessions
           XMPPServer.getInstance().getPresenceUpdateHandler().removedExpiredPresences();
       }
       logger.info("Left cluster as node: " + cluster.getLocalMember().getUuid());
       done = true;
   }
来关注下ClusterManager.fireLeftCluster();具体做了什么:
public static void fireLeftCluster() {
    try {
        Event event = new Event(EventType.left_cluster, null);
        events.put(event);
    } catch (InterruptedException e) {
        // Should never happen
        Log.error(e.getMessage(), e);
    }
}
添加退出集群事件到集群监听事件列表中,在上文openfire的启动时开启监听线程,从事件列表中取出事件,此时取出了退出集群的事件,将集群缓存转成本地缓存,且所有实现了ClusterEventListener的事件监听器列表都会执行leftCluster方法。

4.2.1  集群同步

Hazelcast的缓存实现类ClusteredCache对于集群的同步很关键,因为ClusteredCache类内部实现是Hazelcast的分布式Map数据结构。
在上文中openfire加入集群的处理中,需要将DefaultCahce转成ClusterCahce,此过程调用了ClusteredCacheFactoryStrategy的createCahce(name)方法,转成之后每个openfire实例缓存有变更则会自动同步到各个集群的实例中。

我们来看看这个方法在Hazelcast中具体是如何执行的:

public Cache createCache(String name) {
    // Check if cluster is being started up
    while (state == State.starting) {
        // Wait until cluster is fully started (or failed)
        try {
            Thread.sleep(250);
        }
        catch (InterruptedException e) {// Ignore}
    }
    if (state == State.stopped) {
        throw new IllegalStateException("Cannot create clustered cache when not in a cluster");
    }
    return new ClusteredCache(name, hazelcast.getMap(name));
}

 return new ClusteredCache(name, hazelcast.getMap(name));最重要的是传入的第二个map参数换成了hazelcast的了。这样之后再访问这个缓存容器时已经不再是原先的本地Cache了,已经是hazelcast的map对象。


4.2.2  集群计算

我们来看看Hazelcast的集群策略缓存工厂类ClusteredCacheFactory中集群计算模式实现doClusterTask:
public void doClusterTask(final ClusterTask task) {
        if (cluster == null) { return; }
        Set<Member> members = new HashSet<Member>();
        Member current = cluster.getLocalMember();
        for(Member member : cluster.getMembers()) {
            if (!member.getUuid().equals(current.getUuid())) {
                members.add(member);
            }
        }
        if (members.size() > 0) {
            // Asynchronously execute the task on the other cluster members
         logger.debug("Executing asynchronous MultiTask: " + task.getClass().getName());
            hazelcast.getExecutorService(HAZELCAST_EXECUTOR_SERVICE_NAME).submitToMembers(
                new CallableTask<Object>(task), members);
        } else {
               logger.warn("No cluster members selected for cluster task " + task.getClass().getName());
        }
    }
过程就是,先获取到集群中的实例成员,当然要排除自己。然后hazelcast提供了ExecutorService来执行这个task,方法就是submiteToMembers。这样就提交了一个运算任务。


我们以在集群环境下获取session个数来看下集群计算的应用。
来看下SessionManager类的getUserSessionsCount(boolean onlyLocal)方法:
public int getUserSessionsCount(boolean onlyLocal) {
    int total = routingTable.getClientsRoutes(true).size();
    if (!onlyLocal) {
        Collection<Object> results = 
CacheFactory.doSynchronousClusterTask(new GetSessionsCountTask(true), false);
        for (Object result : results) {
            if (result == null) continue;
            total = total + (Integer) result;
        }
    }
    return total;
}
先是计算了本地JVM的路由表中的session个数,然后调用了CacheFactory的同步计算任务方法doSynchronousClusterTask获取不包括本地JVM的集群中其它实例的所有的sesion个数进行合并。集群中的每个openfire实例都会开启线程运行GetSessionCountTask的run方法,进行本地Session个数的统计:
public class GetSessionsCountTask implements ClusterTask<Integer> {
private Boolean authenticated;
private Integer count;
@Override
public Integer getResult() {
    return count;
}
@Override
public void run() {
    if (authenticated) {
        // Get count of authenticated sessions
        count = SessionManager.getInstance().getUserSessionsCount(true);
    } else {
        // Get count of connected sessions (authenticated or not)
        count = SessionManager.getInstance().getConnectionsCount(true);
    }
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
    ExternalizableUtil.getInstance().writeBoolean(out, authenticated);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    authenticated = ExternalizableUtil.getInstance().readBoolean(in);
}
从run方法中看出,运行计算线程的其它openfire实例的authenticated值为true,调用了SessionManager.getInstance().getUserSessionsCount(true);只计算各自本地路由表中的session个数。

在上文openfire的集群计算中说明了task类应该实现序列化接口,用来在集群网络中传递java对象。在这里authenticated属性的值将被序列化,在集群其它openfire实例接收到序列化java对象后会反序列化获取authenticated的值,保证能正确运行run方法。

4.3  集群session同步与路由

上文已经交代了大体的集群同步过程了,在此我们来详细探讨下session的同步过程:
同步场景一(Openfire服务器启动集群配置加入集群、退出集群):
 
先来看看RoutingTableImpl类是如何执行joinedCluster方法的:
@Override
public void joinedCluster() {
restoreCacheContent();
// Broadcast presence of local sessions to remote sessions when subscribed to presence
PresenceUpdateHandler presenceUpdateHandler = 
XMPPServer.getInstance().getPresenceUpdateHandler();
    for (LocalClientSession session : localRoutingTable.getClientRoutes()) {
        // Simulate that the local session has just became available
        session.setInitialized(false);
        // Simulate that current session presence has just been received
        presenceUpdateHandler.process(session.getPresence());
    }
}
RoutingTableImpl实现的joinedCluster方法,先重置相关的缓存内容(用户缓存usersCache、服务器缓存serversCache、component缓存componentsCache、用户session缓存usersSession)。
接下来广播本地session的出席信息给远程session。模拟本地session已经变成available,接着模拟当前session出席信息已经被接收。restoreCacheContent()方法从本地路由表中取出所有类型的RoutableHandler,设置到所有类型的缓存中:
private void restoreCacheContent() {
    // Add outgoing server sessions hosted locally to the cache (using new nodeID)
    for (LocalOutgoingServerSession session : localRoutingTable.getServerRoutes()) {
        addServerRoute(session.getAddress(), session);
    }
    // Add component sessions hosted locally to the cache (using new nodeID) and remove traces to old nodeID
    for (RoutableChannelHandler route : localRoutingTable.getComponentRoute()) {
        addComponentRoute(route.getAddress(), route);
    }
    // Add client sessions hosted locally to the cache (using new nodeID)
    for (LocalClientSession session : localRoutingTable.getClientRoutes()) {
        addClientRoute(session.getAddress(), session);
    }
}
RoutingTableImpl类是如何执行leftCluster方法的:
@Override
public void leftCluster() {
    if (!XMPPServer.getInstance().isShuttingDown()) {
        // Add local sessions to caches
        restoreCacheContent();
    }
}
只重置了相关的缓存内容。退出集群时,集群缓存变为本地缓存时本地缓存会有集群时其它节点的同步过来的缓存,所以需要重置本地缓存。


对于同步场景二(集群缓存有变更导致的集群同步)
以路由表添加客户端会话(ClientSession)过程为例子来说明下集群的客户端session缓存的添加:
@Override
public boolean addClientRoute(JID route, LocalClientSession destination) {
    boolean added;
    boolean available = destination.getPresence().isAvailable();
    localRoutingTable.addRoute(route.toString(), destination);
    if (destination.getAuthToken().isAnonymous()) {
        .... ... 
} else {
        Lock lockU = CacheFactory.getLock(route.toString(), usersCache);
        try {
            lockU.lock();
added = usersCache.put(route.toString(), new ClientRoute(server.getNodeID(), available)) == null;
        }
        finally {
            lockU.unlock();
        }
        // Add the session to the list of user sessions
        if (route.getResource() != null && (!available || added)) {
            Lock lock = CacheFactory.getLock(route.toBareJID(), usersSessions);
            try {
                lock.lock();
                Collection<String> jids = usersSessions.get(route.toBareJID());
                if (jids == null) {
                    // Optimization - use different class depending on current setup
                    if (ClusterManager.isClusteringStarted()) {
                        jids = new HashSet<>();
                    }else {
          jids = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
                    }
                }
                jids.add(route.toString());
                usersSessions.put(route.toBareJID(), jids);
            }
            finally {lock.unlock();}
        }
    }
    return added;
}
在方法中把新登录的用户信息放入用户缓存usersCache,用户的session信息放入用户session缓存usersSessions,其它openfire的实例的usersCache和usersSession对象都会自动把新登录用户信息同步过来。


由此,可以看出对于session的同步,在openfire的实现上,没有将session同步到每个openfire实例的路由表,根据本地实例的报文路由,都是从路由表中取出RoutableHandler进行报文的路由处理,那么在集群情况下该怎么进行报文的路由?
 
转发报文时,当执行到RoutingTableImpl类的routePacket(JID jid, Packet packet, boolean fromServer)方法,然后进入三种分支(本地域名分支、Component分支、远程域名分支)之一执行,如果判断需要转发到集群中的其它openfire实例,就会调用RemotePacketRouter进行路由转发。
我们看下RemotePacketRouter是如何通过hazelcast来实现集群的报文路由的:
public class ClusterPacketRouter implements RemotePacketRouter {

    private static Logger logger = LoggerFactory.getLogger(ClusterPacketRouter.class);

    public boolean routePacket(byte[] nodeID, JID receipient, Packet packet) {
// Send the packet to the specified node and let the remote node deliver the packet to the recipient
        try {
            CacheFactory.doClusterTask(new RemotePacketExecution(receipient, packet), nodeID);
            return true;
        } catch (IllegalStateException  e) {
            logger.warn("Error while routing packet to remote node: " + e);
            return false;
        }
    }

    public void broadcastPacket(Message packet) {
        // Execute the broadcast task across the cluster
        CacheFactory.doClusterTask(new BroadcastMessage(packet));
    }
}
ClusterPacketRouter类实现了RemotePacketRouter接口,routePacket方法中使用了集群的计算来转发报文,具体深入线程类RemotePacketExecution看下:
public class RemotePacketExecution implements ClusterTask<Void> {
    private JID recipient;
    private Packet packet;

    public RemotePacketExecution() {}

    public RemotePacketExecution(JID recipient, Packet packet) {
        this.recipient = recipient;
        this.packet = packet;
    }

    public Void getResult() {
        return null;
    }

    public void run() {
        // Route packet to entity hosted by this node. If delivery fails then the routing table
        // will inform the proper router of the failure and the router will handle the error reply logic
        XMPPServer.getInstance().getRoutingTable().routePacket(recipient, packet, false);
    }
接收者jid和报文被序列化传到集群中的其它openfire实例,其它openfire实例运行run方法调用RoutingTable的routePacket转发报文。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值