Apache Mesos 性能工作组实践:Master Failover 完成时间缩短 80% 的四大优化手段
Apache Mesos 性能工作组实践:Master Failover 完成时间缩短 80% 的四大优化手段
本文聚焦 Apache Mesos 性能工作组(Performance Working Group)在 2017 年 12 月发布的进展报告,深入解析 Mesos Master Failover(主节点故障切换)完成时间优化背后的四项关键改动:Protobuf 3.5.0 的 move 支持、Libprocess dispatch / defer / install 原语的零拷贝改造、Master 端大量 protobuf 消息拷贝的消除,以及用于度量这些改进的合成基准测试。读完后,你将理解大规模集群中状态重建的成本模型,以及 C++11 移动语义如何具体落地到 actor 消息传递系统中。
背景:性能工作组的成立与目标
可扩展性与性能是 Mesos 的核心特性。报告指出,彼时已有 Mesos 生产集群规模超过 35,000+ 个 agent 和 100,000+ 个活跃任务,但系统在多个方面仍有大量提升空间。性能工作组由此成立,其章程(charter)是:
- 提升整个系统的可扩展性 / 吞吐量 / 延迟;
- 编写基准测试并实现自动化,以度量改进效果并防止性能回退(performance regression)。
在报告发布前的几个月里,工作组聚焦于两大方向:
- Master failover 完成时间:取得 450–600% 的吞吐量提升,完成时间缩短 80–85%(本文主线);
- Libprocess 消息传递吞吐量:这部分改进在官方博客中另文介绍,本文仅涉及其中与 move 语义相关的原语改造。
问题定义:什么是 Master Failover 的“完成”
报告的第一个优化对象是 master failover 的完成时间(time-to-completion),其完成定义为:所有 agent 都成功完成重新注册(reregister)。
理解这个指标需要先理解 Mesos 的高可用架构:
- Mesos 采用中心式 master + 备用 master 的架构,备用 master 参与 quorum 选举以实现高可用;
- 出于可扩展性考虑,主 master(leader)将集群状态保存在内存中,而非每次从存储读取;
- 发生 failover 时,新的 leader 必须依靠所有 agent 的重新注册来重建内存状态;
- 在这个重建窗口期内,master 仍可处理其他请求,但会向 API 消费者暴露不完整的状态(partial state)。
集群规模越大,重建内存状态的成本越高——这正是该优化工作的出发点。
优化手段一:升级 Protobuf 3.5.0 以获得 Move 支持
对 master 做 profile 分析后发现:master 花费了大量时间拷贝 protobuf 消息,尤其是在 agent 重新注册的过程中。为此 Mesos 将 protobuf 升级到 3.5.0 以获得 move 支持,从而在保留值语义(value semantics)的前提下消除 protobuf 消息的拷贝。
当前仓库中可以直接验证这一版本事实:3rdparty/cmake/Versions.cmake 中设置了 set(PROTOBUF_VERSION "3.5.0"),对应的源码包 protobuf-3.5.0.tar.gz 与补丁 protobuf-3.5.0.patch 也在仓库中,CMake 构建通过 3rdparty/CMakeLists.txt 将其声明为 external 依赖并统一编译。protobuf 3.x 系列版本开始支持 move 语义,意味着 Message::Swap、std::move(msg) 等操作可以转移底层缓冲区,而不是逐字段深拷贝。
优化手段二:Libprocess 消息原语的 Move 支持与 Copy Elimination
Mesos 的消息传递基础设施是 Libprocess(仓库内 vendored 于 3rdparty/libprocess)。它提供了多个消息传递原语:
dispatch:向一个本地Process投递(post)一个消息,即调度一个方法在目标 actor 的上下文中执行;defer:延迟式的dispatch,即一个函数对象,被调用时才真正发出dispatch;install:安装一个用于接收 protobuf 消息的处理器。
这些原语最初是在代码库引入 C++11 支持之前加入的,因此不具备 move 支持——protobuf 消息参数在传递过程中会被拷贝。为了消除拷贝,工作组为这些原语增加了**移动参数进出(moving arguments in and out)**的能力。
从源码验证原语的 move 支持
三个原语的 move 支持在当前仓库头文件中都有直接证据:
dispatch:3rdparty/libprocess/include/process/dispatch.hpp 中,内部 dispatch 例程接收的是std::unique_ptr<lambda::CallableOnce<void(ProcessBase*)>>——即整个可调用对象按所有权转移,而非拷贝;defer:3rdparty/libprocess/include/process/defer.hpp 通过宏展开为多个重载,defer会返回一个Deferred<...>对象,在被调用时发出 dispatch;install:3rdparty/libprocess/include/process/protobuf.hpp 中存在成对的处理器签名,例如void (T::*method)(const process::UPID&, const M&)与void (T::*method)(const process::UPID&, M&&),后者以右值引用接收 protobuf 消息,使消息可以在注册流程中以移动而非拷贝的方式流转。
关键 C++ 工具:lambda::partial
defer 的改造引入了一个非平凡的技术细节。defer 采用了与 std::bind 相同的 API(包括占位符),但 std::bind 返回的函数对象不会把绑定参数 move 进存储的可调用对象——也就是说,即使你写 std::bind(f, std::move(msg)),msg 仍然会被拷贝进 bind 表达式内部。
为了解决这个问题,defer 改用 Mesos 新引入的工具 lambda::partial。它的行为与 std::bind 类似地执行偏函数应用(partial function application),区别在于:当对右值的函数对象执行调用时,绑定的参数会被 move 进被存储的可调用对象。该实现在 3rdparty/stout/include/stout/lambda.hpp 中,头文件注释明确写道:“Performs partial function application, similar to std::bind. However, it supports moving the bound arguments through, unlike std::bind.”(实现自第 360 行的 partial(F&& f, Args&&... args) 起),同文件还提供 CallableOnce——一种只能调用一次、必须以右值形式调用的 std::function 变体,这正是内部 dispatch 用来承载移动语义可调用对象的机制。
lambda::partial 也声明了与 std::bind 的已知差异:不支持嵌套 bind 表达式的特殊处理(行为对齐 C++20 的 std::bind_front),且未实现 std::reference_wrapper 支持。这些边界条件对理解其适用场景很有价值。
优化手段三:Master 侧的拷贝消除
在上述基础设施就绪之后,master 自身的代码得以消除大量昂贵的 protobuf 消息拷贝。当前仓库的 src/master/master.cpp 可以印证改造后的调用形态:
reregisterSlave处理器直接以ReregisterSlaveMessage&&(右值引用)接收消息;- 处理过程中通过
defer+std::move(reregisterSlaveMessage)把消息移动到后续的内部处理器_reregisterSlave中继续处理,整条链路上消息只转移一次所有权,而不是逐层拷贝。
从源码结构看,这种“右值引用接参 + defer 移动续接”的模式正是前述三项基础设施(protobuf move、dispatch/defer move、install 的 M&& 重载)共同发挥作用的直接产物。
基准测试:如何度量 Failover 完成时间
为了度量改进,工作组编写了一个**合成基准测试(synthetic benchmark)**来模拟 master failover:它预先准备好所有需要重新注册的 agent 将会发送给 master 的消息。测试设计要点:
- 使用合成 agent:它们只是“知道如何向 master 重新注册”的 actor;
- 每个“agent”发送可配置数量的活跃/已完成任务,归属于可配置数量的活跃/已完成 framework;
- 每个 task 携带 10 个小型 label 以引入元数据开销。
该测试在当前仓库中对应 src/tests/master_benchmarks.cpp。仓库中的版本与报告描述一致并可进一步对照:
TestSlaveProcess是一个ProtobufProcess,在initialize()中构造ReregisterSlaveMessage:为每个活跃 framework 生成指定数量的TASK_RUNNING任务,为每个已完成 framework 生成TASK_FINISHED任务,任务的 label 生成循环恰好是 10 个(for (size_t i = 0; i < 10; i++));- 测试通过
dispatch(process.get(), &TestSlaveProcess::reregister)触发重新注册,并计时到所有 agent 收到SlaveReregisteredMessage为止,即报告所定义的“完成”; - 基准关闭 agent 认证以隔离开销(
masterFlags.authenticate_agents = false),并使用replicated_log作为 registry 以更贴近生产场景; - 该文件还包含第二个基准
MasterStateQuery_BENCHMARK_Test,用于度量 master state API 查询性能(GetState),对应下文 Roadmap 中的工作方向。
报告同时诚实地列出了该基准的四个注意事项(caveats):
- 不使用 executor——如果包含 executor,实际结果会更好,但为了简洁省略了;
- 使用本地消息传递,而真实集群中消息是通过 HTTP 传递的;
- quorum 大小为 1,因此对 master registry 的写入只落在单个本地 log 副本上;
- 合成 agent 不会重试重新注册,而真实 agent 通常带退避策略重试。
测试硬件为 2015 款 MacBook Pro(2.8 GHz Intel Core i7),Mesos 使用 Apple LLVM version 9.0.0 (clang-900.0.38) 编译,且 1.5.0 中启用了 -O2。注意:这些绝对数值仅适用于该测试环境与编译选项,跨环境比较应只参考相对改进比例。
结果:1.3.0 → 1.5.0 的完成时间下降
场景一:无任务历史
第一组结果模拟一个集群:每个 agent 10 个活跃任务、分布在 5 个 framework 上,无已完成任务,从 1,000 到 40,000 个 agent,对应 10,000 到 400,000 个活跃任务。
从 1.3.0 到 1.5.0,吞吐量提升 450–500%,完成时间下降约 80%。
场景二:含任务历史
第二组结果加入任务历史:每个 agent 额外包含 100 个已完成任务,分布在 5 个已完成 framework 上。规模为 1,000 到 40,000 个 agent、10,000 到 400,000 个活跃任务,以及 100,000 到 4,000,000 个已完成任务。
从 1.3.0 到 1.5.0,吞吐量提升 550–700%,完成时间下降约 85%。任务历史(archived tasks)带来的状态重建负担更大,因此带历史的场景获得了更大的相对收益。
后续 Roadmap:v1 API 与 Master State API
报告最后给出了性能工作组正在瞄准的两个方向,对理解后续版本演进很重要:
- v1 API 性能:彼时 v1 API 明显慢于 v0 API,目标是达到持平乃至超越。由于 v1 API 基于 HTTP,其中一部分工作是 Libprocess 的 HTTP 性能改进;
- Master State API 性能:master 状态查询与处理所有 scheduler/agent 消息的是同一个 master actor,查询处理会阻塞 master 处理其他事件,因此用户不得不避免过度查询 master。但实际上由于依赖 master 状态的工具很多(DNS 工具、UI、CLI 等),master 实际承受着不小的查询压力,这是一个关键的用户侧问题。解决思路是借助 state streaming API,把状态流式推送给另一个 actor 来服务 state API 请求,确保昂贵的状态查询不影响 master 的事件处理能力。
小结:可复用的性能优化方法论
这篇进展报告的价值不仅在于 Mesos 自身的 80–85% 指标,更在于它完整展示了大系统性能优化的标准闭环:
- 先度量:用 profile 定位瓶颈(拷贝 protobuf 消息),用合成基准(
MasterFailover_BENCHMARK_Test)量化改进并防止回退; - 改基础设施而非打补丁:从 protobuf 版本、
dispatch/defer/install原语到lambda::partial工具,把 move 语义贯通整条消息链路; - 如实披露基准边界:明确列出 executor 缺失、本地消息传递、quorum=1、无重试等 caveat,让结果可被正确解读。
相关仓库资源供进一步深入:
- 基准测试实现:src/tests/master_benchmarks.cpp
lambda::partial/CallableOnce实现:3rdparty/stout/include/stout/lambda.hpp- 原语定义:dispatch、defer、install
- Master 重新注册处理链:src/master/master.cpp
- protobuf 版本与依赖构建:3rdparty/cmake/Versions.cmake、3rdparty/CMakeLists.txt

