暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

NGINX反代出错,Session:这个锅我不背

一元殿 2019-09-21
893


点击上方“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
 诞生了。
从上面的描述来讲,它就是在一次会话中解决2次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
      <?php
      session_start();
      ?>
      EOF


      Note: 这是个无任何返回值的函数,既不会报错,也不会成功。它的作用是开启session,并随机生成一个唯一的32位的session_id,类似于这样:


      l2jh8fdcb0ihqle7u70t3n59g7

      Session
      全部机制也是基于这个Session_id
      ,它用来区分哪几次请求是一个人发出的。为什么要这样呢?因为HTTP是无状态无关联的,一个页面可能会被成百上千人访问,而且每个人的用户名是不一样的,那么服务器如何区分这次是小A访问的,那次是小B访问的呢?所以就有了找个唯一的session_id 来绑定一个用户。一个用户在一次会话上就是一个session_id,这样成千上万的人访问,服务器也能区分到底是谁在访问了。
      验证下上面的说法,执行以下修改PHP语句如下:
        cat > var/www/html/index.php << EOF
        <?php
        session_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


        实际情况下,只要不关闭网页,怎么刷新都是一样了

        既然我们看到COOKIE中有值了,我们,打开firebug看看到底是什么:

        接下来关闭浏览器,重新打开:

        发现又和第一次打开一样,并且重启后的浏览器session_id
         和第一次不一样
        下面,我们解释下上面的过程产生的原因:
        每次我们访问一个页面,如果有开启session
        ,也就是有session_start(); 
        时,就会自动生成一个session_id
        来标注是这次会话的唯一ID,同时也会自动往cookie
        里写入一个名字为PHPSESSID
        的变量,它的值正是session_id
        ,当这次会话没结束,再次访问的时候,服务器会去读取这个PHPSESSID
        的cookie是否有值有没过期,如果能够读取到,则继续用这个session_id
        ,如果没有,就会新生成一个session_id,同时生成PHPSESSID
        这个cookie。由于默认生成的这个PHPSESSID
        cookie是会话,也就是说关闭浏览器就会过期掉,所以,下次重新浏览时,会重新生成一个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的语句如下所示:

          <?php
          session_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
          的时候,也会根据session_id
          找到这个文件,然后读取需要的session变量。

          这个sess
          文件不会随着客户端的PHPSESSID
          过期,也一起过期掉,它会一直存在,出息GC扫描到它过期或者使用session_destroy()
          函数摧毁


          我们大致总结下:

          HTTP请求一个页面后,如果用到开启session
          ,会去读cookie
          中的PHPSESSID
          是否有,如果没有,则会新生成一个session_id
          ,先存入c
          ookie
          中的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的相关配置,打开php.ini文件,搜索session相关,下面就是几个常用的配置:
              [Session]


              session.save_handler = files


              session.save_path = "d:/wamp/tmp"


              session.use_cookies = 1


              session.name = PHPSESSID


              session.auto_start = 0


              session.cookie_lifetime = 0


              session.serialize_handler = php


              session.gc_divisor = 1000


              session.gc_probability = 1


              session.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改成cookie,就能避开session的一些弊端,能把session改成cookie,就能避开session的一些弊端。

              ip_hash
                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分流,剩下的走其它后端去。


                upstream_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共享的实现方式


                • 使用数据库保存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-utils


                  yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo


                  yum install -y openresty


                  yum --disablerepo="*" --enablerepo="openresty" list available


                  # 修改配置文件,基础用法与Nginx一样
                  vim /usr/local/openresty/nginx/conf/nginx.conf


                  更多内容请参考:http://openresty.org/en/



                  文章转载自一元殿,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                  评论