最近系统上线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,查了一下资料How to handle multiple cookies with the same name?,从中得到的初步结论是:
当前我们主要是通过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也带过去了?
好吧,写段代码测试一下。设置了三个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。
使用第三方软件、库,都通读文档,不要直接”拷贝”配置,即使是所谓官方的示例,像这些软件配置项都很多,几十项到几百项不等,通读一下后续分析会有更多印象。 同时,作为知识沉淀,后续也要开展培训分享,树立技术人的标杆。
再记录一下过程中涉及的知识点和资料,用于释疑。
仔细想想,就可以发现,直接在path后面添加”/”虽然可以绕过IE的骚操作, 但是由于旧cookie的存在(非会话级cookie的锅,要忍耐172800秒,也就是2天才能清除影响),仍然会有”会话丢失”的现象,临时解决办法就是清理IE的cookie并重启IE(或者通过一键IE设置脚本重置)。
考虑之前还有几个系统上线也有类似的隐患,后续考虑先去掉cookie的超时控制,过渡一下再修改Path,可以减少影响。
之前有案例和材料表明,IE对于某个域名的cookie数量限制是50个。所以也曾怀疑过cookie数量过多导致异常(抓包显示接近50个),但查阅相关资料和测试发现,IE11差不多能支持180个cookie。 想测试一下浏览器的cookie限制,可以参考这个链接:Browser Cookie Limits。
目前配置是类似这样的,针对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的, 可以参考代码。
上面提到,同名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已经没人维护了。