写点什么

自主式大数据优化:通过多智能体强化学习实现 Apache Spark 的自调优

作者:Hina Gandhi
  • 2026-02-24
    北京
  • 本文字数:9330 字

    阅读完需:约 31 分钟

引言

大数据系统的快速扩张,暴露了传统优化技术的局限性,尤其是在分布式架构、动态工作负载以及信息不完全等环境中。如今,各类组织每天处理海量数据以提取业务洞察,例如分析客户行为、预测设备故障、优化供应链以及检测欺诈行为。这些分析任务通常依赖多种分布式数据处理框架来执行,而这些框架都提供大量配置参数,这些参数对性能有着至关重要的影响。论文《大数据处理系统自动参数调优综述》(Herodotou、Yuxing 和 Jiaheng 于 2020 年著)指出了同样的问题,并强调需要能够适应动态负载与环境变化的智能自动调优系统。

 

为应对这一需求,本文提出了一种强化学习(Reinforcement Learning,RL)方法,使分布式计算系统能够像“学徒工程师”通过实践学习那样,自主学习最优配置。我们实现了一个轻量级智能体,作为驱动端组件部署,在任务运行前利用强化学习选择配置参数。

 

本文以 Apache Spark 为实践基础。Spark 是一种具有代表性的分布式计算框架,可将计算任务分布到数百台机器上执行。Spark 的性能高度依赖配置参数,而这些参数通常使用静态默认值,或由领域专家手动调优。然而,当工作负载特征和数据分布发生变化时,这种方式难以适应。如果配置选择不当,本应在数分钟内完成的分析可能会延长至数小时,同时显著增加云计算成本。随着数据集日益多样化、工作负载愈加动态,依赖静态或手动调优的方式变得脆弱且在经济上难以持续。

 

在处理数百个作业之后,智能体逐渐形成对模式的“直觉”:数据集较小且类别较少时,只需较少的工作节点;数据规模较大且类别丰富时,则需要更多资源。智能体能够完美记住每一次实验结果,从不遗忘经验教训,并将这些累积的知识自动应用到新的工作负载中,相当于把数月专家调优经验转化为一种 24/7 即时可用的智能能力。工程师无需在数据特征变化时反复重新配置系统,智能体会随着每一次作业的执行而不断变得更加智能。

 

Q-learning 智能体是一种强化学习智能体,它通过迭代估计在特定状态下采取某一行动所能获得的长期期望回报,从而学习最优策略。在实际应用中,智能体会观察数据集特征(例如行数、数据规模、基数以及数据倾斜情况),尝试不同的配置参数组合,测量执行性能,并逐步学习在特定数据模式下哪些参数选择效果最佳。

 

本文比较了 Apache Spark 中三种优化策略:内置的自适应查询执行(AQE)、基于 Q-learning 的独立智能体,以及结合两者的混合策略。比较结果表明,混合策略优于单独使用任一方法,因为它将执行前的智能决策(由强化学习选择最优初始配置)与运行时的动态调整(由 AQE 执行)结合在一起。

 

在单智能体实验结果的基础上,本文进一步讨论了一种多智能体强化学习系统的概念扩展方案。该系统由多个相互独立、具备专业化分工的智能体组成,每个智能体专注于优化不同的配置领域,例如内存分配、CPU 核数或缓存策略。每个智能体在其专属领域内成为专家,同时共同协作以优化整体工作负载性能。通过将强化学习理念与分布式系统相结合,本研究为构建能够从经验中学习、无需依赖静态规则或人工干预的智能自调优大数据基础设施奠定了基础。

 

问题背景:Spark 配置优化

Spark 的性能在很大程度上依赖于诸如 shuffle 分区数、内存分配以及并行度设置等配置参数。静态默认值(例如 200 个 shuffle 分区)无法根据数据规模、基数和数据倾斜等不同数据特征进行自适应调整。手动调优不仅需要深厚的领域知识,而且耗时费力,并且针对某一类工作负载优化过的配置,往往在其他负载下表现不佳。

 

以一家虚构的视频分析公司 StreamMetrics 为例。该公司为内容创作者处理视频观看数据,其数据工程团队每天都面临类似挑战:每天早晨,他们运行一个轻量级报表,对前一天按类别(如科学、音乐、娱乐)统计的观看数据进行分析,数据规模仅为数千行。

 

