关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

2026 Apache Pinot 实时 OLAP 服务器租用拆解:段文件合并、实时表与历史表的内存怎么分

发布时间:2026-10-13

实时看板 P99 从 200ms 掉到 3s:CPU 和磁盘都在摸鱼

上周三下午三点半,一个跑实时看板的 Pinot 集群出了这么个现象:同一个聚合查询,P99 从 200ms 一路爬到 3s 以上,P50 却几乎没动。登上 Server 那台机器看常规指标,top 里 java 进程的 CPU 常年稳在 30% 上下晃,iostat 里数据盘的 %util 不到 10%,网卡流量离上限差得远,Kafka 那边的消费延迟 lag 一直是 0,Helix 上所有段都是 ONLINE。所有能看的常规资源指标都很健康,查询就是慢。重启 Server 能好上半小时,然后原样慢回去。

这种"资源都闲着但就是慢"的现象,在自建 Pinot 集群里几乎必然指向内存划分,跟算力够不够一点关系都没有。把那次排查的结论先摆出来,后面逐条展开:
第一个该怀疑的不是 CPU,是 GC 停顿。堆给了 64G 而活跃对象只有几个 G 时,一次 Full GC 的 STW 可以到秒级。停顿期间 CPU 是空的、磁盘也是空的,监控曲线上什么都看不出来,只有查询耗时那条线在往上蹿。
第二个该怀疑的是段的数量。一张表被切成几千个小段之后,一次查询要在同一台 Server 上敲几千次门——开字典、读索引头、建前向索引 reader、做段级剪枝——这部分开销跟数据总量关系不大,跟段数成正比。
第三个是 mmap 和 page cache 的争用。不可变段靠 mmap 读,真正决定快不快的是内核 page cache 里有没有这一页。JVM 把内存吃走了,page cache 被挤干,段扫描就从内存访问退化成缺页中断。
第四个是实时表正在写的那一块。consuming segment 活在内存里,它能长到多大由 flush 阈值说了算,跟你这张表历史上一共攒了多少数据没有直接关系。

本文不按"先讲概念、再讲配置、最后讲价格"那套顺序来,而是顺着这四个怀疑点一路往下拆:内存到底被谁分走了、实时段和历史段在磁盘上到底是不是同一种东西、段的数量为什么比数据量更致命、堆与 mmap 的比例怎么切、加内存和加节点各自解决什么问题、五条实打实的坑,末尾是八个真会被问住的问题。

一台 128G 的 Pinot Server:内存被四拨人分,谁都拿不满

先看最容易算错的一件事:你给 Pinot Server 配的 -Xmx,只是这台机器内存账目的一部分,甚至往往不是最大的那部分。一台 128G 的机器上,JVM 堆、堆外直接内存、内核 page cache 是三个互相不通气的池子,谁多了都会挤掉别人。把它们一笔一笔拆开,才知道"内存越大越好"这句话在 Pinot 上是错的。

第一拨:实时表正在写的 consuming segment

每张实时表的每个 Kafka 分区,在承担它的那台 Server 上都有一个正在写入的段,叫 consuming segment。它不落盘,数据进来先在内存里的可变索引结构上攒着,攒到 flush 阈值才被 seal,然后转成不可变段写下去。这部分在新版本里默认是走堆外直接内存的,对应配置项大致是 pinot.server.instance.realtime.alloc.offheap 以及与之配套的堆外上限配置(具体开关名与默认值随版本演进有变化,务必查你所用版本的官方配置参考再落地);老版本或者被人改过配置的集群,这部分仍然可能落在堆里,这也是很多团队"升级前后体感完全不同"的来源。

这里有个很实际的问题:consuming segment 不是"有多少数据就占多少",它是"从上次 flush 到现在进来了多少数据就占多少"。所以它的大小是个流量 × 时间的乘积,不是一个存量。你表里有 10TB 历史数据,跟 consuming segment 占 2G 还是 20G,毫无关系。

第二拨:不可变段的 mmap 与它背后的 page cache

段被 seal 并 flush 到磁盘之后,Server 会用 mmap 把它映射进进程地址空间来读。这部分不占 JVM 堆,也不算在 -Xmx 里,但它会占用进程的虚拟地址空间,而且真正决定读得快不快的,是这一页有没有被内核的 page cache 缓存住。

这里就是"内存越大越好"翻车的地方。mmap 读段文件是典型的随机访问模式——跳字典、跳前向索引、跳倒排拉链——它不像顺序扫大文件那样能靠预读救回来。一旦 page cache 装不下热段,每次访问都变成一次缺页,从"纳秒级内存访问"变成"微秒到毫秒级的块设备读",P99 就是这么被拉到秒级的。而你在这台机器上把 -Xmx 从 16G 加到 64G,多出来的 48G 正是从 page cache 嘴里抢走的。

第三拨:查询执行堆

group by、order by、distinct、join、having 这些算子的中间结果都在堆上。一个 group-by 查询要建的哈希表大小,主要由分组键的基数(cardinality)决定,跟你扫了多少行的关系反而没那么直接——扫 1 亿行返回 10 个分组,和扫 100 万行返回 500 万个分组,后者的堆压力是前者的几百倍。

这也是为什么"高基数聚合"是 Pinot 集群最容易 OOM 的场景。一个按 user_id 分组、再按设备型号和时间粒度交叉的查询,分组数很容易到千万量级,再叠上并发,堆就爆了。这类查询通常要靠 numGroupsLimit 之类的查询选项兜底,或者在表设计阶段就用 star-tree 索引把聚合预计算掉。

