- 更新于
Sticky Session 使用指南
- 作者们
- 名称
- Yancy Huang
后端服务我们总是不满足于单节点,就好像好吃的美食当前,自然是希望肚子越大越好。
如果是常用的静态内容也就罢了,只要设置好一层缓存,后面所有的机器地位相当,谁返回什么都是固定的接口,无论反向代理用什么样的逻辑去发送流量,都不是问题。
但是后端服务并非总是无状态的(即服务之间没有状态依赖,谁先谁后也没有讲究,他们在处理事务上是保持相同的地位),有时后端是一个有机的整体,每个实例都有着自己的角色,那么这个时候理应对反向代理的发送逻辑加以干涉。
假设一个场景,出于某种原因,在一次会话中,我们希望这一次会话总是和其中一个后端实例沟通。这个时候该怎么办呢?
有同学可能会说,这样难道不会造成各个后端实例分配不均么?如果是因为希望保持各实例间 session 会话统一,我可以选择使用中心化会话信息存储,或者是冗余的方案,保障会话信息不丢失,不用一直连接到同一个后端机器吧?
首先,是选型的问题,无论是中心化,还是冗余,都需要较多的资源,以及较高的维护和设计成本,相比较之下还是一次会话和后端机器绑定来的更为实惠。
其次,这两种方案并不是所有场景都适用,比如 WebSocket 的连接。
那么上面那个所谓的“会话与后端实例绑定”方案,其实有一个专门的名称,叫做「粘滞会话」(Sticky Session),是不是还挺形象?在各家云厂商那里,又有另一个名称,叫做「会话亲和」(Session Affinity),如果说这两个概念有什么区别的话,「粘滞会话」可以理解为一个行为结果,表现为会话与服务实例的绑定,「会话亲和」更多的是一种方案的选择,可以通过不同逻辑实现会话分流,至于最适合的逻辑是什么,就需要使用者自行判断了,所以「会话亲和」会更复杂,基本所有云服务厂商都设计了专门的方案可供选择。
那么如何去实现一个最小可行的「粘滞会话」?既然是反向代理,那么就绕不开用来做反向代理的香饽饽——Nginx。
「粘滞会话」其实是 Nginx 负载均衡可以实现的一种结果,但从理论上我们可以通过多种方式来实现(以下参考 nginx 官方文档 http-load-balancer):
- IP Hash 如果你的反向代理外侧用户正好在公网,那么完全可以借助 IP Hash 来完成「粘滞会话」!试想,每一个用户在很长一段时间内都没有更换公网 IP(除非时常利用 VPN 在不同地区徜徉),那么 TA 将始终访问到固定一台机器。 这个方案的问题在于,IP 这玩意儿不太可控,特别是在很多运营商那里,很多用户使用同一个公网 IP 的情况很多,另外,一旦经过了另一个代理层,这种方式马上就无法使用了,因为你的其他代理层大概率 IP 就那么几个,你也无法控制用户通过哪个代理 IP 访问。
- 普通哈希 可以直接定义哈希规则,只能使用 nginx 内置的变量,固定字符串,比较死板,但是恰恰在 nginx 的变量里,有一个可以传入 cookie 值,
$cookie_
。只要在后面接对应的 cookie 名称,就能替换掉这里的变量,例如有个 cookie 名字是 appSession,那么这里改成$cookie_appSession
就可以了。nginx 所有变量可以参考这里
从以上对 nginx 开源版本的接口分析得知,如果想要实现一个最小可行版本,只需要在前面架设一个 Nginx 服务器即可。
这是一个云下版本,如果是使用云厂商自己的服务来实现,那么方法多种多样,各类配置也有所不同,不过落实到 K8S 集群上无非就是对两个资源进行额外配置:Service 和 Ingress。如果针对 Service 设置,有一个 K8S 原生的配置sessionAffinity
,可以实现简单的会话亲和,不过它只有两个可选值,None
和 ClientIP
,其中 ClientIP 只能针对客户端 IP 进行筛选,在多层代理情形下,颗粒度太大效果不好,所以这里只针对 Ingress 进行扩展。 ^17e6a9
Ingress 对内和对外的两种
GKE 默认就提供了对内和对外两种 Ingress 负载均衡器,对外就是传统意义上的对公网负载均衡器,借助全球部署的 Google 接入点来提供 HTTP(S)的负载均衡的能力。从技术手段上来说,对外的负载均衡还需要考虑到暴露公网的安全性和可用性的问题,所以无论是 Google 提供的技术上,还是在实现方案上,都需要多考虑一层。 在这里我们只是需要把后端应用暴露给前端,所以通过内部调用即可,不需要暴露到公网,GKE 提供的对内 Ingress 负载均衡器就派上用场了。 假设我们不适用内部 Ingress 方案,直接使用 Service,访问链路如下: 前端 nginx 把所有到后端的流量转发到 Service 暴露给集群内部的 ClusterIP,然后 Service 自行转发流量到 Pod,这也就是上面所说的 Service 转发方案,他对于会话亲和的选项比较少([[Sticky Session 使用指南#^17e6a9]]); 我们把访问链路加一下,改成下面这样子:
中间加了一层内部 Ingress 之后,流量不会通过 Service 来分发了,而是会交给 Google 提供的一个叫做「网络端点组」(NEG)的东西,NEG 会把流量负载直接分发给 Pod IP,这时候我们就可以用上 Ingress 提供的会话亲和配置了。
如何配置会话亲和
其实内部 Ingress 相关的部署在 GKE 官方文档里就有写到https://cloud.google.com/kubernetes-engine/docs/how-to/internal-load-balance-ingress?hl=zh-cn#deploy-app,我们这里就理一下逻辑,方便大家知道每一步配置的作用。 现在假设我们已经搭建好了一个基于 Service 流量负载均衡机制的集群服务,现在就差把内部 Ingress 负载均衡器部署上。
网络环境
那么第一步我们需要准备网络环境,Google 需要一个子网,用来专门代理需要分发的流量,我们的负载均衡器也要部署在上面。
gcloud compute networks subnets create proxy-only-subnet \ --purpose=REGIONAL_MANAGED_PROXY \ --role=ACTIVE \ --region=us-west1 \ --network=lb-network \ --range=10.129.0.0/23
除了子网本身,我们还需要创建一条防火墙规则来允许代理子网的流量进入 Pod 中,所以这条防火墙的规则就是「允许」「来自代理子网的流量」进入「Pod 端口」。
gcloud compute firewall-rules create allow-proxy-connection \ --allow=TCP:CONTAINER_PORT \ --source-ranges=10.129.0.0/23 \ --network=lb-network
服务 NEG 化
我们上面说了,Google 需要通过 NEG 服务来直接把流量分发到 Pod IP,所以我们要把 Service 转化为 NEG,方法是给 Service 加上 annotation:
kind: Service
apiVersion: v1
metadata:
annotations:
cloud.google.com/neg: '{"ingress":true}'
cloud.google.com/backend-config: '{"ports": {"8080":"self-service-backendconfig"}}'
另外,如果满足以下所有条件,系统便会自动给 Service 添加 cloud.google.com/neg: '{"ingress": true}'
注解:
- 您使用的是 VPC 原生集群。
- 您未使用共享 VPC。
- 您未使用 GKE 网络政策。
配置服务的会话亲和属性
根据 GKE 要求,需要通过 Service 的 BackendConfig 来配置 Ingress 属性。 https://cloud.google.com/kubernetes-engine/docs/how-to/ingress-configuration?hl=zh-cn#configuring_ingress_features_through_backendconfig_parameters
我们需要部署另一个资源,叫做 BackendConfig。和 Ingress 相关的属性配置在这里,所以会话亲和性也在这里部署。
apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
name: self-service-backendconfig
spec:
sessionAffinity:
affinityType: "GENERATED_COOKIE"
affinityCookieTtlSec: 50
Ingress
最后是 Ingress 的部署,这里的配置相对简单,只需要指明 ingress 类型使用的是 GCE Internal 即可,然后照常配置后端:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ilb-demo-ingress
namespace: default
annotations:
kubernetes.io/ingress.class: "gce-internal"
spec:
defaultBackend:
service:
name: hostname
port:
number: 80