cover_image

流利说业务网关Kong K8s化之路

LLS BOT-CC 流利说技术团队
2021年12月14日 11:00

导读

像绝大部分公司的业务一样,流利说也经历了业务流量从小到大、后端服务从少数到多数的过程。
业务网关作为所有服务的入口,有着举足轻重的作用。随着业务的发展,业务网关的架构和运维方式也在不断演进,目标是在提供更稳定的服务的同时,既能够更安全可控的对业务网关进行变更,又便于未来能够扩展和更新,以提升运维的灵活性。
目前,流利说通过将网关层K8s化,来实现上述灵活性。本文将简要介绍流利说网关与后端业务架构的”前世今生“,并引出网关层的K8s化过程,供感兴趣的同学作为一个参考。
注:流利说的业务主要部署在云上,本文中所用到的云上资源术语均基于阿里云环境。)

1. 业务网关与后端业务的最初架构

网关的K8s化不是一蹴而就的。在最早期,流利说的流量较小、业务形态较为简单。业务网关层采用的是最简单的单点Nginx部署架构;后端服务采用Docker,部署在不同的ECS上,并将相应的IP + Port通过upstream的方式配置在Nginx的虚拟服务器里 —— 客户端的请求全部打向单节点的Nginx,Nginx通过7层代理,将请求转发到对应的后端服务。当有路由变更需求或者流量切换时,基础架构的同事通过登陆Nginx服务器,修改配置文件的方式来解决,如下图所示

图片


2. 问题与风险

随着用户数的增多,业务流量开始增加、后端业务日趋复杂,相应的问题和风险也暴露了出来。主要有以下三点:
1. 很明显,Nginx存在单点故障风险,且单台Nginx不足以支撑不断增长的业务流量;
2. 后端组件增多且变更频繁,而Docker服务的更新、调度、部署却是人工借助Ansible来完成,在复杂的场景下还是使用不便;
3. 路由配置变更频繁,基础架构部的人力陷入毫无创造性的配置变更泥潭中,且登陆服务器执行配置变更不可不可追溯、容易出错。

3. 架构大演进:Kong + K8s + Custom K8s Controller

关于问题1,可以通过将Nginx部署在ASG(Auto Scaling Group)里,并配合SLB来解决;
关于问题2,我们采取的是将后端业务迁移到K8s上,通过K8s来进行容器编排,并用NodePort Service暴露服务,同时我们基于ArgoCD制作了CD部署工具,供研发自助部署得以解决;
关于问题3,变更可追溯性可以通过GitOps的实践来解决,但还是避免不了基础架构的同事直接对接全公司各业务的路由变更需求,并人肉修改Nginx路由配置的问题;尤其是在Nginx部署至ASG后、NodePort Service的端口是动态的情况下。其核心原因在于Nginx路由配置的更新方式是基于文件的,不便于做自动化,即使通过Ansible去批量修改并重载,还是避免不了基础架构的同事去频繁修改一份冗长的Nginx配置。
后来经调研,我们选择了Kong API Gateway来代替Nginx。Kong基于OpenResty,是一个云原生,高效,可扩展的分布式 API 网关,其核心价值在于高性能和可扩展性。在DB模式下,Kong将路由保存在数据库里,供上层若干的Kong实例使用;与Nginx通过修改文件的方式来更新路由不同,Kong提供了RESTful API的方式来配置路由。这些特性为自动化配置路由提供可能性。
由于彼时后端业务已经逐渐迁入K8s,因此我们很自然想到了利用K8s的List-Watch机制,实现一个自动配置路由的Controller。它监听整个集群应用的Ingress,Servcie的变化,自动生成路由信息,并调用Kong的Restful API,来更新路由配置;配合CD系统,各应用的研发同事可以自行修改Ingress、Service的配置文件,来自助更新路由。
更新后的架构及流程如下所示,以部署一个Nginx换欢迎界面为例:



图片
1. 研发使用我们基于Jsonnet开发的自动生成工具,根据需要对配置项作出简单的修改,即可生成应用所需的K8s yaml文件,其中包括Ingress和NodePort Service;然后git push至Gitlab即可,之后一切交给自动化流程完成;
2. Yaml推送到Gitlab上供审计使用,同时会触发webhook,调用ArgoCD的API;
3. ArgoCD API被触发后,会根据yaml文件,在目标K8s集群中部署应用;
4. Ingress和Service被部署到目标K8s集群后,集群内的自定义路由controller会监听到对应Ingress、Service的变化;
5. 自定义的路由Controller根据Ingress中的annotation、host、path字段值与对应应用的NodePort Service端口信息,生成路由配置,调用Kong的API更新至Kong中。
看到这里,细心的读者可能会产生一个疑问:为什么不用K8s官方的Ingress Controller,而是自行实现一个自定义的路由Controller呢?这是因为在生产环境中,我们的K8s环境有多套,用作灾备。自定义路由Controller不仅负责路由的更新,还负责各集群中服务的熔断与集群间的切流;并且在必要时,我们可以进一步扩展自定义功能。实际的多集群架构如下图所示。

图片


4. 业务网关Kong K8s化之路

至此,研发可以完全自助地对各自应用进行发版与路由变更,而基础架构的同事可以专注于架构优化、链路可视化、监控告警、自动化工具开发等工作。

后来在针对业务网关层Kong维护的过程中,又发现了以下几个痛点:

1. ASG的扩容还是需要一些时间的,通常需要2-5分钟的时间。在每日高峰或业务流量激增的情况下,即时扩容策略存在时延;
2. 尽管有自动化工具Packer、Terraform等的加持,针对Kong本身的配置做更新的流程还是比较重的 —— 我们需要更新好Kong本身的配置 → 重新制作Kong EC2的虚机镜像 → 滚动更新ASG中的实例; 
3. 回滚反应慢。结合1和2可以预见,万一在更新过程中出现了差错,要回滚到Kong的正常配置,还是很耗时的;
4. 可控的更新策略还不够精细。

虽然上述有些问题可以通过云上Auto Scaling Group产品本身的一些特性,并对接云上API开发自动化平台来解决,但是不同云厂家的Auto Scaling Group的产品特性是不一样的,暴露出的API也不一样,无论从云上界面的使用还是对接云上API制作自动化工具,成本都是比较高的。“架构大演进”后,随着我们对K8s的逐渐熟悉,发现可以利用K8s Deployment、HPA解决业务网关层Kong的运维痛点,使网关层的维护更轻量、更安全可控。在网关层K8s化的过程中,主要需要确认以下三个问题:

1. 调用链路延迟是否会增长?
2. kong-proxy Pod能否优雅停止?
3. 如何平滑切换?

4.1. 链路压测

如果Kong K8s化后出现不可接受的延迟增长,那么也没有继续往下做的必要了。为了测试K8s化后,链路延迟是否会有增长,我们将Kong Docker化,在云上部署了专用的K8s,在网络环境、机器规格等方面和已有ASG架构的网关层进行对齐,对链路进行压测。为了最大化地客观反应调用链路本身的延迟情况,在业务Kubernetes集群中的被测服务应当是越轻量越简单越好,因此被测服务是Nginx欢迎页面。


图片

经过七天不间断的压测,从链路延迟的P95和P99结果可以得出,在各项条件对齐的情况下,K8s化后的Kong对整链路的延迟并没有影响。


Kong in ASG 7 days P99 & P95(2021-09-18 14:30~2021-09-25 14:30)
hostnginx-test.llstest.com
7 days P990.005s
7 days P950.004s
Kong in K8s 7 days P99 & P95(2021-09-28 10:15~2021-10-04 10:15)
hostnginx-test.llstest.com
7 days P990.005s
7 days P950.004s

4.2. 能否优雅停止

Kong Pod的优雅停止是指在删除Pod的过程中,比如手动删除Pod以实现重启、Kong Deployment的滚动更新、HPA扩缩容、云上K8s集群自动扩缩容的过程中,是否会中断已有的服务(尤其是长连接服务)。

4.2.1. Pod终止生命周期简介

Pod正常终止时会依次经历以下5个生命周期:

1. Pod被设置为Terminating状态,并将其从对应Service的Endpoints列表中移除

此时,用kubectl get pods所显示的Pod状态为Terminating;Pod只是停止新流量,在Pod中运行的容器不受到影响。

2. 执行preStop hook

preStop hook是一个被发送到Pod中的容器的特殊的命令或http请求。如果应用不能通过接收SIGTERM信号来优雅停止,那么可以利用这个hook来触发优雅停止。

3. SIGTERM信号发送给Pod

此时,Kubernetes将给Pod中的容器发送SIGTERM信号。这个信号让容器知道它们很快就会被关闭。

4. Kubernetes等待宽限期

此时,Kubernetes会等待一个指定的时间,称为终止宽限期,默认时间为30秒。需要注意的是,终止宽限期的倒计时与preStop钩子和SIGTERM信号并行发生,Kubernetes不会等待preStop钩子完成。如果应用在终止宽限期完成之前完成关闭并退出,Kubernetes会立即进入下一步;如果Pod通常需要超过30秒才能关闭,确保通过调整terminationGracePeriodSeconds参数值来提高终止宽限期,比如提高至60秒:
apiVersion: v1kind: Podmetadata:  name: my-podspec:  containers:  - name: my-container    image: busybox  terminationGracePeriodSeconds: 60

5. 向Pod发送SIGKILL信号,移除Pod

如果容器在宽限期后仍在运行,Kubernetes则会向它们发送 SIGKILL 信号并强行删除它们。此时,所有 Kubernetes 对象也被清理干净。

4.2.2. Kong的优雅停止方式