第四拨:JVM 自己的杂项

Metaspace 放类元数据,CodeCache 放 JIT 编译产物,每个查询线程有独立的线程栈(按 JDK 与 -Xss 配置,通常在 512K 到 1M 量级),Netty 有池化的网络缓冲,Broker 与 Server 之间的 grpc/netty 通道也有各自的缓冲。这些加起来不算大头,但在你算 -Xmx 的时候必须留出余量,否则进程 RSS 会超过 -Xmx 一大截,被容器的 memory limit 或 OOM killer 干掉。

为什么"内存越大越好"在 Pinot 上是错的

三个机制,一个比一个实在。

一是压缩指针边界。堆超过 32G 附近(CompressedOops 的可用上限),对象引用从 4 字节退化成 8 字节,同样一批对象的占用会上升一截,同时 GC 要扫描和标记的堆区间也变大。你的 live set 明明只有 4G,却要为 64G 的堆付标记成本。

二是 page cache 挤压。物理内存是定量的,给 JVM 多一分,内核能拿来缓存段文件的就少一分。Pinot 的段是随机读,对 page cache 的依赖比顺序读型负载高得多。

三是 GC 停顿与堆大小正相关。大堆下即使 live set 很小,收集器遍历的区域也大,STW 更长。P50 不受影响(大部分请求没撞上停顿),P99 却直接被拉爆——这正好对应开头那个"P50 没动、P99 翻十倍"的现象。

所以正确的思路不是"内存越大越好",而是"三块池子按比例切,谁缺补谁"。下面的表把集群里四种角色的账一起算清楚。

节点角色 主要内存去向 磁盘诉求 CPU 特征 这一角色扩容时先加什么
Controller 表 schema、段元数据与 Helix/ZK 状态缓存,堆内 4–8G 通常就很富余,几乎没有堆外压力 极小,只有日志和本地缓存,几十 G 足够 低且脉冲式,只在表变更、段上传、rebalance 触发时抬一下 基本不用加资源,先加副本数(2–3 个)做高可用;资源不够先加内存到 8G
Broker scatter-gather 的响应聚合缓冲区、路由表缓存、查询结果序列化,堆是主要消耗,堆外也有网络缓冲 几乎不落业务数据,只写访问日志与查询日志 中等偏高,吃在结果归并、反序列化、协议编解码上 先看 fanout 是否过大(Server 数 × 每 Server 段数),能减段就减段;否则加 CPU 与堆,再考虑加节点
Server(实时 + 离线混合) consuming segment 的堆外直接内存 + 不可变段 mmap 背后的 page cache + 查询执行堆,三者抢同一台机器 段文件本体,容量按 retention × 日增量算;随机读为主,SSD 是硬要求 查询高峰拉满,消费期中等,段加载与 rebalance 时再抬一档 先加节点把段数摊薄;只有 consuming 段算不过来、或缺页率明显偏高时才加内存
Server(纯离线 / 历史表) 几乎全给 mmap 的 page cache,堆可以压到较小;没有 consuming 段就不需要堆外预算 容量大、读多写少,可上大容量 SATA SSD 甚至高速 HDD 混布 扫描型查询吃核数与内存带宽,核越多并行越好,但要控制单核线程数 先加盘与内存(page cache),节点数按副本数与查询并发定,不要盲目横向扩
Minion 段合并任务的临时缓冲,并发任务数越高占用越大;单个大段的合并可能会短暂吃掉整段大小的内存 需要独立工作区放合并中的段,通常要预留源段总量 1 倍以上的临时空间 合并期间直接拉满,是典型批处理负载,跟在线查询抢 CPU 会很致命 先限并发、错峰到业务低峰;再单独加机器,千万不要和 Server 共用一台
ZooKeeper 堆里保存整棵集群状态树,节点数与段数越多树越大,8G 以内是常见区间 事务日志与快照,要求低延迟 fsync,必须独立小容量 SSD,不能和数据盘混 极低,但对延迟极度敏感,同机上有别的重负载就会抖 先换独立 SSD 与独立物理机;节点数固定 3 或 5,不要靠加节点扩容

consuming segment 与 immutable segment:一个在内存里长大,一个躺在 mmap 上

很多人把实时段和历史段当成"同一种东西的新旧版本",这是 Pinot 调优里最贵的误解。它们在内存形态、落盘时机、索引完整度上完全是两回事,混在一台机器上必然互相踩。

同一个表名底下可能躺着两张表

先把结构说清楚。Pinot 里一张"混合表"(hybrid table)实际上由两张同名表组成:一张 OFFLINE,一张 REALTIME。离线段由批处理任务(Spark/Flink/SegmentGenerationAndPush)按小时或按天生成,实时段由 Server 直接从 Kafka 消费生成。Broker 收到查询后同时向两边发子查询,再把结果合并返回。

这个结构决定了两件事。第一,实时段的保留期不需要长——它的存在意义只是"补上离线段还没生成完的那段时间窗口",通常是几小时到几天。等离线段把这一段时间的数据正式生成完,实时段就该被清理掉,或者由 RealtimeToOfflineSegmentsTask 这类 Minion 任务把实时段归并成离线段。第二,离线段才是查询的主力,而离线段吃的是 page cache,不是堆。

实时段在内存里长多大,只跟 flush 阈值有关

consuming segment 的大小由三个阈值里最先到达的那个决定,对应的配置项大致是 realtime.segment.flush.threshold.rows(行数)、realtime.segment.flush.threshold.time(时长)、realtime.segment.flush.threshold.segment.size(段大小)。谁先到听谁的。

