点击上方“icloud布道师”,“星标或置顶公众号”
逆境前行,能帮你的只有自己


************************************
本文制作需要3小时,阅读需要10分钟
Session管理Nginx
Session
Cookie
NGINX的缺陷
例如,有如下架构需求

具体要求:
Nginx作为反向代理,将用户流量分发到后端服务器
Grafana登陆要有认证的界面,用户操作页面不需要重复输入密码
在实际的测试中,将后端服务器做轮询分发时,会发生什么?
为什么会一直刷新出登陆界面?
为了排除是后端的Grafana服务器的问题,我们直接访问Grafana
由此,我们可以确定是Nginx负载均衡的锅:Nginx做负载均衡,会造成session的丢失!
Session简述
什么是session:
Session
的官方定义是:在计算机中,尤其是在网络应用中,称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。
说白了Session
就是一种可以维持服务器端的数据存储技术。session主要有以下的这些特点:
session保存的位置是在服务器端
session一般来说是要配合cookie使用,如果是浏览器禁用了cookie功能,也就只能够使用URL重写来实现session存储的功能
单纯的使用session来维持用户状态的话,那么当同时登录的用户数量较多的时候,或者存在较多的数量的session会导致查询慢的问题
本质上:Session
技术就是一种基于后端有别于数据库的临时存储数据的技术
Note: 需要注意的是,一个Session的概念需要包括特定的客户端,特定的服务器端以及不中断的操作时间。A用户和C服务器建立连接时所处的Session同B用户和C服务器建立连接时所处的Session是两个不同的Session。
这也是前面视频的问题的关键所在
Session之前,我们先了解下为什么会出现session会话,它出现的机理是什么?我们知道,我们用浏览器打开一个网页,用到的是HTTP协议,学过计算机的应该都知道这个协议,它是无状态的,什么是无状态呢?就是说这一次请求和上一次请求是没有任何关系的,互不认识的,没有关联的。但是这种无状态的的好处是快速。
Note: HTTP协议本身是无状态的,这与HTTP协议本来的目的是相符的,客户端只需要简单的向服务器请求下载某些文件,无论是客户端还是服务器都没有必要记录彼此过去的行为,每一次请求之间都是独立的,好比一个顾客和一个自动售货机或者一个普通的(非会员制)大卖场之间的关系一样。
www.a.com/login.php里面登陆了,我在
www.a.com/index.php也希望是登陆状态,但是,这是2个不同的页面,也就是2个不同的HTTP请求,这2个HTTP请求是无状态的,也就是无关联的,所以无法单纯的在index.php中读取到它在login.php中已经登陆了!
Cookie机制出现了,
Cookie的机制是把少量的信息存储到用户自己的电脑上,它在一个域名下是一个全局的;所以只要设置它的存储路径在域名
www.a.com下 ,那么当用户用浏览器访问时,php就可以从这个域名的任意页面读取
Cookie中的信息。所以就很好的解决了我在
www.a.com/login.php页面登陆了,我也可以在
www.a.com/login.php获取到这个登陆信息了。
Cookie是存在用户端,而且它本身存储的尺寸大小也有限,最关键是用户可以是可见的,并可以随意的修改,很不安全。那如何又要安全,又可以方便的全局读取信息呢?于是,这个时候,一种新的存储会话机制:
Session诞生了。
HTTP的请求的关联,让它们产生联系,让2两个页面都能读取到找个这个全局
Session信息,
Session信息存在于服务器端,所以也就很好的解决了安全问题。
session的工作原理
One - 当一个session第一次被启用时,一个独一的标识被存储于本地的cookie Two - 首先使用session_start(),PHP从session仓库中加载已经存储的session变量。 Three - 当执行PHP脚本时,通过使用session_register()函数注册session变量。 Four - 当PHP脚本执行结束时,未被销毁的session变量会被自动保存在本地一定路径下的session库中,这个路径可以通过php.ini文件中的session.save_path指定,下次浏览网页时可以加载使用。
Session机制剖析
它也是一种服务区存储数据的方式,肯定也是存在服务器的某个地方了。确实,它存在服务器的/tmp/
目录下
现在我们实践下,看看Session
的具体执行过程,这里就利用PHP中的Session机制,其他语言差别不大(毕竟PHP是
..en?)
简单的搭建个LAP环境:
yum install httpd && yum install php && systemctl start httpd
首先,开启session,利用session_start();
函数
cat > /var/www/html/index.php << EOF<?phpsession_start();?>EOF
Note: 这是个无任何返回值的函数,既不会报错,也不会成功。它的作用是开启session,并随机生成一个唯一的32位的session_id,类似于这样:
l2jh8fdcb0ihqle7u70t3n59g7
Session的全部机制也是基于这个
Session_id,它用来区分哪几次请求是一个人发出的。为什么要这样呢?因为HTTP是无状态无关联的,一个页面可能会被成百上千人访问,而且每个人的用户名是不一样的,那么服务器如何区分这次是小A访问的,那次是小B访问的呢?所以就有了找个唯一的session_id 来绑定一个用户。一个用户在一次会话上就是一个session_id,这样成千上万的人访问,服务器也能区分到底是谁在访问了。
cat > var/www/html/index.php << EOF<?phpsession_start();echo "SID: ".SID."<br>";echo "session_id(): ".session_id()."<br>";echo "COOKIE: ".$_COOKIE["PHPSESSID"];?>EOF

