1. 摘要
本文讲解在多台服务器进行负载均衡时,实现跨机器的session同步的方法和实践。
2. 负载均衡并实现session共享的方法
在项目实践中,有时我们需要多台服务器进行负载,以扩展服务器的宽带、增加吞吐量和提高网络数据的处理能力,从而提高用户的体验感,保证项目的质量。当一个项目部署在多台服务器上,我们习惯于使用nginx做负载均衡,这样同一个IP访问项目的时候会被自动分配到不同的服务器上; 但是,如果多台服务器的session不同步的话,则会导致很多问题,比如我们的登录状态、用户信息、数字字典等都会归零,都需要重新登录之后才能获取到,这样给用户的体验感就会很差,所以在多台服务器进行负载均衡的时候我们就得要考虑到多台服务器之间的session同步了。
2.1 使用客户端的cookie作为存放登录信息的媒介
cookie是将用户登录信息存储在用户终端的数据载体,与session的最大区别就是,session是存储在服务器端的;所以这就很容易解决这种session的多台服务器共享问题。当我们客户端进行登录的时候,访问的是服务器a,登录成功之后我们将session抽取出来存放在客户端的cookie里面;然后当我们客户端第二次进行访问的时候,访问的是服务器b,这次我们先在服务器b去查找是否有登录成功的session,如果为空,我们再对客户端的cookie进行查找,如果cookie里面已经存储有session,那么再将cookie里面的session同步到服务器b,那么整个流程就能走通了,用户也不用再次登录;
- 优点:这种方法实现起来简单,方便,很容易上手操作,不会加大数据库的负担;
- 缺点:如果客户端把cookie禁掉了的话,那么session就无法同步了,而且cookie的安全性不高,很容易外部被伪造使用;
2.2 使用mysql数据库存储session
既然每个服务器都需要使用同一个session,那么我们可以将session存放在同一个数据库里面,每次访问的时候,我们去数据库check一下是否有这个session或者这个session是否过期,然后就可以进行多台服务器的session同步了;
- 优点:使用这种方法简单、方便,很容易上手操作;
- 缺点:使用数据库来同步session,会加大数据库的IO,增加数据库的负担;同时,每次访问都需要拦截请求、查询数据库,导致多一层访问的业务层以及浪费读取数据库session时间;
2.3 使用memcache或者redis等缓存机制存放session
使用memcache或者redis等分布式缓存机制存放session数据,是现在很多大型项目负载均衡同步session的热门方案;它的原理是项目都使用的是同一个地方的memcache或者redis的缓存,当用户登录的时候,会把session存放在缓存里面,之后不管访问的是项目的那一台服务器,都会从同一个地方去获取session缓存,这样就很轻松实现了session同步;
- 优点:用缓存来同步session,不会加大数据库的负担,也不用手动去判断session是否存在或过期,省去部分业务逻辑,同时,由于redis等缓存是存放于服务器端,安全性也大大提高;
- 缺点:memcache或redis把内存分成很多种规格的存储块,有块就有大小,这种方式也就决定了,memcache或redis不能完全利用内存,会产生内存碎片,如果存储块不足,还会产生内存溢出。
3.4 ip_hash
nginx中的ip_hash技术能够将某个ip的请求固定到同一台后端应用服务器,这样一来这个ip下的某个客户端和某个后端就能建立起稳固的session,ip_hash是在upstream配置中定义的:
代码语言:javascript复制upstream backend {
server 127.0.0.1:8001;
server 127.0.0.1:8002;
ip_hash;
}
- 优点:ip_hash算法可以把一个ip映射到一台服务器上,这样可以解决session同步的问题。这样每个访客固定访问一个后端服务器,可以解决session的问题;
- 缺点:使用ip_hash进行session共享,它的原理是为每个访问者提供一个固定的访问ip,让用户只能在当前访问的服务器上进行操作,保持了session同步的,但是也造成了负载不均衡的问题,如果当前用户访问的服务器挂了的话,那就会出现问题了;
3. 基于Nginx解决前端访问后端服务跨域问题(Session和cookie无效)实践
3.1 问题背景
这两天遇到一个这样的需求: 有两个项目a和b,在a项目中有页面需要调用b中的接口,两项目的域名不同,分别为a.com, b.com。这时候如果直接调用,显然跨域了。一番折腾之后,问题解决了,这里记录一下解决方法。
3.2 解决方法
第一步,解决跨域
这个使用Nginx的代理功能即可,在a服务器的Nginx添加如下示例配置:
代码语言:javascript复制location ~ /xxx/ {
proxy_pass http://b.com;
}
这样就把路径中带有/xxx/的请求都转到了b.com。如果不需要保存cookie,保持session这样的功能,这样就可以了。
然而,本项目就是要用到cookie,所以就有了下边的内容。
第二步,设置domain
因为cookie当中是有domain的,两个服务器的一般不同,比如a服务器返回的Response Headers中是
Set-Cookie:JSESSIONID=_3y4u02v4cbpBw10DoCrMSnjg7m34xuum1XRWBF1Uno; path=/; domain=a.com
而b服务器返回的是
Set-Cookie:JSESSIONID=_3y4u02v4cbpBw10DoCrMSnjg7m34xuum1XRWBF1Uno; path=/; domain=b.com
这时候如果a项目的页面调用了b的接口,浏览器发现接口返回的domain不是a.com,就不会把cookie保存起来,session也就失效了。Nginx 引入了proxy_cookie_domain来解决这个问题。示例:
代码语言:javascript复制location ~ /xxx/ {
proxy_cookie_domain b.com a.com;
proxy_pass http://b.com;
}
这样就可以在Nginx转接请求的时候自动把domain中的b.com 转换成 a.com,这样cookie就可以设置成功了。
但是,对于有些情况这样转换不灵光。比如,b项目的domain是.b.com,前边多了一个小点,那对应的改为 proxy_cookie_domain .b.com a.com; 可以不?通过实践,不行!!!
通过查看Nginx文档,找到了解决办法。其实,除了上边那种配置方式外,Nginx还支持正则配置:
代码语言:javascript复制location ~ /xxx/ {
proxy_cookie_domain ~.?b.com a.com;
proxy_pass http://b.com;
}
这样就可以把domain中的.b.com 转换成 a.com啦。
第三步,设置path
正常情况下完成以上两步就可以了,因为cookie中的path一般默认的是path=/,也就是所有请求都可以访问本cookie。但有些服务器会指定,只允许某个层级下的请求可以访问cookie,比如:
Set-Cookie:JSESSIONID=_3y4u02v4cbpBw10DoCrMSnjg7m34xuum1XRWBF1Uno; path=/sub/; domain=b.com
这样就只允许相对根路径,以/sub/开头的请求路径才能访问cookie。这时候就又可能出现cookie无效的问题了,为了解决这个问题,可以使用proxy_cookie_path。示例:
代码语言:javascript复制location ~ /xxx/ {
proxy_cookie_domain ~.?b.com a.com;
proxy_cookie_path /sub/ /;
proxy_pass http://b.com;
}
这样就把只允许/sub/层级下的请求访问cookie,改为允许所有请求访问cookie了。
4. proxy_cookie_domain误区
Nginx做反向代理的时候,我们一般习惯添加proxy_cookie_domain配置,来做cookie的域名转换,比如
代码语言:javascript复制location /api {
proxy_pass https://b.test.com;
proxy_cookie_domain b.test.com a.test.com;
}
在之前的博客中我也是这么写的,但是最近在项目中发现,不配置这个属性,依然运转正常。这又是为什么呢?
我们知道response在写set-cookie的时候,domain是一个可选项,并不是必填项,所以经常能看到如下这种情况
这个时候由于set-cookie本身就没有domain内容,proxy_cookie_domain也就不没有必要了,这也是为什么在部分项目中不配置proxy_cookie_domain依然正常的原因。但是对于一些设置了domain的项目,比如
这种情况下当你用nginx做反向代理的时候,就必须要转换一下了。
误区回溯
说到这里,我们再看看之前的错误理解:
“proxy_cookie_domain的作用是实现前后端cookie域名转换,保证顺利传递”
乍一看好像也没错,但是现在想想,理解还是不够啊,因为proxy_cookie_domain的作用是单向的,并不是双向转换的。我们先看下cookie的传递过程,上图:
浏览器在发送请求的时候,会在request header中带上cookie项(有内容的话),此时的cookie是一个字符串,一个key=value并用分号分割的字符串,
其中并不包含任何域名信息。这是因为浏览器在设置cookie选项的时候,所选取的内容都是缓存中接口域名下的。然后request的只要请求发送出去之后,cookie中有关domain信息其实是不存在的,它只是一个普通的字符串,随便proxy_pass到任何位置,都会正常携带下去。因此在前端到后端的request的过程中,proxy_cookie_domain是没用的
而server端在做响应的时候,通过set-cookie的domain属性,可以控制cookie的生效域名目标,做到诸如二级域名cookie分离等等,如果前端接收到的set-cookie的domain和当前域名不一致,或者一级域名不一致(二级域名可以共享一级域名下的cookie),这个cookie在后续的通信中就是无效的,所以这里才需要去做domain的转换,也就是说response中set-cookie的domain转换才是有意义的,这也正是proxy_cookie_domain的作用所在。 当reseponse的set-cookie中domain不去设置时,cookie顺利传入浏览器中,浏览器会自动设置这个cookie的生效域名为当前域名。
和这个类似的还有proxy_cookie_path属性,同样的该属性仅作用在修改response set-cookie的path属性,而一般情况下,用的也比较少。
5. 参考
(1)Nginx搭建负载均衡Session共享 https://blog.csdn.net/niuxitong/article/details/90006650
(2)Nginx系列教程(10)基于Nginx解决前端访问后端服务跨域问题(Session和cookie无效) https://blog.csdn.net/junyouyh/article/details/105832963
(3)Nginx系列教程(9)nginx 解决session一致性 ?
(4)Nginx负载均衡,同时实现session共享