于是每台 Server 上实时段的总内存占用可以写成这么一个式子:

M_rt = P × min(N_row × W, R × W × T_flush, S_seg × k)

其中:P 是这台 Server 上承担的 consuming segment 个数,约等于「分区数 × 副本数 ÷ Server 数」;W 是单行在内存中的字节宽(各列内存宽度之和,字符串列按字典编码后通常远小于原始 JSON,估算时按列式落盘行宽的 1.5–3 倍取);R 是单分区每秒写入行数;N_row 是 flush 行数阈值;T_flush 是 flush 时长阈值(秒);S_seg 是 flush 段大小阈值;k 是内存相对落盘的膨胀系数,经验值 1.5–3。

拿一组真实感的数字算一遍。一张实时表 24 个分区、副本因子 2,集群里 4 台 Server 承担它,那么 P = 24 × 2 ÷ 4 = 12,每台机器 12 个 consuming segment。单分区写入 1500 行/秒,单行内存宽度 W 按 300 字节算。

这台机器每秒长出来的实时段内存是 12 × 1500 × 300 = 5.4 MB/s。三个阈值分别会卡在哪:

按时长阈值 6 小时(21600 秒):单段 = 1500 × 21600 × 300 = 9.72 GB,乘 12 段就是 116 GB,直接把机器撑爆。
按行数阈值 500 万行:单段 = 500 万 × 300 = 1.5 GB,需要 3333 秒才攒够,乘 12 段是 18 GB。
按段大小阈值 200 MB(这是落盘口径,内存里按 k=2 膨胀约 400 MB):这个最先到,单段内存约 400 MB,乘 12 段是 4.8 GB。

反过来看更有用。如果你的实时段内存预算只有 4 GB,那么可以反推阈值的上限:

T_flush ≤ M_budget ÷ (P × R × W) = 4 × 1024³ ÷ (12 × 1500 × 300) ≈ 795 秒,也就是 flush 时长阈值不能超过 13 分钟;
N_row ≤ M_budget ÷ (P × W) = 4 × 1024³ ÷ (12 × 300) ≈ 119 万行,也就是 flush 行数阈值不能超过 119 万行。

这套换算的价值在于:它把"要不要加内存"这个拍脑袋的问题,变成了一个可解的算术题。很多团队看到的"实时段吃内存",其实根本不是数据量的问题,是 flush 阈值没调——把段大小阈值从 200M 降到 100M,内存直接减半,代价只是段数变多,而段数变多可以去靠合并任务解决。

还有一条必须说清楚:实时表的 retention(保留期)跟内存没有直接关系,它决定的是磁盘容量和段数量。把 retention 从 3 天改到 7 天,consuming segment 一点都不会变大,但这张表的段数会明显增加,mmap 压力和段扫描开销同时上升。很多人改完 retention 发现"内存没变但查询慢了",原因就在这里。

落盘之后,段就得靠 mmap 活着

段被 seal 之后,Server 会把它写成一组列式文件(前向索引、字典、各类二级索引、元数据),提交到本地段目录(pinot.server.instance.data.dir 指向的路径),同时按配置上传到 deep store(HDFS/S3/GCS 之类的远端存储,用于段恢复)。本地这份副本才是查询真正读的那份。

读的方式是 mmap。这意味着段文件不存在"被读入堆"这件事,它在进程里只是一段虚拟地址映射,真正的页面由内核按需调入 page cache。所以你看 Pinot Server 进程的 RSS 往往会很大,但其中相当一部分是 mmap 映射的驻留页,并不是 JVM 在"占用"它们。用 pmap 去看,能看到大量以段文件为后备存储的映射区间。

这里有个连带问题:page cache 是全局共享的,它不分"这是 Pinot 的段"还是"这是 Kafka 的日志"还是"这是操作系统缓存的别的东西"。所以任何跟 Pinot 抢 page cache 的进程,都会间接拖慢段扫描。这也是后面要讲的"不要和 Kafka 同机"的核心理由之一。

段太多比数据太多更致命:一次查询要在 Server 上敲几千次门

这是自建 Pinot 集群里最反直觉的一条。同样 2TB 数据,切成 400 个 5GB 的段和切成 40000 个 50MB 的段,查询性能可以差一个数量级,而磁盘占用几乎一样。

每个段都有一次性的开门成本

一次查询落到某台 Server 上,要先按路由表拿到本机的段列表,用时间范围、分区、以及各类索引元数据做段级剪枝,剩下的每一个段都要:打开并读取元数据与列字典、建立前向索引 reader、定位各类二级索引、做一次段内扫描、把结果并入本机的中间结果集。这串动作里,有一部分是跟段大小无关的固定开销——读元数据、建 reader、初始化字典——一个 50MB 的段和一个 500MB 的段,这份开销差别不大。

于是总开销里出现了这么一项:固定开销 × 段数。段数从 400 涨到 40000,这一项涨了 100 倍,而它跟你的数据量毫无关系。这就是"段太多比数据太多更致命"的完整解释。

段数与查询并发的换算

把上面的关系写成能用的式子。一台 Server 上,单位时间内要完成的"段扫描次数"约等于:

段扫描次数/秒 = QPS × S_per_query

其中 S_per_query 是一次查询在这台机器上被扫的段数(剪枝之后)。而机器能提供的段扫描能力,在 CPU 核数与查询线程数固定的前提下是个常数。也就是说,QPS 与 S_per_query 成反比:段数减半,并发能力翻倍。