分析一波
SID
这个常量,我们没有给它赋值,它居然能有输出其次
session_id()
这个系统方法是输出本次生成的session_id最后
$_COOKIE['PHPSESSIID']
没有值,这个我们接下来说

奇怪的事情发生了!
SID
没有值了$_COOKIE['PHPSESSID']
中有值了。而且,2次刷新,
session_id
都是一样的:k16ddoavlipof9vksabb4bifh4
实际情况下,只要不关闭网页,怎么刷新都是一样了



session_id和第一次不一样
session,也就是有
session_start(); 时,就会自动生成一个
session_id来标注是这次会话的唯一ID,同时也会自动往
cookie里写入一个名字为
PHPSESSID的变量,它的值正是
session_id,当这次会话没结束,再次访问的时候,服务器会去读取这个
PHPSESSID的cookie是否有值有没过期,如果能够读取到,则继续用这个
session_id,如果没有,就会新生成一个session_id,同时生成
PHPSESSID这个cookie。由于默认生成的这个
PHPSESSIDcookie是会话,也就是说关闭浏览器就会过期掉,所以,下次重新浏览时,会重新生成一个
session_id。
session_id,就用来标识绑定一个用户的,既然session_id生成了。那么当我们往session里面写入数据,是如何保存的,答案是保存在服务器的某个目录里,具体可以参考php.ini的配置
Note: 本文的php为yum直接安装,所以php.ini的位置在/etc/php.ini;
通过
session.save_path
这个配置项来确定session的存放位置
一般来说:
Linux:
/tmp 或 var/lib/php/session
Windows:
C:\WINDOWS\Temp 或 集成环境就存在集成环境的TMP文件夹里
那么它是怎么存的呢?
同样也是用到session_id。session_id是32位的,服务器会用 sess_前缀 + session_id 的形式存在这个临时目录下,比如下面这图:

所以,每一次生成的session_id都会生成一个这样的文件,用来保存这次会话的session信息。
我们往session里写入些数据,来看看session是怎么往这个文件里写数据的,我们同样在页面继续加上写入session的语句如下所示:
<?phpsession_start();echo "SID: ".SID."<br>";echo "session_id(): ".session_id()."<br>";echo "COOKIE: ".$_COOKIE["PHPSESSID"];$_SESSION['hello'] = 123;$_SESSION['word'] = 456;?>

