Tomcat - 如何使用 PersistentManager + FileStore 将会话立即保存到磁盘
Tomcat - How to persist a session immediately to disk using PersistentManager + FileStore
我想将 Tomcat 的 HttpSession 保存到磁盘,以便它可以在可扩展的云环境中使用。关键是将有许多 Tomcat 个节点(在云 PaaS 中),客户端可以定向到其中任何一个。我们希望从共享磁盘单元持久保存和加载会话。
我是这样配置 PersistentManager 的:
context.xml
<Manager className="org.apache.catalina.session.PersistentManager">
<Store className="org.apache.catalina.session.FileStore" directory="c:/somedir"/>
</Manager>
问题是会话显然从未刷新到磁盘。
我更改了 <Manager>
配置,添加了 maxIdleBackup:
<Manager className="org.apache.catalina.session.PersistentManager maxIdleBackup="1">
通过这种方式,我几乎花了一分钟时间才看到会话持久保存到磁盘。奇怪的是,文档指出它应该需要大约一秒钟:
maxIdleBackup: The time interval (in seconds) since the last access to
a session before it is eligible for being persisted to the session
store, or -1 to disable this feature. By default, this feature is
disabled.
其他配置:
按照documentation我设置系统属性
org.apache.catalina.session.StandardSession.ACTIVITY_CHECK -> true
有没有办法立即将会话刷新到磁盘?是否可以立即保留会话中的任何更改?
更新:
我已经尝试使用 maxIdleBackup="0" minIdleSwap="0" maxIdleSwap="1"
强制钝化会话并刷新到磁盘,但仍然需要将近一分钟。
我终于解决了这个问题:
- 我扩展了
org.apache.catalina.session.ManagerBase
覆盖使用 superclass' 会话映射的每个方法,以便它直接攻击文件(或缓存)。
示例:
@Override
public HashMap<String, String> getSession(String sessionId) {
Session s = getSessionFromStore(sessionId);
if (s == null) {
if (log.isInfoEnabled()) {
log.info("Session not found " + sessionId);
}
return null;
}
Enumeration<String> ee = s.getSession().getAttributeNames();
if (ee == null || !ee.hasMoreElements()) {
return null;
}
HashMap<String, String> map = new HashMap<>();
while (ee.hasMoreElements()) {
String attrName = ee.nextElement();
map.put(attrName, getSessionAttribute(sessionId, attrName));
}
return map;
}
重要:
加载和卸载方法必须留空:
@Override
public void load() throws ClassNotFoundException, IOException {
// TODO Auto-generated method stub
}
@Override
public void unload() throws IOException {
// TODO Auto-generated method stub
}
您必须覆盖 startInternal 和 stopInternal 以防止生命周期错误:
@Override
protected synchronized void startInternal() throws LifecycleException {
super.startInternal();
// Load unloaded sessions, if any
try {
load();
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("standardManager.managerLoad"), t);
}
setState(LifecycleState.STARTING);
}
@Override
protected synchronized void stopInternal() throws LifecycleException {
if (log.isDebugEnabled()) {
log.debug("Stopping");
}
setState(LifecycleState.STOPPING);
// Write out sessions
try {
unload();
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("standardManager.managerUnload"), t);
}
// Expire all active sessions
Session sessions[] = findSessions();
for (int i = 0; i < sessions.length; i++) {
Session session = sessions[i];
try {
if (session.isValid()) {
session.expire();
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
} finally {
// Measure against memory leaking if references to the session
// object are kept in a shared field somewhere
session.recycle();
}
}
// Require a new random number generator if we are restarted
super.stopInternal();
}
- 以上允许始终从文件(或缓存)读取,但是写入操作呢?。为此,我扩展了
org.apache.catalina.session.StandardSession
覆盖 public void setAttribute(String name, Object value, boolean notify)
和 public void removeAttribute(String name, boolean notify)
.
示例:
@Override
public void setAttribute(String name, Object value, boolean notify) {
super.setAttribute(name, value, notify);
((DataGridManager)this.getManager()).getCacheManager().getCache("sessions").put(this.getIdInternal(), this);
}
@Override
public void removeAttribute(String name, boolean notify) {
super.removeAttribute(name, notify);
((DataGridManager)this.getManager()).getCacheManager().getCache("sessions").put(this.getIdInternal(), this);
}
重要提示:
在我们的例子中,真正的会话备份最终是一个缓存(不是文件),当我们从它读取扩展的 Tomcat 会话时(在我们的 ManagerBase impl class 中)我们不得不以一种丑陋的方式调整它,以便一切正常:
private Session getSessionFromStore(String sessionId){
DataGridSession s = (DataGridSession)cacheManager.getCache("sessions").get(sessionId);
if(s!=null){
try {
Field notesField;
notesField = StandardSession.class.getDeclaredField("notes");
notesField.setAccessible(true);
notesField.set(s, new HashMap<String, Object>());
s.setManager(this);
} catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
throw new RuntimeException(e);
}
}
return s;
}
你也可以使用这个阀门,它是 Tomcat 发行版的一部分(至少在版本 8 中):
<Valve className="org.apache.catalina.valves.PersistentValve"/>
此节点必须插入到 context.xml
文件中的 <Manager className="org.apache.catalina.session.PersistentManager">
节点之前。
然后它将使用存储来维护每个 http 请求的会话。请注意,文档假定同一客户端一次只会发出一个 http 请求。
这将允许您在 java ee 服务器前使用非粘性会话负载平衡器。
我遇到这个问题是因为在我将 PersistentManager
添加到配置后 Tomcat 需要一分钟才能关闭,但这也与您的问题有关:
您之所以花一分钟时间坚持 PersistentManager
是因为您没有调整 processExpiresFrequency
。此设置规定了 PersistentManager
的后台进程 运行 使会话过期、持久化等的频率。默认值为 6。(参见文档:http://tomcat.apache.org/tomcat-8.5-doc/config/manager.html#Standard_Implementation)
根据代码,此值乘以您在 <Engine>
元素上设置的 engine.backgroundProcessorDelay
。它的默认值为 10。所以 6*10 是 60 秒。如果您在 <Manager>
元素上添加 processExpiresFrequency="1"
,您会发现它会更快关闭(10 秒)。如果这还不够快,您也可以将 backgroundProcessorDelay
调低。您还需要将 maxIdleBackup
设置为 1
。您不会获得绝对立即的持久性,但它非常快并且不需要接受的答案中的 self-described "ugly tweak"。
(参见 http://svn.apache.org/repos/asf/tomcat/tc8.5.x/tags/TOMCAT_8_5_6/java/org/apache/catalina/session/PersistentManagerBase.java 中关于 setMaxIdleBackup 方法的 backgroundProcessorDelay 的评论)
我想将 Tomcat 的 HttpSession 保存到磁盘,以便它可以在可扩展的云环境中使用。关键是将有许多 Tomcat 个节点(在云 PaaS 中),客户端可以定向到其中任何一个。我们希望从共享磁盘单元持久保存和加载会话。
我是这样配置 PersistentManager 的:
context.xml
<Manager className="org.apache.catalina.session.PersistentManager">
<Store className="org.apache.catalina.session.FileStore" directory="c:/somedir"/>
</Manager>
问题是会话显然从未刷新到磁盘。
我更改了 <Manager>
配置,添加了 maxIdleBackup:
<Manager className="org.apache.catalina.session.PersistentManager maxIdleBackup="1">
通过这种方式,我几乎花了一分钟时间才看到会话持久保存到磁盘。奇怪的是,文档指出它应该需要大约一秒钟:
maxIdleBackup: The time interval (in seconds) since the last access to a session before it is eligible for being persisted to the session store, or -1 to disable this feature. By default, this feature is disabled.
其他配置:
按照documentation我设置系统属性
org.apache.catalina.session.StandardSession.ACTIVITY_CHECK -> true
有没有办法立即将会话刷新到磁盘?是否可以立即保留会话中的任何更改?
更新:
我已经尝试使用 maxIdleBackup="0" minIdleSwap="0" maxIdleSwap="1"
强制钝化会话并刷新到磁盘,但仍然需要将近一分钟。
我终于解决了这个问题:
- 我扩展了
org.apache.catalina.session.ManagerBase
覆盖使用 superclass' 会话映射的每个方法,以便它直接攻击文件(或缓存)。
示例:
@Override
public HashMap<String, String> getSession(String sessionId) {
Session s = getSessionFromStore(sessionId);
if (s == null) {
if (log.isInfoEnabled()) {
log.info("Session not found " + sessionId);
}
return null;
}
Enumeration<String> ee = s.getSession().getAttributeNames();
if (ee == null || !ee.hasMoreElements()) {
return null;
}
HashMap<String, String> map = new HashMap<>();
while (ee.hasMoreElements()) {
String attrName = ee.nextElement();
map.put(attrName, getSessionAttribute(sessionId, attrName));
}
return map;
}
重要:
加载和卸载方法必须留空:
@Override
public void load() throws ClassNotFoundException, IOException {
// TODO Auto-generated method stub
}
@Override
public void unload() throws IOException {
// TODO Auto-generated method stub
}
您必须覆盖 startInternal 和 stopInternal 以防止生命周期错误:
@Override
protected synchronized void startInternal() throws LifecycleException {
super.startInternal();
// Load unloaded sessions, if any
try {
load();
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("standardManager.managerLoad"), t);
}
setState(LifecycleState.STARTING);
}
@Override
protected synchronized void stopInternal() throws LifecycleException {
if (log.isDebugEnabled()) {
log.debug("Stopping");
}
setState(LifecycleState.STOPPING);
// Write out sessions
try {
unload();
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("standardManager.managerUnload"), t);
}
// Expire all active sessions
Session sessions[] = findSessions();
for (int i = 0; i < sessions.length; i++) {
Session session = sessions[i];
try {
if (session.isValid()) {
session.expire();
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
} finally {
// Measure against memory leaking if references to the session
// object are kept in a shared field somewhere
session.recycle();
}
}
// Require a new random number generator if we are restarted
super.stopInternal();
}
- 以上允许始终从文件(或缓存)读取,但是写入操作呢?。为此,我扩展了
org.apache.catalina.session.StandardSession
覆盖public void setAttribute(String name, Object value, boolean notify)
和public void removeAttribute(String name, boolean notify)
.
示例:
@Override
public void setAttribute(String name, Object value, boolean notify) {
super.setAttribute(name, value, notify);
((DataGridManager)this.getManager()).getCacheManager().getCache("sessions").put(this.getIdInternal(), this);
}
@Override
public void removeAttribute(String name, boolean notify) {
super.removeAttribute(name, notify);
((DataGridManager)this.getManager()).getCacheManager().getCache("sessions").put(this.getIdInternal(), this);
}
重要提示:
在我们的例子中,真正的会话备份最终是一个缓存(不是文件),当我们从它读取扩展的 Tomcat 会话时(在我们的 ManagerBase impl class 中)我们不得不以一种丑陋的方式调整它,以便一切正常:
private Session getSessionFromStore(String sessionId){
DataGridSession s = (DataGridSession)cacheManager.getCache("sessions").get(sessionId);
if(s!=null){
try {
Field notesField;
notesField = StandardSession.class.getDeclaredField("notes");
notesField.setAccessible(true);
notesField.set(s, new HashMap<String, Object>());
s.setManager(this);
} catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
throw new RuntimeException(e);
}
}
return s;
}
你也可以使用这个阀门,它是 Tomcat 发行版的一部分(至少在版本 8 中):
<Valve className="org.apache.catalina.valves.PersistentValve"/>
此节点必须插入到 context.xml
文件中的 <Manager className="org.apache.catalina.session.PersistentManager">
节点之前。
然后它将使用存储来维护每个 http 请求的会话。请注意,文档假定同一客户端一次只会发出一个 http 请求。
这将允许您在 java ee 服务器前使用非粘性会话负载平衡器。
我遇到这个问题是因为在我将 PersistentManager
添加到配置后 Tomcat 需要一分钟才能关闭,但这也与您的问题有关:
您之所以花一分钟时间坚持 PersistentManager
是因为您没有调整 processExpiresFrequency
。此设置规定了 PersistentManager
的后台进程 运行 使会话过期、持久化等的频率。默认值为 6。(参见文档:http://tomcat.apache.org/tomcat-8.5-doc/config/manager.html#Standard_Implementation)
根据代码,此值乘以您在 <Engine>
元素上设置的 engine.backgroundProcessorDelay
。它的默认值为 10。所以 6*10 是 60 秒。如果您在 <Manager>
元素上添加 processExpiresFrequency="1"
,您会发现它会更快关闭(10 秒)。如果这还不够快,您也可以将 backgroundProcessorDelay
调低。您还需要将 maxIdleBackup
设置为 1
。您不会获得绝对立即的持久性,但它非常快并且不需要接受的答案中的 self-described "ugly tweak"。
(参见 http://svn.apache.org/repos/asf/tomcat/tc8.5.x/tags/TOMCAT_8_5_6/java/org/apache/catalina/session/PersistentManagerBase.java 中关于 setMaxIdleBackup 方法的 backgroundProcessorDelay 的评论)