知乎云原生调度系统实践

360影视 日韩动漫 2025-04-18 15:54 2

摘要:导读调度系统是编写、安排与监控工作流的平台。知乎调度系统架构经历了四轮演进,转向云原生的调度系统,在演进过程中经历了一些问题,也沉淀了很多经验,本文将带领我们一探究竟。

导读调度系统是编写、安排与监控工作流的平台。知乎调度系统架构经历了四轮演进,转向云原生的调度系统,在演进过程中经历了一些问题,也沉淀了很多经验,本文将带领我们一探究竟。

文章主要包括以下五大部分:

1. 知乎调度系统简介

2. 架构演进过程

3. 关键技术

4. 主要成果

5. 问答环节

分享嘉宾|黄巍楠 知乎 调度系统负责人

编辑整理|曾晓辉

内容校对|李瑶

出品社区|DataFun

01

知乎调度系统简介

1. 调度系统的定义

首先来介绍一下什么是调度系统。以 Airflow 给出的定义作为参考,调度系统是一个编写、安排和监控工作流的平台。

在大数据领域,调度系统是核心的基础设施之一,涉及多个角色和场景。对于数仓开发人员,每天依赖调度系统来构建数据生产链路,执行 ETL 任务(如 HIVE、SPARK SQL)。对于算法人员,需要调度系统提供数据准备、特征工程的上游数据。调度系统也用于模型训练和预测任务,如 TensorFlow/PyTorch 训练任务的定时执行。对于业务运营和产品人员,依赖调度系统产出的指标数据,用于数据大屏、BI 报表。对于研发人员,需要调度系统执行离线任务(如日志归档、批量任务处理),运行Shell/Python 脚本完成数据迁移、批量任务、文件处理等。

在知乎,调度系统上运行的任务类型包括:数据同步类任务、SQL 任务(如 HIVE、Spark_SQL)以及代码开发任务(如 SPARK、PYTHON、SHELL)。

调度系统每天拉起约 45000 个调度任务实例,调度系统需要具备依赖管理、任务重试、资源调度、任务并发、失败告警等能力,以确保各个角色的需求得到满足。

2. 调度系统的核心目标

调度系统的核心目标是保证调度作业的稳定性和正确性,确保调度作业能够定时拉起,并按照任务依赖关系依次执行。在此基础上,还需兼顾功能性,如 UI 设计、监控体系和可视化能力,以提升系统的可用性和运维效率。

3. 调度系统的功能

知乎调度系统的生态构成如下图所示。

正如定义中所述,知乎调度系统的核心功能涵盖 任务生产、调度与监控,确保任务按时、准确执行。

生产端:主要面向开发人员,负责 调度任务的编写与配置,定义任务逻辑及依赖关系。调度端:负责 定时触发任务,拉齐任务依赖,确定任务的就绪状态,并 分发到合适的执行节点。监控端:提供 任务运行状态监控,确保任务能够 按时、准确执行,并支持异常恢复与回溯。

在底层计算引擎方面,知乎采用不同技术栈来支持多种业务需求:

数据生产:主要使用 Hive 和 Spark 进行 大规模数据计算与处理。报表查询:依托 Presto 和 Doris 提供 高效的分析查询能力,支撑 BI 报表和交互式查询需求。

02

知乎调度架构演进过程

整个知乎调度系统架构的演进过程已经历四个阶段。

1. 基于 Oozie 的开源任务编排系统

最初,主要使用 Oozie 及其为核心的开源任务编排系统,同时部分部门采用Azkaban。当时业务规模尚小,每日调度的任务量约为 5000 个。

2. Ares1.0 基于进程的提交框架

之所以从开源系统转向自研,主要基于以下考量:

调度系统的核心用户包括业务团队、算法团队、后端产品团队以及运营人员。开源产品在标准化或底层功能方面具有较大优势,功能越通用,其优势越明显。然而,随着业务的发展,我们发现开源调度系统在以下方面存在局限性,使其难以完全满足公司的需求:

业务定制化需求难以满足

