Sticky session不是一个新话题,本文主要是澄清一些常见的误解,以及在cloud native(云原生)环境中,如果正确实现Sticky session。
什么是Sticky session
Sticky session是与load balancer相关联的一个概念。我们都知道,load balancer的作用是将客户端的大量请求尽量合理均匀的转发到后端服务器。而实际应用场景中,有时候需要将客户端的每个会话(session)中的所有请求都转发到同一个后端服务来处理。这就是Sticky session。
注:Session是layer 7(HTTP)的一个概念,一个session(会话)中往往包含多个HTTP请求。一个session往往有一个超时时间,比如1小时,或者随着浏览器的关闭而结束。
下面几种表达都可以用来表示“会话保持”,
Sticky Session
Session Affinity
Session Persistence
Layer 4 (TCP)与Sticky Session
这是很多人迷惑的一个地方,就是工作于Layer 4 (TCP)的应用程序如何实现Sticky Session。我不止一次被别人问到这个问题。这里再强调一遍,session是Layer 7(HTTP)的概念,工作于Layer 4(TCP)的应用程序,是无需担心Sticky的问题。例如,一个客户端程序通过一个load balancer连接(JDBC连接)到数据库集群,这种情况下是不用考虑sticky的问题,也就是说不用担心一个连接内的数据包会被发送到不同的数据库实例。
原因其实也很简单,因为一旦TCP连接建立之后,链路是相对稳定的。在一个连接内的多个数据包是绝对不会发送到不同的数据库服务器。你可能会质疑,为什么这么肯定?我们公司的一个Architect也问过我类似的问题,他问:“连接建立之后,load balancer有没有可能偷偷连接到另一个后端服务器”?这种情况是不会发生的。根据rfc793中的2.7节中的描述,一个TCP连接是由两端的一对socket指定的。原话如下:
A connection is fully specified by the pair of sockets at the ends.
https://www.rfc-editor.org/rfc/rfc793.html#section-2.7
也就意味着TCP连接一旦建立之后,目标服务器就已经选定了。在连接断开前,所有的数据包都是往返于客户端和这个服务器之间。大致示意图如下:
如果load balancer偷偷连接到另一个后端的服务器,那么构成连接(connection)的服务器端的socket发生了变化,也就意味着connection断开了。客户端程序肯定能感知到这种变化,因为当这种情况发生时,客户端程序发送或接受数据自然就会发生异常。
Load balancer for cloud native services
目前常见的load balancer都为HTTP层的应用提供了Sticky session的功能,实现机制要么是通过cookie,要么是在URL中增加额外的参数。
但是要注意一点,通常的Load balancer是部署在K8S cluster之外的,所以它是无法感知到K8S cluster中的service,更无法感知到POD。Load balancer只能将request按某种规则转发到后端某个node上。而对于NodePort类型的service,在每一个node上都会监听同一个端口,无论前端的Load balancer将客户的请求转发到哪个node,对于K8S中的service来说没有任何区别。
当请求被前端的load balancer转发到某个node上之后,再由K8S(kube-proxy + iptables)将请求转发到某个POD。所以实际上有两次负载均衡的过程,图示如下:
在k8S service中增加下面的配置,就可以根据Client IP来实现Sticky Session。将来自同一个Client IP的所有请求都转发到同一个POD处理。
sessionAffinity: ClientIP
复制
但是一定要慎用上面这个配置项,因为当K8S cluster前面部署了Load balancer的时候,kube-proxy(netfilter)看到的ClientIP永远都是load balancer的IP,所以所有用户的请求都会转发给同一个POD。就算前端的load balancer在HTTP头中增加了下面的值,也无济于事,因为kube-proxy是工作于Layer 4(TCP, UDP or SCTP)的load balancer。
X-Real-IP: xxxx
X-Forwarded-For: xxxx
复制
Sticky session for cloud native services
Cloud native的应用要求Sticky session能将一个session内的所有请求转发到同一个POD。为了能感知到POD,Load balancer(或称为reverse proxy)必须部署在K8S cluster之内。这时配置方式与部署在K8S之外的load balancer原理相同。以nginx为例来说,需要将某个service对应的所有POD的URL定义到upstream server列表中,例如:
http {
upstream backend {
server app-1.my-svc-headless.default.svc.cluster.local:8080;
server app-2.my-svc-headless.default.svc.cluster.local:8080;
server app-3.my-svc-headless.default.svc.cluster.local:8080;
sticky cookie my_session expires=1h;
}
}
复制
nginx会在来自upstream的第一个HTTP response中加入一个名为"my_session"的cookie。客户端随后的请求中带上这个cookie,nginx就会将请求转发到同一个upstream server。参数expires是设置客户端浏览器应该保持这个cookie多久,这里设置的是1小时。
这种静态配置的方式显然不够灵活,因为service的replicas数量是可以动态调整的,可能会动态增加或删除POD。每次都需要手动来修改这个配置,然后重启nginx或者reload配置。这样显然不可行。
更可行的方法是借助于DNS动态解析出所有POD的访问地址,例如:
http {
resolver kube-dns.kube-system.svc.cluster.local valid=5s;
upstream backend {
server my-svc-headless.default.svc.cluster.local:8080 resolve;
sticky cookie my_session expires=1h;
}
}
复制
这种动态配置的方式更简洁,只配置了headless service的地址。然后通过kube-dns周期性的动态解析出对应的endpoints列表。valid参数设置解析的周期是5秒。
小结
本文首先指出了session是Layer 7(HTTP)的一个概念,工作于Layer 4(TCP)的程序无需担心Sticky的问题。因为一旦TCP连接建立之后,目标服务器就选定了。
接着阐述了在K8S cluster前端有load balancer的部署下,每个请求实际会经过两次负载均衡的过程。在这种部署下,要慎用kube-proxy提供的基于ClientIP的Sticky Session,因为kube-proxy(netfilter)看到的source IP永远是前端load balancer的IP,从而将所有用户的请求都会转发给同一个POD。
最后以nginx为例给出了如何正确的实现POD级别的sticky session。主要是借助于DNS动态解析出endpoints列表。
--END--