举个数。一台 16 核的 Server,单段扫描平均耗时 8ms(其中约 1.5ms 是上面说的固定开销)。一次查询扫 400 个段,串行扫描就是 3.2 秒,用 16 个线程并行摊下来约 200ms。如果把段合并成 200 个(段大小翻倍,数据量不变),串行扫描降到 1.6 秒,16 线程并行约 100ms——同样的 CPU、同样的内存、同样的数据量,P99 直接少一半。更细地看,400 段里有 600ms 是纯固定开销,200 段里只有 300ms,这部分是被白白烧掉的。

这个换算推出一条很实用的结论:在 CPU 还没打满、磁盘也没打满、查询却慢的时候,第一优先级永远是减少段数,而不是加 CPU。

段数上去了,内核这边也会出问题

每个 mmap 的段会在进程里产生若干虚拟内存区域(VMA)。Linux 对单个进程的 VMA 数量有个上限,由 vm.max_map_count 控制,默认值在常见发行版上是 65530。Pinot 官方文档里明确建议按段规模把这个值调大——单台 Server 上段数上万时,这个默认值就不够用了。撞上之后的表现不是"慢",而是段加载失败、报 OOM、或者进程直接被杀,排查起来很容易走偏。

同时,段数过多会拉低 page cache 的局部性。热数据被打散在几万个小段里,每个段都要单独调入自己的索引页,cache 命中率下降,缺页率上升。缺页在 CPU 层面表现为 sys 时间上升而不是 user 时间上升,所以你会看到"CPU 不是特别忙,但查询就是慢"——这跟开头那个现象对上了。

段太大也不行

反过来说,段不是越大越好。Pinot 的查询并行是按段切分的,不是按行切分的。一个 2GB 的段意味着单个查询线程要在这一个段上干很久,段内没法再并行,最慢的那个段就决定了整个查询的完成时间——这是典型的木桶效应,直接体现在 P99 上。

段太大还有三个运维层面的代价:flush 一次要写的数据多,磁盘写放大明显;rebalance 或者节点恢复时,单个大段的下载与加载耗时长,故障恢复窗口被拉大;合并任务处理大段时的临时空间需求也成倍上升。

多数团队最终落在的区间是:未压缩段大小 100MB 到 500MB,实时段偏小(为了让 flush 快、内存占用可控),离线段偏大(为了减少段数、提高扫描效率)。单表在单台 Server 上的段数控制在几百个以内比较舒服,上千个就该警惕,几千个必须上合并。

合并这件事该谁来做

答案是 Minion,不是 Server。Minion 是 Pinot 里专门跑异步任务的组件,跟段合并相关的任务主要有两类:MergeRollupTask(按时间桶把小段合并成大段,可配置多级时间粒度)和 RealtimeToOfflineSegmentsTask(把实时段归并进离线表)。还有 PurgeTask 负责按 retention 清理过期段。

这两类任务都是 CPU 和内存密集的批处理。把它们放到 Broker 或者 Server 上跑,等于让一个离线作业在在线查询的机器上跟你抢 CPU、抢 page cache、抢网络带宽。合并任务跑起来的时候,那台机器的查询 P99 一定会跳。这是必须单独部署 Minion 的理由,不是"资源够不够"的问题,是"干扰模型"的问题。

堆、堆外、page cache:三块池子的比例怎么切才不互相踩

把前面的账合起来,配置决策就变成了三件事:堆给多少、堆外给多少、剩下多少留给 page cache。这三个数不是各自独立的最优解,是一个此消彼长的分配问题。

堆的上限卡在压缩指针那条线上

先定堆。建议 -Xms 与 -Xmx 设成同一个值,避免运行期动态扩缩带来的抖动;并且 -Xmx 不要超过压缩指针边界,实务上取 30G 到 31G 之间比正好取 32G 更稳,因为堆的实际可用区间还受到 JVM 内部预留的影响。

堆给多少,取决于你的查询形态而不是数据量。判断方法很土:压测时把并发打到目标 QPS,用 JMX 看 Old Gen 的峰值占用,再乘 1.5 到 2 的安全系数,得到的就是该给的 -Xmx。多数明细查询为主的集群,16G 到 24G 是够的;高基数聚合多的集群才需要往上顶。

三块池子的推荐配比

按机器规格给一组可以直接落地的切法(这些是按前面机制推出来的建议值,实际以压测为准):

32G 机器:堆 8–10G,实时段堆外 6–8G,剩余 14–18G 留给 page cache。
64G 机器:堆 12–16G,实时段堆外 10–12G,剩余 36–42G 留给 page cache。
128G 机器:堆 16–24G,实时段堆外 16–24G,剩余 80–96G 给 page cache。
256G 机器:堆 24–31G(顶到压缩指针边界就停),实时段堆外 24–32G,其余全部给 page cache。

归纳成比例:堆不超过物理内存的 25%,实时段堆外不超过 25%,page cache 至少留 50%。这条比例对纯离线节点要反过来用——没有 consuming segment 的节点,堆外预算可以砍到 5% 以下,堆压到 10%–15%,剩下的 80% 以上全给 page cache。

多租户表共存时怎么分

一台 Server 上跑多张表,是绝大多数中小集群的现实。这时候分配逻辑要换一套。