开源调度系统通常面向广泛场景设计,功能较为通用,适用于标准化的数据处理和任务调度。然而,实际业务需求往往涉及特定类型的调度任务,例如与元数据平台、BI 平台、数据治理系统等数据产品的深度集成,以及链路回溯等更强的运维能力。开源产品在这些方面的灵活性有限,难以满足知乎特有的业务逻辑和复杂度。

扩展性与可维护性受限

开源系统的架构和插件机制虽然提供了一定的扩展能力,但要深度改造或优化调度逻辑,往往需要对底层代码进行大幅改动,不仅开发成本高,而且后续升级会受到影响。此外,开源系统可能引入不必要的复杂性,导致维护成本上升。

资源调度与集群管理优化需求

现有的开源调度系统如 Oozie、Azkaban、Airflow 等,大多是基于时间触发或依赖关系触发的任务调度,对于大规模、高并发的任务调度需求支持有限。例如,如何高效地调度大规模计算任务、如何合理分配计算资源、如何在资源紧张时进行任务抢占与调度优化等,都是开源方案难以满足的。

调度监控与故障恢复能力不足

在大规模任务调度场景下,实时监控、任务健康检查、故障恢复等功能至关重要。开源系统的监控能力通常较为基础,无法针对具体业务需求进行深度优化。自研系统可以针对调度任务的执行情况,提供更精细的监控和告警机制,例如异常任务自动重试、故障自动迁移、基线监控报警等。

安全性与权限管理需求

在企业级应用场景下,调度系统往往涉及不同团队和用户的协作,需要严格的权限管理和数据隔离机制。开源调度系统在这方面的支持较为有限,而自研系统可以根据公司内部的安全策略,设计更符合需求的权限模型,例如项目组的粒度进行角色授权的任务权限控制、敏感数据保护等。

在架构上,我们最早的设计版本采用了基于物理机、基于进程的架构,整体由三部分组成:Master 节点、中间的 Worker 节点以及运行节点。这种设计既能保证调度的稳定性,又能在任务执行过程中灵活分配计算资源。随着业务规模的进一步增长,我们逐步对架构进行了优化,使其更具弹性和可扩展性,以更好地支撑知乎的业务发展。

Master 节点采用中心化设计,由 Main 节点 和 Standby 节点 组成。Master 负责核心调度工作,包括任务生成、任务分发以及任务依赖检测。Standby 节点则用于容错,通常不提供服务,只有在主节点发生故障(如通信失败)时,才与 Master 一起进行 Failover 故障转移。两者之间通过 ZooKeeper 进行节点协同,以确保高可用性。

Worker 节点采用 去中心化架构,使任务能够分发至各个节点,并由各自独立执行。每个 Worker 节点执行相同的任务调度逻辑,并通过不同的子进程处理任务。任务执行流程如下:

每个任务会启动一个子进程来创建和管理执行,并定期轮询获取任务状态及日志。Spark、Hive 任务 通过 Worker 进行提交,交由底层计算集群处理。Shell 或 Python 任务 直接在本地 Worker 节点执行,以降低执行延迟并提升调度效率。

Ares 1.0 主要存在以下问题:

资源隔离问题

对于 SparkHive任务,计算过程主要发生在底层计算集群,而 Shell和 Python任务则直接在 Worker 节点上运行。由于部分任务可能会过度占用 Worker 资源,导致其他任务的执行受到影响,进而降低整个集群的吞吐量。在极端情况下,这种资源竞争甚至会引发任务延迟,影响整体调度稳定性。

可扩展性问题调度任务的升级难度高:调度系统上的 Hive 和 Spark 任务需要持续迭代,每次新版本发布时,都需要升级相关依赖组件。在物理机架构下,升级必须逐台进行,测试和部署流程繁琐,耗时较长。扩容成本高,周期长:基于物理机的架构,集群扩容需经过设备采购、审批流程、初始化部署等多个环节,整体周期较长。如果扩容规划不足,可能导致集群运维不稳定,影响业务连续性。潮汐效应

离线任务通常集中在 凌晨 0 点 运行,此时 调度集群负载高,而业务计算集群资源空闲;白天则相反,调度集群空闲,而业务计算集群资源紧张。这种资源使用的不均衡会导致集群在不同时间段出现浪费,降低整体资源利用率。

