Skip to main content

【转载】微服务架构下的分布式Session管理

大家好,很高兴在这里和大家进行微课堂的分享,今天进行分享的主题是《微服务架构下的分布式Session管理》。

主要大纲:

一、应用架构变迁下的Session管理

二、微服务架构下的Session管理

三、Session管理实践分享

一、应用架构变迁下的Session管理

Session一词直译为“会话”,意指有始有终的一系列动作/消息。Session是Web应用蓬勃发展的产物之一,在Web应用中隐含有“面向连接”和“状态保持”两个含义,同时也指代了Web服务器与客户端之间进行状态保持的解决方案。

在Web应用诞生之初,应用服务器与浏览器之间仅仅只是基于HTTP协议进行通信。而HTTP协议是无状态的,也就是说每一个请求之间都是相互独立的,互不关联。但是随着应用业务复杂化,服务器需要按照用户的一系列业务操作向用户提供某些特定的、按需的内容。这时候就需要通过保存用户状态,将用户的请求关联起来。Session管理正是这一问题的解决方案。

早期的Web应用基本都采用的是单体架构,也就是把一个使用了分层架构的Web应用部署在单节点Web服务器上的架构类型。在这种架构中,虽然采用了分层架构,将整个应用分为了表现层、业务逻辑层和数据访问层,每一层各司其职,让Web应用的各个方面都有所改善。但这样的分层只是停留于逻辑上。由于将所有代码部署在单个服务器节点上,随着应用不断迭代开发,单体应用将会发展成巨石型应用,臃肿不堪,难以维护。

在这样的单体架构中,由于所有的用户请求都是由这个唯一的服务器进行响应处理,所以只要把保存了用户信息和状态的Session对象,存放在应用服务器内存里,就能轻松地达到保持用户状态的目的。

随着Web应用的发展,用户访问量和业务复杂度与日俱增,应用的性能和代码的维护难度成为应用的瓶颈,为了突破瓶颈,开发者开始尝试在应用架构中引入负载均衡器,继而演化出了集群和分布式两种架构类型。

  • 集群,是指在多个服务器节点上部署相同的应用,例如上图中的服务器B和C,然后通过负载均衡器的分发功能,把用户请求分发到集群中的任意一个服务器节点上。如果有更大的访问量,只要向集群中增加服务器节点,就能改善压力。集群既能保证应用的高可用,又能提高应用的负载能力。
  • 分布式,是把原来的单体架构应用,通过分而治之的手段,按照业务功能,切分成一些小的模块应用,部署在不同服务器节点上,例如上图中的服务器A和B。然后通过负载均衡器和门户应用整合起来,组成一个完整的应用。

集群和分布式架构中,后端包含了多个服务器节点。当用户进行登录时,登录请求是由其中一个服务器节点进行响应,而后续的用户请求将可能被负载均衡器分发到其他服务器节点,这时候就可能因为这个服务器节点上没有用户Session,导致服务器判定用户是未登录状态,让用户重新登录。

所以,在集群和分布式架构中,必须保证用户进行登陆后,架构中的所有服务器节点都能共享Session数据。常用的Session管理方案有如下3种:

1、将Session对象保存在Cookie,然后存放在浏览器端。每次浏览器向服务器发送请求的时候,都会把整个Session对象放在请求里一起发送到服务器,以此来实现Session共享。这样的方案实现起来特别方便,但是由于Cookie的存储容量比较小,所以这个方案只适用于Session数据量小的场景。

2、Session复制。部分应用服务器能够支持Session复制功能,例如Tomcat。用户可以通过修改配置文件,让应用服务器进行Session复制,保持每一个服务节点的Session数据达到一致。但是这个方案的实现依赖于应用服务器。当应用被大量用户访问时,每个服务器都需要有一部分内存用来存放Session,同时因为大量Session通过网路传输进行复制,将会占用网络资源,还可能因为网络延迟导致程序异常。

3、Session粘滞。利用负载均衡器的分发能力,将同一浏览器上同一用户的请求,都定向发送到固定服务器上,让这个服务器处理该用户的所有请求,这样只要这个服务器上保存了用户Session,就能保证用户的状态一致性。但是这个方案依赖于负载均衡器,而且只适用于横向扩展的集群场景,不能满足分布式场景。

二、微服务架构下的Session管理

微服务架构是将一个应用拆分成一套小而相互关联的微服务,微服务之间通过暴露出来的API被其他微服务或系统所调用。在运行时 ,每个微服务实例通常是一个云虚拟机或者一个Docker。众多微服务综合起来,构成了一个完整的微服务架构应用。

微服务架构中的微服务一般可以分为两类:无状态服务和有状态服务。无状态服务比如应用服务器提供的公共服务,通常不保存数据,方便进行横向扩展。有状态服务则需要进行数据存储,例如数据库服务和缓存服务。在Web应用中,Session用来存储用户状态信息,所以Session管理也是有状态服务的一种。

