趁热记录下,给未来的自己
0 | 前言
前段时间,在项目上,遇到了这样一个业务场景:
我们提供一个应用开发平台,开发者可以在平台上部署自己的应用,部署方式可以是提供:
- 基于gradio的python代码
- 基于模板的html文件修改里面的src地址为用户自己提供的网址
平台会将以上代码打包成镜像,以容器方式对外提供服务,架构如下
那么,用户如果想访问应用 gradio-app1 的话,有哪些访问形式呢?
- 基于subpath,如: abc.com/gradio-app1...
- 基于subdomain,如:gradio-app1.abc.com/xxx
优劣对比:
优势 | 劣势 | |
---|---|---|
subpath | 1. 只需要在ingress里配置好相关的转发路径,无需额外引入其他组件; 2. 只需要配置一个域名解析即可,请求都可以通过subpath进行转发; | 1. 因为有subpath的存在,可能会和前端框架的路由产生冲突:如访问 abc.com/gradio-app1... 当请求到达gradio框架时,gradio返回给浏览器的下一跳路由是/yyy,那么浏览器访问的下一跳地址就是 abc.com/gradio-app1... 而这个地址因为缺少了subpath(gradio-app1),就无法将下一跳的请求转发给gradio-app1这个应用了 |
subdomain | 1. 没有/subpath,只有根路由/, 对前端框架的路由友好,兼容性更强, | 1. subdomain是根据用户行为动态生成的,无法做到提前配置DNS解析,需要实现动态域名解析,这是一个技术重点和难点 |
虽然,部分带前端页面的框架(如code-server,jupyterlab等),本身可以配置public path(即返回给浏览器的下一跳自动带上/gradio-app1/yyy),但还是有相当一部分的框架没有考虑这个特性,所以,subpath的应用场景完全依赖前端框架,导致适用面不广泛。
而基于subdomain的方式,没有subpath方案的这个block点,只需要解决动态域名解析的问题就行。因此本文要解决的问题就聚焦到:如何设计一套动态域名的 DNS 解析和转发。
1 | 方案选择
要实现动态域名解析(DDNS),其实很多DNS解析厂商会有对应的API可以去调用,那么实现逻辑是:
- 用户创建一个应用 gradio-app1;
- 业务后端通过API找DNS解析厂商申请新的域名A解析:gradio-app1.abc.com -> 特定IP;
- A解析添加成功后,返回创建应用成功给到前端用户;
这个方案,会存在几个问题:
- DDNS 依赖厂商API能力(有没有?快不快?稳不稳定?限不限流?解析的域名条数有没有上限?等等)
- 解析的条目和平台应用一一对应,随着应用的增多,解析条目也会日渐庞大,难于查找和管理
- 应用创建速度会因为多了一次外部请求而变慢,创建应用的成功率也可能会降低
因此,该方案只能算是解决了技术上有无的问题,但算不上一个好方案(算是兜底方案)。为了解决以上问题,这里我们提出了一个泛域名解析+nginx代理转发的实现方案:
大概思路:
- 创建一个nginx实例,通过ingress对外暴露;
- 将泛域名*.abc.com都解析到这个ingress绑定的外网IP上;
- 泛域名部分是一个二级泛域名,格式如:gradio-appx 和 static-appx,分别对应不同的APP类型;
- 在nginx-server模块配置转发规则:获取subdomain部分(如gradio-app1),然后将请求proxy-pass给gradio-app1.svc
- 注意,这里要求 gradio 应用对应的service资源名称要和subdomain保持一致(或者具有一一对应关系),这样就无需根据subdomain去查找对应的service名称和对应的k8s内部域名地址。
2 | 方案实施
这里nginx以k8s方式部署。
2.1| Deployment
这是一个Deployment资源,里面包含两个Container,一个是openresty的nginx,另一个是go-dnsmasq。
why openresty? OpenResty是一个基于Nginx的Web应用服务器,它整合了Nginx服务器和一些Lua模块,提供了更高级的功能和可扩展性。它的核心思想是使用Lua脚本语言扩展Nginx的功能,从而实现更复杂的Web应用开发。因为该方案需要在nginx里做一些业务逻辑的处理,需要使用到lua脚本对请求进行改写和转发,所以用openresty会更便捷。
why go-dnsmasq? 这是一个dns解析器,主要是将整个集群+node节点+etc/hosts的dns解析融合在一起代理给nginx,这样nginx可以实现IP、集群域名以及外部域名的解析。这里通过暴露127.0.0.1:53端口,对外提供服务。
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
component: nginx
name: nginx-domain-proxy-deploy
namespace: your_namespace
spec:
replicas: 1
selector:
matchLabels:
component: nginx
template:
metadata:
labels:
component: nginx
spec:
restartPolicy: Always
containers:
- name: nginx
image: "openresty/openresty:centos"
ports:
- name: http
containerPort: 80
protocol: TCP
volumeMounts:
- mountPath: /usr/local/openresty/nginx/conf/nginx.conf
name: config
subPath: nginx.conf
- name: dnsmasq
image: "janeczku/go-dnsmasq:release-1.0.7"
args:
- --listen
- "127.0.0.1:53"
- --default-resolver
- --append-search-domains
- --hostsfile=/etc/hosts
- --verbose
volumes:
- name: config
configMap:
name: nginx-domain-proxy-cm
2.2 | ConfigMap
这个ConfigMap主要定义了nginx.conf的配置。这个配置的前部分比较常规,是一些常规配置(具体的配置需要根据业务特点做适配,请勿直接搬运到生产环境):
worker_processes 4;
:这个指令设置了Nginx服务器的工作进程数量为4个。每个工作进程负责接收和处理客户端请求。error_log /error.log;
:这个指令指定了错误日志文件的路径为/error.log。Nginx会将出现的错误和异常信息写入该文件。events { ... }
:这个部分是Nginx服务器的事件模块配置。其中包含一些事件相关的指令,例如accept_mutex on;
表示开启了互斥锁,multi_accept on;
表示开启了多个并发连接的支持,use epoll;
表示使用epoll事件模型。http { ... }
:这个部分是Nginx服务器的HTTP模块配置。其中包含了一些全局的HTTP相关配置和指令。include mime.types;
:这个指令包含了一个外部文件mime.types
,用于定义文件类型和对应的媒体类型。default_type application/octet-stream;
:这个指令设置了默认的媒体类型为application/octet-stream
,即未知的二进制文件类型。log_format ...
:这里定义了几种日志格式,用于不同的日志记录。每个日志格式包含了一系列变量和对应的日志信息。access_log /access.log main;
:这个指令指定了访问日志文件的路径为/access.log,并使用名为"main"的日志格式记录访问日志。sendfile on;
:这个指令启用了使用sendfile系统调用来传输文件的功能。keepalive_timeout
:这个指令设置了HTTP keep-alive连接的超时时间。
这里,着重解释一下 server{ ... }
模块,主要实现的逻辑是根据请求的hots值和类型,将请求转发到不同的location块进行处理:静态资源的返回、代理请求的转发:
location / { ... }
:这个location块匹配任何URL路径,并且在请求匹配成功后执行定义的rewrite_by_lua指令。rewrite_by_lua ' ... '
: 这个指令定义了一个Lua脚本,在Nginx服务器上执行。这个脚本通过解析请求的host值,提取出"类型-服务.abc.com"的模式,并将提取的值存储在ngx.var变量中。根据类型的不同,执行不同的操作:- 如果类型为"s",则执行
ngx.exec("@static")
,将请求转发到@static location块。 - 如果类型为"g",则执行
ngx.exec("@gradio")
,将请求转发到@gradio location块。
- 如果类型为"s",则执行
location @static { ... }
:这个location块将处理被rewrite_by_lua指令转发到的静态请求。根据匹配到的服务,将其作为根目录下的子目录,并将请求的资源返回给客户端。location @gradio { ... }
:这个location块将处理被rewrite_by_lua指令转发到的gradio请求。将请求转发到名为$service和端口号7860的服务。
swift
apiVersion: v1
kind: ConfigMap
metadata:
labels:
component: nginx
name: nginx-domain-proxy-cm
namespace: your_namespace
data:
nginx.conf: |-
worker_processes 4;
error_log /error.log;
events {
accept_mutex on;
multi_accept on;
use epoll;
worker_connections 4096;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$time_local $remote_user $remote_addr $host $request_uri $request_method $http_cookie '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'$request_time $upstream_response_time "$upstream_cache_status"';
log_format browser '$time_iso8601 $cookie_km_uid $remote_addr $host $request_uri $request_method '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'$request_time $upstream_response_time "$upstream_cache_status" $http_x_requested_with $http_x_real_ip $upstream_addr $request_body';
log_format client '{"@timestamp":"$time_iso8601",'
'"time_local":"$time_local",'
'"remote_user":"$remote_user",'
'"http_x_forwarded_for":"$http_x_forwarded_for",'
'"host":"$server_addr",'
'"remote_addr":"$remote_addr",'
'"http_x_real_ip":"$http_x_real_ip",'
'"body_bytes_sent":$body_bytes_sent,'
'"request_time":$request_time,'
'"status":$status,'
'"upstream_response_time":"$upstream_response_time",'
'"upstream_response_status":"$upstream_status",'
'"request":"$request",'
'"http_referer":"$http_referer",'
'"http_user_agent":"$http_user_agent"}';
access_log /access.log main;
sendfile on;
keepalive_timeout 120s 100s;
keepalive_requests 500;
send_timeout 60000s;
client_header_buffer_size 4k;
proxy_ignore_client_abort on;
proxy_buffers 16 32k;
proxy_buffer_size 64k;
proxy_busy_buffers_size 64k;
proxy_send_timeout 60000;
proxy_read_timeout 60000;
proxy_connect_timeout 60000;
proxy_cache_valid 200 304 2h;
proxy_cache_valid 500 404 2s;
proxy_cache_key $host$request_uri$cookie_user;
proxy_cache_methods GET HEAD POST;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
server_tokens off;
client_max_body_size 50G;
add_header X-Cache $upstream_cache_status;
autoindex off;
resolver 127.0.0.1:53 ipv6=off;
server {
listen 80;
location / {
set $service '';
set $type '';
rewrite_by_lua '
local host = ngx.var.host
local m = ngx.re.match(host, "(s|g|p)-(.+).abc.com")
ngx.var.type = m[1]
ngx.var.service = m[2]
if ngx.var.type == "s" then
ngx.exec("@static")
elseif ngx.var.type == "g" then
ngx.exec("@gradio")
elseif ngx.var.type == "p" then
ngx.exec("@proxy")
end
';
}
location @static {
root /mnt/data/$service;
}
location @gradio {
proxy_pass http://$service:7860;
}
}
}
2.3 | Service
Service 资源比较常规,是 nginx deployment 对应的负载均衡 (注意 label 不要写错)。
yaml
apiVersion: v1
kind: Service
metadata:
labels:
component: nginx
name: nginx-domain-proxy-svc
namespace: your_namespace
spec:
type: ClusterIP
ports:
- name: http
port: 80
targetPort: http
selector:
component: nginx
2.4 | Ingress
这里是 nginx 的动态二级域名对应的 Ingress 配置,这里是监听了泛域名*.abc.com
, 并且配置了 TLS 证书
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-domain-proxy-ing
namespace: your_namespace
spec:
rules:
- host: '*.abc.com'
http:
paths:
- backend:
service:
name: nginx-domain-proxy-svc
port:
number: 80
path: /
pathType: Prefix
tls:
- hosts:
- '*.abc.com'
secretName: generic.abc.com-https
以上。