nginx-ingress-controller会话保持的踩坑记录

诡异的会话丢失

最近系统上线PAAS之后,IE11上出现一些页面打不开、窗口闪退、提示会话过期的问题,但客户反馈在360浏览器是正常的。

为什么IE11有问题,360浏览器却正常呢?难道IE又有什么神奇的兼容性问题了?最好有请求抓包可以看看。

很快,维护找到客户录制的请求抓包,找了几个反馈有问题的页面,除了cookie比较多之外,还发现有些请求报文比较特殊。

	approute=xxxxxxxxxxxxx
	JSESSIONID=yyyyyyyyyyyyyyyyyyyyyy
	approute=uuuuuuuuuuuuu
	JSESSIONID=vvvvvvvvvvvvvvvvvvvvvv

请求头中的cookie如上,居然有同名的cookie,这个系统是比较老的基于Session的Web应用,除了用户认证,用户的业务信息是保存在Session上面的。 上PAAS之后,这些cookie是用来做会话保持用的。

Set-Cookie: approute=2750fcbd2c7fa1fdd1189d95f63c3cd8; Expires=Sat, 27-Mar-21 01:37:04 GMT; Max-Age=172800; Path=/app-next; HttpOnly

在响应中也通过Set-Cookie对cookie进行重设,这样就没办法维护会话绑定了。如果某个请求用到了上一个请求的Session信息,就会出现”会话丢失”的情况。 而且从请求上看,这种情况在后续的请求上反复出现,给人感觉就是设置了没效果。

会话如何保持?

环境部署

正式的部署环境是有多层代理的,其中外层的nginx主要做静态资源托管,会话保持采用nginx-sticky-module-ng模块, 而ingress这一层是用的nginx-ingress-controller自带的Sticky sessions功能。

对应的ingress配置如下,采用cookie来做会话保持。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: approute-ingress
  annotations:
    kubernetes.io/ingress.class: "approute"
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "approute"
    nginx.ingress.kubernetes.io/session-cookie-hash: "sha1"
    nginx.ingress.kubernetes.io/affinity-mode: persistent
    nginx.ingress.kubernetes.io/ssl-redirect: "false"
    nginx.ingress.kubernetes.io/session-cookie-expires: "172800"
    nginx.ingress.kubernetes.io/session-cookie-max-age: "172800"
    nginx.ingress.kubernetes.io/proxy-body-size: 300m
spec:
  rules:
  - http:
      paths:
      - backend:
          serviceName: approute-app
          servicePort: 8080
        path: /app
  ...更多ingress rule

按当前的配置,就应该只有一个cookie来对,那这种同名cookie是怎么来的?

同名Cookie从哪里来?

以前没怎么听过同名Cookie,查了一下资料How to handle multiple cookies with the same name?,从中得到的初步结论是:

  • 怎么产生: 同名cookie是通过设置不同的子域名、子PATH等情况产生的。
  • 发送哪个: 如果不是很旧的浏览器,PATH越具体,优先级越高,排序越靠前。如果是其他属性不同,则没有约定。
  • 识别哪个: 服务端没有有明确的约定。

当前我们主要是通过PATH来设置的,而目前的会话保持也是设置在应用的上下文的。 我初步猜想,是不是有个地方设置PATH为根路径”/”了?

运气不错的是,根据会话丢失的判断,测试环境也拉起多个应用实例,发现可以重现问题。

不过IE11不能直接查看cookie文件了,需要通过ie的导出功能(点击浏览器右上角的星星打开收藏夹-添加收藏夹下拉-选择导入和导出…)。

a.b.c.d	TRUE	/app	    FALSE	1617197971	approute 880dffc13701aebc8315c4a881c82de4
a.b.c.d	TRUE	/app-next	FALSE	1648561945	approute 43d24255920374268ed5abff09360163

如上所示,可以发现两个cookie的值刚好对应/app和/app-next两个PATH。

纳尼,不是应该按路径来划分的么?为什么调用/app-next会把/app的cookie也带过去了?

IE对PATH的骚操作

好吧,写段代码测试一下。设置了三个cookie如下:

Set-Cookie: approute=6d7770821d2aa0bce5739b1a27fca753; Path=/app/; HttpOnly
Set-Cookie: sna_cookie=ACB73F8C4EA88A6; Path=/
Set-Cookie: JSESSIONID=0E817AD9FEF46123A2DAB6A9F99B38AC; Path=/app; HttpOnly