3. Ares 2.0 利用 Kubernetes 实现资源隔离

为了解决上面 Ares1.0 存在的三个问题,知乎决定引入 Kubernetes 来优化调度架构,从 Ares1.0 迈向 Ares2.0。

Cgroups(Control Groups)是 Linux 内核 提供的一项功能,能够实现对 CPU、内存、IO 等资源 的隔离和限制。虽然使用 Cgroups 可以精细化管控资源,但实现成本较高,需要手动配置和维护。

CPU 资源限制:Cgroups采用 CPU Throttling 机制,通过设置 limit 参数来控制 CPU 资源的使用。当进程达到设定的 limit 时,Cgroups 会限制其 CPU 访问权限,因此它是一种强制限制。内存资源限制:Cgroups 的内存管理相对更严格。当进程超出设定的 limit,如果系统资源充足,可能不会立即被限制。但当资源紧张时,Cgroups 会通过 OOM(Out of Memory)机制 杀死超限进程,以保证整体系统的稳定性。

相比手动使用 Cgroups,Kubernetes 提供了更高层次的封装,并在 Pod 级别 自动通过 Cgroups 进行资源管理。使用 Kubernetes 进行资源隔离的方式如下:

在创建Kubernetes 任务 时,直接在 Pod 级别配置CPU 和内存限制,Kubernetes 会自动调用 Cgroups 进行资源管控。任务调度流程:任务分发至 Worker 节点,但 Worker 本身不再执行具体任务,而是作为 Kubernetes 任务的提交节点。Worker 通过调用 Kubernetes API Server 创建任务,生成 Pod。任务实际在 Pod 内部执行,每个 Pod 之间通过 Kubernetes 进行资源隔离,避免任务相互干扰。任务执行过程中,Pod 内部会调用底层 计算集群(如 Spark、Hive)完成数据处理。

相比直接使用 Cgroups,Kubernetes 提供了更自动化、灵活的资源管理方式,并简化了任务的调度与隔离过程,使系统更加稳定和易于扩展。我们最终选用了基于 Kubernetes 自动化资源隔离。

针对 扩展性问题,知乎采用 Kubernetes 架构,通过 镜像管理 实现任务的快速扩展和升级。

在 Kubernetes 中,使用 Dockerfile 定义任务运行的镜像:

构建基础镜像,包含通用的运行环境。打包调度任务的依赖,确保任务可以在任何节点上执行。每次迭代升级 时,仅需更新镜像,无需手动调整每个 Worker 节点的环境。

具体流程如下:

定义并构建镜像,包含调度任务所需的环境依赖。推送镜像至镜像中心,供 Kubernetes 任务拉取使用。创建 Kubernetes Job 时,指定最新版本的镜像,并进行验证。批量替换旧任务,通过 Kubernetes 直接更新 Job,确保所有实例使用最新镜像。

这一方式相当于建立了 标准化任务模板,只需在每次迭代时 更新模板,即可快速扩展并升级任务环境,大幅提升了调度系统的扩展性和维护效率。

在创建 Kubernetes Job 时,需要特别注意 指定,主要包括 CPU 和内存 限制,以确保任务执行的稳定性和集群资源的高效利用。

CPU- limits formula = Math.max( CPU- requests * factor , 1 )

CPU- limits 是申请量乘以超卖系数(factor),并且最小为 1.

Python 任务(较大):分配 2GB 内存,CPU 设为 0.25。Spark / Hive 任务:分配 512MB 内存。CPU Limit 设定:知乎采用 CPU 申请量 × 2 的策略,即任务在资源空闲时可获取 最多 2 倍的 CPU 资源,以提高 CPU 利用率。

在 0 点到 1 点 调度任务高峰时,CPU 利用率可达到 60% 左右,确保高并发任务的稳定运行。

CPU 资源支持超卖:允许任务在低负载时使用更多 CPU,提升资源利用率。内存资源不做超卖:避免集群内存竞争导致任务互相影响,确保系统稳定性。