中午,他们会处理每周趋势分析任务,在约 50 万行数据中识别爆款内容。到月底,他们生成综合创作者报告,聚合数百万行数据,涵盖数百个类别,并且数据分布高度倾斜。例如,“游戏”类别可能拥有数百万次观看,而“折纸教程”等小众类别仅有数百次观看。此外,内容创作者还会在全天发起临时分析请求,其数据规模和分布模式都难以预测。

 

在使用 Spark 默认的 200 个 shuffle 分区时,早晨的报表会浪费资源去协调 200 个几乎为空的小任务;每周分析任务可能“误打误撞”运行得还不错;而月底的大规模报告则会因 200 个分区无法有效处理庞大且倾斜的数据而表现不佳。团队当然可以针对每种负载类型手动调优配置,但随着数据模式不断变化,这需要持续维护;例如,上个月的最优配置可能在本月因某个爆款趋势改变类别分布而失效。这正是强化学习能够改变运维方式的典型动态环境。

 

这里,我编写了一个简单的 Spark SQL 查询,用于在一个包含数千行数据的数据集上按类别(如 Science、Music、Entertainment)对视频观看量进行分组统计。

 

默认情况下,这个 groupBy 操作会创建 200 个 shuffle 分区,如图 1 所示。然而,对于小规模数据集而言,这种配置是低效的,因为 Spark 会启动大量微小的 shuffle 任务并生成大量小文件,相对于实际计算量而言,调度、磁盘 I/O 和元数据开销占比过高。大多数分区几乎为空,导致 CPU 和内存资源浪费,同时 driver 和集群将更多时间用于协调任务而非处理数据。

 

图 1:针对一个小文件创建了 200 个任务(Gandhi,2026)

 

在实践中,Spark 开发者通常通过手动设置一个静态分区数来缓解该问题,例如使用经验法则:将分区数设置为 CPU 核数的 2 倍,或执行器数量的 3 倍(参见《Spark 调优》),以确保任务规模足够大,从而提升效率。

 

另一方面,对于超大数据集而言 200 个分区可能又显得不足:这会导致每个分区的数据量过大,处理时间变长,并增加内存溢出的风险。

 

在某些情况下,分区大小只能通过反复试验确定:在不同数据集和工作负载上进行实验,在性能与开销之间寻找平衡。然而,这类配置往往难以在不同数据规模或负载特征之间泛化。自 Spark 3.0 起,引入了自适应查询执行(AQE)。启用后,Spark 会根据执行过程中观察到的实际数据特征动态调整查询计划,而不是完全依赖查询规划阶段的静态估计。

 

然而,AQE 仍然以默认配置(通常是 200 个 shuffle 分区)开始执行,只有在收集到运行时统计信息后才进行合并或调整。这意味着它优化的是 reduce 阶段,但无法避免前期写入大量小 shuffle 文件所带来的初始开销,因此在小规模或中等规模数据集上仍然存在一定低效。此外,如果性能提升需要超过 200 个分区,AQE 也不会自动增加分区数量。

 

强化学习可以在这一场景中发挥关键作用,通过在不同条件下动态调整这些参数,实现跨场景的性能优化。

 

强化学习

正如《强化学习:导论》(Sutton 与 Barto,2018)所定义,强化学习的核心在于学习在不同情境下应采取何种行动,以最大化一个数值奖励信号。与监督学习不同,学习者不会被直接告知应该采取哪些行动,而是必须通过不断尝试,发现哪些行动能够带来最大的回报。试错式搜索以及延迟奖励,是强化学习最重要的两个区别性特征。

 

从形式上讲,强化学习可以被描述为一个 AI 智能体与环境进行交互的过程:智能体感知环境状态,采取行动,并接收奖励,如图 2 所示。随着时间推移,智能体会学习出一项策略(即从状态到行动的映射),以最大化长期期望回报。

 

在本文的场景中,强化学习智能体会观察数据集特征,尝试不同的分区数量,测量执行性能,并逐步积累关于“哪种配置最适合哪种数据模式”的知识。经过多次执行后,智能体会形成类似经验丰富工程师的“直觉”,能够为不同类型的工作负载自动选择合适的分区数量。

 