微服务架构可以近似地看做是一个大型的分布式架构系统,但是与分布式架构相比,微服务架构让应用模块的划分更为精细,每个微应用大小合适,方便进行维护和管理。通过使用devops平台,可以让微服务模块的开发部署变得很敏捷

当应用由十几个甚至数十个部署在不同节点上的微服务组成时,如果还是沿用分布式架构中的Session管理方案,将会让Session管理的复杂度直线上升,同时很难保证整个架构中Session数据的一致性。

因此,在微服务架构下的Session管理,我们应该另辟蹊径,不再将Session对象保存在服务器的内存里,而是在应用架构中引入独立的中间存储介质,将整个应用架构中的Session对象进行统一管理。

个人认为,一个好的Session集中管理方案,应该具备以下4个特点:

  • 中间存储介质的读写速度要快。因为使用了Session集中管理后,将会在Session的读写操作中引入网络传输,与保存在服务器本地内存相比,读写速率会有所下降,所以必须保证中间存储介质的读写速度。
  • 中间存储介质要高可用。整个应用架构中的Session对象都将被保存在中间存储介质中,如果不能保证存储介质的高可用,整个应用都将变得不稳定。
  • Session管理方案要保证对用户的透明性,切换成集中管理后 ,对用户的使用应该是不会与影响的,用户使用的时候对方案改变无感知。
  • 管理方案不该和某一应用服务器耦合,应该适用于所有常规应用服务器。

从上述特点可以看出,Session集中管理的技术选型,应该从Session存储介质和管理方案实现两方面进行考虑。

三、Session管理实践分享

因为Session存储介质需要有较高的读写性能,所以应该选择缓存服务器作为存储介质。我们在进行微服务Session管理实践的时候,对目前业内常用的两种缓存服务器,Redis和Memcache,进行了对比,准备在两者之中选一个合适的缓存服务器来进行Session存储。

从表中的数据可能看出,虽然Redis的读写性能稍弱于Memcache,但是Redis支持的数据类型较多,而且支持数据的持久化。

此外,Memcache使用Slab Allocation机制进行内存管理,通过将内存分隔成特定长度块来解决内存碎片问题。同时Memcache采用最近最少使用(LRU)算法对slab进行内存回收。这就意味着,如果所有Session对象的大小大致相同,将会被Memcache存放在大小合适的slab中进行存储,一但这些slab被存满,再有新的数据进来,里面的存放的时间相对较早的Session对象就会被清除,导致用户变成未登录状态。

对比之后发现Redis更加适合用于Session存储。而且Redis3.0之后,提供了良好的主从复制和集群能力,能够很好地保证Session存储的高可用。Redis还提供了数据失效和订阅通知的能力,能为Session共享提供良好的支撑。

在Session集中管理方案的实现上,目前常用的方案有两种,一种是Memcache-Tomcat-Session,另一种是Spring Session。

Memcache-Tomcat-Session是一个基于Memcache和Tomcat实现Session集中管理的开源方案,通过修改Tomcat的server.xml文件,使用扩展的SessionManager替换Tomcat原有的Session管理器来实现集中管理。这个方案实现起来比较简单,但是与Tomcat耦合,不适用于其他应用服务器。

Spring Session是Spring提供的一套Session管理方案,通过一个SessionFilter将所有访问应用的请求都拦截下来,然后使用Request包装类接管Session管理。SpringSession不与应用服务器耦合,能适用于常规服务器。同时还提供了在浏览器下对同一应用存储多个Session等功能。

SpringSession优点颇多,所以开始的时候我们曾尝试将SpringSession集成到公司产品中来,进行Session集中管理,但在集成过程中考虑到以下几点,最终放弃了集成。

  • SpringSession功能丰富,但我们并不是都需要。
  • SpringSession的实现代码量较多,还需要配合Spring-data-redis进行使用。如果后续要对Session管理的代码进行修改维护,需要把SpringSession的代码都梳理一遍,学习维护成本比较高。

所以最终我们决定使用Redis作为Session的存储介质,然后参考SpringSession的实现理念,自己设计开发一套轻量级的Session集中管理实现。

同样使用SessionFilter进行用户请求拦截,然后通过Request包装类接管应用服务器的Session管理。在Request包装类中重写getSession方法。让用户在进行Session读写的时候去Redis中进行操作,而且使用Session的方法与过去一样,使得管理方案对用户透明。

在Session共享模块与Redis之间,基于jedis开发了一个分布式缓存SDK,用于进行通信,同时SDK可以提供扩展性,后续如果需要支持其他的缓存服务器,只要对SDK进行扩展开发即可。

然后为了保证Session集中管理方案的高可用,将会搭建Redis集群来存储Session对象。

这是一个推荐客户搭建的最小Redis集群,集群中共有4个Redis节点,两主两从。两个Master节点用来对Session数据进行分片存储,而Slaver节点用来对Matser进行数据备份和读写分离。

总结

maven filter和resource

1. 利用filter实现对资源文件(resouces)过滤