这一资源隔离方案既保证了任务执行的稳定性,又最大化了 资源利用率,提升了调度系统的整体效率。

针对 潮汐现象(即调度集群与业务集群在不同时间段资源利用率不均衡的问题),知乎采用 Namespace 标记 + 动态资源复用 的方式进行优化。

保障任务调度的稳定性,避免高峰期资源竞争影响任务执行。提高集群利用率,减少资源浪费,实现 离线计算与在线服务的资源动态调度。

Ares2.0 架构中也遇到了一些技术挑战:

API Server 的调用压力优化

初始方案:每个线程独立请求 API Server 获取 Pod 状态,但由于任务并发量大,导致 API Server 负载过高,影响 Kubernetes 集群稳定性。

优化方案:改为 单个后台线程轮询 API Server 并缓存 Pod 状态,每个任务线程 直接从缓存读取,极大降低 API Server 负载,同时提升任务状态获取的稳定性。

Cgroups 资源泄露问题

问题现象:

在高峰期,Kubernetes Node 可能报 NodeNotReady 错误。

监测到 Cgroups 资源异常飙升,可达到 1 万到 2 万 级别,可能影响集群任务执行。

临时解决方案:

手动运行 Shell 脚本清理 Cgroups 资源,避免 Node 进入 NotReady 状态,确保调度任务的正常执行。

待优化方向:

进一步分析是否为 架构设计问题 导致 Cgroups 泄露,并探索自动化治理方案,如 Kubernetes DaemonSet 监控 & 清理 Cgroups。

任务一致性问题

Spark on YARN 支持 YARN-Client 和 YARN-Cluster 模式,知乎采用 Cluster 模式,任务提交到 YARN 后,若仅终止调度任务,底层计算任务仍可能继续运行,导致任务状态不一致。优化方案采用了 Kubernetes preHooks 机制:

在任务运行过程中,记录每个 Spark 任务的 Application ID。当用户发起终止操作时,通过 preStopHook 调用 YARN/Spark API,确保调度系统 & 底层计算任务同步终止,保证任务状态一致性。

Ares 2.0 上云后的优化与挑战

Ares 2.0 架构已经将 worker 迁移到云端,极大提升了调度系统的扩展性和弹性,但仍然存在一些待优化的问题:

知乎的大数据架构每 3~4 年都会进行一次变更,涉及 跨机房、跨区域的集群迁移。基于物理机的架构使得迁移成本高,涉及硬件采购、部署、数据迁移、网络拓扑调整等复杂工作,影响业务连续性。

4. Ares3.0 云原生架构

为了进一步降低成本,Ares 3.0 对架构进行了优化,将 Master 也迁移至云端,重点解决两个问题:

在 Ares 2.0 架构中,Master 和 Worker 之间的通信依赖 Akka,采用 Actor 模型 进行节点间消息传递。然而,Akka 需要在启动时指定 SeedNode,且必须绑定 物理 IP,导致 Master 上云后 IP 变动问题 无法轻松解决。

优化方案:移除 Worker,简化架构

在 Ares 3.0 中,移除 Worker 节点,将其功能拆分:任务提交交由 Master 直接处理,Master 直接调用 Kubernetes API Server 进行 Job 创建,无需 Akka 进行节点间通信。日志采集独立拆分,避免 Master 负担过重,提高系统稳定性。

这一改动剔除了 Akka 这一重量级组件,减少了复杂度,同时也避免了 Master IP 变化带来的问题。

在 Ares 2.0 中,Worker 负责日志采集,由于日志采集分布在各个 Worker 节点,IO 压力较小。

但在 Ares 3.0 移除 Worker 后,Master 无法承担日志采集任务,否则会导致:

Master IO 负载过高,影响调度任务的正常运行。扩展性受限,Master 无法支撑大规模日志采集。

优化方案:引入 Filebeat + Kafka

采用 DaemonSet 方式在每个 Node 上部署 Filebeat,确保所有 Pod 的日志能够被采集。Filebeat 采集日志并推送到 Kafka,Kafka 作为日志中转层,保证日志流量不会直接冲击 Master。单独的日志消费服务从 Kafka 读取日志,进行存储和分析,保证日志系统的高扩展性。

