上周帮一个团队看作业一条日处理量不到两亿条消息的实时链路他们给了 64 核、256G 的 TaskManager并行度直接按核数拉满到 64。上线之后端到端延迟比之前 16 核的版本还高了三成CPU 使用率却只有一半出头。翻火焰图时间全耗在跨线程队列竞争、JVM GC 和网络 buffer 上业务逻辑的占比反而被挤掉了。这类问题在 Flink 性能调优里出现的频率高到离谱根子几乎都在同一个地方CPU 核数与并行度到底怎么对应。很多人脑子里有一条默认公式——有几个核就开几个 slot并行度等于核数机器越多越快。这条公式在某些场景下成立更多场景下是坑。这篇就来把这件事拆开讲透并行度的四层覆盖关系、slot 与核数的真实绑定逻辑、怎么从吞吐倒推资源、超线程要不要算、算子级并行度怎么分段设置、调大反而变慢的几种典型原因以及一套能直接抄的配置与排查流程。不管你是刚接手 Flink 作业的开发还是在做集群资源规划都能从里面找到能立刻用的东西。1. 先把并行度这层窗户纸捅破1.1 四层并行度谁覆盖谁Flink 的并行度不是一个值而是一组值按优先级从高到低排下来有四层。搞不清这个覆盖链你会经常遇到我明明在代码里写了 16跑起来怎么是 8这种问题。层级设置位置作用范围优先级算子级map(...).setParallelism(8)单个算子最高执行环境级env.setParallelism(16)当前 Job 内未单独指定的算子次高提交参数flink run -p 32整个 Job 的默认值中等集群默认parallelism.default: 1所有未指定并行度的 Job最低实际生效规则很简单就近覆盖。算子自己设了就听自己的没设就往上找执行环境执行环境没设就看命令行-p-p也没有才落到集群配置文件。这条链路里最容易踩的坑是-p有些团队在启动脚本里硬编码了-p 4后面代码里env.setParallelism()也没写结果整个作业一直跑在 4 并行度上加机器加内存完全不起作用排查半天才发现是启动脚本的问题。我的习惯是把并行度统一收口到配置中心代码里只读配置、不写字面量-p参数在提交脚本里显式传入同一个值。这样至少保证看到的配置和跑起来的配置是同一份不会被某一层悄悄覆盖。1.2 Slot、TaskManager 与 CPU 的三角关系要理解核数和并行度的关系先把 Slot 这个概念摆正。Slot 是 TaskManager 上的资源切分单位本质上是把 TaskManager 管理的内存切成若干份每份配一个任务执行名额。一个 Slot 里可以跑一条完整的算子链chain也就是多个算子的 subtask 首尾相连跑在同一个线程里。关键点在这里Flink 默认不对 Slot 做 CPU 隔离。taskmanager.numberOfTaskSlots这个参数切的是内存和管理名额不是 CPU 时间片。哪怕你配了 8 个 slotTaskManager 进程还是能看到宿主机全部核心线程调度由操作系统决定。所以1 slot 1 核从来不是 Flink 强制的东西而是业界根据经验约定出来的一个平衡点。为什么这个约定能流传开因为一个 Slot 里通常跑一条算子链链上有一个处于活跃状态的算子在做实际计算其余算子在传递数据。给一个 Slot 配一个物理核可以让活跃算子的线程基本不和其他 Slot 抢 CPU 时间片减少上下文切换。这属于够用且不浪费的折中而不是什么硬性约束。顺带说清另一个常被混淆的点taskmanager.numberOfTaskSlots决定的是一个 TaskManager 能承载多少个 subtask而 Job 的并行度总和决定需要多少个 subtask。两者相除就是你需要几台 TaskManager。所需 TaskManager 数 ceil(Job 最大并行度 / 每台 slot 数)举例Job 最大并行度 48每台 TaskManager 配 8 个 slot那就需要 6 台。1.3 为什么一核一 slot不能生搬硬套这个约定在三种情况下会失效而且失效得很隐蔽。第一种是算子链里有重计算算子。假设一条链上是source - flatMap(重解析) - filter - sinkflatMap 占了 90% 的 CPU 时间那这个 Slot 实际吃满的是一个核配 1 核刚好。但如果链上是source - map(轻量转换) - sink全程 IO 密集一个 Slot 可能只用到 20% 的核这时候还坚持 1 slot 1 核就是白白浪费 80% 的算力。IO 密集型作业完全可以一个核跑 2 到 3 个 Slot。第二种是超线程的存在。一台 32 物理核、开超线程的机器lscpu看到的是 64 个逻辑核。你到底按 32 还是 64 配 slot我的经验是按物理核数配最多上浮 20%。超线程对计算密集型任务的实际增益通常只有 15% 到 30%但对内存带宽和缓存的压力是实打实的。Flink 处理链路里大量的序列化、反序列化、hash 计算都是计算密集型按逻辑核配 slot 会明显增加 cache miss延迟抖动也会变大。第三种是Slot 内多算子并存的场景。如果你手动拆了 slot sharing group把 source 和 sink 分到不同组同一个 TaskManager 上就会同时跑多组算子。这时候每个 Slot 的 CPU 需求不再统一按组内最重的那个算子来估更靠谱。注意taskmanager.numberOfTaskSlots只在 standalone 和 session 模式下由你手工配置。跑在 Kubernetes 或 YARN 上时如果开启了动态资源分配Slot 数往往由算子资源规格自动推导改配置文件不一定生效得改部署描述里的资源请求。2. CPU 核数怎么算从吞吐倒推资源2.1 单核处理能力的实测标定几乎所有资源规划翻车都是因为跳过了这一步直接拍脑袋。核数不是猜出来的是量出来的。方法有两个一个粗一个细。粗的方法看 Flink 自带的指标。作业稳定运行后去 Web UI 或者指标系统里拉两个值numRecordsInPerSecond每秒输入记录数和busyTimeMsPerSecond每秒忙碌毫秒数。后者是每个 subtask 的值取值 0 到 1000表示这个 subtask 的线程有多大比例的时间在真正干活不含等待、阻塞。单 subtask 实际吞吐 numRecordsInPerSecond / 该 subtask 的并行度 单核处理能力 单 subtask 实际吞吐 / (busyTimeMsPerSecond / 1000)假设观测到某个 subtask 每秒处理 5000 条busyTime 是 600ms那它的单核处理能力大约是 5000 / 0.6 ≈ 8333 条/秒。这个数字就是你的标定基准。注意要取多个 subtask 的中位数别拿最高值因为存在数据倾斜时最高值会严重高估。细的方法是自己压测。把作业并行度固定成一个明确的值比如 4用真实数据回放或者构造接近真实分布的流量逐步加压直到 busyTime 稳定在 850ms 以上此时的吞吐除以 4 就是单核能力。这个值比线上指标更可信因为线上环境往往已经被各种限流、超时拖慢了。两种方法我一般都用先用线上指标快速估个范围再用压测校准。两者的偏差如果超过 30%说明线上存在明显的资源浪费或者外部依赖瓶颈得先查清楚再谈扩容。2.2 并行度计算公式与实例拆解拿到单核能力剩下的就是除法加余量。完整公式理论所需核数 峰值吞吐 / 单核处理能力 实际配置核数 ceil(理论所需核数 × (1 冗余系数))冗余系数我一般取 0.3 到 0.5。为什么要留这么多因为流量是有毛刺的大促、整点、上游补数都会造成短时峰值同时 GC 停顿、检查点对齐、外部依赖抖动都会吃掉一部分算力。留 30% 是底线关键链路留 50%。拿个具体例子走一遍。某实时数仓项目Kafka 输入做维度关联和指标聚合后写进下游存储。峰值吞吐目标20 万条/秒实测单核处理能力8000 条/秒理论所需核数 200000 / 8000 25冗余系数取 0.4实际核数 25 × 1.4 35取整到方便切分的数字36 核36 核怎么落到集群上如果每台 TaskManager 给 4 核 8G那就是 9 台如果给 8 核 16G就是 4 台 32 核多出来的 4 核留给 JobManager 和其他组件。这里有个细节TaskManager 的核数不要跨 NUMA 节点。一台双路机器如果每路 16 核最好让每个 TaskManager 实例落在单个 NUMA 节点内用 cgroup 或 taskset 绑核跨节点的内存访问延迟会高出一截在低延迟场景里很致命。再往下拆到算子层面这个总并行度要分配给各个算子。原则是中间算子可以比 source 高但不要盲目高。source 的并行度上限是 Kafka 分区数比如 24 个分区source 最多 24 有意义的并行度再多就是空转线程中间经过keyBy之后的算子并行度可以设成 36但要考虑后面 sink 的承受能力。2.3 超线程、NUMA 与绑核的实际取舍这三个话题经常被混在一起谈其实解决的是不同层面的问题。超线程影响的是你把什么算作一个核。前面说过按物理核配 slot 更稳。判断方法很简单lscpu看Thread(s) per core如果是 2就说明开了超线程。再看Core(s) per socket和Socket(s)物理核数 socket 数 × 每 socket 核数。配置 slot 时以这个数字为准。NUMA 影响的是内存访问延迟。Flink 的处理链路里有大量的堆内对象创建和读取如果线程跑在 socket 0 而内存分配在 socket 1每次访问都要跨节点互联延迟能差 1.5 倍以上。容器化部署时这个问题被弱化了因为容器通常被限制在有限的核上调度器会尽量保证局部性。但如果是裸机部署强烈建议用numactl --cpunodebind0 --membind0把 TaskManager 进程绑到单个节点。绑核CPU 亲和性解决的是上下文切换和缓存抖动。Flink 的线程模型比较特殊每个 subtask 的主线程加上异步 IO、定时器、网络等辅助线程线程数远多于核数。不绑核时操作系统会在核心间反复迁移线程每次迁移都会丢 L1/L2 缓存。用taskset -c 0-7把进程绑到 8 个核上实测在 GC 压力大的作业里能减少 10% 到 20% 的长尾延迟。代价是灵活性下降某几个核忙死、某几个核闲着的情况会更明显所以要配合监控看每个核的使用率是否均衡。提示绑核之前先确认你的 JVM 参数里没有开UseDynamicNumberOfGCThreads之类的动态调整项否则 GC 线程会被限制在你绑定的核上反而可能拖慢 Full GC 的回收速度。2.4 内存与 CPU 的配比不能偏科调 CPU 的时候如果不同步看内存大概率会掉进另一个坑。Flink 的内存模型里TaskManager 总内存被切成几块JVM 元空间、JVM 堆内框架堆 任务堆、堆外网络 buffer、Managed Memory、JVM 直接内存。Slot 数直接决定了每块内存的切分粒度。每 Slot 的堆内存 (TaskManager 总内存 - 预留 - 元空间 - 网络内存) / slot 数一个常被忽略的事实slot 数翻倍每个 slot 的堆内存就减半。堆变小之后Young GC 会更频繁对象晋升到老年代的速度变快老年代更容易堆积。很多并行度调大之后反而变慢的案例本质上是 GC 压力上去了。我习惯的配比是按每 slot 4G 到 8G 堆内存来定。如果算出每个 slot 只有 2G 堆那就说明核数给多了或者内存给少了要么加内存要么减少 slot 数、改用更高的单 slot 处理能力。另外两个参数值得盯一眼taskmanager.memory.network.fraction默认 0.1网络 buffer 池。并行度上调之后跨算子的网络传输量会上升尤其是经历了 rebalance 或者 keyBy 之后的 redistribute默认 0.1 在大并行度下容易触发 buffer 不足表现为Insufficient number of network buffers。我一般调到 0.15 到 0.2。taskmanager.memory.managed.fraction默认 0.4RocksDB 状态后端用这块。状态大的作业要往上调但调高之后堆内存会被挤压得一起算总账。3. 并行度配置的实操落地3.1 全局与算子级配置的写法配置思路是全局定基线算子做微调。全局基线用前面算出来的中间算子并行度然后针对 source、sink 和重计算算子单独设置。StreamExecutionEnvironment env StreamExecutionEnvironment.getExecutionEnvironment(); // 全局基线对应前面算出的 36 env.setParallelism(36); // source受上游分区数约束24 个分区就设 24 DataStreamEvent source env .addSource(new FlinkKafkaConsumer(...)) .setParallelism(24) .name(kafka-source) .uid(kafka-source); // 重计算算子单独拔高 DataStreamEnriched enriched source .keyBy(Event::getUserId) .flatMap(new HeavyEnrichFunction()) .setParallelism(48) .name(enrich) .uid(enrich); // sink受下游连接数约束比如连接池只有 16 个连接 enriched .addSink(new JdbcSink(...)) .setParallelism(16) .name(jdbc-sink) .uid(jdbc-sink);几个实操要点uid()一定要写。从状态恢复时Flink 靠 uid 和算子哈希匹配历史状态。不写 uid 的话改了并行度或者调整了算子顺序状态就对不上轻则丢状态重则起不来。这是用状态换来的血泪教训。source 并行度不要超过分区数。超过的部分会分到空分区这些 subtask 一直处于 idle 状态但依然占着 slot 和线程。从 Flink 1.14 开始对空闲 source 有一定优化但没必要主动制造这种浪费。sink 并行度不要超过下游承受能力。数据库连接池、下游服务的 QPS 上限、写入分片的数量任何一个卡住都会让上游反压。写 JDBC 的场景sink 并行度乘以每并行度的连接数不能超过数据库的最大连接数。3.2 算子链、rebalance 与分段设置的配合并行度不一致的算子相邻时Flink 会在中间插入 redistribute 类型的连接rebalance、rescale、forward、hash。默认情况下并行度从低到高会走rebalance轮询分发从高到低也是轮询。听起来挺公平问题在于数据倾斜的时候轮询解决不了热点。举个例子source 24 并行度enrich 48 并行度。24 到 48 是扩大每个上游 subtask 的数据会被轮询发给下游 2 个 subtask看起来很均匀。但如果某个 key 的数据量占了 40%经过keyBy之后这些数据全落到同一个 subtask 上下游那个 subtask 就爆了其余 47 个半闲。这种情况下调并行度完全无效得上两阶段聚合或者给热点 key 加随机前缀。另一个细节是算子链的合并条件。相邻算子并行度相同且连接方式是forward时默认会 chain 在一起跑在同一个线程里。这能减少线程间传递开销但也意味着链上最慢的算子会拖累整条链。如果链上有个调用外部接口的算子我会用disableChaining()或者startNewChain()把它单独拆出来让它自己的并行度独立生效。// 把重 IO 的算子单独成链避免拖累前后 stream .map(new LightTransform()) .startNewChain() // 从这里开始新链 .map(new RemoteCallMap()) .disableChaining() // 后面也断开 .map(new AnotherTransform());3.3 slot sharing group 的拆分时机默认所有算子共享一个 slot sharing group一个 slot 里能塞下整个 pipeline 的全部 subtask。这个设计的意图是让 CPU 密集型算子和 IO 密集型算子混在一个 slot 里互相填补等待时间。大部分场景下这是最优解不要乱拆。需要拆的场景不多但都挺典型场景一source 和 sink 都很重。比如 Kafka source 和 ES sink 都吃网络混在一起会争抢同一个 slot 的网络 buffer 和线程。这时候把 source 单独放一个组sink 单独放一个组中间计算放默认组各自独立调度。source.slotSharingGroup(source-group); sink.slotSharingGroup(sink-group);场景二状态差异很大。有状态的算子比如大的 RocksDB 状态和纯无状态算子放一个 slot 里内存切分很难兼顾。把有状态的算子单独成组可以给它配更大的托管内存。场景三需要隔离故障域。某类算子容易抛异常重启单独成组之后重启范围可控不会波及整条 pipeline。拆组的代价是资源利用率下降。原本一个 slot 能装 8 个 subtask拆完之后每个组可能只装 3 到 4 个slot 总数反而要增加。所以拆之前先算清楚拆完之后的 slot 总数不能超过集群容量否则作业会一直处于 pending 状态。3.4 提交命令与资源配置的完整样例把前面的结论落成一份可执行的配置。假设场景是峰值 20 万条/秒单核 8000 条/秒冗余 40%总核数 36每 slot 6G 堆Kafka 24 分区。flink-conf.yaml里的关键项# 每个 TaskManager 的 slot 数 物理核数 taskmanager.numberOfTaskSlots: 8 # 单 TaskManager 总内存8 slot x 6G 堆 网络 托管 预留 taskmanager.memory.process.size: 64000m taskmanager.memory.managed.fraction: 0.4 taskmanager.memory.network.fraction: 0.15 taskmanager.memory.network.min: 256mb # 默认并行度给个保守值实际提交时用 -p 覆盖 parallelism.default: 8提交命令flink run \ -t kubernetes-application \ -p 36 \ -D taskmanager.numberOfTaskSlots8 \ -D taskmanager.memory.process.size64000m \ -D taskmanager.memory.network.fraction0.15 \ -c com.example.jobs.RealTimeAggJob \ /opt/jobs/realtime-agg.jar按 36 并行度、每台 8 slot 计算需要ceil(36/8) 5台 TaskManager实际会启动 5 个实例最后一个实例只用 4 个 slot。验证配置是否按预期生效作业起来之后做三件事Web UI 的 Job Graph 里逐个算子确认并行度和预期对不上的就是被覆盖了。看busyTimeMsPerSecond的分布如果所有 subtask 都在 850 以上说明核数吃紧了要扩容如果中位数在 300 以下说明核数给多了可以砍。看backPressuredTimeMsPerSecond任何一个 subtask 超过 200ms先查反压来源别急着加并行度。4. 调优过程中的坑与排查4.1 并行度调大反而变慢的四种典型原因这个现象比想象中普遍原因基本集中在四个方面。第一GC 压力上升。前面提过slot 数翻倍意味着每 slot 堆内存减半。128 个 slot 分 128G 堆每 slot 只有 1GYoung 区可能就 300M稍微有点对象创建就触发一次 Young GC。并行度上调之后总的对象创建速率不变但每次 GC 要扫描的区域变小了GC 频率成倍增加整体 CPU 花在 GC 上的比例可能从 10% 涨到 35%。判断方法看 GC 相关的指标面板GarbageCollector下的Count和Time如果 Time 增长速度和并行度增长成正比甚至更快就是这个原因。第二跨线程数据交换变多。并行度提高之后同一份数据在算子间要切分给更多的 subtask网络传输和序列化反序列化的开销同步上涨。如果作业本身是网络密集型的比如频繁的rebalance、大规模的keyBy洗牌提高并行度可能让网络成为新瓶颈。表现是 CPU 使用率不高但outPoolUsage一直很高buffer 排队。第三小文件和小批次问题。并行度高了之后写 HDFS 或者对象存储时每个 subtask 都开自己的 writer文件数等于并行度乘以桶数容易产生大量小文件。这既是存储压力也是 NameNode 压力。用StreamingFileSink的话要调大滚动间隔和文件大小阈值。第四外部依赖承受不住。最典型的是数据库。并行度从 16 调到 64连接数可能从 16 涨到 64数据库侧直接被打满响应时间从 2ms 涨到 50ms反而拖慢了整体吞吐。这种场景该做的是批量写入加连接池收敛而不是继续加并行度。4.2 从反压指标定位真正的瓶颈反压是并行度设置的直接反馈信号但很多人看到反压的第一反应是加并行度这是最危险的操作。正确顺序是先定位反压发生在哪个算子再判断这个算子加并行度有没有用。Flink Web UI 的 Backpressure 面板会给出每个算子的反压等级OK / LOW / HIGH。但这个面板是采样结果有时候会误导。更精确的做法是看几个细分指标指标含义判读方式backPressuredTimeMsPerSecond该 subtask 每秒被下游反压的毫秒数持续大于 200 说明下游堵塞idleTimeMsPerSecond空闲时间高 idle 低 busy 上游供不上busyTimeMsPerSecond忙碌时间接近 1000 说明计算饱和outPoolUsage输出 buffer 池使用率持续接近 1 说明下游消费慢inPoolUsage输入 buffer 池使用率持续接近 1 说明上游发送快判读逻辑走一遍所有算子busyTime都接近 1000backPressuredTime也高说明整体算力不足加并行度有效。这时候扩到当前的两倍左右比较稳妥。只有某个算子busyTime高下游backPressuredTime高说明是这个算子拖慢了整条链。加它的并行度之前先看它是不是有状态算子——有状态的话并行度变化会影响状态分布得规划好。某个算子idleTime高、busyTime低inPoolUsage接近 1说明它上游供得太快或者它自己在等外部 IO。这时候加并行度没用得看外部依赖。sink 算子busyTime高、outPoolUsage打满那是下游存储的问题扩 sink 并行度要看下游能不能接住。注意backPressuredTimeMsPerSecond有滞后性作业刚调节并行度后会有几分钟的采样不准别拿刚重启的数据做判断。等作业稳定跑够 10 分钟再看。4.3 数据倾斜会让并行度计算完全失真前面所有的核数计算都基于一个假设流量在 subtask 之间均匀分布。这个假设在keyBy之后经常不成立。判断倾斜的方法看 Web UI 里同一个算子的各 subtask 的numRecordsInPerSecond或者直接看busyTimeMsPerSecond。如果最大值和最小值差 3 倍以上基本就是倾斜了。倾斜的危害不只是慢而是你按最大值规划的核数绝大多数核心都在浪费。40% 的数据压在一个 subtask 上你就算把并行度配到 128那个 subtask 该多慢还是多慢。处理倾斜有几种手段按见效速度排给热点 key 加随机前缀做两阶段聚合。第一阶段对 key 加 0 到 N 的随机后缀让同一个 key 的数据分散到 N 个 subtask 做局部聚合第二阶段去掉前缀做全局聚合。// 第一阶段加随机前缀扩大聚合的并行面 DataStreamTuple2String, Long partial stream .map(e - Tuple2.of(e.getKey() _ random.nextInt(16), 1L)) .keyBy(t - t.f0) .sum(1); // 第二阶段去前缀做最终聚合 DataStreamTuple2String, Long result partial .map(t - Tuple2.of(t.f0.split(_)[0], t.f1)) .keyBy(t - t.f0) .sum(1);这个方案代价是多了一轮洗牌吞吐会掉 10% 到 20%但能换回均衡。只在确认有热点 key 时才用。单独处理热点 key。如果热点 key 是少数几个比如默认值、异常值可以先用filter把它们分流出来走一条并行度为 1 的旁路单独处理其余数据走主链路。这招在脏数据场景下特别管用。调整 key 的选取。有时候倾斜不是数据本身的问题而是 key 设计得不好。比如用userId做 key 时如果存在大量游客账号共用一个 ID就会集中。换成userId sessionId或者加一层哈希分布立刻均匀。这个改动最小、收益最大优先试。4.4 常见问题速查表把前面散落的排查经验整理成一张表出问题时按图索骥。现象最可能的原因排查动作处理方式加了并行度吞吐没涨瓶颈在下游存储或外部依赖看 sink 的busyTime和outPoolUsage优化下游写入批量连接池而不是继续加并行度加并行度后延迟升高GC 频繁或线程竞争看 GC Count/Time 增长曲线减少 slot 数、增大单 slot 堆内存或换 G1/ZGC部分 subtask 一直 100%数据倾斜对比各 subtask 的numRecordsInPerSecond两阶段聚合、热点分流、改 key 设计作业一直 pending 起不来并行度 集群总 slot 数对比并行度与 slot 总量减少并行度或扩 TaskManager频繁报 buffer 不足网络内存比例偏低看taskmanager.memory.network.fraction调到 0.15 到 0.2状态恢复失败并行度或 uid 变更检查算子 uid 和maxParallelism固定 uidmaxParallelism一次性设大不轻易改source 有大量 idle subtask并行度超过分区数对比 source 并行度和 Kafka 分区数source 并行度设为分区数的整数倍或相等检查点超时并行度大导致 barrier 对齐慢看 checkpoint 的 alignment 耗时开unaligned-checkpoints或降低并行度CPU 使用率低但延迟高线程阻塞在外部 IO抓线程栈看阻塞点改异步 IO 客户端提高单并行度处理能力补充几个表里塞不下的细节。maxParallelism这个参数只能在作业首次启动时设定之后改了就从状态恢复不了。默认值是根据并行度推算的通常够用但如果你预计未来会扩到很大的并行度建议首次就显式设成 128 或者 256。设置方法env.setMaxParallelism(256);或者对单个算子stream.keyBy(...).window(...).setMaxParallelism(256);maxParallelism决定了 key group 的数量key group 是状态分片的单位。并行度变化时状态通过 key group 重新分配只要maxParallelism不变状态就能正确恢复。所以这个值宁大勿小设大了只是每个 key group 稍小一点没有实质代价。还有一个容易被忽略的点JobManager 也是要占资源的。并行度 128 的作业JobManager 要管理上千个 subtask 的调度、检查点协调、指标上报1 核 1G 肯定不够。我一般按并行度 / 32核来给最少 2 核 2G。最后说一个我在实际项目里反复验证过的经验并行度的调整要一次只动一个变量。同时改并行度、改内存、改 slot 数出了问题根本分不清是哪个改动造成的。我的做法是先在压测环境把并行度调到目标值观察 30 分钟稳定后再动下一个参数如果某个改动让指标变差立刻回滚记录下这个组合不可用。这套流程看着慢但比在线上反复试错要快得多。另外别迷信核数等于并行度这种一刀切的说法。同一条链路里source 受分区数限制、中间算子受计算量限制、sink 受下游连接数限制三段该配的并行度本来就不一样。把每一段的约束条件想清楚比背任何公式都有用。