1. 从现象说起为什么调线程池大小不能靠经验和猜接手任何一个多线程业务第一关永远绕不开线程池参数配置。很多团队一上来就复制一套标准参数corePoolSize8、maxPoolSize16、queueCapacity1000看起来天经地义实际一压测就出问题。这个问题的根子在于密集计算任务的线程模型和普通业务请求处理模型是完全不同的不能共用同一套调优套路。所谓密集计算指的是任务的运行时间主要消耗在CPU计算、内存读写、数据聚合这类本地计算上而不是在等待数据库、外部HTTP接口、消息队列这些远端资源返回。两者的本质区别在于IO密集任务里线程大部分时间在阻塞等待线程数可以开得很大而密集计算任务里线程大部分时间在拼命跑线程数开多了反而互相争抢CPU时间片上下文切换的开销比计算本身更贵。举个例子。某个数据处理系统每秒钟要处理几万条日志每条日志需要做正则提取、字段规整、内存累积排序单条耗时大约1.5毫秒。最初团队按照一个请求一个线程的思路把线程池开到200以为处理得更快结果CPU直接冲到95%以上但吞吐量反而比线程数CPU核心数时还低。问题就出在200个线程在同一批物理核上反复切换光保存和恢复线程上下文就吃掉大量CPU周期。这篇文章会告诉你如何准确判断你的业务属于什么类型的任务怎么用一套可计算、可验证的方法去确定线程数、队列长度和拒绝策略以及在线程池之外架构层面上还有哪些隔舱、熔断和分片手段能压榨密集型任务的最大吞吐。全程不靠直觉全部用参数计算和压测数据说话。不管你是写实时计算中间件的开发者、维护后台批处理系统的主力工程师还是刚接触并发编程不久想弄明白为什么线程池调大反而变慢的新人这篇文章都值得读完。顺便强调一点接下来讲的调优思路不仅适用于Java技术栈。只要你的语言运行时里有线程、协程或者Actor模型这套先建模、再计算、后压测验证的方法论都通用。2. 任务建模先行搞不清类型调优无从谈起2.1 CPU密集型、IO密集型与混合型的判定方法进入参数调优之前先花五分钟做任务画像。你要回答三个问题单个任务里纯计算时间占比是多少有没有同步等待等待的粒度是什么CPU密集型任务基本不阻塞跑满一个线程的所有时间片。典型场景图像滤镜计算、金融风控评分模型推理、批量编解码、数据压缩。判断依据任务方法堆栈里几乎看不到wait/sleep/epoll之类的调用线程状态绝大部分时长为RUNNABLE。IO密集型任务大量时间在等待网络响应或磁盘IO。典型场景HTTP接口调用、数据库读写、文件搬运。混合型先查缓存、再计算、再写结果每一段都有阻塞也有计算。绝大多数业务代码属于这一类。如何量化和判断有一个简单办法在测试环境用单线程跑一遍真实任务分别统计CPU占用时间和阻塞等待时间。如果你的采样工具不方便可以粗略采用经验比例如果等待时间长、计算时间短IO占比高线程数可以开大。如果两个时间差不多按混合型处理。如果计算时间远大于等待时间你是典型的CPU密集型人云亦云的线程数CPU核心数1才能发挥作用。很多开发者容易忽略一点同样一段代码在不同硬件环境下类型可能发生变化。例如本地缓存命中率很高时某服务是CPU密集型一旦缓存命中率下降大量请求穿透到数据库它就变成了IO密集型。所以任务建模不是一次性工作每轮架构重构后都应该重新评估。2.2 性能指标基线吞吐量、延迟与资源利用率的三角考量调线程池本质上是沙雕游戏——在吞吐量每秒处理任务数、延迟单个任务完成时间、资源利用率CPU/内存三个指标之间找平衡点。不可能三角里极端追求吞吐往往会牺牲延迟抖动。我建议在建模阶段就把这三个指标明确的量化目标写出来。比如指标目标值统计口径P99延迟≤ 800ms从任务进入队列到执行完成吞吐量≥ 5000 tasks/s每秒完成任务数CPU占用≤ 85%单机整体CPU利用率预留峰值缓冲没有目标就去调线程池等于拿着方向盘不知道目的地最后只能靠看起来不高和感觉还行来验收这是很多线上事故的源头。另外要建一套压测基准。压测工具推荐直接用JMH做单任务耗时的微基准然后用wrk或者自研压测脚本测整体吞吐。只有把单点耗时和全局吞吐分开测量你才知道自己的优化到底起效在哪个环节。某些时候单任务耗时没变但吞吐翻倍了说明线程池调度效率提升了反过来单任务耗时恶化但吞吐没变说明线程分配策略给任务执行本身拖了后腿。3. 线程池参数调优不再背公式而是算账3.1 核心参数的关系corePoolSize、maximumPoolSize、queueCapacity和拒绝策略线程池的设计比大多数人以为的复杂它像一个讲究流程的工厂车间corePoolSize车间里常驻的工人数量。不忙的时候也要养着。maximumPoolSize急单下来车间最多可以临时招到多少工人。queueCapacity车间门口的待处理工单架能堆多少活儿。拒绝策略工单架也满了、工人都满了新单子怎么处置。一个重要事实是任务提交顺序不是先启动corePoolSize线程满了直接扩容到maximumPoolSize而是线程数达到corePoolSize后新的任务先进队列排队队列填满之后才会继续创建线程直至maximumPoolSize线程数到maximumPoolSize且队列也满了才触发拒绝策略。这个顺序对密集计算任务至关重要。如果队列开得很大线程数永远涨不到maximumPoolSize你配置的扩容参数形同虚设。反过来如果把队列开得极小甚至用SynchronousQueue那又等于完全不做缓冲一有任务就尝试创建新线程极端情况下线程频繁创建销毁非常伤性能。密集计算场景下我推荐直接使用有界队列最常用的是ArrayBlockingQueue。队列长度不要拍脑袋下文给出计算方法。3.2 密集计算场景的核心线程数计算方法Ncpu 1 背后的真相与分析网上流传最广的经验公式是CPU密集型线程数 CPU核数 1IO密集型线程数 CPU核数 × (1 平均等待时间 / 平均计算时间)先说CPU核数 1这条公式。它源自经典操作系统的线程调度实验当线程数等于核心数时线程恰好占满每个核心但偶尔一个线程因为缺页中断、锁竞争或者GC停顿而短暂让出CPU就少了一个核心在干活。多出来的1个线程正好填补这类短暂空档让CPU保持着满载状态。它是用1个线程的微小上下文切换开销换取CPU空转概率大幅下降的划算买卖。但这只是理论值。现代服务器CPU通常开启了超线程一个物理核呈现为2个逻辑核Runtime.getRuntime().availableProcessors()拿到的是逻辑核数量。对于纯计算密集任务超线程带来的吞吐提升大约只有15%到30%并非翻倍。直接拿逻辑核数去算线程数不一定最优。我建议自主机器的场景直接按物理核数量容器环境则要认真考究一下。3.3 容器环境下的CPU核数获取与参数动态化配置容器场景是个深坑。在Kubernetes或者Docker里Java进程如果不配合容器参数availableProcessors()可能返回的是宿主机总核数而不是容器配额内的核数。比如宿主机64核容器限制4核Java仍然认为自己是64核机器线程池按64核去计算直接超卖。从JDK 8u191开始JVM默认启用容器感知的CPU资源限制availableProcessors()能正确返回容器配额。如果你还停留在旧版本必须显式加上JVM参数java -XX:ActiveProcessorCount4 -jar application.jar如果你的服务是按Pod维度独立部署每个Pod独占一个实例那么核心线程数可以基于容器配额进行计算。如果多个Pod混部在同一批物理机上还需要把物理机层面的竞争考虑进去我团队的做法是预留至少20%的CPU余量不把线程数按满额计算防止宿主上其他容器的毛刺拖垮自己。动态配置也很重要。不建议把这些参数写死在代码里最好用配置中心下发Configuration ConfigurationProperties(prefix task.executor) public class ThreadPoolProperties { private int corePoolSize; private int maxPoolSize; private int queueCapacity; private String threadNamePrefix calc-task-; // 标准的getter/setter略 }3.4 队列容量和拒绝策略的确定延迟还是丢弃要用数据说话队列容量在密集计算中决定的是背压机制。队列越长任务堆积容忍度越高但代价是任务等待时间拉长而且一旦系统崩溃内存里积压的任务全部丢失。队列太短等于拒绝策略频繁触发直接丢弃业务请求导致任务成功率下降。正确做法是结合预期的峰值流量和可接受的排队延迟来算。假设任务平均耗时T毫秒队列里最多容忍的任务等待时间为W毫秒那么合理容量约等于W / T。比如单条任务平均耗时100ms你允许任务在队列里最多等5秒那么队列容量500左右。超过500的时候就要思考是继续排队等还是丢一部分任务保护系统稳定或者让上游限流。在拒绝策略上Java内置了四种策略但密集计算任务推不推荐使用CallerRunsPolicy的呼声比较两极分化。CallerRunsPolicy的执行逻辑是如果线程池满任务就回退到调用方线程执行。对IO密集场景它天然像一个反向限流阀调用方线程被占住自己就不发新请求了。但密集计算场景里调用线程往往是接收请求的核心线程如果它被回退任务拖住后续请求没人接收很可能是灾难。所以我的建议是密集计算场景如果任务允许丢弃用DiscardPolicy并配套监控如果不允许丢弃宁可自定义BlockingPolicy用生产者线程阻塞等待也不要让调用线程去执行任务。自定义阻塞式拒绝策略代码片段如下public class BlockWhenQueueFullPolicy implements RejectedExecutionHandler { Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { try { executor.getQueue().put(r); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RejectedExecutionException(任务提交中断, e); } } }阻塞队列的put会让提交线程阻塞直到队列有空位相当于手动把背压传导给上游。期间配合信号量控制最大排队任务数防止内存无限增长。4. 真正的量化调优流程压测、监控与二次校准4.1 从理论公式到初步参数一个具体案例的计算拆解我拿最近做的一个模拟计算服务来举例。硬件环境一台物理机8核16线程业务是风控特征计算每个任务平均计算耗时80ms任务之间无依赖允许部分丢弃要求P99延迟不超过2秒。按CPU密集型初步计算物理核数8理论核心线程数819。预留20% CPU余量保守设置为7。队列容忍等待时间 目标P992000ms - 任务耗时80ms ≈ 1920ms。队列容量 1920 / 80 ≈ 24。于是初始参数ThreadPoolExecutor executor new ThreadPoolExecutor( 7, // corePoolSize 12, // maximumPoolSize 30L, TimeUnit.SECONDS, // 非核心线程空闲回收时间 new ArrayBlockingQueue(24), new NamedThreadFactory(feature-calc), new ThreadPoolExecutor.DiscardPolicy() );maximumPoolSize为什么是12理论上最大线程数应该是排队也堵不住时的最大工人数考虑到短时流量尖峰允许比核心线程多出约50%~70%的容量。这里7-12就是结合历史上峰值流量的经验值而非公式值。4.2 压测工具的选择与执行JMH测准单点wrk测准整体单点耗时基准用JMH写一个简单的微基准State(Scope.Thread) BenchmarkMode(Mode.AverageTime) OutputTimeUnit(TimeUnit.MILLISECONDS) public class FeatureCalcBenchmark { Benchmark public void testCompute() { // 构造一个真实特征计算任务 FeatureCalculator.calculate(sampleFeature); } }注意JMH的默认fork和warmup配置一定要保留否则JIT编译导致的结果偏差会误导你。多试几次取平均值和方差。整体吞吐和延迟压测我更推荐直接用自研的多线程压测客户端模拟生产环境的提交模式。比如用50个线程以每秒固定速率提交任务到被测服务统计P50、P90、P99延迟和吞吐量。wrk对HTTP服务顺手但这个场景的压测重点在于任务提交模式的真实性。4.3 监控指标怎么定队列深度、活跃线程数、拒绝次数才是核心调优之后最怕的是以为改了参数就完事。上线前必须把JMX暴露的线程池指标接入已有监控系统activeCount活跃线程数如果长期等于corePoolSize说明核心线程已经满载要看队列增长趋势。queue.size()队列深度如果持续增长说明消费速度跟不上生产速度。completedTaskCount完成任务总数。两次采样差值除以间隔时间就是真实吞吐量。rejectedCount拒绝任务数这个只要有增长就必须告警说明系统已经过载。我把它们打包成一个线程池包装器定期将指标推到监控系统。实际踩过的坑是刚开始只监控了吞吐量没有监控队列深度结果队列积压到3万条内存暴涨服务OOM。密集计算任务的数据对象往往比较大队列里积压的每条任务都占内存计算时务必把队列任务的内存占用纳入容量规划。4.4 二次校准参数不是一次定死而是跟随流量波动的活值压测数据往往只能反映编写压测脚本当时的状态。线上流量是波动的比如早晨高峰和凌晨低谷的提交速率可能差10倍以上。我建议做两件事第一给核心线程数设置一个合理的空闲回收时间让线程池在低峰期自动释放部分线程。核心线程默认不会被回收看业务情况决定是否允许allowCoreThreadTimeOut(true)。第二高峰低谷切换时动态调整corePoolSize和queueCapacity。现在Java的ThreadPoolExecutor提供了setCorePoolSize()方法可以在运行时调整。不过要注意调低corePoolSize并不会立刻中断正在跑的线程只会影响后续的线程回收节奏所以调整要提前不要等线程数已经膨胀才想起缩小。5. 进阶武器虚拟线程与ForkJoin在密集计算中的适用边界5.1 虚拟线程能解决密集计算问题吗一个容易踩的认知误区JDK 21虚拟线程出来以后很多团队欢呼雀跃线程数终于可以随便开了但这里有一个必须说清楚的认知误区虚拟线程的优势在于大幅降低阻塞型任务的调度开销它解决的是IO密集型任务中线程阻塞浪费资源的问题而不是CPU密集型任务中核心算力不够的问题。把100万个虚拟线程扔给密集计算任务每个虚拟线程在CPU上执行时仍然要和其他虚拟线程轮流占用物理核心一旦任务不吃IO、纯粹跑计算虚拟线程和物理线程调度的底层开销差异就消失了。更麻烦的是虚拟线程栈在JDBC这类同步IO场景会触发固定但在纯计算场景并不会带来吞吐提升反而因为线程数量太大任务上下文和迁移的开销在某些实现里是个负担。我的建议如果你的任务类型是计算但内部有同步等待比如混合型任务虚拟线程值得尝试。如果是纯CPU计算老老实实按上一节的模型配平台线程池不要为了新技术而新。这里不是不推荐使用而是用之前先做一次小范围压测对比拿数据下结论。5.2 ForkJoinPool在工作窃取场景中的正确用法ForkJoinPool适合的是一个大任务可以被拆成无数个小任务小任务结果可以合并的递归分治计算。典型的例子大数组排序、矩阵乘法、树结构遍历。密集计算场景里如果任务颗粒度足够细ForkJoin默认的工作窃取机制会让空闲线程主动去偷其他线程队列里的任务让CPU核心尽量不闲着。但要注意ForkJoinPool的并行度参数默认等于availableProcessors()这个数和平台线程池的核心线程数会叠加竞争。我在框架层常用的策略是把全局线程池和ForkJoinPool托管的线程总数控制在CPU核心数附近。要么计算任务统一走自定义线程池要么统一走ForkJoinPool不要双层并行否则两者抢CPU资源可能互相拖垮定位问题时还很难查是哪个池的问题。5.3 轻量级限流组件信号量与令牌桶在提交端的配合线程池本身的队列是背压点但只在队列满时才触发。更好的做法是在提交端就加上限流让线程池始终工作在安全水位上。我提到的信号量限流方案很成熟public class TaskSubmitGate { private final Semaphore gate; public TaskSubmitGate(int permits) { this.gate new Semaphore(permits); } public boolean trySubmit(Runnable task, ThreadPoolExecutor executor, long timeout, TimeUnit unit) { boolean acquired false; try { acquired gate.tryAcquire(timeout, unit); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } if (!acquired) { return false; // 或者在这里记录限流日志 } try { executor.execute(() - { try { task.run(); } finally { gate.release(); } }); return true; } catch (RejectedExecutionException e) { gate.release(); return false; } } }信号量许可数就相当于线程池正在处理的队列里等待的任务总数上限。这样即使线程池参数没调到完美提交端已经上了一道保护层。专业的架构里限流一定要放在最前面线程池只是第二道防线。6. 架构设计层面的密集计算优化从单机到分布式的思路演进6.1 任务分片与合并用最小分片粒度控制CPU利用率单机内部优化有天花板要进一步提高密集计算的吞吐就要在任务进门之前把任务拆好。数据分片的核心是每片任务大小要均匀。如果一份任务分成8份其中7份每份1秒最后一份10秒最终完成时间被最慢的拖到10秒其他线程全部空等。均匀切分的计算粒度在密集计算场景里直接决定了CPU利用率上限。分片策略适用场景注意点按ID哈希取模订单、用户维度数据数据倾斜时部分分片巨大按数据量动态切割日志、事件流需要提前知道总量或抽样估算按耗时自适应的二段切分计算本身不均匀先粗分再按实际耗时二次细分我自己用得最多的是总量已知时按记录数均分总量未知时按分片耗时反馈动态调整的组合实测下来比固定分片提升了近40%的CPU利用率。6.2 本地缓冲 批量提交减少任务排队次数提高吞吐密度方案设计上把任务的颗粒度放大不要在业务层一次只提交一条记录。举例一个数据清洗任务如果一行一行提交每次都要走一次任务封装和队列交互开销占比可能超过10%。把1000行封装成一个批次任务线程池的压力瞬间降低1000倍而且批量计算能更好地利用CPU缓存局部性。实现方式可以是在生产者侧做一个缓冲队列积累到指定条数后一次性提交public class BatchAccumulator { private final ListRecord buffer new ArrayList(); private final int batchSize; public synchronized void add(Record record) { buffer.add(record); if (buffer.size() batchSize) { flush(); } } private void flush() { ListRecord batch new ArrayList(buffer); buffer.clear(); executor.execute(() - batchProcessor.process(batch)); } }这里的批量大小不是越大越好。批量太大会导致批次内处理时间过长P99延迟飙升。我一般以单批次执行时间不超过500ms为标准来倒推批量大小。6.3 分布式扩展分区队列与结果聚合的模式选择当单机怎么调都到不了目标吞吐量时就要上多节点。分布式密集计算的本质是把任务分到多台机器把结果收回来。最常见的模式是生产者-消费者模式变的生产者把任务写入消息队列多个消费者进程并行消费。注意这里消息队列就是线程池之外的第二层背压相比本地内存队列它能承受的堆积量大得多。队列分区数最好等于消费者实例数这样每个消费者独占一个分区避免多个消费者抢同一分区的锁竞争。还有一种模式是直接使用并行流式的框架核心思想是把任务集分割成多个子任务下发到不同节点并行执行再收集子结果合并。适合计算密集且可并行化的统计类、聚合类任务。但如果任务之间有依赖比如下一步需要上一步的结果就不适合盲目并行需要先做依赖拓扑分析。6.4 熔断、隔离与降级过载时保住核心吞吐前面都在讲如何提高吞吐但架构上还有一个重要议题过载时怎么办。密集计算服务最容易出现的问题是流量突然翻倍线程池瞬间打满队列堆积然后GC压力暴增最后OOM。整个服务挂掉不如直接丢弃一部分不重要的任务。我的实践经验是三层保护第一层提交端限流。拒绝率超过阈值时直接在上游丢弃低优先级任务。第二层线程池隔离。不同优先级或不同类型的计算任务用不同线程池不要混用。比如实时评分任务和离线批量报表任务就应该隔离。一个批处理高峰把实时业务卡死是架构设计的耻辱。第三层熔断降级。当线程池的拒绝率连续超过5%主动熔断非核心功能保证核心P99指标不崩溃。下面是一个基础的熔断记录器public class CircuitBreaker { private final AtomicLong recentRejections new AtomicLong(); private volatile boolean open; public void recordRejection() { long count recentRejections.incrementAndGet(); if (count 100 !open) { open true; // 触发降级逻辑例如拒绝离线报表任务 } } public void reset() { recentRejections.set(0); open false; } }7. 常见问题与排查技巧实录7.1 线程池看起来没满任务却迟迟不执行排查思路先看任务提交到了哪个队列如果队列积压说明消费速度不够。可能是某个任务里不小心加了同步锁导致线程全部在等锁。最典型的场景是任务内部调用了某个全局单例的synchronized方法虽然是密集计算却发生了隐性的IO等待锁竞争。解决办法是在线程池任务执行前后打印线程状态快照用jstack看看大量线程是RUNNABLE还是BLOCKED。如果是BLOCKED直接找锁。7.2 调大线程池后吞吐量不升反降的经典原因CPU密集型任务调大线程数到超过物理核数量上下文切换开销占比上升吞吐量反而下降。用top -H查看各个线程的CPU占用率如果大量线程处于R状态但单线程CPU时间片被切得很碎基本就是这个原因。另一个隐藏因素是JVM的GC线程。并行垃圾回收器线程数默认随CPU核数增长如果线程池把CPU全部占满GC线程启动会让所有计算线程一起STW延迟抖动非常严重。这种情况下可以考虑调整GC线程数或者在线程池设计时故意留出一部分CPU余量。7.3 任务结果丢失或重复计算的补偿处理使用DiscardPolicy时任务被丢弃后是静默消失的。生产环境必须确保丢弃的任务有重试或者补偿机制。最简单的补偿方案被拒绝的任务记录到本地失败表中额外一个定时任务扫描重试。如果任务是可重入的用DiscardPolicy加失败记录表如果任务不可重入就别用丢弃策略了改用BlockWhenQueueFullPolicy牺牲一点上游速度换取不丢数据。这块取舍在系统设计的时候就要想清楚不能在流量高峰时才来纠结。7.4 小而快与大而重的任务混跑如何隔离一句话不要混跑。CPU密集场景下一个小任务占5ms一个大任务占5秒如果同一个线程池小任务会被大任务堵在后面P99会被拉得极难看。标准做法是拆分为两个线程池fastPool核心线程数CPU核心数×80%队列很小用于短计算任务。slowPool核心线程数CPU核心数×20%队列较大用于长计算任务。两套池的资源总和不要超过物理核数否则仍是竞争加互相干扰。7.5 排查手段速查表现象初步判断工具与命令处理方案吞吐低、CPU高上下文切换频繁vmstat 1查看cs列降低线程数延迟高、CPU低锁等待或IO等待jstack看线程状态找锁、拆锁拒绝数增长队列已满监控rejectedCount限流、扩容内存持续攀升队列积压arthas查看queue.size减小队列、加大消费速度8. 几个很容易被忽略的底层支撑点8.1 任务对象的创建与GC开销密集计算场景里每创建一个任务Runnable对象就是一份内存垃圾。如果每秒提交几万个任务瞬时对象产生量非常可观Young GC频率会被拉高。优化方向有两个。第一尽量复用任务封装对象比如批量任务代替单条任务显著减少对象数量。第二如果你们用JDK 17可以尝试用record类定义轻量任务对象减少运行时开销。另外注意代码里不要在任务执行过程中大量创建中间数组尽量复用工作数组。8.2 线程工厂定制给每条线程一个可识别的名字和优先级出线上问题时jstack里如果全是pool-3-thread-1整个排查过程会很痛苦。从第一天起就用自定义线程工厂public class BusinessThreadFactory implements ThreadFactory { private final String prefix; private final AtomicInteger counter new AtomicInteger(0); public BusinessThreadFactory(String prefix) { this.prefix prefix; } Override public Thread newThread(Runnable r) { Thread t new Thread(r, prefix - counter.incrementAndGet()); t.setDaemon(false); t.setUncaughtExceptionHandler((thread, throwable) - log.error(Unhandled exception in thread {}, thread.getName(), throwable)); return t; } }给线程取名这种小事排查故障时能省下一两个小时非常划算。任何生产环境的线程池都不应该看到默认的pool-x-thread-y命名。8.3 JVM参数层面的配合不要忽视新生代大小与CPU核数匹配密集计算任务在运行时会大量产生临时对象新生代太小会导致频繁Minor GC线程被频繁暂停。实践中的经验是并行度越高新生代应该越大。因为线程数多同时产生的垃圾量也大新生代太小会让GC成为全局瓶颈。一个比较保守的启动参数参考java -Xms4g -Xmx4g -Xmn2g -XX:SurvivorRatio8 -XX:UseG1GC -jar application.jarG1的Region动态调整机制在密集计算场景下表现比较稳。如果你还在用CMS建议借这个机会迁移到G1线上的长尾延迟能改善不少。9. 最后再分享一点我这些年的切身体会调线程池这件事踩过的坑越多越能明白一个道理不要试图用一套万能参数应对所有场景。每次调优之前先花30分钟画一张流程图弄清楚任务到底从哪来、要经过哪些处理、最终去哪这30分钟能省下后面3天的排查时间。画图的过程就是任务建模的过程。什么情况下用大线程池、什么情况下用小线程池、什么情况下要上消息队列做缓冲画完图基本就有答案了。另外一个心得是团队里一定要有一个人专门负责线程池参数的维护和文档记录否则过了三个月没人知道当初为什么配成9、为什么队列长度是24也就没法做后续优化。我见过太多团队参数被改来改去线上出了问题就随便调大一个值结果越调越糟。把每次调整的原因和压测数据固化下来这比参数本身更重要。如果你要做一次彻底的密集计算优化可以按照任务建模 → 参数初算 → 单点压测 → 整体压测 → 调整参数 → 监控复盘这个顺序走一遍每一步都用真实数据说话。等你经历过一次从CPU跑不满到吞吐翻倍的优化过程你对并发的理解会上一个台阶这套方法论也能复用到团队里的其他服务上。
阅读完成 · 觉得有帮助?