Kubernetes 创建 Pod 有三种模式:

ReplicaSet:用于 Web 无状态服务,可根据负载动态扩缩容。StatefulSet:适用于 数据库、分布式存储等有状态应用,确保 Pod 具有唯一标识和固定存储。DaemonSet:适用于 日志采集、监控代理,确保每个 Node 都运行一个 Pod,用于 Filebeat 部署。

在每个 Node 节点上部署一个 Filebeat 的 Pod,可实时获取到每一个 Pod 中的日志,然后将这些日志写入到 Kafka,再通过单独的服务消费 Kafka 来记录日志,这样可保证无论 Node 节点怎么扩展,filebeat 也可以把日志拿下来,最终由另外一个服务去消费。

移除 Worker,简化架构,Master 直接与 API Server 交互,剔除 Akka,提升维护性。引入 Filebeat + Kafka 解决日志采集问题,避免 Master 负载过高,提升扩展性。采用 Kubernetes DaemonSet 部署 Filebeat,确保日志采集随 Node 扩展自动扩展。

通过这一系列优化,Ares 3.0 更具云原生特性,架构更加轻量级,资源利用率更高,且大幅降低了维护成本。

03

关键技术

知乎调度系统在设计上采用了多项关键技术,以保证 高可用性、稳定性和快速恢复能力

1. 高可用设计

调度系统对可靠性要求极高,特别是 Master 节点不能成为单点故障(SPOF),否则一旦 Master 崩溃,整个系统的不可用时间会被无限放大。因此,知乎调度系统的高可用设计聚焦于两个核心目标:

层级职能Web 层负责 UI 交互,提供增删改查 (CRUD) 接口,主要是可扩展的轻量级服务,直接与数据库交互。Master 层负责核心调度逻辑,采用 主备架构 (Master-Main / Master-Standby),保证高可用。Worker 层运行在 Kubernetes 上,负责执行具体的任务,并上报执行状态。

这种分层设计可以确保:

Web 层与 Master 层解耦,即使 Master 故障,Web 仍可正常提供服务。Worker 独立运行,不会因为 Master 故障而影响任务执行。

知乎调度系统采用主从架构 (Master-Main / Master-Standby) 结合 Zookeeper 进行主备选举,确保 Master 失效时能快速切换:

主备架构:主 Master 负责调度,备用 Master 处于待命状态。Zookeeper 选举:当主 Master 失效时,备用 Master 自动接管调度任务,实现 快速切换。状态恢复:新选出的 Master 通过检查 任务调度实例的状态,确保所有任务能够正确恢复执行,避免任务丢失或重复调度。

这一设计能有效避免 Master 故障导致的 系统不可用问题,确保调度任务能够快速恢复并继续运行。

2. 定时调度与任务依赖检测

在调度系统中,任务间存在完整的依赖关系,下游任务只有在所有上游任务完成后才能启动。因此,如何高效检测依赖是否就绪是调度系统的核心问题之一。

知乎调度系统通过 内存队列 + Quartz 调度 + 任务状态轮询 实现依赖就绪检测:

Quartz 定时触发任务

知乎使用 Quartz 开源定时框架 负责任务调度:Quartz 触发任务时,将任务放入 新建队列 (New Queue),暂存任务信息。Quartz 触发后立即 执行依赖检测,判断该任务是否可执行。

依赖就绪检测未就绪任务进入等待队列 (Waiting Queue):如果任务的上游依赖 尚未全部完成,将其放入 等待队列。就绪任务进入可执行队列 (Ready Queue):若所有依赖任务都已完成,则任务进入 就绪队列,等待分发执行。任务优先级排序

为了优化调度,知乎在任务分发前,会对所有 就绪任务(Ready Queue)进行优先级排序:用户可 自定义业务优先级,保证 高优先级任务 先执行。任务按照 优先级+依赖关系 进行调度,确保调度逻辑正确。

3. 任务回溯

任务回溯是调度系统中除生产、调度、监控之外的核心功能之一,对于保障数据生产的连续性和稳定性具有重要意义。它能够快速修复故障,减少数据损失,确保数据链路的完整性。

