管理多个 Trino 集群是一项繁琐且易错的工作。不同业务团队对计算资源、数据源连接器、JVM 参数的需求各不相同,导致配置文件急剧膨胀。传统的手动修改 Helm values.yaml
并执行 helm upgrade
的方式,不仅效率低下,而且缺乏审计、回滚和环境一致性的保障。当集群数量超过个位数时,这种模式的运维成本便无法接受。
我们的目标是构建一个平台,允许数据分析师或平台用户通过一个简单的接口来“声明”他们所需的 Trino 集群规格,后续的资源配置、部署、更新和销毁应完全自动化。这里的核心挑战在于,如何将动态的、来自用户输入的集群定义,转化为 Kubernetes 环境中静态的、由 GitOps 工具管理的声明式配置。
经过评估,我们确定了如下技术栈:
- MongoDB: 作为动态集群配置的“单一事实来源”。其灵活的文档模型非常适合存储结构复杂的 Trino 集群定义,包括内嵌的连接器配置、资源请求等。
- Ansible: 充当编排引擎。它将作为“翻译官”,定期从 MongoDB 拉取集群定义,利用其强大的模板能力(Jinja2)生成对应的 Kubernetes 清单和 Argo CD Application 定义。
- Argo CD: 作为 GitOps 的核心执行者。它只关心 Git 仓库中的状态,并确保 Kubernetes 集群的状态与 Git 中定义的保持一致。
- Trino on Kubernetes: 使用官方或社区维护的 Helm Chart 进行部署,这是目前在 K8s 上运行 Trino 的主流方式。
整个工作流的设计必须保证幂等性和声明式特性。无论 Ansible playbook 运行多少次,对于一个没有变化的 MongoDB 文档,其在 Git 仓库中生成的配置都应该是稳定不变的。
数据模型:在 MongoDB 中定义 Trino 集群
一切始于一个健壮的数据模型。我们需要在 MongoDB 的一个 collection(例如 trino_clusters
)中定义一个文档结构,该结构必须足以描述一个完整的 Trino 集群。
一个真实项目中的文档结构可能如下所示。这里的关键在于将配置分层,并且允许定义一个动态的 connectors
数组,每个元素都直接对应一个 Trino 连接器配置文件。
{
"_id": { "$oid": "653a1a7b8e1f4a1e9c5d8e1a" },
"clusterId": "analytics-prod",
"owner": "data_analytics_team",
"enabled": true,
"trinoVersion": "426",
"clusterConfig": {
"coordinator": {
"replicas": 1,
"resources": {
"requests": { "cpu": "4", "memory": "16Gi" },
"limits": { "cpu": "8", "memory": "24Gi" }
},
"jvm": {
"maxHeapSize": "20G"
}
},
"worker": {
"replicas": 5,
"resources": {
"requests": { "cpu": "8", "memory": "32Gi" },
"limits": { "cpu": "8", "memory": "32Gi" }
},
"jvm": {
"maxHeapSize": "28G"
}
}
},
"properties": {
"query": {
"max-memory": "100TB",
"max-memory-per-node": "26GB"
},
"node": {
"environment": "production"
}
},
"connectors": [
{
"name": "hive-prod",
"properties": {
"connector.name": "hive",
"hive.metastore.uri": "thrift://hive-metastore.data.svc.cluster.local:9083",
"hive.config.resources": "/etc/trino/hadoop/core-site.xml,/etc/trino/hadoop/hdfs-site.xml"
}
},
{
"name": "mysql-rds",
"properties": {
"connector.name": "mysql",
"connection-url": "jdbc:mysql://prod-rds.ap-east-1.rds.amazonaws.com:3306",
"connection-user": "trino_user",
"connection-password": "{{ MYSQL_RDS_PASSWORD }}"
}
},
{
"name": "tpch",
"properties": {
"connector.name": "tpch"
}
}
],
"lastUpdated": { "$date": "2023-10-27T00:00:00.000Z" }
}
这个模型有几个设计考量:
-
clusterId
: 既是 MongoDB 的业务主键,也将用作 Kubernetes 中的 namespace 和 Argo CD Application 的名称,保证全局唯一。 -
enabled
: 一个布尔标记,用于软删除或禁用集群。我们的 Ansible 脚本将只处理enabled: true
的文档。 -
connectors
: 这是一个对象数组,每个对象包含连接器名称 (name
) 和一个键值对的properties
。这种结构可以直接映射到 Trino 的catalog
目录下的配置文件。 - 密码处理:注意
mysql-rds
连接器中的connection-password
。我们没有硬编码密码,而是使用了一个占位符{{ MYSQL_RDS_PASSWORD }}
。这暗示我们的自动化流程需要与某种 Secret 管理系统(如 HashiCorp Vault 或 Kubernetes Secrets)集成。在实际生成配置文件时,Ansible 会负责将这个占位符替换为真实的 secret 值。
Ansible 作为编排核心:从 MongoDB 到 Git
Ansible 的角色是周期性地执行一个 playbook,完成从读数据库到写 Git 的全过程。这个 playbook 通常由 CI/CD 工具(如 Jenkins、GitLab CI)或一个 Kubernetes CronJob 来调度执行。
项目结构
一个典型的 Ansible 项目结构如下:
.
├── ansible.cfg
├── inventory
├── playbook.yml
├── roles
│ └── trino_cluster_generator
│ ├── tasks
│ │ └── main.yml
│ └── templates
│ ├── argo-app.yaml.j2
│ ├── catalog
│ │ └── connector.properties.j2
│ └── values.yaml.j2
└── vars
└── secrets.yml
核心 Playbook 逻辑 (playbook.yml
)
---
- name: Generate and sync Trino cluster configurations for GitOps
hosts: localhost
connection: local
gather_facts: false
vars:
gitops_repo_path: "/path/to/local/clone/of/gitops-repo"
mongodb_uri: "mongodb://user:[email protected]:27017/trino_config_db"
tasks:
- name: Ensure GitOps repo path exists
ansible.builtin.file:
path: "{{ gitops_repo_path }}/trino-clusters"
state: directory
mode: '0755'
- name: Fetch active Trino cluster definitions from MongoDB
community.mongodb.mongodb_info:
uri: "{{ mongodb_uri }}"
database: "trino_config_db"
collection: "trino_clusters"
filter:
enabled: true
register: mongo_results
# 在真实项目中,错误处理至关重要
# 如果数据库连接失败,整个流程应该立即中止并告警
failed_when: mongo_results.failed
- name: Include the generation role for each cluster
ansible.builtin.include_role:
name: trino_cluster_generator
tasks_from: main.yml
loop: "{{ mongo_results.actions[0].documents }}"
loop_control:
loop_var: cluster_definition
- name: Commit and push changes to GitOps repository
block:
- name: Check for changes in the git repository
ansible.builtin.command: git status --porcelain
args:
chdir: "{{ gitops_repo_path }}"
register: git_status
changed_when: git_status.stdout != ""
- name: Commit and push if there are changes
when: git_status.changed
block:
- name: Add all changes
ansible.builtin.command: git add .
args:
chdir: "{{ gitops_repo_path }}"
- name: Commit changes
ansible.builtin.command: "git commit -m 'Automated sync of Trino cluster definitions'"
args:
chdir: "{{ gitops_repo_path }}"
- name: Push to remote
ansible.builtin.command: git push origin main
args:
chdir: "{{ gitops_repo_path }}"
rescue:
- name: Git operation failed
ansible.builtin.fail:
msg: "Failed to commit or push to GitOps repo. Please check credentials and repository state."
核心生成逻辑 (roles/trino_cluster_generator/tasks/main.yml
)
这个 role 是整个流程的核心,它接收 loop
传来的单个 cluster_definition
变量,并为其生成所有必需的文件。
---
# This task file is executed for each document from MongoDB.
# The 'cluster_definition' variable holds the current document.
- name: Define cluster-specific paths and variables
ansible.builtin.set_fact:
cluster_path: "{{ gitops_repo_path }}/trino-clusters/{{ cluster_definition.clusterId }}"
# 这里的 secret 查找逻辑需要根据实际的 secret management 工具进行实现
# 例如,使用 HashiCorp Vault lookup plugin
# mysql_rds_password: "{{ lookup('hashi_vault', 'secret=kv/data/trino:mysql-rds token={{ vault_token }}') }}"
- name: Create directory for the cluster
ansible.builtin.file:
path: "{{ cluster_path }}"
state: directory
mode: '0755'
- name: Generate Helm values.yaml from template
ansible.builtin.template:
src: templates/values.yaml.j2
dest: "{{ cluster_path }}/values.yaml"
mode: '0644'
- name: Create catalog directory for connectors
ansible.builtin.file:
path: "{{ cluster_path }}/catalog"
state: directory
mode: '0755'
- name: Generate connector properties files
ansible.builtin.template:
src: templates/catalog/connector.properties.j2
dest: "{{ cluster_path }}/catalog/{{ item.name }}.properties"
mode: '0644'
loop: "{{ cluster_definition.connectors }}"
loop_control:
loop_var: item
# 关键点:当连接器需要密码时,这里的 item 变量包含了占位符
# 模板文件内部需要有逻辑来处理这个占位符的替换
- name: Generate Argo CD Application manifest
ansible.builtin.template:
src: templates/argo-app.yaml.j2
dest: "{{ gitops_repo_path }}/trino-applications/{{ cluster_definition.clusterId }}-app.yaml"
mode: '0644'
Jinja2 模板的威力 (templates/values.yaml.j2
)
这是将 MongoDB 文档翻译为 Trino Helm Chart 的 values.yaml
的关键。
# This file is auto-generated by Ansible. DO NOT EDIT MANUALLY.
# Cluster ID: {{ cluster_definition.clusterId }}
# Owner: {{ cluster_definition.owner }}
server:
workers: {{ cluster_definition.clusterConfig.worker.replicas }}
coordinator:
# ... 其他 coordinator 配置 ...
resources:
requests:
cpu: "{{ cluster_definition.clusterConfig.coordinator.resources.requests.cpu }}"
memory: "{{ cluster_definition.clusterConfig.coordinator.resources.requests.memory }}"
limits:
cpu: "{{ cluster_definition.clusterConfig.coordinator.resources.limits.cpu }}"
memory: "{{ cluster_definition.clusterConfig.coordinator.resources.limits.memory }}"
config:
jvm:
maxHeapSize: "{{ cluster_definition.clusterConfig.coordinator.jvm.maxHeapSize }}"
properties:
query.max-memory: "{{ cluster_definition.properties.query['max-memory'] }}"
query.max-memory-per-node: "{{ cluster_definition.properties.query['max-memory-per-node'] }}"
node.environment: "{{ cluster_definition.properties.node.environment }}"
worker:
# ... 其他 worker 配置 ...
resources:
requests:
cpu: "{{ cluster_definition.clusterConfig.worker.resources.requests.cpu }}"
memory: "{{ cluster_definition.clusterConfig.worker.resources.requests.memory }}"
limits:
cpu: "{{ cluster_definition.clusterConfig.worker.resources.limits.cpu }}"
memory: "{{ cluster_definition.clusterConfig.worker.resources.limits.memory }}"
config:
jvm:
maxHeapSize: "{{ cluster_definition.clusterConfig.worker.jvm.maxHeapSize }}"
# 这是最巧妙的部分:将 catalog 配置文件作为额外的 volume mount 进去
# Helm chart 需要支持 `additionalVolumes` 和 `additionalVolumeMounts`
# 并且支持通过 `configMaps` 来创建 catalog 文件
additionalCatalogs:
{% for connector in cluster_definition.connectors %}
{{ connector.name }}.properties: |
{% for key, value in connector.properties.items() %}
{{ key }}={{ value }}
{% endfor %}
{% endfor %}
Argo CD Application 模板 (templates/argo-app.yaml.j2
)
最后,我们需要为每个 Trino 集群生成一个 Argo CD Application
资源。这个资源告诉 Argo CD 去哪里找配置(Git repo),以及如何部署(Helm)。
# This file is auto-generated by Ansible. DO NOT EDIT MANUALLY.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: trino-{{ cluster_definition.clusterId }}
namespace: argocd
# 添加 finalizer 确保在删除 Application 时,关联的 K8s 资源也会被清理
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: 'https://github.com/your-org/gitops-repo.git'
targetRevision: main
path: 'trino-clusters/{{ cluster_definition.clusterId }}'
helm:
valueFiles:
- values.yaml
destination:
server: 'https://kubernetes.default.svc'
namespace: trino-{{ cluster_definition.clusterId }}
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
这个 Application Manifest 定义了一个独立的 Trino 应用,它的所有配置都源自 Git 仓库中由 Ansible 生成的特定路径。CreateNamespace=true
确保每个集群都部署在自己的命名空间中,实现了良好的隔离。
整体架构与数据流
现在,我们可以用一个流程图来串联起所有组件。
graph TD A[用户/API 更新 MongoDB] -->|写入集群定义| B(MongoDB: trino_clusters collection) subgraph "CI/CD or K8s CronJob" C[Ansible Playbook 执行] end B -->|定时拉取| C subgraph "Ansible Orchestration" C --> D{为每个 enabled:true 的文档...} D --> E[使用 Jinja2 模板生成] E --> F[1. Helm values.yaml] E --> G[2. Connector.properties] E --> H[3. ArgoCD Application Manifest] end H --> I(GitOps Repo: /trino-applications) F & G --> J(GitOps Repo: /trino-clusters) J -->|git push| K[Git Remote Repository] I -->|git push| K subgraph "Argo CD Sync" L[Argo CD Controller] end K -->|Webhook 或轮询| L L -->|发现配置变更| M{应用 Application Manifest} M --> N[在 K8s 中创建/更新 Trino Namespace 和资源] subgraph "Kubernetes Cluster" N end
局限性与未来迭代方向
这套基于 Ansible 的 “翻译” 模式,虽然实现相对简单且功能强大,但在生产环境中也暴露出一些需要考量的点。
首先,Ansible 本身是无状态的。删除操作需要额外的逻辑。如果一个 MongoDB 文档的 enabled
字段从 true
变为 false
,或者文档被直接删除,当前的 playbook 并不会自动清理 Git 仓库中对应的文件。需要实现一个 “清理” 逻辑,对比 MongoDB 中的 clusterId
列表和 Git 仓库中的目录列表,删除那些不再存在的集群配置。
其次,整个流程的触发是周期性的。从用户在 MongoDB 中修改配置,到最终在 Kubernetes 中生效,存在一个延迟(Ansible playbook 的执行周期 + Argo CD 的同步周期)。对于需要秒级响应的场景,可能需要引入事件驱动机制,例如使用 MongoDB Change Streams 来触发一个 webhook,直接调用 Ansible Runner。
最后,随着逻辑变得更复杂(例如,需要根据集群负载动态调整 worker 数量并写回 MongoDB),纯 Ansible 模式可能会变得笨重。一个更云原生的替代方案是开发一个自定义的 Kubernetes Operator。这个 Operator 可以直接 watch 一个 CRD (Custom Resource Definition),例如 kind: TrinoCluster
,然后在其调谐循环(Reconcile Loop)中完成所有配置生成和应用逻辑。这种方式省去了 MongoDB 和 Ansible,将“单一事实来源”直接置于 Kubernetes API 之上,但开发和维护成本显著高于当前的实现。
尽管存在这些权衡,对于中等规模的平台工程团队而言,使用成熟的工具如 Ansible 和 MongoDB 组合来驱动 GitOps 流程,是在开发效率和系统健壮性之间取得的一个极佳平衡。它将复杂的 Trino 配置管理问题,分解为了一个清晰、可审计、且高度自动化的数据转换与同步任务。