第一步是按表的"吃内存类型"分租户,而不是按业务线分。用 Pinot 的 tenant 机制(Broker tenant 与 Server tenant,表配置里指定归属)把表分成两批:一批是实时表 + 高基数聚合表,它们吃堆和堆外;另一批是低基数的离线明细表,它们吃 page cache。两批放在不同的 Server 组上,各自的配比按上面的规则独立调,互不干扰。这一步做与不做,集群的稳定性差别非常大。

第二步是堆的预算按并发算,不按表数算。同一台机器上的 10 张表不会同时被打到峰值,所以堆不需要按 10 倍给。经验做法是:单查询的中间结果预算 = 堆 ÷(预期并发数 × 1.5),然后按这个预算去反推每张表的聚合基数上限,超出的查询要么改预聚合索引,要么单独放到高堆的节点组上。

第三步是多表共存时主动把堆压小。表越多,段越多,mmap 压力越大,page cache 越金贵。所以共存 5 张表以上的节点,建议把堆压到单表场景的 60%–70%,省下来的全给 page cache。具体配比建议:多租户(≥5 张表)时堆 20%、堆外 20%、page cache 60%;单表大吞吐时堆 25%、堆外 30%、page cache 45%。

线程数与核数

查询执行线程数(pinot.server.query.executor 相关的几个配置项,具体名称与默认值随版本变化,以所用版本的官方配置参考为准)不要盲目往上调。段数多的集群加线程,只会让上下文切换和锁竞争更严重,CPU 的 sys 时间上升而 user 时间不动,看起来"很忙"其实没干活。经验值是线程数与物理核数同量级,超线程带来的虚拟核可以按 0.5 到 0.7 折算。段数降下来之后,同样的核数能跑出的 QPS 会自己上去。

GC 与内核参数

大堆场景建议用 G1 并显式设一个 MaxGCPauseMillis,不要留默认值;堆超过 32G 又确实需要低停顿的,可以评估 ZGC,但要注意 ZGC 自身会多占一部分内存,这部分要从 page cache 预算里再扣一次。内核侧至少确认三件事:vm.max_map_count 按段规模调大、swappiness 调低(Pinot 被 swap 出去基本等于宕机)、透明大页(THP)在延迟敏感场景下通常建议关闭。

加内存还是加节点:这道账算的是段数,不是数据量

扩容方向的选择,是自建 Pinot 集群最花冤枉钱的地方。先把判断顺序钉死:慢的位置不同,加的东西完全不同。

先定位慢在哪一环

P99 慢有四种典型成因,各有各的解法。GC 停顿导致的慢,特征是 P50 完全正常、P99 突然跳一个数量级,GC 日志里有秒级 STW——解法是调小堆或换收集器,加内存反而可能更糟。段扫描导致的慢,特征是 CPU user 时间随 QPS 线性上升、段数在几千量级——解法是合并段或者加节点,加 CPU 效果有限。page cache 缺页导致的慢,特征是磁盘读 IO 上升、CPU 的 sys 时间偏高、内存几乎被 JVM 占满——解法是把堆调小把内存还给内核,或者加内存。Broker 合并导致的慢,特征是 Server 端耗时正常但端到端慢、Broker CPU 偏高——解法是降 fanout 或者加 Broker。

什么时候该纵向加内存

三种情况。一是 consuming segment 按前面的公式算出来放不下——这时候加内存是唯一解,或者反过来把 flush 阈值调小。二是 page cache 明显不足,表现为缺页率高、磁盘读 IO 上来了,而堆已经压到合理比例——这时候加内存直接有效。三是 live set 确实大,压测下 Old Gen 峰值确实吃满了——这是真正"该加堆"的情况,但加到 31G 就该停,再往上要靠拆查询或者加节点。

什么时候该横向加节点

段数已经过多(单台 Server 上单表段数上千)、加内存也救不回来的情况,必须横向。因为段数这个变量,加内存只能缓解 page cache 那一半,缓解不了"固定开销 × 段数"那一半。把机器翻倍,每台机器上的段数减半,固定开销就真的减半了。

CPU 成为扫描瓶颈时也该横向。这种场景的特征是 CPU user 时间已经打满、段数却不高、加大内存没用——说明你的查询就是吃算力,横向是对的。还有一种容易被忽略的情况:副本数不足导致单台慢机器拖垮整个 P99。Pinot 的一个查询要等所有相关段的响应,一台机器慢,这个查询就慢,横向扩容加上足够副本,能让长尾被平均掉。

加节点要付的代价

横向不是免费的。加节点要触发 rebalance,段要在节点之间重新分配并下载,这个过程会吃掉磁盘 IO 和网络带宽,期间查询性能会下降。集群里 Server 数越多,ZooKeeper 上维护的状态树越大,Controller 的协调开销也越大。Broker 的 fanout 等于 Server 数 × 每台被扫的段数,Server 数增加会让 Broker 的合并压力上升。

所以横向也有边界。实务经验是:单集群的 Server 数到了几十台这个量级,就该考虑按租户拆成多个逻辑集群,而不是继续往一个集群里堆机器。拆集群的代价是运维复杂度上升,收益是故障域隔离、rebalance 范围可控、参数可以按租户差异化。

租用形态怎么选,钱花在哪几个字段上

自建 Pinot 的机器,压力集中在三个字段:内存容量、随机读 IOPS、内网带宽。这三个字段里,内网带宽最容易被忽略——Broker 与 Server 之间传的是中间聚合结果,大并发下这个流量不小,同机房万兆内网或者至少同交换机部署是基本要求,跨机房拉段基本不可行。

