-
Nov05
Resin Session 分析
Posted in 软件开发, 1,260 views
-
Author: Wooce
1. Resin的Reliability和Load balance机制
Resin在Web Server中的内嵌模块处理请求的顺序:
1) Web Server接收到请求
2) 内嵌模块(mod_caucho, mod_isapi等)检查是否由Resin处理的请求
3) 内嵌模块选中一个后台JVM,也就是一个
: a. 如果是已有的session, 则选中拥有该session的那一个JVM
b. 如果是新的请求,则采用轮循策略交由下一个
处理 4) 内嵌模块通过TCP Socket把请求发给选中的后台JVM
5) 内嵌模块再通过同一TCP Socket连接接收后台JVM的响应结果.
所有的有关内嵌模块和后台JVM均在resin.conf这一配置文件中配置, 使维护变得容易.
因为内嵌模块需要决定选用哪一个JVM,所以负载平衡由内嵌模块控制.从JVM的角度
看,来自内嵌模块的请求和HTTP请求是同样的,除了编码稍有不同.例如, Resin1.2或以上,同一个JVM可以在同一个端口8080同时作为srun和httpd服务器服务.
轮循策略虽然简单, 但它实际上和其他复杂的负载平衡测率一样有效, 并且正由于简单, 它更加健壮和快速.
1.1 单机
最廉价的备份策略. 它在单台机器上运行一个Web Server和两个JVM. 其中一个是主服务JVM, 另一个是后备JVM, 如果主JVM失效, 将启用后备JVM维持Servlet Engine仍然可用. 在resin.conf中的相应配置例子:
...
启动时应该分别单独地启动这两个sun进程. 例如unix下:
unix> httpd.sh -pid srun1.pid -server a start
unix> httpd.sh -pid srun2.pid -server b start在NT下:
c:\resin1.2> bin/httpd -install-as ResinA -server a
c:\resin1.2> bin/httpd -install-as ResinB -server b1.2 单Web Server, 多JVM
当有多台计算机的时候, 可以把负载分担到一台Web Server和多个JVM上, 这比用路由器实现负载平衡较为廉价. 并且采用Resin自己的负载平衡机制,可以保证使同一个session停留在同一台计算机上.
配置举例:
...
c:\resin1.2> bin/httpd -install-as ResinB -server b各台计算机可以共享使用同一个resin.conf, 但也可以为Web Server和每个JVM分别使用不同的resin.conf.
为了确保session停留在固定的机器上, Resin对cookie的编码中加上了主机号标识.
1.3 多Web Server和JVM
对于大型的服务网站, 一般使用多台计算机, 并且每台计算机都分别运行一个Web
Server和一个JVM, 由Router负责把负载分担到不同的机器上.
对于这种方法, 由于Router随机地把负载分配到某台机器上, 所以发给某台host的请求的Session不一定是这台Host拥有的.因此需要把任何持久的session维护在一台中央服务器如JDBC数据库上或者采用其他方法使能够由session的id找到拥有该session的机器.
即使对于这种Router负载平衡方法, 仍然可以用Resin自己的load balance策略提高可靠性. 对于每一台host上的web server, 都应该首先使用同一台host上运行的JVM进行处理, 但同时可以另一台Host上运行的JVM作为后备.
在某一台host上的resin.conf配置举例:
...
上面例子中host1, host2,host3上的JVM只有当localhost上的JVM失败时才会被启用.
另一个例子:
1.4 多Web Server, 单JVM
多个Web Server可以同时使用同一个JVM处理请求. 例如, 我们有一个高速的一般的
Web Server和一个采用SSL加密的Web Server的时候, 这两个Web Server可以由同一个JVM处理请求(虽然如果有一个后备JVM的话会更好), 这简化了引入SSL的开发.
2. Resin内部实现distributed sessions的机制
2.1 session的backing store的概念和内存中的session的同步更新
在Resin设置成load balance over multiple machine的情况, 各台运行web server和JVM的机器组成一个load balancing pool.为实现load balance over multiple machine下的distributed session, 必须考虑一个session在被生成后, 在load balancing pool中其他不拥有该session的机器如何访问它, 以及如何同步它们. 在Resin中是通过把session存储到backing store中来解决的, 一个JVM创建了一个新的session后,就把它存储到backing store去, 然后只要保证以下条件:
(1)每个内存中有此session的JVM如果更新了此session, 就把更新后的session写到backing store中去, 并且backing store里做到在写某一session的时候不会同时接受读取同一session的请求.
(2)每个JVM在读取session的时候,如果它的内存cache中还没有该session, 一定到backing store去读取.
(3)某个JVM: Server A在读取session的时候,如果它的内存cache中已有该session, 这时只要满足以下的其中一种情况即可:
a. 在(1)中写session到backing store里时保证同时更新了Server A的内存cache中的session
b. 到backing store中再load出session并以此更新内存cache中的session
显然, 满足了以上3个条件, 不同的机器存取同一session的时候就有很好的同步
性, 并且亦有较好的性能.
在Resin中, backing store可采用3种实现方式: (参看src/com/caucho/server/http/下的
SessionStore.java,SessionFileStore.java,SessionJdbcStore.java和SessionRingStore.java)
(1) SessionFileStore(这一方式在我所看的Resin2.0.2的代码里实际上还未实现)
session的backing store在生成它的Host的文件系统(Resin似乎考虑在将来实现一个在不同的运行JVM的机器之间共享文件的网络文件系统VFS)上.
(2) SessionJdbcStore
在一台中央服务器上的database作为backing store, 每台Host上生成的session都通过
JDBC存储到中央服务器的database中去.
(3) SessionRingStore
在这种方式中, load balancing pool中所有的运行JVM的机器按host id从小到大(host id
的首字母从’a’到’z’的顺序)组成一个JVM的index号的环, 当环中的某一台Host生成了一个session(session里记录了生成它的JVM的index号)的时候, 就以生成该session的JVM的index号开始, 在环上顺序查找, 只要某一个index的JVM是可用的, 就把session存储到运行该JVM的机器的本地文件系统中, 这样在两台机器上都分别存储了这个session的副本的时候就停止. 明显可看出, 一般情况下(所有JVM都没有crash down)每个session都有两个副本分别存储在生成它的那台机器和环中的下一台机器的文件系统中.
Resin采用上述3种方式中的哪种方式来实现distributed session决定于:
IF resin.conf中session-config/file-store和其下的directory的tag不为空 THEN 采用SessionFileStore;
ELSE IF resin.conf中session-config/tcp-store为true THEN 采用SessionRingStore;
ELSE IF resin.conf中session-config/jdbc-store不为空 THEN 采用SessionJdbcStore;
在Resin的实现中,
(1)SessionRingStore方式一定能满足上面所说的3个条件, 同步性最好.而对于(2)SessionFileStore和SessionJdbcStore两种方式
a. 当resin.conf中session-config/always-load-session为false的时候, 如果一个JVM读入了session在内存cache中, 以后除非timeout再从backing store中reload的情况, 以后有请求都一定从内存cache中读该session, 不能看到其他JVM在此期间对该session的更新, 这时同步性最差, 但由于不需从backing store中读取, 响应速度最快;
b. 当resin.conf中session-config/always-load-session为true的时候, 这时任何请求都会到backing store中读取, 同步性好, 但由于session的内存cache实际上不起作用, 对请求的响应速度最慢.
2.2 相关类分析
2.2.1 QSession类
实现HttpSession接口, 在Resin系统中某个JVM内存中的session就是一个QSession对象, JVM内存cache中的QSession对象需要和backing store中的session保持同步更新.
重要数据成员和函数:
变量名
类型
功用values
HashMap
存储一个session中包含的所有session变量的变量名和变量值maxInactiveInterval
long
Session的最长可生存时间srunIndex
int
生成本对象的JVM在load balancing pool的所有JVM中的index号(如果生成本对象的JVM运行在的host的id的首字母在’a’到’z’之间, 则就等于该首字母和’a’的ASCII码的差值, 否则为-1)accessTime
long
本session的最近访问时间needsLoad
boolean
是否需要从Backing store中reload本session函数
功用void setNeedsLoad(boolean needsLoad)
设置needsLoad标志void setAccess(long now)
对本session对象操作成功时, 设置本session的最近访问时间为now.void reset(long now)
操作时发现本session未正确保存在backing store中, 清楚本session的内容.2.2.2 VirtualHost类
在一个JVM上处理的所有Servlet application的集合.
2.2.3 SrunConnection类
维护本JVM到另一个JVM的一个用以交换session的socket连接.
2.2.4 ServletServer类
实现Server接口
重要数据成员和函数:
变量名
类型
功用sessionBackingCache
LRUCache(1024)
在内存中cache本JVM所拥有的所有session的SessionBacking对象srunIndex
Int
本JVM在包含各JVM的load balancing pool中的index号(Resin的某个版本, 在ResinServer.java中根据resin.conf中"srun-index"的设定值设置)srunRing
SrunConnection []
维持到load balancing pool中其他的JVM的预分配的SrunConnection连接的数组hosts
HashTable(table中的每项以hostname为key, 以VirtualHost为value)
各个JVM的(hostname,VirtualHost)列表, 在initHosts()函数中调用addHost()初始化uniqueHosts
HashTable(table中的每项以Host uniqueHost为key, 以VirtualHost为value)
各个JVM的(Host uniqueHost,VirtualHost)列表函数
功用getSessionBacking(String id, SessionManager manager)
以id为key查找session id等于id的session相应的SessionBacking, 如果找到即返回它(首先在sessionBackingCache中找, 如果找不到再到其他地方找).int getSrunCount()
返回load balancing pool中的所有JVM的数目SrunConnection getSrunPath(int index)
根据index返回本JVM和load balance pool中该index对应的JVM之间的连接ReadWritePair getRecycleConnection(int index)
分配一个空闲的用于跟index号对应的JVM交换数据的ReadStream/WriteStream TCP-stream pair,ReadWritePair getSessionConnection(int index)
新建立一个用于跟index号对应的JVM交换数据的ReadStream/WriteStream TCP-stream pair其他:
hostList: ArrayList类型(VirtualHost对象)
addHost(String id, RegistryNode node, String appDir)函数:
根据(id, node, appDir)新建一个VirtualHost对象并存入HashTable类型成员hosts中.
init()函数:
初始化invocationCache(LRUCache类型)等内存中的Cache;调用initHosts()函数初始化hosts等;
2.2.5 SessionStore类
负责session的访问(读取/存储)操作的基类, SessionFileStore,SessionJdbcStore,
SessionRingStore等都是它的子类.
重要数据成员和函数:
函数
功用boolean load(QSession session);
负责从backing store中读取QSession对象, 隐藏backing store究竟在什么地方的细节, 在SessionFileStore, SessionJdbcStore, SessionRingStore类中分别被重载和具体定义实现细节Void store(QSession session);
负责存储QSession对象到backing store中, 隐藏backing store究竟在什么地方的细节, 在SessionFileStore, SessionJdbcStore, SessionRingStore类中分别被重载和具体定义实现细节boolean load(QSession session, InputStream is,int oldUpdateCount);
从InputStream读出QSession对象(通过Serializable序列化字节流的方式读取)boolean store(QSession session, OutputStream os)
把QSession对象写到OutputStream(通过Serializable序列化字节流的方式写入)2.2.5 SessionBacking类
封装session存储在file system上的backing path上时的访问操作的接口.
重要数据成员和函数:
变量名
类型
功用sessionPath
Path
file system上的backing path, session存储于其中函数
功用boolean load(QSession session);
在backing path上打开ReadStream, 然后去调用SessionStore类的boolean load(QSession session, InputStream is,int oldUpdateCount);函数完成从backing path里读出QSession对象的操作void save(QSession session)
在backing path上打开WriteStream, 然后去调用SessionStore类的boolean store(QSession session, OutputStream os)函数完成存储QSession对象到backing path中的操作2.2.6 SessionManager类
管理一个servlet application的所有session
重要数据成员和函数:
变量名
类型
功用reloadEachRequest
boolean
当有request到达的时候, 是否一定要到backing store中取出session,以它来更新内存中的sessionsessions
LRUCache
存放所有active session(以session id为key), 容量由resin.conf里的session-config/session-max定义sessionTimeout
long
由resin.conf的session-config/session-timeout定义sessionList
ArrayList
session timeout的时候被放入其中函数
功用QSession createSession(String oldId, long date, int sessionGroup,HttpServletRequest request);
新生成一个QSession对象:1)如果oldId为空或者长度小于4, 则调用createSessionId()生成新的session的id, 否则仍使用oldId作为新的session的 id
2)以新的session的id为参数调用createSession(id,date)生成QSession对象
3)以id为key, 把QSession对象放入sessions中.
4)如果新的session的id采用oldId, 说明backing store中已经有该session,只是在本JVM的内存cache中没有该QSession对象了, 所以调用load(session, date)从backing store中读取QSession对象所含的各session变量的变量名和值.
QSession getSession(String key, long now)
读取session id等于参数key的QSession对象, 如果没有则会创建一个QSession对象1) 在sessions中查找该QSession对象
2) 如果没有找到并且now>0, 则调用createSession()生成新的QSession对象并插入到sessions中
3) 在需要load的情况(get出来的QSession对象的needsLoad()为true, 或者是2)步中新生成的等),
调用load(session,now)函数从backing store中读取QSession对象所含的各session变量的名和值.
void load(QSession session, long now)
尝试从backing store中load出QSession对象, 如果成功则更新其最近访问时间, 如果失败则清除QSession对象的内容, 可在刚执行createSession()函数后调用以检查新生成的session是否已存储成功.2.2.6 SessionRingStore类
当以Ring方式的backing store存储distributed sessions时SessionStore类的实现.
SessionRingStore.java的void store(QSession session)函数:
int sessionIndex = session.getSrunIndex(); //生成该QSession对象的JVM的index号
int srunIndex = manager.getSrunIndex(); //执行本函数的JVM的index号
int srunCount = manager.getSrunCount(); //Load balancing pool中所有JVM的数目
if (sessionIndex < 0)
sessionIndex = srunIndex;
int index = sessionIndex; //在环中从生成该QSession对象的JVM的index号开始
ReadWritePair rws = null;
boolean hasSelf = false;
int saveCount = 2; //分别存储2个副本到2个JVM的file system中
for (int i = srunCount - 1; i >= 0 && saveCount > 0; i--) {//在环上查找一周,并且成功存//储2个副本时就停止
if (index == srunIndex) { //环上的当前index号是本机的index号, 令hasSelf为true
hasSelf = true; //使在后面的代码段中将会调用StoreSelf()函数存储Session副
saveCount--; //本到本机的file system中
}
else {
rws = server.getRecycleConnection(index); //环上当前检查的index号是其他机器,
if (rws != null) { //取出可用的socket连接把QSession对象传给index号host让它
try { //存储或更新到它的本地file sytem(由RunnerRequest.java处理) saveSession(tempStream, backing.getId(), index, rws);
saveCount--;
} catch (IOException e) {
rws.getReadStream().close();
rws.getWriteStream().close();
rws = null;
}
}
if (rws == null) {
rws = server.getSessionConnection(index);
if (rws != null) {
try {
saveSession(tempStream, backing.getId(), index, rws);
saveCount--;
} catch (IOException e) {
rws.getReadStream().close();
rws.getWriteStream().close();
rws = null;
}
}
}
}
index = (index + 1) % srunCount; //取环中的下一个JVM
}
if (hasSelf) {
ReadStream is = tempStream.openRead(true);
storeSelf(session, is); //存储session到本地file system
is.close();
storeSelf()函数如下:
private void storeSelf(QSession session, ReadStream is)
{
SessionBacking backing = session.getBacking();
synchronized (backing) { //通过异步synchronized避免多个请求时的写冲突
try {
Path sessionPath = backing.getPath(); //获得session在本地file system上的存储路径
WriteStream os = sessionPath.openWrite();
os.writeStream(is);
os.close();
} catch (IOException e) {
if (dbg.canWrite())
dbg.log(e);
}
}
boolean load(QSession session)
throws Exception
{
SessionBacking backing = session.getBacking();
if (backing == null)
return false;
int sessionIndex = session.getSrunIndex();
int srunIndex = manager.getSrunIndex();
int srunCount = manager.getSrunCount();
if (sessionIndex < 0)
sessionIndex = srunIndex;
int updateCount = session.getUpdateCount();
/* servers A and B don't need auto-reloading, but server C does.
这里server A和server B即指存有session副本的那两个server,
当本JVM是server A或server B之一的时候, 显然本JVM的内存cache里的session
肯定是整个load balancing pool中最新的, 所以从这里退出来后回到SessionManager类//的QSession getSession(String key, long now)函数往下执行到
else if (sessionStore != null && now > 0 &&
(reloadEachRequest || session.needsLoad()))
load(session, now);
这里session.needsLoad()函数必须返回false, 反之不是server A和server B的情况session.needsLoad()函数就必须返回true以便调用load(session,now)从server A和server B上获取最新更新的session, 所以在这里的符合条件判断时的代码段内有session.setNeedsLoad(true);语句
*/
if (srunIndex != sessionIndex &&
(sessionIndex + 1) % srunCount != srunIndex) {
updateCount = -1;
session.setUpdateCount(-1);
session.setNeedsLoad(true);
}
ReadWritePair rws = null;
int index = sessionIndex;
int loadCount = 2;
// Find a live server
for (int i = srunCount - 1; i >= 0 && loadCount > 0; i--) {
try {
if (srunIndex != index) {
rws = server.getRecycleConnection(index);
if (rws != null) {
try {
if (loadSession(session, index, backing.getId(),
rws, updateCount)) {
loadCount--;
updateCount = session.getUpdateCount();
}
} catch (IOException e) {
rws.getReadStream().close();
rws.getWriteStream().close();
rws = null;
}
}
if (rws == null) {
rws = server.getSessionConnection(index);
if (rws != null &&
loadSession(session, index, backing.getId(),
rws, updateCount)) {
updateCount = session.getUpdateCount();
loadCount--;
}
}
}
else if (loadSelf(session, updateCount)) {
if (dbg.canWrite()) {
dbg.log("[" + server.getServerId() + "] self-load(" + index + ") " +
backing.getId() + " " + backing.getPath());
}
updateCount = session.getUpdateCount();
loadCount--;
}
} catch (Exception e) {
if (dbg.canWrite())
dbg.log(e);
}
if (srunCount > 1)
index = (index + 1) % srunCount;
}
return loadCount < 2 && updateCount > 0;
}
2.3 对Session的处理流程
2.3.1 新的请求(需要新生成一个原来在backing store中没有的session)的情况
当有新的请求时,
1) 执行Request.getSession(boolean create=true) (指调用Request类的getSession(Boolean create)函数, 并传参数create为true,下同) .
2) Request.getSession(boolean create)调用AbstractRequest.createSession(boolean create, boolean hasOldSession)函数.
3) AbstractRequest.createSession(boolean create, boolean hasOldSession)最后调用SessionManager.createSession(String oldId, long date, int sessionGroup,HttpServletRequest request)创建QSession对象(但只在内存中, 并未存储到backing store)
4) 对Request的处理完成, 调用Request.finish().
5)Request.finish()调用QSession.finish().
6)QSession.Finish()调用SessionStore.Store(QSession Session)完成将session存储到backing store的过程.
2.3.2 对旧有Session的请求(backing store中存在此id的session)的情况
1) 执行Request.getSession(Boolean create=false)
2) Request.getSession(boolean create)调用AbstractRequest.createSession(boolean create, boolean hasOldSession).
3) AbstractRequest.createSession(boolean create, boolean hasOldSession)调用
SessionManager.getSession(String key, long now)
4) SessionManager.getSession(String key, long now)在内存Cache中查找此QSession对
象, 如果需要reload, 还要从backing store中读取QSession对象的更新的值, 然后返回QSession对象.
2.3.3 非法请求(整个系统中不存在此id的session)的情况
在Request类中的boolean isRequestedSessionIdValid()函数即能判断, 它将调用Request.getSession(false)在本JVM的内存cache和backing store中查找此id的session, 如果都找不到则返回false.
附AbstractRequest.java的createSession(boolean create, boolean hasOldSession)函数的注解:
protected QSession createSession(boolean create, boolean hasOldSession)
{
Application application = getApplication();
SessionManager manager = application.getSessionManager();
String id = getRequestedSessionId(); //读取请求中发过来的session id
long now = Alarm.getCurrentTime();
QSession session;
if (id != null && id.length() > 6) {
session = manager.getSession(id, now); //从内存cache中读取此id的QSession对象, 没
if (session == null) { //有则创建一个
}
else if (session.isValid()) //成功则返回该QSession对象
return session;
else
id = null;
}
if (hasOldSession)
id = null;
if (! create)
return null;
// Must accept old ids because different applications in the same
// server must share the same cookie
//
// But, if the session group doesn't match, then create a new
// session.
session = manager.createSession(id, now, getSessionGroup(), this); //新建QSession对象
if (session != null) //注意创建QSession对象和创建一个存储在backing store中//session不是一回事.
setHasCookie();
if (id != null)
return session;
if (manager.enableSessionCookies())
getResponse().setSessionId(session.getId());
return session; //返回新创建的QSession对象
}
Related posts:

Leave a comment | Trackback 这篇文章还没有评论.