找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

6091

积分

0

好友

779

主题
发表于 前天 23:15 | 查看: 0| 回复: 0

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

社交媒体评论截图,内容为“大量程序员薪资太高了,不降薪说不过去”,评论建议将程序员薪资分给其他职业

这个判断的问题,不在于该不该提高其他职业收入。农民收入该提高,基层劳动者的议价能力也值得改善。但“某个职业收入高,所以把他的工资砍掉再分给别人”,把工资理解成了一个固定资金池。

工资不是按职业人数平均发的。企业愿意为一个岗位付多少钱,通常跟供需、替代成本、业务收益和风险有关。

技术系统里也经常有人犯类似的错:看到一个数字高,就直接砍。

比如线上接口慢了,第一反应是:

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%的快速失败是否能接受,要看业务,不能只为了让监控全绿把拒绝率藏掉。

系统调优最忌讳盯着一个显眼数字动刀。线程数高不一定该砍,工资高也不等于钱被谁“拿走了”。先弄清楚这个数字为什么在那里,再谈怎么分配资源。




上一篇:HBM存储的信仰:获利盘极限测试与消失的自由现金流
下一篇:Scanopy 网络拓扑扫描:别再手搓 draw.io,文档自动跟着机房长
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-9-13 17:38 , Processed in 1.491304 second(s), 45 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表