形态上,Pinot Server 对虚拟化层比较敏感。内存超售会直接放大 GC 与缺页的抖动,磁盘 IO 共享会让段加载和 flush 的时间不可预测。所以对延迟有要求的生产集群,多数团队选裸金属而不是共享型云主机;开发测试、压测环境、或者段数还不多的冷集群,用弹性云更划算。

价格口径要说清楚:一万网络的裸金属服务器 E5-2698v4×2 32G/1T 这一档官网明示 ¥3999 起(以官网实时报价为准);弹性云产品线 ¥25 起,适合跑 Controller、Broker 和 Minion 这类非数据面组件。至于更大内存(128G/256G)、多盘位 NVMe、万兆内网这些 Pinot 真正需要的规格,公开渠道差异较大,需实时询价,价格主要受 CPU、内存、存储、带宽、IP 和线路影响。计费上,包年通常比包月省 1–2 个月(这是行业惯例的预估值,以咨询为准)。

真正该做的不是在页面上比价,而是在下单前把几件事问到明确数值:内存是否独享不超售、能不能加盘以及最多几个盘位、盘的介质是 SATA SSD 还是 NVMe、内网是不是万兆、同机柜能不能保证、硬件故障之后的迁移时长是多少。一万网络深耕 IDC 19 年(成立于 2007 年),提供 7×24 中文工单、平均 5 分钟响应,这几项正好可以在付款前通过工单逐项问清楚并留痕——Pinot 这种对抖动敏感的组件,事前问清楚比事后救火便宜得多。

五条自建 Pinot 集群最容易踩的内存坑

坑一:把 -Xmx 顶到物理内存的 60%–70%

为什么坑:这是从其他 Java 中间件迁移过来的习惯。Pinot 不一样,它的热数据不走堆,走 mmap + page cache。堆吃掉七成内存,等于把段文件的缓存空间砍掉七成,段扫描全部退化成缺页。更糟的是堆一大,GC 停顿变长,P99 被拉爆,而这期间 CPU 和磁盘都闲着,监控上什么都看不出来。

怎么避:-Xmx 按压测的 Old Gen 峰值乘 1.5–2 来定,且不超过 31G。剩下的内存里,先给 consuming segment 留够堆外预算,其余全部留给内核。判断标准很简单:跑一轮稳态压测,看磁盘读 IO 是不是接近 0——接近 0 说明 page cache 够用,明显有读说明不够。

坑二:只按数据量算盘和内存,不算段数

为什么坑:容量规划时算的是"2TB 数据、保留 30 天、需要 3TB 盘",算完觉得没问题。上线之后每天生成一个小时级段,30 天就是 720 个段,多张表叠起来几千个,vm.max_map_count 撞上限,段加载失败,查询 P99 崩溃。这时候盘是够的,内存也是够的,段数不够。

怎么避:容量规划表里必须有两列——数据量列和段数列。段数列按"保留时长 ÷ 段生成间隔 × 表数 × 副本数 ÷ Server 数"算。算出来单台超过几千,就在上线前把 MergeRollup 配好,同时把 vm.max_map_count 按官方建议调大。

坑三:把 Minion 的合并任务塞在 Server 上跑

为什么坑:小集群为了省机器,Minion 和 Server 装在一起。合并任务一启动,CPU 拉满、page cache 被合并过程中的大块顺序读冲掉、网络被段上传占满。表现在业务侧就是"每天某个固定时段看板变卡",而且这个时段恰好是离线批处理窗口。排查时大家都在查查询,没人想到是合并任务。

怎么避:Minion 独立部署,哪怕只给它一台配置一般的机器。任务调度错峰到业务低峰,并限制单节点并发任务数。合并任务需要的临时磁盘空间按源段总量的 1 倍以上预留,别让它在合并到一半时把盘写满。

坑四:实时表 retention 设得很长,flush 阈值却一个没动

为什么坑:为了"数据能查更久",把实时表保留期设成 14 天甚至 30 天。consuming segment 没变大,但实时段的总数暴涨——实时段是按 flush 阈值切的,保留 30 天意味着 30 天的段全部堆在 Server 上,段数可能是离线段的几十倍。同时实时段没有离线段那些经过优化的索引,扫描效率也更低。

怎么避:实时表保留期按"离线段生成周期 × 2 + 安全余量"来定,通常是 3–7 天,够 RealtimeToOfflineSegmentsTask 把数据并进离线表就行。历史数据一律走离线表。同时用前面那两个公式把 flush 阈值反推出来,别留默认值。

坑五:多张表不分租户,全塞在同一个 Server 组里

为什么坑:一张高基数聚合表的重查询打爆堆,同机上所有其他表的查询一起遭殃。更隐蔽的是,一张写得很凶的实时表会把 page cache 冲掉,导致同机上的离线明细表查询集体变慢,看起来像"离线表出了问题",其实是被邻居拖累。

怎么避:按吃内存的类型分 Server tenant——实时表和高基数聚合表一组,低基数离线明细表一组。给实时组配更大的堆外预算,给离线组配更大的内存和盘。Broker 侧同样分 tenant,避免大 fanout 的查询把小查询的响应拖慢。

FAQ:八个自建 Apache Pinot 实时 OLAP 集群真会被问住的问题

问一:Pinot 和 Elasticsearch 在明细查询上到底差在哪