图 2:标准强化学习中智能体与环境交互循环示意图(Sutton 与 Barto,2018)

 

实现流程:构建一个基于 Q-Learning 的强化学习智能体

我们构建的 Q-Learning 强化学习智能体,是一个在 Apache Spark 之上开发的自定义智能体,部署在 driver 程序中。该实现通过在作业提交流程外层包裹一个智能体层,对 Spark 进行了扩展。

 

以下工作流程展示了我们自定义的 Q-learning 强化学习智能体如何感知 Spark 环境、采取行动、接收反馈,并随着时间推移不断学习。在现实世界中,每天处理数十亿事件的大规模数据平台也面临类似挑战:其数据工程团队需要运行多样化的工作负载,包括基于最新数据的实时仪表盘、针对数百万条记录的周期性聚合报表,以及针对高度倾斜数据分布的综合分析查询。

 

Q-learning 强化学习智能体可以为这些多样化工作负载自动完成配置调优,消除人工干预,通过优化资源分配来降低云计算成本,并加速查询性能,使工程团队能够将更多精力投入到功能开发,而不是反复调整参数。

 

第一步:智能体感知环境(状态观测)

当一个 Spark 作业被提交时,智能体的状态观测模块会拦截该作业,并分析数据集以理解当前环境状态。

print("\nLoading data...")df = spark.read.csv(data_path, header=True, inferSchema=True)row_count = df.count()
复制代码

 

智能体随后提取刻画工作负载的关键特征:

num_rows = df.count()sample_rows = df.limit(1000).collect()from collections import Countercategory_values = [row.category for row in sample_rows]category_counter = Counter(category_values)category_cardinality = len(category_counter)counts = list(category_counter.values())skew_factor = np.std(counts) / np.mean(counts)
复制代码

智能体所观测到的特征包括:

  • 行数:数据量越大,通常需要更多分区

  • 列数:列越多的数据集可能需要更多分区

  • 类别唯一值数量(基数):基数越高,通常意味着需要更多分区

  • 数据大小(MB):数据越大,通常受益于更多分区

  • 平均行大小(字节):用于衡量数据密度

  • 数据倾斜因子(skew factor):衡量数据分布是否不均衡;倾斜严重时需要额外调整

 

智能体的设计选择

智能体仅抽样 1000 行数据(约 100ms),而不是扫描整个数据集,从而在准确性与实时决策之间取得平衡。这种轻量级观测机制,使智能体即便在大规模数据集上也能快速作出决策。

 

第二步:状态编码(为泛化进行离散化)

状态编码模块将连续特征转换为离散状态表示,从而使智能体能够在相似工作负载之间泛化已学到的知识。

# 为智能体设计的自定义离散化分桶row_buckets = [100, 1000, 10000, 100000, 1000000]size_buckets = [1, 10, 100, 1000]card_buckets = [5, 10, 20, 50, 100]skew_buckets = [0.1, 0.3, 0.5, 0.8, 1.0]
复制代码

 

例如,智能体处理一个包含 5000 行、1.23 MB、12 个类别、倾斜度 0.48 的数据集时:

# 智能体离散化逻辑如下::# 行数 5000 → bucket_2# 数据大小 1.23 MB → bucket_1 # 基数 12 → 位于 10–20 区间 → bucket_2# 倾斜度 0.48 → 位于 0.3–0.5 区间 → bucket_3state_key = "rows_bucket_2|size_bucket_1|card_bucket_2|skew_bucket_3"
复制代码

 

为什么离散化至关重要?

如果不进行离散化,智能体会将 5000 行与 5001 行的数据集视为完全不同的状态,从而导致学习难以收敛。通过分桶处理,智能体能够识别“1000 到 10000 行的数据集具有相似优化模式”,进而将之前作业中学到的经验应用到新的、但结构相似的工作负载上。

 

第三步:智能体选择行动(ε-贪婪策略)

行动选择模块查询 Q 表,并在“探索”(exploration)与“利用”(exploitation)之间进行权衡,决定尝试哪个分区数。

