从ZooKeeper源代码看如何实现分布式系统(四)session管理

这篇看看ZooKeeper如何管理Session。 Session相关的接口如下: 

Session: 表示session的实体类,维护sessionId和timeout两个主要状态

SessionTracker: Session生命周期管理相关的操作

SessionExpier: Session过期的操作

 

先看看Session接口和它的实现类SessionImpl,维护了5个属性:sessionId, timeout表示超时时间,tickTime表示客户端和服务器的心跳时间,isClosing表示是否关闭,owner表示对应的客户端

 

[java]  view plain  copy
 
  在CODE上查看代码片 派生到我的代码片
  1. public static interface Session {  
  2.         long getSessionId();  
  3.         int getTimeout();  
  4.         boolean isClosing();  
  5.     }  
  6.   
  7. public static class SessionImpl implements Session {  
  8.         SessionImpl(long sessionId, int timeout, long expireTime) {  
  9.             this.sessionId = sessionId;  
  10.             this.timeout = timeout;  
  11.             this.tickTime = expireTime;  
  12.             isClosing = false;  
  13.         }  
  14.   
  15.         final long sessionId;  
  16.         final int timeout;  
  17.         long tickTime;  
  18.         boolean isClosing;  
  19.   
  20.         Object owner;  
  21.   
  22.         public long getSessionId() { return sessionId; }  
  23.         public int getTimeout() { return timeout; }  
  24.         public boolean isClosing() { return isClosing; }  
  25.     }  



 

 

SessionTracker的实现类是SessionTrackerImpl,它是一个单独运行的线程,根据tick周期来批量检查处理当前的session。SessionTrackerImpl直接继承了Thread类,它的定义和构造函数如下,几个主要的属性:

expirer是SessionExpirer的实现类

expirationInterval表示过期的周期,可以看到它的值是tickTime,即如果服务器端在tickTime里面没有收到客户端的心跳,就认为该session过期了

sessionsWithTimeout是一个ConcurrentHashMap,维护了一组sessionId和它对应的timeout过期时间

nextExpirationTime表示下次过期时间,线程会在nextExpirationTime时间来批量过期session

nextSessionId是根据sid计算出的下一个新建的sessionId

sessionById这个HashMap保存了sessionId和Session对象的映射

sessionSets这个HashMap保存了一个过期时间和一组保存在SessionSet中的Session的映射,用来批量清理过期的Session

 

