问题
I want to persist Tomcat's HttpSessions to disk so that it can be used in a scalable cloud environment. The point is that there will be a number of Tomcat nodes up (in a cloud PaaS) and clients can be directed to any of them. We want to persist and load the sessions from a shared disk unit.
I have configured the PersistentManager this way:
context.xml
<Manager className="org.apache.catalina.session.PersistentManager">
<Store className="org.apache.catalina.session.FileStore" directory="c:/somedir"/>
</Manager>
The problem is that sessions are, apparently, never flushed to disk.
I changed the <Manager>
config adding maxIdleBackup:
<Manager className="org.apache.catalina.session.PersistentManager maxIdleBackup="1">
This way it takes almost a minute until I see the session persisted to disk. Oddly enough, the doc states that it should take around a second:
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.
Other config:
Following the documentation I set the system property
org.apache.catalina.session.StandardSession.ACTIVITY_CHECK -> true
Is there a way to immediately flush the session to disk? Is is possible to make that any change in the session is also persisted right away?
UPDATE:
I have tried to force the passivation of the session and flushing to disk with maxIdleBackup="0" minIdleSwap="0" maxIdleSwap="1"
, but it still takes almost a minute.
回答1:
You can also use this valve which is part of the Tomcat distribution (at least in version 8) :
<Valve className="org.apache.catalina.valves.PersistentValve"/>
This node has to be inserted before the <Manager className="org.apache.catalina.session.PersistentManager">
node in the context.xml
file.
It will then use the store to maintain the session on each http request. Note that the documentation assumes that only one http request will be made by the same client at a time.
This will allow you to use non sticky session load balancer in front of your java ee servers.
回答2:
I came across this because Tomcat was taking a minute to shutdown once I added the PersistentManager
to the configuration, but it relates to your problem too:
The reason you it takes a minute to persist with the PersistentManager
is because you haven't adjusted the processExpiresFrequency
. This setting regulates how often the PersistentManager
will run it's background processes to expire session, persist them, etc. The default value is 6. (See docs: http://tomcat.apache.org/tomcat-8.5-doc/config/manager.html#Standard_Implementation)
Per the code, this value is multiplied by engine.backgroundProcessorDelay
, which you set on your <Engine>
element. It's default value is 10. So 6*10 is 60 seconds. If you add processExpiresFrequency="1"
on your <Manager>
element, you'll see it will shutdown much quicker (10 seconds). If that's not fast enough, you can adjust the backgroundProcessorDelay
to be lower too. You'll also still want to set maxIdleBackup
to 1
. You won't get absolutely immediate persistence, but it's very quick and doesn't require the self-described "ugly tweak" in the accepted answer.
(See comments about backgroundProcessorDelay on setMaxIdleBackup method in http://svn.apache.org/repos/asf/tomcat/tc8.5.x/tags/TOMCAT_8_5_6/java/org/apache/catalina/session/PersistentManagerBase.java)
回答3:
I finally managed to solve this:
- I extended
org.apache.catalina.session.ManagerBase
overriding every method that used the superclass' sessions map, so that it attacked a file (or cache) directly.
Example:
@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;
}
IMPORTANT:
load and unload methods must be left empty:
@Override
public void load() throws ClassNotFoundException, IOException {
// TODO Auto-generated method stub
}
@Override
public void unload() throws IOException {
// TODO Auto-generated method stub
}
You have to override startInternal and stopInternal to prevent Lifecycle errors:
@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();
}
- The above allows to read always from the file (or cache) but what about the write operations?. For this, I extended
org.apache.catalina.session.StandardSession
overridingpublic void setAttribute(String name, Object value, boolean notify)
andpublic void removeAttribute(String name, boolean notify)
.
Example:
@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);
}
IMPORTANT:
In our case the real session backup ended up being a cache (not a file) and when we read the extended Tomcat session from it (in our ManagerBase impl class) we had to tweak it in an kind of ugly way so that everything worked:
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;
}
来源:https://stackoverflow.com/questions/35917945/tomcat-how-to-persist-a-session-immediately-to-disk-using-persistentmanager