从 SSH 到 REST:Slack EMR 数据管道的安全现代化
到 2024 年,Slack 的数据平台已经积累了 700 多个基于 SSH 的任务操作器,用于编排关键数据管道:包括每天处理 TB 级数据的搜索索引任务,以及支撑商业智能分析的作业。每个任务都需要直接 SSH 访问生产环境中的 AWS Elastic MapReduce(EMR)集群。
这带来了巨大的安全暴露面,也阻碍了后续基础设施现代化。团队的目标很明确:彻底消除 SSH,将 700 多个任务迁移到基于 REST 的架构,并在 8 个数据区域完成迁移,同时保持零停机。
背景:为什么一开始会使用 SSH
Slack 的数据平台大约在 2017 年形成了一个直接而简单的模式:Airflow 作为数据管道编排器,需要在 EMR 集群上运行任务,而 SSH 是最直接的路径。
# 旧方式:简单,但问题不少
task = SSHOperator(
task_id='run_spark_job',
ssh_conn_id='emr_master',
command='spark-submit /path/to/job.py',
)
这种模式很快扩散到整个平台。不同团队为不同场景构建了自定义的 SSH 操作器。到了盘点阶段,生产环境中已有 700 多个任务通过这种方式运行,包括 MapReduce 作业、AWS CLI 命令和自定义 Python 脚本。
它能工作,但也带来了多方面问题。
SSH 的真实成本
潜在安全风险
- 直接 SSH 访问计算集群会扩大潜在攻击面。
- 在编排工作节点之间分发和轮换密钥会增加运维负担。
- 要实现细粒度审计,通常需要关联多个系统中的日志。
- 权限管理会变得复杂,往往需要专门的安全组和自定义配置。
运维问题
- 作业直接运行在 EMR master 节点上,而不是分布式执行,容易造成资源争用。
- Kubernetes Pod 重启时,SSH 连接会中断,任务可能失败。
- 长时间运行的任务可能变成“僵尸任务”:连接终止后仍继续执行。
- 当连接断开时,很难可靠判断任务到底成功还是失败。对于处理 TB 级数据的作业来说,这尤其棘手。
基础设施演进被阻塞
SSH 依赖也限制了更大的平台演进:
- 无法推进 Spark on Kubernetes 或 EMR on AWS Elastic Kubernetes Service(EKS),因为必须先消除 SSH 依赖。
- 无法完成将主账号中的最后一批 EMR 集群迁移到子账号的相关计划。该计划旨在通过子账号提升 AWS 基础设施的安全性与网络隔离能力。
- 无法实现更完善的作业监控与可观测性。
一个典型例子是搜索基础设施团队的管道:它每天从 TB 级数据中构建 Solr 搜索索引,支撑 Slack 的搜索功能。任何中断都可能影响大量用户的搜索质量,而该管道仍依赖基于 SSH 的任务提交方式。
REST 化作业提交的核心思路
SSH 的问题在于,它依赖直接的、有状态连接。当你 SSH 到一台机器并执行命令时,如果连接中断,例如 Kubernetes Pod 重启,命令可能继续运行、可能失败,也可能留下孤儿进程。客户端很难重新连接并可靠查询状态。
REST 模式则不同。现代计算引擎通常提供 HTTP API 来提交和管理任务,例如 YARN、Trino、Snowflake 等。基本流程是:
- 使用
POST提交作业请求,并获得作业 ID。 - 使用
GET根据作业 ID 查询状态,判断作业是运行中、已完成还是失败。 - 使用
DELETE在需要时取消作业。
这样,作业生命周期由服务端管理。即使客户端崩溃或重启,作业也可以继续运行,客户端恢复后仍能通过 ID 查询状态。
难点:不是所有任务都有现成 REST API
对于 Hadoop 工作负载,如 MapReduce、Spark、Hive,YARN 是资源管理器,并提供 REST API。Spark 可以使用 Livy REST API,Hive 可以使用 HiveServer2。
但问题在于,平台上还有 300 多个 CLI 型任务,会运行任意 Shell 命令,例如:
aws s3 synchadoop distcp- 自定义 Python 脚本
这些任务没有现成的 REST API。这部分是迁移中最棘手的环节。
团队评估过多个方案,包括:
- 构建自定义包装服务来远程执行命令。
- 使用 Ansible 或 Salt 等远程执行框架。
- 从零开始在 YARN 中创建新的作业类型。
但这些方案要么复杂度过高,要么需要自定义安全实现,要么会引入新的长期维护依赖。
关键突破:YARN Distributed Shell
最终的突破来自 YARN Distributed Shell。
这是 YARN 中一个相对不常被提到的功能,类名为:
org.apache.hadoop.yarn.applications.distributedshell.ApplicationMaster
它允许任意 Shell 脚本在正式的 YARN 容器中运行,并具备资源分配和生命周期管理能力。更关键的是,它已经是 YARN 的一部分,可以使用相同的 REST API,也不需要额外构建自定义安全层。
这使得 CLI 型任务也可以迁移到 REST 提交模式。
工作方式示例
1. 将命令上传到 S3
例如,将下面的脚本上传到 S3:
# command.sh
aws s3 sync /tmp/data/ s3://bucket/output/
2. 使用 Distributed Shell 配置提交到 YARN
{
"application-type": "MAPREDUCE",
"am-container-spec": {
"commands": {
"command": "{{JAVA_HOME}}/bin/java org.apache.hadoop.yarn.applications.distributedshell.ApplicationMaster ..."
},
"environment": {
"DISTRIBUTEDSHELLSCRIPTLOCATION": "s3://bucket/command.sh",
"DISTRIBUTEDSHELLSCRIPTLEN": "548",
"DISTRIBUTEDSHELLSCRIPTTIMESTAMP": "1768529627000"
}
}
}
3. 由 YARN 分配容器、下载脚本并执行
YARN 会负责:
- 资源限制,例如内存和 vCore。
- 容器隔离。
- 重试和容错。
- 干净地取消任务。
- 通过 YARN UI 提供日志。
这一架构选择使所有基于 SSH 的任务迁移成为可能。不只是 Hadoop 工作负载,任意 Shell 命令都可以放入 YARN 容器中执行,包括 aws s3 sync、hadoop distcp 和自定义 Python 脚本。
迁移带来的架构变化
迁移前,Airflow 任务通过 SSH 连接到 EMR master 节点并直接执行命令。这种方式依赖连接状态,也让 master 节点承担了额外执行压力。
迁移后,Airflow 通过 REST API 提交作业,获得作业 ID,再基于 ID 查询状态或取消任务。作业运行在由 YARN 管理的容器中,生命周期管理、资源隔离、日志和取消逻辑都交由服务端处理。
这种变化的核心不是简单地把连接协议从 SSH 换成 HTTP,而是把“远程登录机器执行命令”的模式,改造成“提交可管理的作业”的模式。
对工程团队的启示
这个案例说明,历史上“简单可用”的工程路径,随着规模扩大可能会变成安全、运维和平台演进的负担。
Slack 的迁移重点并不是为了追求新技术,而是围绕几个明确目标展开:
- 降低直接 SSH 访问生产计算集群带来的安全暴露面。
- 减少密钥分发、轮换和权限管理的复杂度。
- 改善长任务的状态管理、取消和日志能力。
- 为 Spark on Kubernetes、EMR on EKS 以及账号隔离等后续基础设施演进扫清障碍。
其中最值得关注的是 YARN Distributed Shell 的使用:它复用了已有的 YARN 能力,而不是重新构建远程执行系统。对于仍有大量脚本型数据任务的团队,这类思路具有参考价值。
