(1)-不恰当的request.getSession()
在HttpServlet中,HttpSession对象通常在request.getSession(true)方法调用时才创建。 HttpSession的使用是有代价的,需要占用服务器资源,本着能不浪费就不浪费的原则,我希望系统中的session都在掌握之中,在需要创建时由 我们的代码明确创建。但是最近在开发中发现,新的session对象经常在意料之外出现,究竟是谁在创建session呢?
最常见的地方是错误的使用request.getSession()函数,通常在action中检查是否有某个变量/标记存放在session中。这个场景中可能出现没有session存在的情况,正常的判断应该是这样:
private boolean ifFlagExistInSession(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session != null) { if (session.getAttribute("flagName") != null) { return true; } } return false; }
1
.
private
boolean
ifFlagExistInSession(HttpServletRequest request)
...
{
2. HttpSession session = request.getSession(false);
3. if (session != null) ...{
4. if (session.getAttribute("flagName") != null) ...{
5. return true;
6. }
7. }
8. return false;
9. }
而下面的写法,则可能会生成一个新的不在我们意图之外的session:
private boolean ifFlagExistInSession(HttpServletRequest request) { HttpSession session = request.getSession(); // a new session created if no session exists if (session.getAttribute("flagName") != null) { return true; } return false; }
1
.
private
boolean
ifFlagExistInSession(HttpServletRequest request)
...
{
2. HttpSession session = request.getSession(); // a new session created if no session exists
3. if (session.getAttribute("flagName") != null) ...{
4. return true;
5. }
6. return false;
7. }
注意request.getSession() 等同于 request.getSession(true),除非我们确认session一定存在或者sesson不存在时明确有创建session的需要,否则 请尽量使用request.getSession(false)。
(2)-悄悄干活的jsp
jsp文件是session创建的一个源头,这里指的不是在jsp文件中用代码或者标签来操作session,这些都是在控制中的。容易忽视或者说根本就 不会意识到的(比如我,就是写jsp三年后才发现的)是,jsp有自动创建session的机制,在jsp页面中,如果没有明确的给出 <% @page session="false"%>,jsp页面会非常乖巧(如果刚好需要)或者说是偷偷摸摸(如果不需要)的自动在生成的java文件中增加一 句: javax.servlet.http.HttpSession session = request.getSession(true)。
为了验证这个说法,我们做以下测试,先来一个最简单的jsp文件,名字也简单a.jsp,放到resin下。a.jsp的内容如下,注意里面有一个<%=1%>,后面会详细解释为什么需要这句话:
- <%@ page session="true"%>
-
-
- test
-
-
- <%=1%>
-
-
<%@ page session="true"%> test <%=1%>
用页面访问一下,然后到resin下webapp目录的WEB-INF/work/_jsp目录下找到_a__jsp.java,打开可以看到
public class _a__jsp extends com.caucho.jsp.JavaPage
有关jsp页面是如何转换为java文件再被编译成class的介绍,请google。看我们关心的public void _jspService()方法:
- public void _jspService(javax.servlet.http.HttpServletRequest request,
- javax.servlet.http.HttpServletResponse response)
- throws java.io.IOException, javax.servlet.ServletException
- {
- javax.servlet.http.HttpSession session = request.getSession(true);
- com.caucho.server.webapp.Application _jsp_application = _caucho_getApplication();
public void _jspService(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { javax.servlet.http.HttpSession session = request.getSession(true); com.caucho.server.webapp.Application _jsp_application = _caucho_getApplication();
可以看到第一行,明确的调用了request.getSession(true),session就是再这里被自动创建的,这里也就是JSP中隐含的session对象的来历。
使用抓包软件,可以看到请求这个jsp页面的http response里面有以下内容:
Set-Cookie: JSESSIONID=abc0zn72YuHtacvaaORBr; path=/
这个是刚才创建的session的jsessionid,被保存到cookie中。
然后继续测试,设置为
<%@ page session="false"%>
打开java文件:
- public void _jspService(javax.servlet.http.HttpServletRequest request,
- javax.servlet.http.HttpServletResponse response)
- throws java.io.IOException, javax.servlet.ServletException
- {
- com.caucho.server.webapp.Application _jsp_application = _caucho_getApplication();
public void _jspService(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { com.caucho.server.webapp.Application _jsp_application = _caucho_getApplication();
没有javax.servlet.http.HttpSession session = request.getSession(true);
这行代码了,同时http response 中没有Set-Cookie: JSESSIONID=***的语句了。
ok,这下清晰了。
再来解释一下为什么要在刚才的jsp文件里面增加<%=1%>这行,我们先做测试,将<%=1%>删除,同样测试<%@ page session="true/false"%>两种情况。可以看到
- public void _jspService(javax.servlet.http.HttpServletRequest request,
- javax.servlet.http.HttpServletResponse response)
- throws java.io.IOException, javax.servlet.ServletException
- {
- javax.servlet.http.HttpSession session = request.getSession(true);
public void _jspService(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { javax.servlet.http.HttpSession session = request.getSession(true);
则不管是否有<%@ page session="true"%>都不自动创建session。考虑删除<%=1%>后的jsp文件内容
- <%@ page session="true"%>
-
-
- test
-
-
-
-
<%@ page session="true"%> test
这个是最简单的纯html页面,估计是resin的实现考虑优化了这点。(resin: 都纯html了,还要session干嘛?)
最后再澄清一点,发现网络上很多人持有一个观点: session在第一次访问时创建。这个明显的是被jsp文件自动创建(默认是true哦)session给误导了,其实只有明确的调用 request.getSession()/request.getSession(true)才会生成session。只是大多数人的jsp页面不会明 确加入<%@ page session="false"%>,也不了解这个机制,造成了错误的理解。
(3)-凑热闹的webwork标签
按照前面的方法,将jsp页面加入<%@ page session="false"%>之后,发现还是有新的session会意外的蹦出来。仔细检查action没有操作session,那这次是谁 干的呢?将目光转回jsp文件,首先来次狠的,将这个jsp文件的内容删空为只有纯html内容。重新运行后发现不会自动生成session,ok,问题 在jsp文件里面了。将原来的内容一点点的加回去,反复测试直到session自动创建的问题再次出现, 哈,发现问题出现在webwork的标签上。
这次出问题的a.jsp代码内容为:
- <%@ page contentType="text/html; charset=UTF-8" %>
- <%@ page session="false" %>
- <%@taglib prefix="ww" uri="webwork" %>
-
- "-//W3C/m/DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
- "'msg'">
- "http://www.w3.org/1999/xhtml">
-
-
-
-
- <%=1%>
-
-
-
<%@ page contentType="text/html; charset=UTF-8" %> <%@ page session="false" %> <%@taglib prefix="ww" uri="webwork" %> <%=1%>
部署到webapp的根目录,在浏览器中直接用http://****/a.jsp访问,通过抓包工具发现有
Set-Cookie: JSESSIONID=abclEpuvWZhHD_UWW7WBr; path=/
将上述文件复制为b.jsp,删除
标签,代码修改为:
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ page session="false" %>
<%@taglib prefix="ww" uri="webwork" %>
- "-//W3C/m/DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
- "http://www.w3.org/1999/xhtml">
-
-
-
-
- <%=1%>
-
-
<%=1%>
再次测试,发现不再创建session,由此可以确认是webwork的标签所为。
继续追踪为什么webwork的标签会如此处理,还是google大法,很快在webwork的2.2文档中发现了这么一段:
http://wiki.javascud.org/display/ww2cndoc/WebWork+2.2+Migration+Notes
"session map wrapper (在ActionContext里建立的) 已经改变了不在为每个请求创建session. 如果你的应用程序依赖session会被自动创建,WebWork 2.2已经不在那样做了.作为替代,你必须自己创建session或者当把一个数据放到session Map里时session会被创建. "
对照了一下我当前测试的版本, webwork2.1.7,看来是这里了。马上上webwork的网站下了最新的2.2.6版本,最快速度搭建了一个测试环境。将刚才的 a.jsp/b.jsp拉过去测试了以下,ok,果然新的2.2版本修改了原来的做法,不再创建session。搞定!
(4)-为什么要关注session的创建
看到有留言,对我如此“执着”的关注session创建很好奇,解释一下吧。
首先是关注性能,前面提到过session的使用是有代价的,需要在保存在服务器端内容中,每次request.getSeesion()方法获取 session时,实际是在服务器段的一个大的hasp结构中以当前的jsessionid为key,获取对应的value HttpSession对象,这个过程是需要消耗cpu的,当然目前hash算法比较好,这里消耗不那么明显。而一般的应用,消耗的cpu远比这个小开销 大出2-3个数量级,因此通常情况不敏感。如果这个session是我们需要使用的,那么付出这些内存和cpu的代码是完全值得的。但是,如果产生大量的 没有任何用处的"垃圾session",对大容量,大并发,需要长期稳定运行的系统会带来很无谓的负载。
注意,我们要讨论是"垃圾session",即是在我们计划外因为某个原因创建,从不使用,完全浪费的session。正常使用的session不在讨论 范围内,虽然也有些比较极端的系统号称不使用session来提高服务器性能,有些对性能比较关注的系统/框架则采用其他的方式来避免使用 session,有兴趣的可以google找资料看。
下面我们来进行一个简单的性能测试,模拟一下比较极端的情况,我在linux下启动resin,只跑两个最简单的jsp文件:
a.jsp不会自动生成session:
- <%@ page contentType="text/html; charset=UTF-8" %>
- <%@ page session="false" %>
-
- "-//W3C/m/DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
- "http://www.w3.org/1999/xhtml">
-
-
-
-
- <%=1%>
-
-
<%@ page contentType="text/html; charset=UTF-8" %> <%@ page session="false" %> <%=1%>
b.jsp基本相同,但是设置为<%@ page session="true" %>,这个将自动增加
HttpSession session = request.getSesson(true)
语句,我们进行最恶劣的假设,每次请求都生成新的session,看看会是什么情况:
测试工具采用loadrunner,部署在我的笔记本上(dell d620机器,intel 双核 2g内存)。测试比较简单,测试方法和过程忽略(loadrunner的使用也不复杂),只给出结果:
测试中两个场景的运行情况相同:100个线程并发,每次只访问一下a.jsp/b.jsp,运行时间2分钟。使用top命令在服务器段看resin的内存消耗和cpu使用情况,这个只能大概估计,不好准确衡量。
补充:第一次测试时忘了设置resin的参数了,后来写了一个sessionLinstener做计数 器, resin的默认没有配置session-max,这个时候resin取的值是默认4096,计数器打印日志发现session总数达到4096后,就会 remove掉已经存在的session以保证不超过4096.下面的测试数据时将session-max设置为4096000后测试的结果
测试1:
a.jsp 不生成session
resin内存消耗始终是在175-177之间,基本没有变化。cpu基本稳定在19%-22%。
测试2:
b.jsp 每次生成session
resin内存消耗从181m逐步增加,到2分钟测试结束时达到283m,大概增加了102M。cpu大体在25%-30%间。
下面可以总结了:
1. 垃圾session会占用内存
上面只测试了2分钟,考虑通常session的超时时间会是30分钟,这意味着这些占据的内存至少要到30分钟之后resin才能判定超时从resin的session hash结构出移除,之后再被jvm回收。
创建的session总数为202941,102M内存/202941次请求= 527字节/session,这个数据比较粗糙,但是还是能大体反映问题。一个垃圾session大概要浪费我们0.5k的内存,时间长达30分钟。
2. 垃圾session的调用消耗了cpu资源
HttpSession session = request.getSesson(true),每次都要new出新的HttpSession对象,然后resin还要给这个HttpSession算 出一个jsessionid,再将jsessionid/session以key/value保存到hash结构中。以后resin检查session超 时的线程每次检查时都要查看每个垃圾session,看是否超时。
3. 垃圾session增加正常获取session的开销
前面提到垃圾session也是要保存hash结构中,request.getSesson()每次都要用jsessionid在这个hash结构中取一 次数据。当垃圾session大量充斥时,获取当正常有用的session的时间也会增加,具体就要看这个hash结构的算法如何了。
当前上述的测试都是建立在最苛刻最恶劣的情况下,大多数情况我们的系统不会这么糟糕,也不是每个系统都对访问量/性能有高要求。如果觉得可以浪费的起,那浪费好了,只是我这边系统的情况不同,我们的产品对性能很敏感,能省就省点。
除考虑性能外, session的创建在我们新设计的系统中,是必须非常严格控制的.这个和我们目前的系统结构,包括部署/用户身份认证有关.简单的说我们的系统是基于 apache + resin的多机分布, 各个功能模块是作为不同的webapp发布的,session的生成和jsessionid的传递必须可控,因此我必须严格掌控系统中session的生 成情况。
另外说一点个人意见,知不知道有这些session自动创建的情况,和决定是否要在自己的代码中严格控制session创建,不是同一个概念。完全可以在 了解情况后,根据自己的实际需要和个人习惯去做决定,比如选择无视。但是如果不知道呢?呵呵,很惭愧,在这次探索之前,我对jsp/webwork标签自 动生成session是没有概念的,我们的原有系统是不使用HttpSession的(和当时的分布式方案有关),但是现在看来,由于 jsp/webwork标签的存在,其实每次都有session被创建,这些session也就成了我上面说的垃圾session: 在不需要创建时意外创建,从来不使用,除了浪费资源外没有其他存在价值。
整理一下留言中的内容,感谢大家的关注:
疑问:
我觉得系统的瓶颈不会出在session生成上 ,花时间在后台缓存和sql优化、架构调整这些事情上来的实惠.
回答:
滴水的水龙头要不要拧紧的问题?
没有哪个家庭的财政会因为那个滴水的水龙头造成收支失衡以致破产,那是否就意味着可以无视它的存在,只管专心挣钱,再每个月少吃一次大餐?后台缓存和 sql优化、架构调整,这些当然是更重要的,但不意味着其他东西就可以完全忽略。我无意强调session的这些细节的高度,只是希望告诉大家,在某些角 落里,有些我们没有意识到的小水龙头,在一滴一滴的浪费资源。
后台缓存和sql优化、架构调整,到处可以找到资料。可是我google了一圈,上述session的几个问题根本没有成文的全面一点的资料可以参考,我相信jsp世界中肯定还有很多很多类似的小水龙头在悄悄地滴水,而主人们根本没有看见。
ps: 这个系列的文章最初发在blogjava,现在复制到javaeye(担心哪天某家blog不在了丢失文章),感觉这个话题很少看到有人谈起,我斗胆发到论坛上来看看,是否有人和我持有相同的看法。