对于Chrome来说,访问/app-next发送的cookie是

JSESSIONID=0E817AD9FEF46123A2DAB6A9F99B38AC
sna_cookie=ACB73F8C4EA88A6

对于IE11来说,访问/app-next发送的cookie是

sna_cookie=ACB73F8C4EA88A6

从测试结果上看,IE直接采用prefix来判断Path,IE对cookie的Path处理方式是不同于chrome的,有点反常识,纯属骚操作。

修复方案

看来只能调整一下Path的设置方式,考虑到路径的优先级,尝试使用根路径”/”来设置,发现没有作用,nginx-ingress-controller的路由逻辑是去到不同的Path才来处理的, 由于找到对应的值,返回的时候又重新写回了一个,导致它还是不断的变。

看来只能直接给规则文件中的所有的path结尾都添加一个”/”了。如下所示:

spec:
  rules:
  - http:
      paths:
      - backend:
          serviceName: approute-app
          servicePort: 8080
-       path: /app
+       path: /app/

同时考虑到这种cookie应该会话级的,而且后续方便调整,去掉了cookie的超时控制。

-nginx.ingress.kubernetes.io/session-cookie-expires: "172800"
-nginx.ingress.kubernetes.io/session-cookie-max-age: "172800"

后续有什么可借鉴的?

首先,为什么上线前没有发现?简单的说,还是部署方式,组网不同,很多老应用就是基于Session的,而不是无状态的,但是在环境上只起一个实例, 只有在压测等情况才进行扩容。后续要考虑增加集群资源,让此类应用至少可以保持在2个,提前发现问题。

另外,对cookie的设置要慎重,虽然本次的cookie非业务用,但cookie始终是在客户端控制的,变更、维护都不能随心所欲。 同时,必须使用cookie,也要建议尽量使用会话级cookie

使用第三方软件、库,都通读文档,不要直接”拷贝”配置,即使是所谓官方的示例,像这些软件配置项都很多,几十项到几百项不等,通读一下后续分析会有更多印象。 同时,作为知识沉淀,后续也要开展培训分享,树立技术人的标杆。

FAQ

再记录一下过程中涉及的知识点和资料,用于释疑。

  • 修改Path的可能影响

仔细想想,就可以发现,直接在path后面添加”/”虽然可以绕过IE的骚操作, 但是由于旧cookie的存在(非会话级cookie的锅,要忍耐172800秒,也就是2天才能清除影响),仍然会有”会话丢失”的现象,临时解决办法就是清理IE的cookie并重启IE(或者通过一键IE设置脚本重置)。

考虑之前还有几个系统上线也有类似的隐患,后续考虑先去掉cookie的超时控制,过渡一下再修改Path,可以减少影响。

  • cookie的数量限制

之前有案例和材料表明,IE对于某个域名的cookie数量限制是50个。所以也曾怀疑过cookie数量过多导致异常(抓包显示接近50个),但查阅相关资料和测试发现,IE11差不多能支持180个cookie。 想测试一下浏览器的cookie限制,可以参考这个链接:Browser Cookie Limits

  • 第一层nginx反向代理的会话保持策略

目前配置是类似这样的,针对upstream会设置一个sticky指令。

    upstream test {
         sticky expires=1h path=/;
         server a.b.c.d:10086   max_fails=2;
    }

上面也提到,这是nginx-sticky-module-ng模块实现的(通过nginx -V可以看到加载的模块),从文档上可以看出它也是基于cookie的,默认的cookie名称是route。 但是如果你发现你没看到这个route的cookie,那么说明upstream server只有一个,这种情况下,它是不会增加这个cookie的, 可以参考代码

  • nginx-ingress-controller怎么处理同名cookie

上面提到,同名cookie发过去的时候,越具体的Path,越靠前,但服务端的处理没有约定。

对于nginx-ingress-controller来说,它的会话保持功能不是通过nginx stricky模块或者其他方式实现的,是自己通过lua实现的。

通过sticky.lua#L40 看到cookie是由lua-resty-cookie这个模块实现的。

继续深入,可以发现cookie.lua#L109 就是解析cookie放到一个字典去的。

所以后面的cookie会覆盖前面的cookie,这也符合我们处理cookie的常见做法。另外,有个PR23也提到要支持多Path的情况, 不过似乎lua-resty-cookie已经没人维护了。