继续刷新界面,由于没有关闭浏览器,session_id肯定还是peo4be8en93gql35c6q40drrh3

是序列化的数据,我们肉眼也能读出来。当我们往$_SESSION
全局变量里写数据时,它会自动往这个文件里写入。读取session
的时候,也会根据session_id
找到这个文件,然后读取需要的session变量。
这个sess
文件不会随着客户端的PHPSESSID
过期,也一起过期掉,它会一直存在,出息GC扫描到它过期或者使用session_destroy()
函数摧毁
我们大致总结下:
HTTP请求一个页面后,如果用到开启session
,会去读cookie
中的PHPSESSID
是否有,如果没有,则会新生成一个session_id
,先存入cookie
中的PHPSESSID
中,再生成一个sess_
前缀文件。当有写入$_SESSION
的时候,就会往sess_
文件里序列化写入数据。当读取的session
变量的时候,先会读取cookie
中的PHPSESSID
,获得session_id
,然后再去找这个sess_sessionid
文件,来获取对应的数据。由于默认的PHPSESSID
是临时的会话,在浏览器关闭后,会消失,所以,我们重新访问的时候,会新生成session_id
和sess
_这个文件。
好。session
生成和保存将清楚了。我们再来看前面提到的几个变量:
echo "SID: ".SID."<br>";echo "session_id(): ".session_id()."<br>";echo "COOKIE: ".$_COOKIE["PHPSESSID"];
SID
是一个系统常量,SID包含着会话名以及会话 ID 的常量,格式为 "name=ID"
,或者如果会话 ID 已经在cookie 中设定时则为空字符串,第一次显示的时候输出的是SID的值,当你刷新的时候,因为已经在cookie中存在,所以显示的是一个空字符串。
session_id()
函数用来返回当前会话的session_id,它会去读取cookie中的name,也就是PHPSESSID
值。
[Session]session.save_handler = filessession.save_path = "d:/wamp/tmp"session.use_cookies = 1session.name = PHPSESSIDsession.auto_start = 0session.cookie_lifetime = 0session.serialize_handler = phpsession.gc_divisor = 1000session.gc_probability = 1session.gc_maxlifetime = 1440
session.save_handler = files
表示的是session的存储方式,默认的是files文件的方式保存,sess_xxxx, 保存在session.save_path = tmp
里,所有这2个都是可配值的。我们上面的例子就是用的这种默认的方式。save_handler
不仅仅只能用文件files,还可以用我们常见的memcache 和 redis 来保存。session.use_cookies
默认是1,表示会在浏览器里创建值为PHPSESSID的session_id,session.name = PHPSESSID 找个配置就是改这个名字的,你可以改成PHPSB, 那这样就再浏览器里生成名字为PHPSB的session_id 。session.auto_start = 0
用来是否需要自动开启session,默认是不开启的,所有我们需要在代码中用到session_start();函数开启,如果设置成1,那么session_id 也会自动就生成了。session.cookie_lifetime = 0
这个是设置在客户端生成PHPSESSID这个cookie的过期时间,默认是0,也就是关闭浏览器就过期,下次访问,会再次生成一个session_id。所以,如果想关闭浏览器会话后,希望session信息能够保持的时间长一点,可以把这个值设置大一点,单位是秒。gc_divisor
,gc_probability
,gc_maxlifetime
这3个也是配合一起使用,他们是干嘛的呢?他们是干大事情的,回收这些sess_xxxxx 的文件,它是按照这3个参数,组成的比率,来启动GC删除这些过期的sess文件。gc_maxlifetime是sess_xxx文件的过期时间。
NGINX对Session的处理
能把session改成cookie,就能避开session的一些弊端,能把session改成cookie,就能避开session的一些弊端。
upstream backend {server 127.0.0.1:8080 ;server 127.0.0.1:9090 ;ip_hash;}
Note:
ip_hash是容易理解的,但是因为仅仅能用ip这个因子来分配后端,因此ip_hash是有缺陷的,不能在一些情况下使用:
1/ nginx不是最前端的服务器。ip_hash要求nginx一定是最前端的服务器,否则nginx得不到正确ip,就不能根据ip作hash。譬如使用的是squid为最前端,那么nginx取ip时只能得到squid的服务器ip地址,用这个地址来作分流是肯定错乱的。
2/ nginx的后端还有其它方式的负载均衡。假如nginx后端又有其它负载均衡,将请求又通过另外的方式分流了,那么某个客户端的请求肯定不能定位到同一台session应用服务器上。这么算起来,nginx后端只能直接指向应用服务器,或者再搭一个squid,然后指向应用服务器。最好的办法是用location作一次分流,将需要session的部分请求通过ip_hash分流,剩下的走其它后端去。
为了解决ip_hash的一些问题,可以使用upstream_hash这个第三方模块,这个模块多数情况下是用作url_hash的,但是并不妨碍将它用来做session共享:
假如前端是squid,他会将ip加入x_forwarded_for这个http_header里,用upstream_hash可以用这个头做因子,将请求定向到指定的后端:
Note:
可见这篇文档:http://www.sudone.com/nginx/nginx_url_hash.html
在文档中是使用$request_uri做因子,稍微改一下:
hash $http_x_forwarded_for;
这样就改成了利用x_forwarded_for这个头作因子,在nginx新版本中可支持读取cookie值,所以也可以改成:
hash $cookie_jsessionid;
假如在php中配置的session为无cookie方式,配合nginx自己的一个userid_module模块就可以用nginx自发一个cookie,可参见userid模块的英文文档:
http://wiki.nginx.org/NginxHttpUserIdModule
Session共享
稍大一些的网站,通常都会有好几个服务器,每个服务器运行着不同功能的模块,使用不同的二级域名,而一个整体性强的网站,用户系统是统一的,即一套用户名、密码在整个网站的各个模块中都是可以登录使用的。各个服务器共享用户数据是比较容易实现的,只需要在后端放个数据库服务器,各个服务器通过统一接口对用户数据进行访问即可。但还存在一个问题,就是用户在这个服务器登录之后,进入另一个服务器的别的模块时,仍然需要重新登录,这就是一次登录,全部通行的问题,映射到技术上,其实就是各个服务器之间如何实现共享 SESSION 数据的问题。
Session共享的实现方式
使用数据库保存session
文章开始介绍的工具Grafana就可以把Session保存到mysql等数据库中,但是这种方式也不是完美的,因为使用数据库记录session信息,session的使用频率比较高,如果存在数据库中,频繁的读取会对数据库产生较大的压力,网站性能瓶颈一般都存在数据库,
使用同步工具同步session
使用同步工具对session文件进行同步,保证负载服务器的session文件都是一致的,这种做法虽然可以解决session共享的问题,同样的内容会存在多个服务器上,而且部分服务器存在的session文件可能从开始到结束完全没有使用到,浪费了服务器的资源。【rsync,inotify-tools等】
使用Redis或者Memcache保存session
相比文件取信息,从内存取数据速度要快很多,而且在多个服务器需要共用 session 时会比较方便,将这些服务器都配置成使用同一组 memcached 服务器就可以,减少了额外的工作量。其缺点是 session 数据都保存在 memory 中,一旦宕机,数据将会丢失。但对 session 数据来说并不是严重的问题。
写在最后
给大家推荐一款Nginx的升级工具:Openresty
OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。
OpenResty® 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。
OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。
如何安装:
yum install -y yum-utilsyum-config-manager --add-repo https://openresty.org/package/centos/openresty.repoyum install -y openrestyyum --disablerepo="*" --enablerepo="openresty" list available# 修改配置文件,基础用法与Nginx一样vim /usr/local/openresty/nginx/conf/nginx.conf
更多内容请参考:http://openresty.org/en/