两者的设计目标就不同。Elasticsearch 出身于全文检索,倒排索引是核心,明细查询(按条件捞出原始文档)是它的主场,但它做大规模聚合要靠 doc_values 和额外的聚合框架,高基数分组时内存与耗时都很吃紧,而且它默认保留原始 _source,存储放大明显。Pinot 出身于 OLAP,段是纯列式的,字典编码 + 前向索引 + 各类二级索引 + star-tree 预聚合是它的核心,聚合类查询(几亿行里按几个维度 group by)比 ES 快一个量级且内存可控。反过来说,Pinot 做"按关键词模糊匹配后返回完整原始行"这种典型检索场景并不占优,它的字符串处理与打分能力弱得多。所以判断标准很直接:查询形态以聚合、分组、时序看板为主,选 Pinot;以检索、模糊匹配、返回原文为主,选 ES。也有团队两者并存,用 Pinot 做看板、ES 做检索。

问二:Server 节点 32G 内存到底够不够

够不够要看这张节点上放的是什么。纯离线节点(只放历史段,没有实时表),32G 可以撑住几百 GB 的段数据——因为堆只要 8–10G,剩下 20G 出头全给 page cache,只要热段能装进 page cache 就没问题,冷段走盘也算正常。混合节点(同时有实时表和离线表)就紧了:堆 8G、consuming segment 堆外 6–8G,page cache 只剩 14–18G,段数一多立刻不够。判断方法不是看数据量,是看两个指标——稳态压测下磁盘读 IO 是否接近 0(page cache 够不够),以及 Old Gen 峰值是否吃到 -Xmx 的七成以上(堆够不够)。只要有一个亮红灯就加。我的建议是:32G 起步只适合开发测试与冷集群,生产混合节点从 64G 起步,实时表多的往 128G 走。

问三:实时表保留多久合适

先纠正一个常见混淆:决定内存的是 flush 间隔,不是 retention;retention 决定的是磁盘容量与段数量。保留多久合适,取决于你的离线段生成周期——离线段由批处理按小时或按天生成,实时段只需要覆盖"离线段还没生成完的那个窗口"再加一份安全余量。离线任务是 T+1 的,实时表保留 3 天就很宽裕;离线任务是小时级的,保留 1–2 天足够;离线链路经常延迟或者失败重跑的,给到 7 天留缓冲。超过 7 天基本没有收益,只有代价:实时段数量暴涨、这些段没有离线段那种优化过的索引、Server 上的段数与 mmap 压力直线上升。真正需要长期查的历史数据,一定要让它走离线表,别指望实时表扛。

问四:段文件多大算合适

多数团队最终落在未压缩 100MB 到 500MB 这个区间,实时段偏小、离线段偏大。往下看,段太小(比如几十 MB)的代价是固定开销占比过高——每段都要读元数据、建 reader、初始化字典,段数一多这部分就吃掉大半耗时,同时 mmap 的 VMA 数量会撞 vm.max_map_count。往上看,段太大(比如 1–2GB)的代价是段内无法并行——Pinot 的并行是按段切的,最慢的那个段决定整个查询的完成时间,直接体现在 P99;而且 flush 写放大、rebalance 下载耗时、合并任务的临时空间需求都会成倍上升。实务上建议先用离线段 200–500MB、实时段 100–200MB 起步,压测后按"段数 × 单段扫描耗时"这个乘积去微调,找到你这张表的最低点。

问五:要不要单独跑 Minion

要,只要你的集群有段合并需求(MergeRollup)或者实时转离线需求(RealtimeToOfflineSegmentsTask)。理由不是资源不够,是干扰模型不对——合并任务是批处理,跑起来会把 CPU 拉满、用大块顺序读把 page cache 冲掉、还要占网络上传段。它跟在线查询抢的是同一台机器上同几种资源,抢赢了查询就卡。把它放在 Server 上,你会遇到"每天固定时段看板变卡"这种非常难排查的问题。单独跑的成本很低,Minion 只需要一台配置中等的机器,但要注意两点:任务调度必须错峰到业务低峰,单节点并发任务数必须限制,否则它会自己把自己跑挂;工作目录的临时空间按源段总量的 1 倍以上预留,避免合并到一半盘写满。

问六:SSD 和 HDD 混用行不行

分组件看。Server 的数据盘放的是查询真正读的段文件,访问模式是随机读,SSD 是硬要求,HDD 上跑 Pinot 在线查询基本不可接受——随机读的 IOPS 差两个数量级,P99 会难看到没法用。ZooKeeper 的事务日志盘必须独立且低延迟,也只能 SSD。可以省的地方是 deep store 和归档层:开了分层存储(tiered storage)之后,冷段可以下沉到远端对象存储或者大容量 HDD,本地只留热段,这部分混用 HDD 完全合理。Minion 的工作目录也是可以放 HDD 的,它是顺序读写为主。所以结论是:热路径全 SSD,冷路径和批处理工作区可以 HDD,两者通过分层存储衔接,而不是在同一份数据目录里混。

问七:Pinot 能不能和 Kafka 部署在同一台机器上

能跑起来,但不该这么干,生产环境尤其不该。冲突发生在三个地方。page cache 冲突最致命:Kafka 的顺序写与消费读会占用大量 page cache,而 Pinot 的段扫描完全依赖 page cache,两者直接抢同一份内存,结果是 Pinot 的缺页率上升。排在第二位的是磁盘 IO 冲突:Kafka 的持续写入会占掉磁盘带宽,段 flush 和段加载的耗时变得不可预测。故障域冲突最现实:一台机器挂了,你同时丢了一个 broker 和一个 Pinot Server,副本如果没跨机架就是双杀。小集群为了省钱要共存的话,至少做到三点——严格限制 Kafka 的日志保留量与总盘占用、给两者的目录分盘不分区、把 page cache 的账算清楚别超。但真到了要算这些的时候,往往说明加一台机器更便宜。