# 行动空间由若干自定义候选分区数组成actions = [8, 16, 32, 64, 128, 200, 400]# 智能体的探索参数 epsilon = 0.3# 智能体的判断逻辑if random.random() < epsilon:    action = random.choice(actions)  # EXPLORE: Try something new    action_type = "explore" else:   action = max(Q[state_key],key=Q[state_key].get)# EXPLOIT: Use best known action_type = "exploit"
复制代码

 

智能体维护一张 Q 表,用于存储每个“状态-行动”组合的价值估计。例如在某个状态下:

Q["rows_bucket_2|size_bucket_1|card_bucket_2|skew_bucket_3"] = {    8: -0.405,      # Agent tried this, took 0.405 seconds    16: -0.523,     # Agent tried this, took 0.523 seconds    32: -0.650,     # Agent tried this, took 0.650 seconds    64: 0.0,        # Agent hasn't tried this yet    128: 0.0,       # Agent hasn't tried this yet    200: -0.745,    # Agent tried this, took 0.745 seconds (worst so far)    400: 0.0        # Agent hasn't tried this yet}
复制代码

 

智能体的决定: 

智能体会选择 Q 值最高的分区数(在此例中为 8),因为其对应的执行时间最短(-0.405 接近于零)。

 

智能体的学习策略: 

初始 ε = 0.3(30% 探索概率):在学习初期,智能体保持较高的探索率。随着训练过程推进,ε 逐步衰减至 0.05。然而,ε 并未降至 0,而是保留最低 5% 的探索概率,以适应不断演化的工作负载分布,并避免陷入次优策略。

 

第四步:智能体作用于环境(应用配置)

配置管理模块将智能体选定的分区数写入 Spark 配置,然后执行作业。

# Agent injects its learned configuration into Sparkspark.conf.set("spark.sql.shuffle.partitions", "8")# Spark job executes with agent-selected configurationresult_df = df.groupBy("category").count()result_df.show()
复制代码

 

关键点: 

智能体并不修改 Spark 的内部执行逻辑,而是作为一个“智能包装层”,在作业执行前设置最优配置,随后交由 Spark 原生执行引擎完成实际计算。

 

第五步:智能体接收奖励(性能反馈)

当 Spark 完成作业后,奖励计算模块计算执行时间,并将其作为学习信号。在本实现中,奖励函数定义为:reward = -execution_time。也就是说,执行时间越短,奖励越高。需要注意的是,该实现仅以执行时间为优化目标,并未显式考虑运行成本、内存压力、失败风险或资源利用率等多目标因素。更复杂的系统可能会构建多目标奖励函数。

# Agent measures job performancestart_time = time.time()result_df = df.groupBy("category").count().collect()execution_time = time.time() - start_time# Agent's reward signal (negative because lower time is better)reward = -execution_time  # e.g., -0.321 seconds
复制代码

 

第六步:智能体学习(Q 值更新)

学习引擎根据 Q-learning 更新公式更新 Q 表并结合观测奖励

Q(s,a)←Q(s,a)+α(r+γxmax_{a′}Q(s′,a′)−Q(s,a))

 

# Q-learning 更新公式(在智能体学习引擎中实现) alpha = 0.3 # 学习率:根据当前学习需要调整多少 gamma = 0.1 # 折扣因子:未来奖励价值多少 old_q_value = Q[state_key][action] max_future_q = max(Q[state_key].values()) new_q_value = old_q_value + alpha * (reward + gamma * max_future_q -  old_q_value)# 智能体更新记忆Q[state_key][action] = new_q_value
复制代码

学习情况示例: 

如果某一状态下,之前执行时间为 0.4 秒(reward = -0.4),而最新一次执行时间为 0.6 秒(reward = -0.6),那么 Q 值会被向下调整,表示该行动表现不如预期。下一轮中,智能体更可能探索其他分区数。

 

智能体的持续改进机制 

智能体会将 Q 表持久化(例如以 JSON 形式保存),在不同作业之间保留学习结果,从而在数周或数月内逐步积累组织级知识。每一个新作业都是一次学习机会,随着经验积累,智能体的策略将不断精细化,实现真正意义上的自调优系统。

 

实验结果

为验证智能体的有效性,我们在相同工作负载下,对三种优化策略进行了对比实验:

  • 仅 AQE:使用 Spark 内置的自适应查询执行

  • 仅 RL 智能体:使用自定义 Q-learning 智能体,并关闭 AQE

  • 混合策略(AQE + RL):由 Q-learning 智能体选择初始配置,并结合 AQE 进行运行时自适应调整

 

性能对比:

下方图表(图 3)展示了在一个小规模数据集(1000 行)且数据倾斜度较低(0.162)的情况下得到的实验结果。

图 3:小规模数据集的执行时间(Gandhi,2026)

 

实验结果表明,性能得到了显著提升。

 

同样的实验还在一个包含 75,000 行数据、且数据高度倾斜(倾斜度 1.241)的大规模数据集上进行了测试。结果显示,性能提升会随着数据规模和倾斜复杂度的增加而更加明显,如图 4 所示。

图 4:超大规模高倾斜数据集的执行时间(Gandhi,2026)

 

 

关键发现

混合策略优于“仅 AQE”和“仅 RL”两种方法。这验证了一个核心观点:执行前智能决策(RL 选择最优初始配置)与运行时自适应调整(AQE 的动态优化)分别解决了互补的优化问题。

 

对比分析:两个核心洞见

强化学习智能体相比标准的规则驱动型 AQE,能够实现显著更快的执行时间。其优势来源于能够学习并选择最优的初始分区数量(例如在小数据集上选择 8 个分区)。这种前置式配置优化在作业执行开始之前就消除了 shuffle 阶段的额外开销。而 Spark 默认的 AQE 无法完全实现这一点,因为 AQE 只能在 shuffle 块已经写入磁盘之后,才对过多分区进行合并和调整。

 

混合方法实现最佳性能。将 RL 与 AQE 结合,形成了一个两阶段优化机制:

  • 阶段一(执行前):RL 智能体基于历史学习结果设置最优初始配置。

  • 阶段二(运行时):AQE 在执行过程中根据实际观测到的情况(例如运行中发现的数据倾斜、分区大小不均等)进行动态调整。

 

这些实验结果展示了该方法在现实大规模数据平台中的实际价值。对于每天处理数十亿事件的数据系统而言,只需启用 AQE(大多数 Spark 3.0 及以上版本部署已默认支持),并引入 RL 智能体,即有可能在多样化工作负载下获得显著性能提升。这些性能改进可以转化为:通过优化资源分配降低云计算成本、加速查询执行,缩短业务洞察交付时间,以及释放工程团队精力,使其专注于功能开发而非参数调优。

 

扩展至多智能体系统与系统架构

尽管单一的分区优化智能体已经带来了显著性能提升,但在大规模数据平台中,现实情况要复杂得多。例如,在每日聚合作业中,RL 智能体已经成功设置了最优的 shuffle 分区数,但作业仍然因内存溢出而失败,因为执行器内存未针对大规模 join 操作进行合理配置。再例如,实时仪表盘在某个分区数下运行高效,但由于未启用缓存机制,相同的中间数据被反复计算,浪费了大量 CPU 资源。

 

如果仅优化分区数量,仍然会遗留大量可观的性能与成本优化空间。生产级工作负载往往需要在多个维度上进行同步优化,包括:不同操作类型(如 join 与 aggregation)对应的内存分配策略、不同负载强度(I/O 密集型 vs 计算密集型)下的 CPU 并行度配置,以及面向数据复用模式的智能缓存决策。手动协调这些配置的复杂度呈指数级增长。工程师不仅需要调优分区数,还必须考虑:分区数量如何影响内存使用、内存配置如何影响 CPU 利用率,以及缓存策略如何进一步影响上述所有因素。因此,有必要将单一智能体方法扩展为多个独立学习组件,每个组件分别优化特定配置域。

 

单一分区优化智能体已经验证了强化学习在 Spark 配置优化中的可行性,但生产环境中的工作负载通常需要跨多个维度的联合优化。一个自然的扩展方式是在 Spark driver 上部署多个专用智能体,每个智能体负责不同的配置领域,并根据作业执行反馈独立学习。在这种多智能体架构中,引入一个协调器。该协调器是一个轻量级控制层,负责按照固定顺序应用各智能体的决策,但本身不进行学习或策略优化。它与分区智能体协同,额外编排三个专用智能体。

 

内存智能体通过监控内存使用模式、垃圾回收频率以及磁盘溢写事件来优化执行器内存分配。基于工作负载特征(例如大量 join 操作需要构建大型哈希表,对比包含过滤操作的查询语句只占用较低内存),该智能体会动态配置 spark.executor.memoryspark.memory.fraction,以及 spark.memory.storageFraction 以在性能与资源浪费之间取得平衡。

 

核心数智能体通过跟踪 CPU 核心利用率、任务等待时间以及线程竞争情况,学习最优的并行度设置。其调整的参数包括: spark.executor.coresspark.task.cpus,以及 spark.executor.instances 。该智能体能够识别 I/O 密集型任务通常受益于更高并行度,以及计算密集型任务在过度并行时会因频繁上下文切换而性能下降。

 

缓存智能体通过测量缓存命中率、缓存淘汰模式以及重复计算成本,学习智能缓存策略。其决策包括:是否缓存中间 DataFrame、选择适当的存储级别(仅内存、内存+磁盘、仅磁盘),以及配置 spark.storage.memoryFractionspark.rdd.compress。该决策基于数据复用模式以及可用内存资源进行动态调整。

 

每个智能体均采用与分区优化智能体相同的 Q-learning 框架:提取与其领域相关的状态特征、维护独立的 Q 表,以及基于作业执行性能奖励进行更新。这种解耦设计使得每个智能体能够在自身领域内形成专业能力,同时整个系统实现全面的工作负载优化。

 

图 5 展示了上述多智能体系统的高层架构。

图 5:Apache Spark 的高层多智能体系统架构(Gandhi,2026)

 

结论

本文展示了强化学习如何将传统上依赖人工、且容易出错的 Spark 配置调优过程,转变为一种自主、可自适应的优化系统。通过实现一个基于 Q-learning 的强化学习智能体,使其能够观测数据集特征、尝试不同分区数量,并根据性能反馈持续学习,系统逐步形成了类似资深工程师的优化能力,同时具备完美记忆和系统化探索机制。

 

实验结果验证了该方法的有效性。单独使用 RL 智能体时,其性能优于 Spark 默认的自适应查询执行(AQE);而将 AQE 与 Q-learning 结合的混合策略取得了最佳整体性能。这表明,执行前智能决策(RL 选择最优初始配置)与运行时自适应调整(AQE 的动态优化)分别解决了互补的优化问题。

 

需要指出的是,本研究的实验基于相对较小的数据集(1000 至 75,000 行),相比每天处理数十亿事件的生产级系统仍有差距。尽管结果验证了基于 RL 的配置优化方法在概念层面的可行性,但若能在 PB 级数据规模、更加复杂的查询模式下进行验证,将进一步增强其在生产环境中部署的可信度。此外,当前实现仅聚焦于单一配置维度(shuffle 分区);扩展至涵盖内存、CPU 与缓存等多维度的多智能体优化体系,还需要进一步实验,以验证智能体之间的交互效果并确保稳定收敛。

 

本文提出的多智能体架构,将上述思想扩展至全面的工作负载优化场景。多个专用智能体分别针对内存分配、CPU 核心调度以及缓存策略等领域独立学习优化策略,在各自领域形成专业能力。展望未来,该架构为多个研究方向提供了可能,包括:跨集群环境的迁移学习、用于连续状态空间的深度 Q 网络(DQN),以及融合集群拓扑信息的上下文感知策略。对于管理生产级 Spark 工作负载的数据工程师而言,这一方法提供了一条可行路径:对作业进行性能指标采集,实现一个简化版的 Q-learning 智能体用于 shuffle 分区优化,与现有系统并行部署,并让其在真实生产流量中持续学习。

 

该方法能够积累组织级知识,将数月的调优经验转化为可复用的策略,供未来所有作业使用。强化学习与分布式系统的结合不仅是一种优化技术,更代表着基础设施演进方向的转变——从依赖静态规则的系统,迈向能够基于经验持续学习与自我优化的自主型基础设施。随着大数据系统复杂度不断提升,配置参数成千上万,工作负载持续演化,能够自主学习、适应并优化的智能体将不再只是便利工具,而将成为必需能力。

原文链接:

https://www.infoq.com/articles/agent-reinforcement-learning-apache-spark/