[java]  view plain  copy
 
  在CODE上查看代码片 派生到我的代码片
  1. public interface SessionTracker {  
  2.     public static interface Session {  
  3.         long getSessionId();  
  4.         int getTimeout();  
  5.         boolean isClosing();  
  6.     }  
  7.     public static interface SessionExpirer {  
  8.         void expire(Session session);  
  9.   
  10.         long getServerId();  
  11.     }  
  12.   
  13.     long createSession(int sessionTimeout);  
  14.   
  15.     void addSession(long id, int to);  
  16.   
  17.     boolean touchSession(long sessionId, int sessionTimeout);  
  18.   
  19.     void setSessionClosing(long sessionId);  
  20.   
  21.     void shutdown();  
  22.   
  23.     void removeSession(long sessionId);  
  24.   
  25.     void checkSession(long sessionId, Object owner) throws KeeperException.SessionExpiredException, SessionMovedException;  
  26.   
  27.     void setOwner(long id, Object owner) throws SessionExpiredException;  
  28.   
  29.     void dumpSessions(PrintWriter pwriter);  
  30. }  
  31.   
  32. public class SessionTrackerImpl extends Thread implements SessionTracker {  
  33.     private static final Logger LOG = LoggerFactory.getLogger(SessionTrackerImpl.class);  
  34.   
  35.     HashMap<Long, SessionImpl> sessionsById = new HashMap<Long, SessionImpl>();  
  36.   
  37.     HashMap<Long, SessionSet> sessionSets = new HashMap<Long, SessionSet>();  
  38.   
  39.     ConcurrentHashMap<Long, Integer> sessionsWithTimeout;  
  40.     long nextSessionId = 0;  
  41.     long nextExpirationTime;  
  42.   
  43.     int expirationInterval;  
  44.   
  45. public SessionTrackerImpl(SessionExpirer expirer,  
  46.             ConcurrentHashMap<Long, Integer> sessionsWithTimeout, int tickTime,  
  47.             long sid)  
  48.     {  
  49.         super("SessionTracker");  
  50.         this.expirer = expirer;  
  51.         this.expirationInterval = tickTime;  
  52.         this.sessionsWithTimeout = sessionsWithTimeout;  
  53.         nextExpirationTime = roundToInterval(System.currentTimeMillis());  
  54.         this.nextSessionId = initializeNextSession(sid);  
  55.         for (Entry<Long, Integer> e : sessionsWithTimeout.entrySet()) {  
  56.             addSession(e.getKey(), e.getValue());  
  57.         }  
  58.     }  


 

 

 

看一下SessionTrackerImpl这个线程的run方法实现,实现了批量处理过期Session的逻辑

1. 如果下一次过期时间nextExpirationTime大于当前时间,那么当前线程等待nextExpirationTime - currentTime时间

2. 如果到了过期时间,就从sessionSets里面把当前过期时间对应的一组SessionSet取出

3. 批量关闭和过期这组session

4. 把当前过期时间nextExpirationTime 加上 expirationInterval作为下一个过期时间nextExpiration,继续循环

 

其中expirer.expire(s)这个操作,这里的expirer的实现类是ZooKeeperServer,它的expire方法会给给客户端发送session关闭的请求

 

[java]  view plain  copy
 
  在CODE上查看代码片 派生到我的代码片
  1. // SessionTrackerImpl   
  2. synchronized public void run() {  
  3.         try {  
  4.             while (running) {  
  5.                 currentTime = System.currentTimeMillis();  
  6.                 if (nextExpirationTime > currentTime) {  
  7.                     this.wait(nextExpirationTime - currentTime);  
  8.                     continue;  
  9.                 }  
  10.                 SessionSet set;  
  11.                 set = sessionSets.remove(nextExpirationTime);  
  12.                 if (set != null) {  
  13.                     for (SessionImpl s : set.sessions) {  
  14.                         setSessionClosing(s.sessionId);  
  15.                         expirer.expire(s);  
  16.                     }  
  17.                 }  
  18.                 nextExpirationTime += expirationInterval;  
  19.             }  
  20.         } catch (InterruptedException e) {  
  21.             LOG.error("Unexpected interruption", e);  
  22.         }  
  23.         LOG.info("SessionTrackerImpl exited loop!");  
  24.     }  
  25.   
  26. // ZookeeperServer  
  27. public void expire(Session session) {  
  28.         long sessionId = session.getSessionId();  
  29.         LOG.info("Expiring session 0x" + Long.toHexString(sessionId)  
  30.                 + ", timeout of " + session.getTimeout() + "ms exceeded");  
  31.         close(sessionId);  
  32.     }  
  33.   
  34. private void close(long sessionId) {  
  35.         submitRequest(null, sessionId, OpCode.closeSession, 0nullnull);  
  36.     }  


再看一下创建Session的过程

 

1. createSession方法只需要一个sessionTimeout参数来指定Session的过期时间,会把当前全局的nextSessionId作为sessionId传给addSession方法

2. addSession方法先把sessionId和过期时间的映射添加到sessionsWithTimeout这个Map里面来,如果在sessionById这个Map里面没有找到对应sessionId的session对象,就创建一个Session对象,然后放到sessionById Map里面。最后调用touchSession方法来设置session的过期时间等信息

3. touchSession方法首先判断session状态,如果关闭就返回。计算当前session的过期时间,如果是第一次touch这个session,它的tickTime会被设置成它的过期时间expireTime,然后把它加到对应的sessuibSets里面。如果不是第一次touch,那么它的tickTime会是它当前的过期时间,如果还没过期,就返回。如果过期了,就重新计算一个过期时间,并设置给tickTime,然后从对应的sessionSets里面先移出,再加入到新的sessionSets里面。 touchSession方法主要是为了更新session的过期时间。

 

[java]  view plain  copy
 
  在CODE上查看代码片 派生到我的代码片
  1. synchronized public long createSession(int sessionTimeout) {  
  2.         addSession(nextSessionId, sessionTimeout);  
  3.         return nextSessionId++;  
  4.     }  
  5.   
  6. synchronized public void addSession(long id, int sessionTimeout) {  
  7.         sessionsWithTimeout.put(id, sessionTimeout);  
  8.         if (sessionsById.get(id) == null) {  
  9.             SessionImpl s = new SessionImpl(id, sessionTimeout, 0);  
  10.             sessionsById.put(id, s);  
  11.             if (LOG.isTraceEnabled()) {  
  12.                 ZooTrace.logTraceMessage(LOG, ZooTrace.SESSION_TRACE_MASK,  
  13.                         "SessionTrackerImpl --- Adding session 0x"  
  14.                         + Long.toHexString(id) + " " + sessionTimeout);  
  15.             }  
  16.         } else {  
  17.             if (LOG.isTraceEnabled()) {  
  18.                 ZooTrace.logTraceMessage(LOG, ZooTrace.SESSION_TRACE_MASK,  
  19.                         "SessionTrackerImpl --- Existing session 0x"  
  20.                         + Long.toHexString(id) + " " + sessionTimeout);  
  21.             }  
  22.         }  
  23.         touchSession(id, sessionTimeout);  
  24.     }  
  25.   
  26. synchronized public boolean touchSession(long sessionId, int timeout) {  
  27.         if (LOG.isTraceEnabled()) {  
  28.             ZooTrace.logTraceMessage(LOG,  
  29.                                      ZooTrace.CLIENT_PING_TRACE_MASK,  
  30.                                      "SessionTrackerImpl --- Touch session: 0x"  
  31.                     + Long.toHexString(sessionId) + " with timeout " + timeout);  
  32.         }  
  33.         SessionImpl s = sessionsById.get(sessionId);  
  34.         // Return false, if the session doesn't exists or marked as closing  
  35.         if (s == null || s.isClosing()) {  
  36.             return false;  
  37.         }  
  38.         long expireTime = roundToInterval(System.currentTimeMillis() + timeout);  
  39.         if (s.tickTime >= expireTime) {  
  40.             // Nothing needs to be done  
  41.             return true;  
  42.         }  
  43.         SessionSet set = sessionSets.get(s.tickTime);  
  44.         if (set != null) {  
  45.             set.sessions.remove(s);  
  46.         }  
  47.         s.tickTime = expireTime;  
  48.         set = sessionSets.get(s.tickTime);  
  49.         if (set == null) {  
  50.             set = new SessionSet();  
  51.             sessionSets.put(expireTime, set);  
  52.         }  
  53.         set.sessions.add(s);  
  54.         return true;  
  55.     }  


SessionTracker这个接口主要被ZooKeeperServer这个类来使用,ZooKeeperServer表示ZooKeeper的服务器类,负责维护ZooKeeper服务器状态。

 

在ZooKeeperServer的startup方法中,如果sessionTracker对象为空,就先创建一个SessionTracker对象,然后调用startSessionTracker方法启动SessionTrackerImpl这个线程

 

[java]  view plain  copy
 
  在CODE上查看代码片 派生到我的代码片
  1.  public void startup() {          
  2.         if (sessionTracker == null) {  
  3.             createSessionTracker();  
  4.         }  
  5.         startSessionTracker();  
  6.         setupRequestProcessors();  
  7.   
  8.         registerJMX();  
  9.   
  10.         synchronized (this) {  
  11.             running = true;  
  12.             notifyAll();  
  13.         }  
  14.     }  
  15.   
  16.  protected void createSessionTracker() {  
  17.         sessionTracker = new SessionTrackerImpl(this, zkDb.getSessionWithTimeOuts(),  
  18.                 tickTime, 1);  
  19.     }   
  20.   
  21. protected void startSessionTracker() {  
  22.         ((SessionTrackerImpl)sessionTracker).start();  
  23.     }  


在ZooKeeperServer的shutdown方法中,调用sessionTracker的shutdown方法来关闭sessionTrackerImpl线程

 

 

[java]  view plain  copy
 
  在CODE上查看代码片 派生到我的代码片
  1.  public void shutdown() {  
  2.         LOG.info("shutting down");  
  3.   
  4.         // new RuntimeException("Calling shutdown").printStackTrace();  
  5.         this.running = false;  
  6.         // Since sessionTracker and syncThreads poll we just have to  
  7.         // set running to false and they will detect it during the poll  
  8.         // interval.  
  9.         if (sessionTracker != null) {  
  10.             sessionTracker.shutdown();  
  11.         }  
  12.         if (firstProcessor != null) {  
  13.             firstProcessor.shutdown();  
  14.         }  
  15.         if (zkDb != null) {  
  16.             zkDb.clear();  
  17.         }  
  18.   
  19.         unregisterJMX();  
  20.     }  
  21.   
  22. // SessionTrackerImpl  
  23. public void shutdown() {  
  24.         LOG.info("Shutting down");  
  25.   
  26.         running = false;  
  27.         if (LOG.isTraceEnabled()) {  
  28.             ZooTrace.logTraceMessage(LOG, ZooTrace.getTextTraceLevel(),  
  29.                                      "Shutdown SessionTrackerImpl!");  
  30.         }  
  31.     }  


ZooKeeperServer的createSession方法给连接ServerCnxn创建一个对应的session,然后给客户端发送一个创建了session的请求

 

 

[java]  view plain  copy
 
  在CODE上查看代码片 派生到我的代码片
  1. long createSession(ServerCnxn cnxn, byte passwd[], int timeout) {  
  2.         long sessionId = sessionTracker.createSession(timeout);  
  3.         Random r = new Random(sessionId ^ superSecret);  
  4.         r.nextBytes(passwd);  
  5.         ByteBuffer to = ByteBuffer.allocate(4);  
  6.         to.putInt(timeout);  
  7.         cnxn.setSessionId(sessionId);  
  8.         submitRequest(cnxn, sessionId, OpCode.createSession, 0, to, null);  
  9.         return sessionId;  
  10.     }  


ZooKeeperServer的reopenSession会给断开了连接后又重新连接的session更新状态,使session继续可用

 

1. 如果session的密码不对,调用finishSessionInit方法来关闭session,如果密码正确,调用revalidateSession方法

2. revalidateSession方法会调用sessionTracker的touchSession,如果session已经过期,rc = false,如果session未过期,更新session的过期时间信息。最后也调用finishSessionInit方法

3. finishSessionInit方法会给客户端发送响应对象ConnectResponse,如果验证不通过,会关闭连接  cnxn.sendBuffer(ServerCnxnFactory.closeConn)。验证通过,调用cnxn.enableRecv(); 方法来设置连接状态,使服务器端连接注册SelectionKey.OP_READ事件,准备接收客户端请求

 

[java]  view plain  copy
 
  在CODE上查看代码片 派生到我的代码片
  1. public void reopenSession(ServerCnxn cnxn, long sessionId, byte[] passwd,  
  2.             int sessionTimeout) throws IOException {  
  3.         if (!checkPasswd(sessionId, passwd)) {  
  4.             finishSessionInit(cnxn, false);  
  5.         } else {  
  6.             revalidateSession(cnxn, sessionId, sessionTimeout);  
  7.         }  
  8.     }  
  9.   
  10. protected void revalidateSession(ServerCnxn cnxn, long sessionId,  
  11.             int sessionTimeout) throws IOException {  
  12.         boolean rc = sessionTracker.touchSession(sessionId, sessionTimeout);  
  13.         if (LOG.isTraceEnabled()) {  
  14.             ZooTrace.logTraceMessage(LOG,ZooTrace.SESSION_TRACE_MASK,  
  15.                                      "Session 0x" + Long.toHexString(sessionId) +  
  16.                     " is valid: " + rc);  
  17.         }  
  18.         finishSessionInit(cnxn, rc);  
  19.     }  
  20.   
  21. public void finishSessionInit(ServerCnxn cnxn, boolean valid) {  
  22.         // register with JMX  
  23.         try {  
  24.             if (valid) {  
  25.                 serverCnxnFactory.registerConnection(cnxn);  
  26.             }  
  27.         } catch (Exception e) {  
  28.                 LOG.warn("Failed to register with JMX", e);  
  29.         }  
  30.   
  31.         try {  
  32.             ConnectResponse rsp = new ConnectResponse(0, valid ? cnxn.getSessionTimeout()  
  33.                     : 0, valid ? cnxn.getSessionId() : 0// send 0 if session is no  
  34.                             // longer valid  
  35.                             valid ? generatePasswd(cnxn.getSessionId()) : new byte[16]);  
  36.             ByteArrayOutputStream baos = new ByteArrayOutputStream();  
  37.             BinaryOutputArchive bos = BinaryOutputArchive.getArchive(baos);  
  38.             bos.writeInt(-1"len");  
  39.             rsp.serialize(bos, "connect");  
  40.             if (!cnxn.isOldClient) {  
  41.                 bos.writeBool(  
  42.                         this instanceof ReadOnlyZooKeeperServer, "readOnly");  
  43.             }  
  44.             baos.close();  
  45.             ByteBuffer bb = ByteBuffer.wrap(baos.toByteArray());  
  46.             bb.putInt(bb.remaining() - 4).rewind();  
  47.             cnxn.sendBuffer(bb);      
  48.   
  49.             if (!valid) {  
  50.                 LOG.info("Invalid session 0x"  
  51.                         + Long.toHexString(cnxn.getSessionId())  
  52.                         + " for client "  
  53.                         + cnxn.getRemoteSocketAddress()  
  54.                         + ", probably expired");  
  55.                 cnxn.sendBuffer(ServerCnxnFactory.closeConn);  
  56.             } else {  
  57.                 LOG.info("Established session 0x"  
  58.                         + Long.toHexString(cnxn.getSessionId())  
  59.                         + " with negotiated timeout " + cnxn.getSessionTimeout()  
  60.                         + " for client "  
  61.                         + cnxn.getRemoteSocketAddress());  
  62.                 cnxn.enableRecv();  
  63.             }  
  64.                   
  65.         } catch (Exception e) {  
  66.             LOG.warn("Exception while establishing session, closing", e);  
  67.             cnxn.close();  
  68.         }  
  69.     }  
  70.   
  71. // NIOServerCnxn  
  72. public void enableRecv() {  
  73.         synchronized (this.factory) {  
  74.             sk.selector().wakeup();  
  75.             if (sk.isValid()) {  
  76.                 int interest = sk.interestOps();  
  77.                 if ((interest & SelectionKey.OP_READ) == 0) {  
  78.                     sk.interestOps(interest | SelectionKey.OP_READ);  
  79.                 }  
  80.             }  
  81.         }  
  82.     }  

 

猜你喜欢

转载自jaesonchen.iteye.com/blog/2337543