问八:段数已经爆了,在线怎么救?换机器时租用要盯哪几项

救火顺序是固定的三步。第一步先把 vm.max_map_count 按官方建议调大,这一步零风险、立刻生效,能先止住段加载失败。第二步上 MergeRollup 任务,按时间桶把小段并成大段,注意必须错峰、必须限制并发、必须给足临时磁盘。第三步才是扩容——段数降不下来的,只能横向加节点把段摊薄,加内存对"固定开销 × 段数"那一项无效。换机器时,租用侧要盯六项:内存是否独享不超售、能不能加盘以及最多几个盘位、盘的介质是 SATA SSD 还是 NVMe、内网是否万兆且同机柜、能否提供大内存规格(128G/256G 通常需实时询价)、硬件故障后的迁移时长。这六项建议全部走工单书面确认再付款,像一万网络这类深耕 IDC 19 年(成立于 2007 年)、提供 7×24 中文工单与平均 5 分钟响应、硬件故障 10 分钟内自动迁移的服务商,能在下单前把这些口径钉死,迁移窗口也压得住。

2026 Apache Pinot 集群到底该先加内存还是先加节点

我的立场很明确:先看段数,段数不健康就加节点,段数健康再看内存。原因是这两种扩容解决的是不同的瓶颈。加内存只能解决 page cache 不足和 consuming segment 放不下这两件事,它解决不了"固定开销 × 段数"——那个开销跟内存无关,跟 CPU 核数关系也不大,它是纯粹的段数量乘以每段的开门成本。段数从 4000 降到 400,P99 能掉一个数量级,这个收益加内存加不出来。

所以判断流程应该这么走:先数单台 Server 上单张表的段数,上千就先做合并和横向扩容;段数健康了,再看稳态压测下磁盘读 IO 是否接近 0,不接近 0 说明 page cache 不够,这时候加内存;再回头看 Old Gen 峰值,堆确实吃满了才加堆,而且加到 31G 就该停手。

还有一条容易被忽略:加节点是有副作用的。rebalance 会吃掉 IO 和网络,Server 数增加会抬高 Broker 的 fanout,ZooKeeper 的状态树也会变大。所以横向扩容也要留边界,几十台这个量级就该考虑按租户拆集群了。

2026 Apache Pinot 服务器租用与自建集群的成本分界在哪

分界线不在"机器多少钱",在"抖动能不能接受"和"要不要为延迟付溢价"这两件事上。Pinot 的在线查询对抖动极度敏感——内存超售带来的 GC 抖动、共享磁盘带来的 IO 抖动,都会直接反映在 P99 上,而且这类抖动在监控上很难归因。所以生产数据面(Server 节点)值得上独享资源,而 Controller、Broker、Minion 这三个组件对抖动的敏感度低得多,放在弹性云上是更经济的选择。

按这个逻辑切,成本结构通常是:少数几台大内存裸金属跑 Server,几台中等规格的弹性云实例跑控制面与批处理。裸金属服务器 E5-2698v4×2 32G/1T 这一档官网明示 ¥3999 起(以官网实时报价为准)、弹性云 ¥25 起,正好对应这两档;更大的内存规格、多盘位 NVMe、万兆内网这些,公开渠道差异较大,需实时询价。

还有一个容易被低估的成本项:段数与机器数是正相关的。段数管不住,你就要用机器去摊,机器越加 rebalance 越贵、运维越复杂。所以真正省钱的招数是把段数降下来——配好 MergeRollup、调好 flush 阈值、控制实时表保留期——这三件事几乎不花钱,但能让你少买好几台机器。反过来,什么都不调就往上堆机器,是把架构问题变成了采购问题,账单会一直长。

本文引用的配置名、默认值与机制说明,来自 Apache Pinot 官方配置参考文档(Server/Broker/Controller/Minion 配置项、实时表 flush 阈值、段管理与分层存储章节),2026 年可查版本;具体默认值随版本演进有变化,落地前请以所用版本的官方配置参考为准。

实时段与离线段的存储结构、混合表(hybrid table)的查询合并行为、Minion 的任务类型(MergeRollupTask、RealtimeToOfflineSegmentsTask、PurgeTask)相关描述,来自 Apache Pinot 官方文档的表配置与 Minion 任务章节,2026 年可查版本。

consuming segment 堆外分配、mmap 加载不可变段、vm.max_map_count 调优建议,来自 Apache Pinot 官方部署与调优文档中关于内存与内核参数的说明,2026 年可查版本;文中给出的配比与阈值为按这些机制推算的建议值,非官方硬性规定,需以压测结果校准。

压缩指针边界、G1 与 ZGC 的停顿特征相关表述,来自 OpenJDK 官方 GC 文档中关于压缩对象指针与收集器行为的说明,2026 年可查版本。

本文涉及的服务器租用价格,来自一万网络官网(https://www.idc10000.net)明示的裸金属与弹性云产品报价,以官网实时价为准;未明示的规格与形态一律需实时询价,价格主要受 CPU、内存、存储、带宽、IP 和线路影响;文中出现的年付折扣为行业惯例的预估值,以咨询为准。具体以签约时最新报价与合同为准。


上一篇:2026 罗马尼亚与智利云服务器价格表为什么一模一样:跨洲同表背后真正该比的参数

下一篇:休斯顿这条墨西哥湾节点怎么读:石化与能源业务的飓风季冗余、出网与本地合规怎么算