maven filter可利用指定的xxx.properties中对应的key=value对资源文件中的${key}进行替换,最终把你的资源文件中的username=${key}替换成username=val

       <!-- 过滤器,用于过滤resource中的各个文件 --> 
        <filters>
            <filter>src/main/resources/filtersTest.properties</filter>
        </filters>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <!-- 是否使用过滤器 -->
                <filtering>true</filtering>
            </resource>
        </resources>

2. 利用profile来切换环境

<profiles>
    <profile>
        <id>dev</id>

        <!-- 默认激活开发配制,使用config-dev.properties来替换设置过虑的资源文件中的${key} -->
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <build>
            <filters>
                <filter>config-dev.properties</filter>
            </filters>
        </build>
    </profile>
    <profile>
        <id>test</id>
        <build>
            <filters>
                <filter>config-dev.properties</filter>
            </filters>
        </build>
    </profile>
    <profile>
        <id>product</id>
        <build>
            <filters>
                <filter>config-product.properties</filter>
            </filters>
        </build>
    </profile>
</profiles>

3. 命令


maven clean install -Ptest — 激活id=”test”的profile

如何在android虚拟机上执行java jar文件

大家都知道,andriod apk大部分代码都是java代码,但是apk并不是运行在java虚拟机,而是运行在DVM上,而且编译好的java 代码也不能直接运行在DVM上。接下来就简单说下怎么运行jar在andriod手机上。

1.DVM & JVM

Dalvik虚拟机是Google等厂商合作开发的Android移动设备平台的核心组成部分之一。它可以支持已转换为.dex(即Dalvik Executable)格式的Java应用程序的运行,.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统.

Java虚拟机,大家可以google,很多详细的资料。

2.如何在andriod手机上运行jar

# 编辑java源文件
javac -cp src -d build src/Hello.java

# 在jvm上测试
java -cp build Hello

#通过dx工具把java class转化为dex文件
dx –dex –output=hello.jar Hello.class
cd ..

# 在andriod虚拟机上执行
dalvikvm -cp build/hello.jar Hello

如何快速地把页面另存为pdf

最近在设计企业风险报告的产品,我们就希望可以直接把页面保存为pdf文档,这样方便客户保存和分享。

常规的做法有以下几种:

1. 通过浏览器的打印,另存为pdf

这个方法,自己使用很方便,但是有个局限就是:不是所有的页面都能保存为pdf,尤其是动态的页面。具体怎么操作,可以baidu,很多这样的文章。

2. 装一个chrome 插件

https://chrome.google.com/webstore/detail/take-webpage-screenshots/mcbpblocgmgfnpjjppndjkmgjaogfceg
fireshot插件

这个插件可以很方便地截取整个页面或者你想选择的区域

3. 通过pdf库生成页面所有的组件

这个方法需要写很多代码,尤其如果页面有图表的时候,需要花很多的时间去维护和更新。

4. html2canvas

利用html2canvas库,可以很方便地把dom元素保存为base64编码的图片,在后台,利用pdf库把图片转化为pdf
优势:html2canvas可以把后台不容易处理的组件用图片来代替,后台可以专门处理常规的展示,比如表格,文字等等。

最后上代码:
html2canvas 代码
[code]
function exportAsPdf() {
console.log("export as pdf")
let imageBase64 = []
html2Canvas(document.querySelector("#fullPage")).then(canvas => {
let imgData = canvas.toDataURL(‘image/png’)
imageBase64.push(imgData)
}).then(event => {
dispatch({
type: "dashboard/exportAsPdf",
payload: {
name: dashboard.companylaw.name,
imageBase64: imageBase64,
}
})
});
};
[/code]

java生成pdf代码
[java]
private boolean generatePdf(String title, String[] imagesPath, File outputFile) {
try {
PDDocument doc = new PDDocument();
for (int i = 0; i < imagesPath.length; i++) {
String image = imagesPath[i];
File imageFile = new File(image);
if (!imageFile.exists()) {
log.warn("Image file:{} can’t be found", image);
continue;
}

BufferedImage bufferedImage = ImageIO.read(imageFile);
int width = PDF_PADDING * 2 + bufferedImage.getWidth();
int height = PDF_PADDING * 2 + bufferedImage.getHeight();
int imageY = 0;
if (i == 0) {
height += PDF_TITLE_HEIGHT;
imageY += PDF_TITLE_HEIGHT;
}

PDPage page = new PDPage(new PDRectangle(width, height));
PDPageContentStream contents = new PDPageContentStream(doc, page);

if (i == 0) {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("fonts/simhei.ttf");
PDFont formFont = PDType0Font.load(doc, inputStream);
contents.beginText();
contents.newLineAtOffset(15, height – imageY);
contents.setFont(formFont, 30);
contents.showText(title);
contents.endText();
}
PDImageXObject pdImage = PDImageXObject.createFromFileByContent(imageFile, doc);
contents.drawImage(pdImage, PDF_PADDING, PDF_PADDING);
contents.close();
doc.addPage(page);
}
doc.save(outputFile);
doc.close();
log.info("Generate pdf file:{}", outputFile);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
}

return false;
}
[/java]

参考资源
html2canvas