使用 traefik 在 k8s 中代理作业应用服务

背景

传统的 HPC 环境一般仅开放登录节点,计算节点严格限制内网访问。所以在原先基于 Slurm 调度的 HPC 集群中,如果想方便用户启动一个类似 jupyter notebook 的网页服务并访问时,主流的做法是 Open OnDemand 的 Apache 监听端口并通过 Lua 脚本解析路径映射 + Nginx 转发的方式实现反向代理。而在最近在做 k8s 相关设计时,发现想实现作业服务转发选择相对会多不少。

简单 Vibe Research 一下主流方案有以下几种:

  • 基于 Ingress Controller(Ingress Nginx / Tarefik)
  • 基于 API 网关(APISIX / Kong)
  • 基于 Service Mesh(Istio)

Why Traefik

从需求本身出发,需要反向代理的目的仅仅为了节点 web 服务的转发和自定义路径映射。那么上面提到的很多相对较重的服务所具备的特性(负载均衡、强鉴权等)对我们都没有意义,从轻量优先的角度自然选择 Traefik 作为优先考虑的实现方式。

Traefik 有以下几个优势特性:

  • 开源 Go 语言编写
  • 配置热更新
  • 自带一个 Dashboard

What Is Tarefik

Traefik(发音为 “traffic ”)是一款现代化的 HTTP 反向代理和负载均衡器,可轻松部署微服务。

Traefik 会实时监听你的服务注册中心或编排器的 API,并自动动态生成路由,从而将你的微服务无缝连接到外部世界——完全无需你再进行任何额外的手动干预。

Quickstart

部署

国内环境按照官方文档的 helm 仓库配置部署会因为网络连接问题报错,可以 git clone 官方的 Traefik Helm Chart 项目进行部署。

1
2
3
4
git clone https://github.com/traefik/traefik-helm-chart.git
cd traefik-helm-chart
helm dependency update ./traefik
helm install traefik ./traefik -n traefik --create-namespace

注意 helm 版本兼容性(需要 v3.9.0+ 版本)

新建一个 values 配置,通过 NodePort 转发 dashboard(注意不同版本格式区别较大,这里使用的是 v3.6.7 的 traefik)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# my-values.yaml
# 1. 禁用默认 Dashboard 路由 (依然保持 false,因为我们走 NodePort)
ingressRoute:
dashboard:
enabled: false
# 2. 这里的参数保持不变
additionalArguments:
- "--api.insecure=true"
- "--api.dashboard=true"
# 3. 此处配置 Service 为 NodePort 类型
service:
enabled: true
type: NodePort
# 4. 端口配置
ports:
traefik:
port: 9000
expose:
default: true
# 指定一个固定的节点端口 (范围 30000-32767)
# 如果不指定,K8s 会随机分配一个
nodePort: 30900
protocol: TCP

编辑保存后安装应用:

1
2
3
4
# 首次安装
helm install traefik ./traefik -f my-values.yaml --namespace traefik --create-namespace
# 如果已经安装过,则直接更新配置
helm upgrade traefik ./traefik -f my-values.yaml --namespace traefik

本地访问服务器 30900 端口的 /dashboard 端点({node-ip}:30900/dashboard)即可进入 dashboard:

同时可以通过命令确认 web 端口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@mgt ~]# kubectl get svc traefik -n traefik -o yaml | grep -A 20 "ports:"
ports:
- name: traefik
nodePort: 30900
port: 8080
protocol: TCP
targetPort: traefik
- name: web
nodePort: 31376
port: 80
protocol: TCP
targetPort: web
- name: websecure
nodePort: 30960
port: 443
protocol: TCP
targetPort: websecure
selector:
app.kubernetes.io/instance: traefik-traefik
app.kubernetes.io/name: traefik
sessionAffinity: None
type: NodePort

代理应用服务

接下来就可以尝试转发 k8s 中的应用服务了,这里以 jupyter 为例。

作为 AI 平台我们一般期望使用 url 路径来隐藏端口,那么就需要修改 jupyter 启动参数中的 NotebookApp.base_url。路由规则的话用作业名称区分相对比较合理(暂时假定单个作业对应单个应用服务转发,如果存在多个服务则需要配置子路径)。

需要注意的问题是 k8s 中资源声明相对静态,而启动 jupyter 时已经需要指定具体的 base_url 了,所以要用到 Downward API 接口(参数定义和注入)来暴露作业名称给容器的启动命令。

你可以通过 fieldRef 从可用的 Pod 级字段传递信息。在 API 层面,Pod 的 spec 始终至少定义一个容器。你可以通过 resourceFieldRef 从可用的容器级字段传递信息。

环境变量声明:

1
2
3
4
5
6
7
8
 spec:
containers:
- args:
env:
- name: VC_JOB_NAME
valueFrom:
fieldRef:
fieldPath: metadata.labels['volcano.sh/job-name']

容器启动参数:

1
2
3
4
5
 spec:
containers:
- args:
- jupyter lab --ip=0.0.0.0 --port=8888 --no-browser --NotebookApp.token=''
--NotebookApp.base_url=/app/${VC_JOB_NAME}

启动之后还需要建两个资源和 traefik 路由匹配:

  • Service:ClusterIP,暴露端口
  • IngressRoute:匹配 PathPrefix 路由到 Service

IngressRoute 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: {{ jobName }}-route
namespace: {{ namespace }}
ownerReferences: # 随 Job 自动清理
- apiVersion: batch.volcano.sh/v1alpha1
kind: Job
name: {{ jobName }}
uid: {{ jobUID }}
controller: true
spec:
entryPoints:
- web
routes:
- kind: Rule
match: PathPrefix(`/app/{{ jobName }}`)
services:
- name: {{ jobName }}-svc
port: 8888

创建后就可以通过 http://{traefik_addr}:{traefik_port}/app/jupyter-test-9z7w9 直接访问 jupyter 服务了。

dashboard 中也可以观测到路由和服务的绑定等信息。