有人觉得程序员工资太高,应该直接砍掉一半,把钱补给农民、金融从业者、中介和保险销售。理由也很直接:程序员拿得太多,社会上的钱都让这个群体赚走了。

这个判断的问题,不在于该不该提高其他职业收入。农民收入该提高,基层劳动者的议价能力也值得改善。但“某个职业收入高,所以把他的工资砍掉再分给别人”,把工资理解成了一个固定资金池。
工资不是按职业人数平均发的。企业愿意为一个岗位付多少钱,通常跟供需、替代成本、业务收益和风险有关。
技术系统里也经常有人犯类似的错:看到一个数字高,就直接砍。
比如线上接口慢了,第一反应是:
Tomcat 线程太多,先砍一半。
这和“工资高就砍一半”一样,数字看着扎眼,不代表它就是问题来源。
下面看一个更接近真实生产环境的接口。
线程池明明还有线程,接口为什么已经排队了
订单接口调用链:
POST /api/order/confirm
|
+-- MySQL 查询订单
|
+-- inventoryExecutor
| +-- 库存服务
|
+-- priceExecutor
+-- 价格服务
线上监控出现这种数据,以下都是示例值:
HTTP P50: 86ms
HTTP P95: 620ms
HTTP P99: 1830ms
Tomcat threads:
busy = 74
max = 200
inventoryExecutor:
active = 16
pool = 16
queue = 438
HikariCP:
active = 18
idle = 2
pending = 0
max = 20
看到这里,先别急着把 Tomcat:
server:
tomcat:
threads:
max: 200
改成:
server:
tomcat:
threads:
max: 400
Tomcat 只用了 74 个工作线程,真正已经顶住的是:
inventoryExecutor.active = 16
inventoryExecutor.queue = 438
请求不是慢在 Controller,也不是没有 Web 线程,而是在等库存线程池。
问题到这里才刚露头。
原来的代码可能是这样:
Future<StockResult> stockFuture =
inventoryExecutor.submit(() -> inventoryClient.check(orderId));
Future<PriceResult> priceFuture =
priceExecutor.submit(() -> priceClient.calculate(orderId));
StockResult stock = stockFuture.get();
PriceResult price = priceFuture.get();
这段代码能跑,但线上我不太敢这么放。
get() 没超时。
库存服务一旦出现长尾请求,线程池里的任务迟迟退不出来,后面的任务继续进队列。
假设:
正常库存RT = 35ms
异常库存RT = 900ms
线程数 = 16
请求量 = 1200 QPS
只要慢请求比例上升,16 个线程很快就会被占满。
然后看到的并不是:
库存RPC执行了1800ms
而可能是:
queueWait = 870ms
rpcCost = 42ms
totalCost = 912ms
真正执行只用了 42ms。
前面 870ms 什么都没干,纯排队。
所以日志我会先拆成两个时间:
long enqueueAt = System.nanoTime();
inventoryExecutor.execute(() -> {
long startedAt = System.nanoTime();
try {
StockResult result = inventoryClient.check(orderId);
log.info("stock_check orderId={} queueMs={} runMs={}",
orderId,
ms(startedAt - enqueueAt),
ms(System.nanoTime() - startedAt));
} catch (Exception ex) {
log.warn("stock_check_failed orderId={}", orderId, ex);
}
});
辅助方法不用搞复杂:
private static long ms(long nanos) {
return TimeUnit.NANOSECONDS.toMillis(nanos);
}
如果日志出来是:
orderId=98121 queueMs=6 runMs=31
orderId=98122 queueMs=9 runMs=38
orderId=98123 queueMs=214 runMs=40
orderId=98124 queueMs=617 runMs=43
orderId=98125 queueMs=903 runMs=39
就不用继续怀疑库存接口本身执行慢了。
线程池已经形成排队。
这时候加线程,可能把 MySQL 一起拖死
有人会把:
corePoolSize = 16
maximumPoolSize = 16
直接改成:
corePoolSize = 64
maximumPoolSize = 64
先看库存调用里面还有什么:
public StockResult check(long orderId) {
OrderSnapshot order = orderRepository.findSnapshot(orderId);
return inventoryGateway.query(order.getSkuId(), order.getCount());
}
也就是说,每个任务先拿数据库连接。
HikariCP:
spring:
datasource:
hikari:
maximum-pool-size: 20
connection-timeout: 800
线程从16加到64之后,很可能出现另一组指标:
executor.active = 58
executor.queue = 0
hikari.active = 20
hikari.idle = 0
hikari.pending = 37
队列确实没了。
只是等待从:
ThreadPool queue
搬到了:
Hikari connection pending
接口还是慢。
甚至更慢。
所以数据库这里也要继续拆:
long begin = System.nanoTime();
try (Connection conn = dataSource.getConnection()) {
long acquired = System.nanoTime();
OrderSnapshot order = loadOrder(conn, orderId);
long finished = System.nanoTime();
metrics.timer("db.connection.wait")
.record(acquired - begin, TimeUnit.NANOSECONDS);
metrics.timer("db.query.execute")
.record(finished - acquired, TimeUnit.NANOSECONDS);
return order;
}
假设拿到的数据是:
db.connection.wait.p99 = 742ms
db.query.execute.p99 = 18ms
这时候再去:
EXPLAIN SELECT ...
意义已经没那么大了。
SQL只跑18ms。
慢的是拿连接。
反过来,如果数据是:
db.connection.wait.p99 = 3ms
db.query.execute.p99 = 680ms
才继续往 SQL、锁等待、索引方向查。
比如:
EXPLAIN ANALYZE
SELECT sku_id, quantity, status
FROM order_item
WHERE order_id = 98125
AND status = 1;
发现:
-> Filter: status = 1
-> Index lookup on order_item using idx_order_id
rows=12684
一个订单不应该扫一万多行,就查数据分布:
SELECT order_id, COUNT(*)
FROM order_item
WHERE order_id = 98125
GROUP BY order_id;
如果确实存在异常大订单,再决定索引是不是应该调整成:
CREATE INDEX idx_order_status
ON order_item(order_id, status);
不是看到 SQL 慢就机械加索引,也不是看到线程池排队就机械加线程。
每一刀都得知道等待发生在哪里。
真正该限制的是无边界资源
线程池还有一个常见坑:
new LinkedBlockingQueue<>()
默认容量接近无限。
业务高峰时,下游处理能力已经到顶,上游还可以不停提交任务。
最终可能出现:
10:21 queue=320
10:22 queue=1800
10:23 queue=7600
10:24 queue=21400
内存开始涨:
heap used: 2.1G -> 3.4G -> 5.8G
young gc: 8/min -> 31/min
old gen: 68%
我更愿意先把容量明确下来:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
16,
16,
30,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(300),
new NamedThreadFactory("inventory"),
new ThreadPoolExecutor.AbortPolicy()
);
然后在入口处理拒绝:
try {
executor.execute(task);
} catch (RejectedExecutionException ex) {
metrics.counter("inventory.reject").increment();
throw new ServiceBusyException("库存服务繁忙");
}
为什么宁愿拒绝,也不无限排?
假设库存检查超过800ms,对用户已经没有价值。
那么:
排队 1500ms + 执行 40ms
这种任务即使执行成功,也只是制造一个1540ms的慢请求。
再补一个简单的压测验证:
wrk -t8 -c300 -d60s \
--latency \
http://127.0.0.1:8080/api/order/confirm
不要只看平均值:
Avg 142ms
重点看:
P95
P99
reject rate
executor queue wait
executor run time
DB connection pending
DB execute time
downstream RPC P99
修改前:
P99 1840ms
queueWait P99 1210ms
reject 0%
heap peak 5.7G
修改后示例:
P99 410ms
queueWait P99 120ms
reject 0.7%
heap peak 3.1G
0.7%的快速失败是否能接受,要看业务,不能只为了让监控全绿把拒绝率藏掉。
系统调优最忌讳盯着一个显眼数字动刀。线程数高不一定该砍,工资高也不等于钱被谁“拿走了”。先弄清楚这个数字为什么在那里,再谈怎么分配资源。