大型互联网应用的技术要求是高并fa、高可用、高性能、大容量,这就要求系统具有很好的可伸缩性,实现方式:Scale up 和 Scale out
通过采用计算速度更快的CPU、更大的内存、配置更好更贵的机器来支撑系统的可伸缩性不是一个很好的办法,单台机器性能终归有限,而且高端机器价格不菲,这就是所谓的Scale up。
另一种方式就是采用多台普通机器,将它们有效集群起来,让每台机器参与贡献一小部分力量,那么整体的处理能力和性能往往轻松超过昂贵的大型机。并且这种集群方式理论上是无上限的,可以根据需要水平方向任意加减机器,系统的可伸缩性得到很好保证,这就是所谓的Scale out。
如何设计Scale out?实现方式:
Scale out是大型互联网应用架构的基本准则,前面介绍的Redis sharding技术,就是典型的Scale out。通过集群多个Redis实例,来完成单Redis实例无法完成的大规模处理需求。那么在逻辑应用层面,我们如何设计从而保证这种Scale out呢?大家知道,虽然Http是无状态的,但web应用通常是有状态的,前面介绍的SSO也大量地谈到了这一问题。这种有状态,其实就是浏览器客户端和应用服务端维持的一种会话,这种会话,基本的实现方式就是会话标识保存在浏览器Cookie中,对Java web应用来说,就是这个缺省名叫jsessionid的cookie。会话本身保存在服务端,一般Java应用服务器提供这项基础服务,即Session服务。应用服务器的Session是基于JVM内存实现的,这就带来了一个问题:运行在其上的web应用有状态会话,也是基于JVM内存的。那么,当规模上来,需要部署多个点,即多台机器部署同样应用共同分担压力时,这种有状态web应用支持么?即支持Scale out么?只能这么说,有条件地支持!应用状态保存在本应用节点上,当后续同一会话的Http请求到来时,它必须落在同样的应用节点上,落到其它应用节点上,由于访问不到这个JVM中的会话,状态丢失。
1.前置负载均衡器的Session sticky
这就要求,来自浏览器客户端的请求,一旦访问了某个应用节点,它的后续访问也必须同样是这个节点,不能是集群中的其它节点。这对前置的负载均衡器(有软、硬之分,软的如nginx负载均衡插件)有一定要求,即要设置成会话粘滞模式(session sticky)。Session sticky,某种程度上解决了应用节点的线性可伸缩。但它也有自身不足。首先,要实现合适的负载均衡算法,既要高效不影响性能,有要保证会话均衡灵活分布。其次,大型互联网应用往往有高可用需求,当某一应用节点失效时,前端的负载均衡器虽然可以判断出该节点失效,实施失效转移,把后续请求转移到其它节点上,可会话状态无法转移了,跑在这一节点上的会话将全部失效。
2.有状态分离,将应用设计成无状态的
有没有更好的办法?这就涉及到大型互联网应用架构的一个要点:有状态分离,将应用设计成无状态的。
2.1 无状态应用,意味着来自同一会话的http请求,可以在任何一个应用节点处理,集群中的任何应用节点,都是无差别的。即使某应用节点失效了,请求仍可在其它节点得到处理。这就好比乘客到车站购票,某个窗口售票员有其它事情关闭窗口暂停售票,乘客可到其它窗口继续购票一样,系统高并fa,高可用性得到很好满足。
2.2有状态分离,分离到哪里呢?可以把它分离到session服务器中,由session服务器专门管理应用的状态。
利用Redis来实现Session状态服务器有几个显著优点:
1.session的结构是name-value属性名称值对,即Map结构,而Redis支持Map数据类型,正好匹配,这样可以直接操作session中的属性,不需要将整个session取出,粒度细,效率高。
2.session都有有效期控制,这是因为服务端无法判断准确客户端是否在线,当在一定时间内session没有被访问时即认为用户已经离kai,即我们要给session一个生存有效期,有效期过, session自动销毁。Redis的键值本身就支持有效期特性,实现起来很方便。
3.既然session服务器是用来解决大规模应用的高并fa、高可用的,那它本身也得支持应用的高并fa高可用,即session服务器应该是线性可伸缩的,而前面文章我们介绍的Redis sharding,很好地满足这一需求。同时,Redis基于内存,很好地满足了系统的高性能需求。
下面就简要介绍下基于Redis的Session服务器实现基本原理。首先,我们看下主体设计图:
由图可知,Session服务器对外提供了一个接口,叫SessionManager。SessionManager处于web接入层,负责浏览器客户端会话标识与服务端会话之间关系维护,是应用使用Session功能的入口。Session服务的内部实现对应用来说,是看不见的。SessionManager提供了往会话中设置属性名称值对、根据属性名返回其属性值以及销毁整个会话等方法。
SessionManagerImpl是SessionManager的实现,SessionManagerImpl重要处理cookie中session标识与服务端session的对应关系,
而session内容的具体存储管理交由另一个组件SessionRegistry负责。
SessionManagerImpl核心代码如下:
/** * 1.往session中添加属性 * 2.并更新session服务器中的session * 3.并更新会话的Cookie */ @Override public void setAttribute(String name, Object value,HttpServletRequest request, HttpServletResponse response) { //1.往session中添加属性 Cookie[] cookies = request.getCookies(); if(null!=cookies){ for(Cookie cookie : cookies){ if(cookie.getName().equals(SessionManager.SESSION_ID)){//1.根据sessionid确定存放session标识的cookie if(sessionRegistry.isValid(cookie.getValue())){ //2.通过session标识去验证是否在session服务器注册 logger.debug("sessionId key {} exists and is valid.",cookie.getValue()); sessionRegistry.setAttribute(cookie.getValue(),name,value); //3.往session中添加属性 return; } break; } } } //2.在Session服务器中新建Session,覆盖旧的Session String sessionId = sessionIdGenerator.getId(); //生成新的session标识 logger.debug("setting a new cookie for sessionId {}.", sessionId); sessionRegistry.setAttribute(sessionId, name, value); //3.新建Cookie,覆盖旧的Cookie Cookie newCookie = new Cookie(SessionManager.SESSION_ID,sessionId); newCookie.setDomain(domain); newCookie.setPath(path); response.addCookie(newCookie); } /** * 获取session中指定键的值 */ @Override public Object getAttribute(String name, HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if(null!=cookies){ for(Cookie cookie : cookies){ if(cookie.getName().equals(SessionManager.SESSION_ID)){ //根据sessionid到Cookie中寻找session标识 return sessionRegistry.getAttribute(cookie.getValue(), name);//根据session标识和键,到session服务器中寻找对应的值 } } } //没找到会话标识 logger.debug("not found.the session does not exist.", name); return null; } /** * 销毁session: * 1.销毁session服务器中的session本身 * 2.销毁Cookie中的session标识 */ @Override public void destroySession(HttpServletRequest request, HttpServletResponse response) { Cookie[] cookies = request.getCookies(); if(null==cookies) return; for(Cookie cookie : cookies){ if(cookie.getName().equals(SessionManager.SESSION_ID)){ //1.销毁session服务器中的session本身 sessionRegistry.destroySession(cookie.getValue()); logger.debug("deleting the sessionId cookie.", SessionManager.SESSION_ID); //2.销毁Cookie中的session标识 //销毁Cookie的方式,就通过设置Cookie生存时间为0 cookie.setDomain(domain); cookie.setPath(path); cookie.setMaxAge(0); response.addCookie(cookie); return; } } }
RedisSessionRegistry是SessionRegistry的具体实现,基于Jedis客户端访问Redis,其核心代码如下:
/** * 根据session标识覆盖对应的session,并往session中添加属性 */ @Override public void setAttribute(String sessionId, String name, Object value) { logger.debug("setting session key {} 's field {} with value {}", sessionId, name, value); Jedis jedis = jedisPool.getResource(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = null; try{ oos = new ObjectOutputStream(bos); oos.writeObject(value); }catch(IOException e){ logger.error("serializing value {} error.", value); }finally{ try{ if(null!=oos) oos.close(); }catch(IOException e){ logger.error("closing error when serializing value {} to redis.", value); } } jedis.hset(sessionId.getBytes(), name.getBytes(), bos.toByteArray()); refreshSession(sessionId); jedis.close(); } /** * 获取属性 */ @Override public String getAttribute(String sessionId, String name) { Jedis jedis = jedisPool.getResource(); byte[] value = jedis.hget(sessionId.getBytes(), name.getBytes()); //获取属性的核心方法 if (null==value){ logger.error("the field {} is not found or the key {} does not exist. ", name, sessionId); return null; } try { ByteArrayInputStream bais = new ByteArrayInputStream(value); ObjectInputStream ois = null; ois = new ObjectInputStream(bais); String s = (String)ois.readObject(); refreshSession(sessionId); return s; }catch (final Exception e) { logger.error("Failed deserializing {}. ", value, e); }finally{ jedis.close(); } return null; } /** * session服务器中是否存在指定的会话 */ @Override public boolean isValid(String sessionId) { logger.debug("validating if the sessionId key {} exists . ", sessionId); boolean result = false; Jedis jedis = jedisPool.getResource(); if(jedis.exists(sessionId.getBytes())) result = true; jedis.close(); return result; } /** * 刷新session,目的在于获取最新的session */ @Override public void refreshSession(String sessionId) { logger.debug("refreshing the sessionId key {} . ", sessionId); Jedis jedis = jedisPool.getResource(); jedis.expire(sessionId.getBytes(), timeout); jedis.close(); } /** * 销毁session服务器中的session本身 */ @Override public void destroySession(String sessionId) { logger.debug("destroying the sessionId key {} . ", sessionId); Jedis jedis = jedisPool.getResource(); jedis.del(sessionId.getBytes()); jedis.close(); }