Mesos这个名字在容器编排大行其道的今天提起来多少有点“上古神器”的味道。但如果你早几年在搞大数据平台或大规模微服务基础设施一定绕不开它。简单来说Mesos是一个开源的集群资源管理器把整个机房里所有物理机或虚拟机的CPU、内存、磁盘集中成一个资源池再按“细粒度”的方式分配给上层不同的计算框架。所谓细粒度不是指单台机器上的百分比配额而是指每一次资源分配都以一个很小的资源请求为单位动态划拨任务跑完马上回收再分配给其他人。这篇文章打算站在一个实际使用者的角度聊聊Mesos到底是怎么实现这种细粒度共享的它解决的核心问题是什么以及如果你现在还想学它、用它的思路去解决资源利用率问题最该关注哪些点。无论你是做基础设施平台研发还是维护一个跑着多套计算引擎的离线任务集群Mesos的这套设计至今仍然非常值得借鉴。1. 内容整体设计与思路拆解1.1 为什么需要“细粒度的资源共享”先想一个场景。假设你手上有几十台服务器上面既要跑实时的流式计算又要跑周期性的报表任务还要给在线服务预留一部分资源做弹性扩容。大多数人的第一反应是“分环境”实时计算用十台离线任务用十台在线服务用剩下十台。这种做法叫静态分区优点是隔离彻底缺点也极其明显——某一组机器空闲时无法借给另一组用整体利用率常年高不起来。我在实际运维中见过不少集群离线任务白天几乎不跑晚上才有波峰而在线服务刚好反过来静态分区等于互相眼睁睁看着对方那边的内存白白浪费。Mesos换了个思路物理隔离保持住但逻辑资源全部共享。所有机器上有一个常驻的Agent进程把自己的资源总量上报给中央调度器Master上层各种框架Framework通过注册的方式接入向Master主动“要”资源Master觉得谁合适就把当前空闲的CPU、内存等资源打包成一份Offer发给谁。拿到Offer的框架可以接受也可以拒绝甚至可以只要其中一小部分剩下的还回去。整个分配过程没有全局的锁也没有“给你三台机器你就自己看着办”这种粗粒度划分。这就是细粒度共享的核心含义以单个任务的实时资源需求为分配单位资源请求和释放都不是预绑定的而是运行态不断发生。任务生命周期短回收速度快同一个资源片段今天可能给日志清洗脚本用明天就给实时计算任务用利用率自然上去了。1.2 两级调度让框架自己说了算很多人第一次看Mesos架构图都会被“两级调度”这个词困惑。其实理解起来不复杂。第一级调度是Mesos的Master决定“把集群里哪些空闲资源给你这个框架”这是一种粗粒度的、面向资源量的分配第二级调度是框架内部的调度器决定“拿到这些资源后具体跑什么任务”这个逻辑完全由框架自己控制。举个例子。某个集群里跑着Marathon负责长驻服务和Aurora负责短任务Mesos Master向它们各自发了一份Offer每份Offer包含8核CPU和16GB内存。Marathon拿到后觉得自己的服务只需要4GB内存和2核CPU那就只接受其中一部分资源其余归还Aurora拿到后发现当前任务是批处理型需要6核CPU和4GB内存就全部接收启动对应的Executor。Master完全不关心每个框架内部怎么分配任务只关心资源Offer是否被接受、用了多少、剩了多少。这个设计的精妙之处在于把“集群资源分配策略”和“具体任务编排策略”彻底解耦。Mesos不用为每类任务写不同的调度算法框架开发者只需要在自己的调度器里实现业务逻辑然后通过Mesos的API表达资源偏好就行。改一个框架的任务调度逻辑完全不影响集群的资源管理机制。1.3 细粒度带来的收益与代价细粒度共享的收益很容易理解更高的资源利用率更灵活的应用部署方式。但代价也藏在细节里——调度开销变大资源碎片变多。因为每次Offer都是一个小片段如果框架的资源表达和策略写得不好拿到的资源可能零零碎碎根本凑不出一个完整的任务需求。所以Mesos提供了一堆机制来缓解碎片问题比如过滤条件Filter里可以设置最少CPU、最少内存Master就会过滤掉不满足要求的OfferFine-Grained的Offer还可以在框架侧对资源进行二次整合。真正的做法通常是让框架根据任务需求对Offer做“凑单”式的选择一个Offer不够就从多个Offer里各取一部分拼接只要最终拼出的总量够用就行。后面我在实操部分会具体演示这个“凑单”逻辑怎么写。2. 核心细节解析与实操要点2.1 Resource Offer模型资源是以“邀请”形式出现的Mesos里最核心的抽象就是Resource Offer。通俗讲它就是Master代表集群里的各台机器向框架发出的一封“邀请函”我在哪台机器的Agent上有多少空闲CPU、多少内存、多少磁盘、多少端口你想不想要Offer是带约束的里面会写明资源所在的Agent ID、Hostname还会附带上一个唯一ID。框架的调度器收到Offer后可以选择接受Accept或拒绝Decline。接受的时候要明确声明我要用这个Offer里的哪几个资源、运行哪些任务、用哪个Executor启动。这个“声明资源”的过程实际上就是对Offer进行再次细粒度切分。我在实际操作中体会最深的一点是Offer的发布频率直接决定了调度延迟。如果你的框架需要频繁启动大量短任务Offer发得太稀疏任务只能干等但如果Offer太密集Master的通信开销又会上来。比较稳妥的做法是在框架侧设置合理的过滤参数比如minMem、minCpus让Master给你发Offer时提前过滤一遍减少无意义的网络往返。2.2 资源分配算法从MaxMin公平到DRFMesos默认的资源分配策略是DRFDominant Resource Fairness也就是“主导资源公平”算法。这个算法听起来高深实际上思想很朴素每个框架的资源需求由多种维度组成比如CPU和内存判断一个框架在集群里的“占比”看的是它占用最多的那种资源占集群总量的比例这个最大占比就叫主导份额。举个具体例子。框架A的每个任务需要2核CPU和8GB内存集群总量是100核CPU和200GB内存。那么A的一个任务消耗CPU占集群总量的2%消耗内存占4%主导资源就是内存。框架B的每个任务需要8核CPU和2GB内存它的主导资源是CPU。DRF的调度目标就是让不同框架各自的主导资源占比尽量相等。这个算法非常契合细粒度共享场景因为它天然考虑了多资源维度的公平性不会出现某框架疯狂吃掉内存但CPU只用一点点却还能继续无限申请的情况。你如果想要控制某个框架的资源份额上限单独设置权重还不够还得配合角色Role的配额机制后面我会细讲。2.3 角色、权重与预留、配额角色是Mesos里做资源隔离和优先级控制的核心概念。每个Framework注册时都要指定一个角色Master在分配资源时会根据角色把资源划分为不同的“池子”。你可以在Agent上配置某部分资源只给某个角色这就是预留在节点层面的实现也可以通过设置角色权重让不同角色按比例多分资源。实操中的常见做法是给重要生产任务单独设一个角色把该角色的权重调高同时给这个角色设置配额Quota保证它能拿到多少资源是一个硬保证。注意配额不是预分配它更像一个“最低保障线”。比如你给roleA设置了quota 50核CPU那么即便集群空闲资源不足在调度时也会优先满足这50核的需求。具体的实现手段是Mesos Master内部会有一个quota角色资源统计通过定期向所有Agent请求资源并暂扣Offer来实现。2.4 Agent端资源隔离与Executor生命周期资源真正用起来是在Agent上。每个Agent会启动若干个ExecutorExecutor负责拉起具体任务进程。Mesos的隔离机制依赖容器技术比如Linux上可以用cgroups做CPU和内存隔离也可以用Docker容器来提供镜像环境。细粒度资源共享在这个层面体现在同一个Agent上可以同时运行多个不同框架的任务它们的隔离靠内核的容器机制保证而资源回收靠Agent的监控线程完成。如果某个任务超量使用内存cgroup会触发OOM杀掉超限进程Mesos随后会把这个任务标记为失败并把资源状态更新给框架。如果你处理不好Executor的缓存和日志清理问题Agent上的磁盘会被吃满到时候调度完全卡住。我的经验是给Agent端设置Overhead即框架估算Executor额外资源时预留的固定CPU/内存开销时不能拍脑袋必须根据实际任务类型测试后填写宁可略高不能略低。3. 实操过程与核心环节实现3.1 搭建一个能跑起来的Mesos集群先别急着写代码我们要先把环境跑起来。Mesos虽然有点老派但它的编译安装流程相当标准。以Linux服务器为例核心步骤是装好依赖库包括libcurl、libevent、zlib、openssl等然后下载源码包执行./configure --disable-java make -j4 make install。如果嫌编译慢直接用发行版仓库里的二进制包也可以只是版本可能偏旧。安装完成后你至少需要启动三个组件Master、Agent、以及一个Framework比如Marathon或自定义的调度器。Master的启动命令大致是./bin/mesos-master.sh --ip192.168.1.10 --work_dir/var/lib/mesos --quorum1 --zkzk://192.168.1.11:2181/mesos这里的--quorum1表示单机模式如果生产环境至少三台Masterquorum要配成2。Agent端命令./bin/mesos-agent.sh --masterzk://192.168.1.11:2181/mesos --work_dir/var/lib/mesos/agent --resourcescpus:24;mem:65536;disk:500000resources参数就是你向集群申报的节点资源总量如果机器实际只有16核你填24没问题但调度时任务超配最终会直接在cgroup层面失败。我的建议是申报值略低于实际值留出系统进程和Agent自身的开销。启动完两个组件后通过http://master-ip:5050打开Web控制台能看到当前空闲资源和注册上来的框架列表。到这里集群的基础环境就算通了。3.2 实现一个最小Framework学会接受Offer真正体现细粒度调度逻辑的地方是框架的调度器代码。这里用Java或Python写一个简单的自定义Framework先注册到Master然后不断接收Offer对Offer做过滤和接受处理最后启动一个任务在Agent上执行。以Python为例核心逻辑分为三步第一步实现registered回调。在这里创建MesosSchedulerDriver并注册框架信息。注意注册时设置的角色和失败超时时间framework_info mesos_pb2.FrameworkInfo( userroot, namepy-framework, rolebatch-role )如果不指定userMesos默认以当前系统用户身份去Agent上拉任务生产环境很容易因为权限不对导致任务启动失败。第二步实现resourceOffers回调。这个回调收到的是Offer列表我们要从每个Offer里解析出CPU和内存判断能不能拼出一个任务需要的资源。这里要注意一个Offer可能不够但多个Offer加起来够同时尽量在同一个Offer上组织任务避免任务跨节点启动导致数据本地性变差。代码上可以做个简单的二维匹配def resourceOffers(self, driver, offers): tasks [] task_id_seq 0 requested_cpus 1.0 requested_mem 256 remaining_cpus requested_cpus remaining_mem requested_mem launch_groups [] for offer in offers: offer_cpus get_cpus(offer) offer_mem get_mem(offer) if offer_cpus remaining_cpus and offer_mem remaining_mem: launch_groups.append((offer, get_mem(offer), get_cpus(offer), task_id_seq)) remaining_cpus 0 remaining_mem 0 break elif not launch_groups: # 存放暂不满足的offer留待后续拼接 pending_offers.append(offer) for offer, mem, cpus, tid in launch_groups: task mesos_pb2.TaskInfo( task_idmesos_pb2.TaskID(valuestr(tid)), namepy-task-%s % tid, agent_idoffer.agent_id, resourcesbuild_resources(cpus, mem) ) tasks.append(task) if tasks: driver.launchTasks(offer.id, tasks)第三步写Executor。Mesos提供mesos-executor的默认执行器只需把命令写成shell脚本就能运行任务。但如果你需要定制化的环境清理逻辑最好自己写一个Python Executor在launchTask回调里拿到任务命令执行。细粒度调度的灵魂其实在Executor阶段——资源分配完之后如何高效利用、及时回收都靠它。3.3 部署Marathon让服务长时间稳定在线如果你想在Mesos上跑长驻服务最省事的方案是上Marathon。Marathon本身也是一个Framework它在资源Offer的基础上增加了应用定义、健康检查、负载均衡集成等能力。部署时只需把Marathon的--master参数指向Mesos的ZK地址即可。有几个参数对细粒度共享影响很大--task-launch-timeout可以控制任务启动超时避免一个Executor启动过慢把Offer占住不放--decline-offer-duration控制框架拒绝Offer后多久内不再接收同类Offer一般设成5到10秒比较合适。如果没有设置默认会频繁接收不想要的Offer造成调度器无谓的计算压力。我在生产部署Marathon时踩过的坑主要有三个。第一个是健康检查网络Marathon默认用HTTP请求做健康检查如果服务监听在了内网IP而Marathon Web服务跑了另一个网卡上健康检查就会一直失败任务反复重启同时反复申请资源导致集群出现假性资源不足。第二个是配置卷在Mesos上跑Docker容器时尤其是数据类框架一定在容器路径里显式声明持久卷否则容器重启后数据丢失应用不可用。第三个是端口分配Mesos的端口是和CPU/内存并列的可分配资源类型Marathon里配置ports: [0]是随机分配如果服务之间互调需要使用固定端口就要精心规划稍不留神就端口冲突。3.4 细粒度调优从“能跑”到“跑得稳”集群搭好、任务能跑只是第一步真正体现内功的是后面的调优。我通常会按下面的顺序对资源分配进行调整。第一打开资源复核。Mesos Master有一个参数--recovery_agentAgent重启后要上报自己的资源状态Master会进行资源复核。建议打开它避免Agent宕机后资源被重复分配。第二调整--allocation_interval和--offer_timeout。这个参数决定Master每隔多少毫秒汇总一次空闲资源出Offer。默认的1秒对于大部分场景够了但如果你的框架会频繁拒绝Offer整体调度延迟会叠加可以考虑缩短到500毫秒代价是Master的CPU开销上升。第三动静结合区分任务类型。实时业务用Marathon任务翻倍扩容时通过Mesos的预留和配额配合一把让关键角色优先抢到资源离线批处理框架比如Aurora则设置较低权重并且允许它们被抢占。这样在高峰期长任务不会因为短任务抢占了资源而缩容。另一件特别值得做的事是监控每个Agent上的真实资源使用率而不是只看分配率。Mesos Web界面显示的Allocated只是调度器分配给框架的量级实际进程可能远未用满。我在实践中看到过有框架申请了8GB内存实际进程只用了几百兆还一直占着配额不放。这时候你需要去框架侧设置资源软上限或者在框架的申请资源声明里把资源缩到更接近真实值。细粒度共享要往深处做一定绕不开对这套机制的持续治理。4. 常见问题与排查技巧实录4.1 “资源不足”假象明明总量够任务就是起不来这种情况一般分两类。一类是Offer碎片化严重。比如要启动一个需要2.5个CPU的任务集群里剩下3个Offer每个只有1个CPU总量都够但单个Offer不够调度器如果不会拼接这些Offer任务就只能排队等待。解决办法是引入一个“资源整合”层在框架侧维护一个待拼接Offer的缓存超过一定时间拼不齐就拒绝掉避免过多Offer积压。另一类是因为“角色限制”。如果你给某个角色设置了配额那么该角色可用资源是先从集群总量里划走一个保障额度即便这个额度当前没被任务用完其他角色也用不了。这就会出现“全局资源还有富余但指定角色资源不足”的假象。这类问题排查时可以用mesos-resources命令在命令行查看当前各角色的资源分配情况或者直接在Web界面看角色视图。4.2 任务被反复打入“Lost”状态任务状态变成Lost最常见的根因在Agent和Master之间的状态同步。比如Agent本地的Executor崩溃后如果Master因为网络抖动没有及时收到状态更新任务状态就会进入长时间未知最后被标记为LOST。另一种情况是Agent重启后之前分配的Offer没有对应的Executor恢复Master会在reconciliation过程中将相关任务标记为LOST。处理这个问题的思路分两层。应用侧要检查框架的故障恢复机制如果框架没有实现reregistered回调它重启后无法重新接管已运行的Executor那么所有running任务都可能被打丢。系统侧可以开启--recover参数让Agent重启后尝试恢复旧Executor并重新注册到Master。但注意旧Executor必须支持无状态恢复否则恢复后任务状态也不可靠。4.3 细粒度带来的调度风暴所谓调度风暴就是框架和Master之间的Offer交互频率突然飙高导致Master的CPU被打满粉丝晚到。我遇到过一次确定性问题某个框架里有一个无限循环的bug收到Offer就立即拒绝并且不设置过滤条件结果Master每秒收到几百个Decline整个集群调度延迟从几十毫秒涨到几秒。排查过程用了三步找线索是打开Master的--log_levelDEBUG看到日志里大量Sending decline记录接着在Web界面查看该Framework的Offer统计发现它的“Decline”计数在极短时间内上涨几千次最后是修复框架侧的bug给每个拒绝的Offer设置过滤条件并在调度器里加一个“最短接收间隔”避免同一个Offer被反复处理。从此之后调度风暴再也没有出现。4.4 磁盘和端口资源回收不及时细粒度共享不只针对CPU和内存磁盘和端口一样重要。端口资源尤其容易出问题如果任务退出后Executor没有及时释放声明的端口端口会一直被占用新任务启动时端口资源不足。这类问题在Marathon和Aurora上我都遇到过。排查时可以直接访问Agent Agent的/state.json接口查看当前占用的端口范围和Executor状态修复方法是在框架的Executor销毁逻辑里增加强制端口释放操作。磁盘问题相对隐蔽任务进程虽然退出但容器层或工作目录里遗留下大量日志和临时文件。Mesos本身不会帮你做清理你必须维护一个定时脚本扫描Agent的work_dir删除超过保留期限的历史任务目录。我通常配合磁盘额度disk quota来做但前提是每个任务都声明了合理的disk资源否则框架按需申请时会报错。4.5 安全加固和权限隔离Mesos默认的鉴权配置非常宽松任何人都可以通过HTTP API提交任务。细粒度共享的场景下多框架共用一个集群权限控制必须提前配置。生产环境至少要做四件事开启Master和Agent之间的认证使用--authenticate和--authenticate_http参数配置MesosSecret作为认证凭证不要用明文密码给每个Framework分配不同的role并对role的提交、注册权限做ACL限制网络层控制好5050、5051端口的访问来源。别看这些是基础工作我接手过的不少老集群就是因为当初没做这些加固后来被内部人员用调试接口误操作而中断在线业务。5. 细粒度调度之外真正的工程经验写到这里想要专门留一个段落说说Mesos之外的工程经验这可能比具体配置更有长期价值。Mesh的细粒度调度并不是银弹。当集群里运行的框架数量增多时你感受到的压力更多来自“资源表达的一致性”和“生态治理”这两个层面。不同的框架对资源语义的理解可能不一样有的框架就像一个急性子拿到Offer就启动任务不用完不释放有的框架刚好相反极度精打细算导致任务启动延迟偏高。这些差别最终都会反馈到资源利用率数据上。我的建议是如果你决定在一个大集群里上Mesos一定要从第一天就建立“资源角色治理”制度。不能允许框架用默认role裸奔所有框架都按业务单元划分清晰的role定期Review每个role的资源使用趋势清理不再使用的角色和废弃的框架。很多团队在初期只关注调度算法是否公平却忘了“用的人是否自律”结果资源利用率虽然高但业务互相抢资源的争吵也伴随了一整个项目周期。多框架之间的依赖管理同样需要前置设计。比如A框架的任务可能依赖B框架生成的数据但B框架的资源配额如果被抢占数据产出延迟会反过来拉长A的任务时间。划清框架之间的数据依赖边界、设置好任务超时上限和重试策略比追着源码调调度器参数更重要。另外Mesos社区的演进历程也值得细细品味。从早期主导资源公平性算法到后来引入容器编排支持再到被Kubernetes生态逐渐挤压Mesos没有“落后”它只是验证了一种极有价值的设计思路把资源管理和任务调度分开。今天再看Kubernetes的调度器你会发现它的扩展点设计、调度框架、队列与优先级机制很多思想都可以在Mesos架构里找到影子。所以如果你有兴趣学Mesos真的不必抱着“学一个过时技术”的包袱把它当作一门分布式资源调度的“教科书”来读收获会大得多。6. 一个我踩过坑的案例复盘最后不总结什么宏大叙事就复盘一个自己经历的真实故障算是给细粒度调度实践补一个活生生的注脚。有一段时间我们集群的在线服务总是每隔几天就出现一波“假死”应用没挂但新请求进来后响应明显变慢过十几分钟又恢复。一开始怀疑是应用自身线程池抖动排查了三天没有任何进展。后来实在没办法调取了最近一次故障时段的Mesos Master日志发现了一个意外的规律——每次故障前都会有一大批Offers被同一个离线批处理框架集中Accept导致集群可分配内存短时间内下降一大截在线服务的Marathon应用得不到新的资源Offer来扩容。原因水落石出批处理框架的角色权重被调得过高而Marathon所属角色的配额没有设硬性保障。高峰期批处理任务大量涌入把集群资源占得满满的在线服务想扩容的资源被挤掉等到批处理任务波峰过去资源缓慢释放在线服务又能扩容了于是出现“十几分钟恢复”的周期。修复也很简单给Marathon角色设置一个不低于在线服务预测峰值的配额同时给批处理角色设置相对较低的权重并开启抢占机制保证在极端情况下在线服务可以挤掉低优先级任务。踩过这个坑之后我形成了一个习惯凡是租户多、框架多的共享集群资源配额不用“平均主义”而是先给不可抢占的OLTP类服务定一个安全的底线剩下的再让批处理类任务去抢。这就是Mesos这类细粒度调度平台真正教会我的东西——资源分配算法再精巧也得靠扎实的运营策略托底。不要让框架在资源池里自由裸奔也不要迷信某个默认参数可以一劳永逸。无论你用的是Mesos还是Kubernetes这个道理大体都通用。