我们面临的第一个问题,并非技术选型,而是运维流程的崩溃。业务增长带来了越来越多的独立租户,每个租户背后都是一套独立的搜索集群,最初用一个集中式的 Nginx Ingress 做反向代理。每当新租户上线或旧租户配置变更,就意味着一次 nginx.conf
的修改、测试,以及一次心惊胆战的 nginx -s reload
。这个过程不仅容易出错,而且严重拖慢了业务交付速度。我们需要一个系统,能将路由规则的管理,像应用程序代码一样,通过版本控制和自动化流程进行交付,彻底消除手动干预。
初步构想很简单:将路由配置从网关实例中剥离出来,使其成为可独立部署的“资产”。Kubernetes 的声明式 API 给了我们灵感,而 GitOps 则是实现这一构想的最佳实践。技术栈的轮廓逐渐清晰:在 Azure Kubernetes Service (AKS) 上,使用 Envoy Proxy 作为高性能的数据平面处理流量,并利用 Argo CD 监听一个专门存放路由规则的 Git 仓库,实现配置的自动同步。这样,路由变更就简化成了一次 Git Pull Request。
阶段一:静态 Envoy 部署的基线
在引入动态化之前,必须先建立一个稳定、可预测的静态部署基线。这里的核心是构建一个包含基础配置的 Envoy 镜像,并在 AKS 中以标准 Deployment 的形式运行起来。
我们的 Envoy 配置没有太多花哨的东西,它只做三件事:监听 8080 端口,设置基本的管理接口,以及定义一个静态路由,将所有流量转发到一个预定义的后端服务。
envoy.yaml
配置文件如下,这是我们将打包进 Docker 镜像的基础:
# envoy.yaml - 静态基础配置
admin:
access_log_path: /dev/stdout
address:
socket_address:
address: 0.0.0.0
port_value: 9901
static_resources:
listeners:
- name: listener_0
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
access_log:
- name: envoy.access_loggers.stdout
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
http_filters:
- name: envoy.filters.http.router
typed_config: {}
route_config:
name: local_route
virtual_hosts:
- name: backend_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
# 这是一个硬编码的路由,用于验证基础设置
cluster: "fallback_service"
clusters:
- name: fallback_service
connect_timeout: 0.25s
type: STRICT_DNS
dns_lookup_family: V4_ONLY
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: fallback_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
# 在k8s环境中,这通常是另一个服务的SVC name
address: "some-default-backend.default.svc.cluster.local"
port_value: 80
对应的 Dockerfile 极为简单,仅仅是把这个配置文件复制到官方镜像中。
# Dockerfile
FROM envoyproxy/envoy:v1.24.0
COPY envoy.yaml /etc/envoy/envoy.yaml
# 暴露监听端口和管理端口
EXPOSE 10000 9901
在 AKS 中部署它,也只是标准的 Kubernetes 资源清单。
# envoy-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: envoy-search-gateway
spec:
replicas: 2
selector:
matchLabels:
app: envoy-search-gateway
template:
metadata:
labels:
app: envoy-search-gateway
spec:
containers:
- name: envoy
image: your-acr.azurecr.io/envoy-search-gateway:1.0.0 # 替换为你的ACR镜像地址
ports:
- containerPort: 10000
name: http
- containerPort: 9901
name: admin
---
apiVersion: v1
kind: Service
metadata:
name: envoy-search-gateway-svc
spec:
selector:
app: envoy-search-gateway
ports:
- protocol: TCP
port: 80
targetPort: 10000
type: LoadBalancer # 在Azure上会自动创建一个公网IP和负载均衡器
到这一步,我们拥有了一个功能正常但完全僵化的网关。任何路由变更都需要重新构建镜像、推送、更新 Deployment,这与我们最初的目标背道而驰。
阶段二:引入动态配置与 GitOps
真正的挑战在于如何让 Envoy 在不重启、不重新部署的情况下更新其路由规则。Envoy 的 xDS (Discovery Service) API 是为此而生的。完整的 xDS 需要一个复杂的控制平面,但在我们的场景下,可以采用一种更轻量级的方案:**基于文件的 xDS (File-based Discovery)**。
Envoy 可以监控文件系统中的特定文件,当文件内容发生变化时,它会自动、平滑地加载新的配置。这个机制与 Kubernetes ConfigMap 的能力完美契合:我们可以将路由配置存储在 ConfigMap 中,然后将其作为文件挂载到 Envoy Pod 内部。当 ConfigMap 更新时,Kubernetes 会自动更新挂载的文件,从而触发 Envoy 的热重载。
整个流程的架构如下:
graph TD subgraph Git Repository ["Git Repo (Routing Configs)"] A[routes/tenant-a.yaml] B[routes/tenant-b.yaml] end subgraph Azure DevOps/GitHub Actions C[CI Pipeline] --> D{Combine YAMLs}; D --> E[Push to K8s Manifest Repo]; end subgraph Kubernetes Manifest Repo F[configmaps/routes-configmap.yaml] end subgraph AKS Cluster G(Argo CD) -- watches --> F; G -- updates --> H(ConfigMap: envoy-routes); I(Envoy Pod) -- mounts --> H; I -- reads file --> J[RDS/CDS Files]; J -- triggers hot-reload --> K(Envoy Runtime); end UserRequest -- traffic --> K A --> C; B --> C; F --> G;
改造 Envoy 配置以支持文件发现
首先,修改 envoy.yaml
,将 route_config
和 clusters
的定义从静态改为动态,指向文件系统中的路径。
# envoy.yaml - 改造为动态配置
admin:
# ... (同上)
static_resources:
listeners:
# ... (listener_0 定义同上)
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
# ... (HttpConnectionManager 定义同上)
# RDS (Route Discovery Service) 指向一个配置文件
rds:
config_source:
path: "/etc/envoy/routes/routes.yaml" # 关键:指向路由配置文件
route_config_name: "dynamic_routes"
# ...
clusters:
# CDS (Cluster Discovery Service) 指向一个配置文件
- name: cds_cluster
type: EDS
eds_cluster_config:
eds_config:
path: "/etc/envoy/clusters/clusters.yaml" # 关键:指向集群配置文件
# 注意:静态的 clusters 部分被移除了,完全依赖CDS
这个新的 envoy.yaml
不再包含任何具体的路由或上游集群信息,它只告诉 Envoy 去哪里 找 这些信息。
创建路由和集群的 ConfigMap
现在,我们将为租户 tenant-a
创建它的路由和集群定义。这些将是存在于 Git 仓库中的独立文件。
routes.yaml
(用于 RDS):
# routes.yaml - 动态路由定义
resources:
- "@type": type.googleapis.com/envoy.config.route.v3.RouteConfiguration
name: "dynamic_routes"
virtual_hosts:
- name: "tenant_services"
domains: ["*"]
routes:
# 规则一:根据Header路由到租户A的搜索服务
- match:
prefix: "/search"
headers:
- name: "x-tenant-id"
exact_match: "tenant-a"
route:
cluster: "tenant_a_search_cluster"
timeout: 5s
retry_policy:
retry_on: "5xx"
num_retries: 2
per_try_timeout: 1.5s
clusters.yaml
(用于 CDS):
# clusters.yaml - 动态集群定义
resources:
- "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster
name: "tenant_a_search_cluster"
connect_timeout: 1s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: "tenant_a_search_cluster"
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: "tenant-a-search-svc.tenant-a.svc.cluster.local" # 指向租户A的 Kubernetes Service
port_value: 9200
# 添加健康检查,这是生产环境必须的
health_checks:
- timeout: 1s
interval: 10s
unhealthy_threshold: 3
healthy_threshold: 2
http_health_check:
path: "/_cluster/health"
expected_statuses:
- start: 200
end: 299
然后,在我们的 Kubernetes 清单仓库中,创建对应的 ConfigMap。
# envoy-dynamic-configs.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: envoy-routes-config
data:
routes.yaml: |
resources:
- "@type": type.googleapis.com/envoy.config.route.v3.RouteConfiguration
name: "dynamic_routes"
# ... (routes.yaml 内容)
---
apiVersion: v1
kind: ConfigMap
metadata:
name: envoy-clusters-config
data:
clusters.yaml: |
resources:
- "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster
name: "tenant_a_search_cluster"
# ... (clusters.yaml 内容)
更新 Deployment 以挂载 ConfigMaps
最后一步是修改 envoy-deployment.yaml
,将这两个 ConfigMap 挂载到 Envoy Pod 的指定路径。
# envoy-deployment.yaml - 更新后
apiVersion: apps/v1
kind: Deployment
metadata:
name: envoy-search-gateway
spec:
replicas: 2
selector:
matchLabels:
app: envoy-search-gateway
template:
metadata:
labels:
app: envoy-search-gateway
spec:
containers:
- name: envoy
image: your-acr.azurecr.io/envoy-search-gateway:2.0.0 # 使用包含动态配置的镜像
ports:
# ...
volumeMounts:
- name: envoy-routes-volume
mountPath: /etc/envoy/routes
- name: envoy-clusters-volume
mountPath: /etc/envoy/clusters
volumes:
- name: envoy-routes-volume
configMap:
name: envoy-routes-config
- name: envoy-clusters-volume
configMap:
name: envoy-clusters-config
配置 Argo CD
现在,所有 Kubernetes 资源都已定义完毕。我们在 Argo CD 中创建一个 Application,指向存放这些清单的 Git 仓库。
# argocd-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: envoy-gateway
namespace: argocd
spec:
project: default
source:
repoURL: 'https://github.com/your-org/k8s-manifests.git' # 你的清单仓库
path: 'gateways/search'
targetRevision: HEAD
destination:
server: 'https://kubernetes.default.svc'
namespace: 'gateway-system'
syncPolicy:
automated:
prune: true
selfHeal: true
部署完成后,整个系统就活了。
阶段三:真实世界的考验与优化
理论上完美的系统,在生产环境中总会遇到意想不到的问题。
问题一:配置验证
一个常见的错误是,开发人员提交了一个有语法错误的 YAML 路由文件。Argo CD 会忠实地同步这个错误的 ConfigMap,导致 Envoy 加载失败,新路由无法生效,并在日志中产生大量错误。虽然 Envoy 不会因此崩溃,但这是一个潜在风险。
解决方案:CI 阶段的静态验证。我们在 CI 流水线中增加了一个步骤,在合并 Pull Request 之前,使用 envoy --mode validate
命令来检查配置文件的语法和基本逻辑。
# ci-validation-script.sh
# 假设这是在CI Runner中运行
# 下载一个临时的envoy二进制文件
docker cp $(docker create envoyproxy/envoy:v1.24.0):/usr/local/bin/envoy ./envoy
# 准备一个临时的、包含所有配置引用的根配置文件
cat > ./temp-envoy-validate.yaml <<EOF
# (包含 admin, static_resources, listeners 的基础配置)
# ...并引用本地文件路径的 rds 和 cds
rds:
config_source:
path: "./routes.yaml"
cds:
path: "./clusters.yaml"
# ...
EOF
# 运行验证
./envoy --mode validate -c ./temp-envoy-validate.yaml --service-cluster validation_cluster --service-node validation_node
if [ $? -ne 0 ]; then
echo "Envoy configuration validation failed!"
exit 1
fi
echo "Envoy configuration is valid."
这个脚本确保了任何进入主分支的配置都是 Envoy 可以解析的。
问题二:可观测性
系统运行后,我们如何知道路由是否正常工作?哪个租户的请求最多?哪个后端的延迟最高?
解决方案:集成 Prometheus 和 Grafana。Envoy 内置了强大的统计能力,可以轻松暴露 Prometheus 指标。只需在 envoy.yaml
的 admin
部分增加配置。
# envoy.yaml - 增加Prometheus指标
admin:
access_log_path: /dev/stdout
address:
socket_address:
address: 0.0.0.0
port_value: 9901
# 新增的Prometheus端点
prometheus_scraping:
path: "/stats/prometheus"
# 限制指标数量,防止打爆Prometheus
used_stats_matcher:
inclusion_list:
patterns:
- "cluster.*.upstream_rq_*"
- "listener.*.downstream_cx_*"
同时,在 Deployment 中添加 annotations,让 AKS 的 Prometheus Operator 能够自动发现并抓取这些指标。
# envoy-deployment.yaml - 添加Prometheus annotations
template:
metadata:
labels:
app: envoy-search-gateway
annotations:
prometheus.io/scrape: "true"
prometheus.io/path: "/stats/prometheus"
prometheus.io/port: "9901"
通过 Grafana 仪表盘,我们可以清晰地看到每个 cluster
(即每个租户)的请求量、成功率、P99 延迟等关键 SLI 指标,为容量规划和故障排查提供了数据支持。
当前方案的局限与未来演进
这个基于文件和 ConfigMap 的 xDS 实现,在租户数量达到几百个的规模下工作得相当不错。它简单、可靠,并且完美地融入了我们现有的 GitOps 工作流。
然而,它的局限性也很明显。当租户数量增长到数千级别,将所有路由和集群定义塞进一两个巨大的 ConfigMap 中,会触及 Kubernetes 对单个对象大小的限制(通常是 1MB)。此外,每次微小的变更都需要 Argo CD 同步整个 ConfigMap,这在规模扩大后可能会有效率问题。
下一个演进方向,是构建一个轻量级的 Go xDS 控制平面。这个控制平面将不再依赖 ConfigMap,而是直接监听 Kubernetes API(例如,通过 CRD 定义路由规则),或者从一个专门的数据库(如 etcd 或 Consul)中读取配置,并通过 gRPC 将配置流式传输给 Envoy 实例。这将彻底解决规模化问题,并允许我们实现更复杂的逻辑,比如基于后端健康状况的动态权重调整。但这将是另一个阶段的工程挑战了。