知乎调度系统支持三种任务回溯模式:

单个任务回溯:针对单个失败任务进行回溯和重试。级联回溯:从 某个任务开始,向下游依赖的任务级联回溯。链路回溯:指定 上游起点任务和下游终点任务,回溯整个依赖链路。

知乎调度系统采用标记清除算法进行任务回溯检测,核心流程如下:

DAG 任务实例列表生成:每天 生成 DAG 实例列表,用于记录任务依赖关系。当用户需要执行任务回溯时,系统 根据 DAG 结构计算回溯范围。确定回溯任务范围:用户指定 回溯的上游依赖节点 和 下游叶子节点。通过 DAG 生成算法 计算需要执行的任务实例集合。任务状态检测 (标记清除):访问 数据库,查询任务的最新状态。比对当前任务状态,找出 失败、异常、未完成的任务实例,进行标记。任务回溯列表生成:过滤出需要重新执行的任务,并生成回溯任务列表。将任务列表返回给用户,并提供一键回溯功能,实现自动化恢复。

知乎调度系统通过 DAG 依赖计算 + 标记清除算法,实现了 精准、高效、自动化 的任务回溯机制,确保 数据生产的连续性和稳定性,降低故障对业务的影响。

04主要成果

经过云原生改造,知乎调度系统在 成本优化、扩展能力、稳定性 等方面取得了显著成效,为大规模数据调度提供了坚实支撑。

机器成本降低:Master 和 Worker 节点全面上云,减少了 物理服务器的冗余投入,降低 Standby 资源浪费。运维成本降低:去除了 Akka 复杂的 SeedNode 绑定问题,并引入 Zookeeper 主从选举机制,减少手动维护成本。迁移成本降低:支持 跨机房、跨区域调度,适应业务变化,避免了传统物理机集群迁移的高成本。云原生架构:简化 Worker 角色,使 Master 直接提交任务,减少 Akka 依赖,提高架构弹性。日志采集优化:采用 Filebeat + Kafka 进行分布式日志收集,解决 Master IO 压力问题,实现无损日志采集。跨机房调度:通过 Kubernetes API Server 调度模式,实现跨数据中心的任务调度能力,提升任务分发效率。高可用保障:Master 主从架构 + Zookeeper 选举,确保 Master 故障时可快速恢复。任务执行采用 Quartz 定时 + 依赖就绪检测,保证调度任务拉起逻辑正确。任务执行效率:任务就绪延迟达到 P995 级别,确保秒级调度能力。任务依赖检测采用 内存队列优化,支持高并发任务触发。异常任务自动修复:任务回溯机制通过 标记清除算法,支持单任务、级联任务、全链路任务快速回溯,减少数据损失。

以上就是本次分享的内容。

05

问答环节

Q1:master 有性能瓶颈吗?

A:在知乎最早的架构设计中,也讨论过这个问题。这个得看 master 具体的职能,若 master 维护的调度实例在 10 万级别,是可以很好地支持的,再往上也没有具体测过。早期有性能瓶颈,后面把 IO 瓶颈这部分职能摘出去了,如果只是做任务拉起状态的判断,还有 Kubernetes client 的调用,它的并发能力大概能够达到 2000,是没有问题的。

Q2:在 Spark 版本升级中,通过拉取镜像部署的方式是如何做到灰度的?在集群中难免存在多个 Spark 版本,可能需要同时并行存在多个版本,怎么去做灰度的验证?

A:做镜像部署之前是要做灰度测试。首先会有一些有代表性的测试任务,每一种类型的任务都会挑选一些,这些任务会覆盖通用性比较大的一些场景,或者是历史出现过问题的一些场景。在这种场景下,在 Kubernetes 客户端创建 job 的过程中,需指定 container,container 中指定镜像。对于灰度的任务,会单独去设定它的镜像。在灰度过程中,会调用这些灰度的任务,对一些指定的调度任务做一些冒烟测试。

以上就是本次分享的内容,谢谢大家。

来源:DataFunTalk

相关推荐