执行优雅停止命令即可:
Usage: kong quit [OPTIONS] Gracefully quit a running Kong node (Nginx and otherconfigured services) in given prefix directory. This command sends a SIGQUIT signal to Nginx, meaning allrequests will finish processing before shutting down.If the timeout delay is reached, the node will be forcefullystopped (SIGTERM). Options: -p,--prefix   (optional string) prefix Kong is running at -t,--timeout  (default 10) timeout before forced shutdown -w,--wait (default 0wait time before initiating the shutdown $ kong quit -p ${PREFIX_DIR} -t ${TIMEOUT}

4.2.3. Kong Pod优雅停止的实现

结合4.2.14.2.2,Kong Pod优雅停止的实现方式已经很明显了:在Pod终止时生命周期中的preStop hook阶段执行Kong的优雅停止命令,并合理设置Kong的优雅停止超时时间和Pod优雅停止的宽限时间,即可实现Kong in K8s的优雅停止。terminationGracePeriodSeconds是Pod进入terminating状态时就开始倒计时了,代表了Pod最多等待terminationGracePeriodSecondspod中指定的时间后,一定会被杀掉。因此在设置宽限时间时,terminationGracePeriodSeconds的值一定要大于Kong优雅停止命令中的timeout值。关键配置如以下示例Yaml所示:
apiVersion: apps/v1kind: Deploymentmetadata:  name: kong-proxy  labels:    app: kong    name: kong-proxy  namespace: kong-proxyspec:  replicas: 2  selector:    matchLabels:      app: kong      name: kong-proxy  template:    metadata:      annotations:        prometheus.io/scrape: "true"      labels:        name: kong-proxy        app: kong    spec:      containers:      - name: kong-proxy        image: our.registry.com/fake-repo-here/cutom-kong-docker:version        imagePullPolicy: Always        command: ["start_command"]        args: ["some", "args", "at", "here"]        ports:        - name: http-proxy          containerPort: 80        # start of readiness probe & liveness probe        readinessProbe:          exec:            command: ["kong_health_check.sh"]          initialDelaySeconds: 8        livenessProbe:          exec:            command: ["kong_liveness_check.sh"]          periodSeconds: 10        # end of readiness probe & liveness probe        workingDir: /you_guess        resources:          requests:            memory: "1Gi"            cpu: "1"          limits:            memory: "2Gi"            cpu: "2"        # Graceful shutdown settings - Timeout in Kong container level        lifecycle:          preStop:            exec:              command: ["kong", "quit", "-p", "/kong/prefix/path", "-t", "2700"]        # Graceful shutdown settings - Timeout in Kong container level      nodeSelector:        app: kong      tolerations:      - key: app        operator: Equal        value: kong        effect: NoSchedule      # Graceful shutdown settings - Timeout in pod level      terminationGracePeriodSeconds: 3600
另外需要注意的是,必须要部署至少2台kong-proxy Pod。原因是若只有1个kong-proxy Pod,当该Pod进入terminating状态时,此时通过K8s Service新进入的流量不会再打到这个Pod上,而是将其分配到新创建的Pod上;但是新创建的Pod需要启动时间,在running之前的这一段时间,服务会不可用,出现50X的错误。若有2台及以上的kong-proxy Pod的话,一个Pod进入terminating状态时,流量会立刻打到另一个可用的Pod上,待新的Pod创建完毕后,流量会再次负载到新Pod上。
经测试,在合理设置宽限时间的情况下,当Kong Pod进入终止状态时,该Pod上已有的长连接服务:如耗时的http请求、websocket服务均未受到影响;而新进入的请求流量会被K8s自动调度到正常运行的Pod上。直至长连接服务完成请求后,该Pod才会被终止掉;当HPA缩容或集群缩容时,Pod依然遵守该终止生命周期,因此依然能够优雅停止。

4.3. 平滑切换

平滑切换的要求是:首先流量的入口不能发生改变,流量要逐渐放行到Kong in K8s上,若发现异常,可以快速将流量切回Kong in ASG的架构上。K8s的Loadbalancer type Service就十分契合这个场景,其可以通过annotation,设置云上已有的SLB来暴露Pod服务。以阿里云上ACK中Loadbalancer type Service配置文件为例:
# 把kong-proxy pod加入到和Kong in ASG相同的虚拟服务器组里apiVersion: v1kind: Servicemetadata:  annotations:    # 指定已有的loadbalancer id    service.beta.kubernetes.io/alibaba-cloud-loadbalancer-id: lb-loadbalancer-id-that-currently-used    # 设置目标虚拟服务器组ID,及将流量转发到组里服务器的服务端口号    service.beta.kubernetes.io/alibaba-cloud-loadbalancer-vgroup-port: rsp-virtual-server-port:80    # 设置打向Pod上的流量权重    service.beta.kubernetes.io/alibaba-cloud-loadbalancer-weight: "10"    # 设置不要覆盖已有的监听配置    # service.beta.kubernetes.io/alicloud-loadbalancer-force-override-listeners: 'false'  labels:    app: kong-proxy    name: kong-proxy  name: kong-proxy  namespace: kong-proxyspec:  ports:  - name: http-proxy    port: 80    protocol: TCP    targetPort: 80  selector:    app: kong    name: kong-proxy  type: LoadBalancer

通过该方式将Kong Pod接入到已有SLB中提供服务,其架构如下所示。此时Kong in Asg和Kong in K8s两个架构可以在环境中双跑,架构的切换和发现问题时架构快速回滚的方案,就简化成了在SLB上切换目标服务器的权重。

图片


5. 总结

当前流利说已经完成业务网关Kong的K8s化。通过CronHPA,我们可以实现每日在业务高峰来临前提前扩容Kong Pod的实例;当K8s的计算资源不够时,能够通过云上的Cluster Auto Scaler自动扩展集群节点;Kong本身的配置或版本更新也更轻量化,结合已有的GitOps流程和CD系统,基础架构的同学只需要更新Dockerfile和相关的配置文件,然后git push即可触发自动构建镜像、自动业务不中断地滚动更新网关;通过Deployment控制器中提供的参数控制,可以灵活地定制Kong Pod滚动更新或回滚的策略。和原先相比,能够更轻松、更安全可靠地对网关层进行维护。

在将网关层K8s化的同时,流利说后端业务K8s也已经逐渐接入Istio;利用K8s LoadBalancer type Service和云上LB带来的优势,在未来我们计划将网关层的Kong平滑地替换成Istio Gateway,以提升整体链路的可观测性。

图片


继续滑动看下一个
流利说技术团队
向上滑动看下一个