《Java 服务端通信工程》附录 B 诊断与压测命令

整理 macOS 上排查 linkd-v3 的可复制命令:JDK 与 Maven、keytool 与 openssl、lsof 与 netstat、tcpdump lo0、ab、jcmd 与 JFR,每条说明用途、平台限制与风险,并给出从端口到线程栈再到 JFR 的组合排查路径。

使用前提与本机口径

这一页只收 macOS 上能直接执行的命令。macOS 没有 ss,也没有 Linux 那套 socket 统计工具;连接诊断靠 lsof、netstat、tcpdump,JVM 诊断靠 jcmd、jstack、jfr。Linux 部署侧的差异单独标注。

Apple M1 Pro,10 核,32 GB,macOS 26.3 (25D125)
Temurin 25.0.5+8
gRPC-Java 1.76.0(grpc-netty-shaded)

先把 JDK 固定住,避免 jcmd 用 JDK 8 去连 JDK 25 的进程:

export JAVA_HOME=~/.jdks/jdk-25.0.5+8/Contents/Home
export PATH="$JAVA_HOME/bin:$PATH"

端口约定:libraryd 8080,comm-server gRPC 9093,gateway HTTP 9094,指标与管理 9095。下文默认排查 9095 和 9093;查网关时替换为 9094。

标注「本机」的输出必须来自真实执行;本附录的命令是模板,除平台限制外不附带伪造输出。JDK 25 上运行 gRPC shaded Netty 可能打印 sun.misc.Unsafe::allocateMemory 的终结弃用警告,这是依赖内部行为,不是命令或示例的错误。

JDK 与构建

java -version
mvn -v
mvn -q -DskipTests package
mvn -q test
mvn -q dependency:tree -Dincludes=io.grpc:grpc-netty

用途:

  • java -version 确认运行时是 25.0.5,不是默认的 JDK 8。
  • mvn -v 确认 Maven 3.9.12 和它使用的 JAVA_HOME。
  • package 产出四个模块的 jar;test 跑单测与契约测试。
  • dependency:tree 确认只有 grpc-netty-shaded,没有非 shaded 的 grpc-netty 混入。

风险:mvn test 会启动真实端口(测试用 0 号随机端口),如果测试里写了固定端口,可能与本地服务冲突。看 surefire 报告确认端口来源,不要靠猜。

证书与 TLS

keytool -list -v -keystore /etc/linkd/server.p12 -storetype PKCS12
keytool -genkeypair -alias linkd -keyalg RSA -keysize 2048 \
  -validity 365 -keystore /tmp/linkd-test.p12 -storetype PKCS12 \
  -dname 'CN=localhost, OU=Linkd, O=Plume, L=Shanghai, ST=Shanghai, C=CN' \
  -ext 'SAN=dns:localhost,ip:127.0.0.1'
openssl x509 -in /etc/linkd/server.crt -noout -subject -issuer -dates
openssl verify -CAfile /etc/linkd/ca.crt /etc/linkd/server.crt
openssl s_client -connect 127.0.0.1:9093 -alpn h2 -servername localhost </dev/null

用途:

  • keytool -list 看 keystore 里的别名、算法、有效期和证书链。
  • -genkeypair 生成自签测试证书,SAN 必须包含客户端实际使用的主机名或 IP,否则主机名校验失败。
  • openssl x509 -dates 看 notBefore / notAfter,这是证书过期最快的判断方式。
  • openssl verify 验证证书链;失败通常是缺中间 CA。
  • s_client 做真实握手,-alpn h2 确认协商到 HTTP/2。

风险:s_client 会建立真实 TLS 连接,可能触发服务端握手日志;不要在生产高峰大量执行。私钥和 keystore 口令不要写进命令历史,用 -storepass 从环境变量或交互输入读取。

端口与连接

lsof -nP -iTCP:9093
lsof -nP -iTCP:9093 -sTCP:LISTEN
lsof -nP -iTCP:9093 -sTCP:ESTABLISHED
lsof -nP -p "$(pgrep -f 'comm-server.*jar' | head -1)"
netstat -anv -p tcp | awk 'NR==1 || $4 ~ /\.9093$/ || $5 ~ /\.9093$/'

用途:

  • -n 不做 DNS 反解,-P 不把端口换成服务名,避免命令卡住。
  • -sTCP:LISTEN 只看向监听;没有这一行就是没启动或绑错地址。
  • -sTCP:ESTABLISHED 数连接;服务端与客户端各一条,数连接数时只数服务端 PID。
  • netstat -anv -p tcp 看内核视角的 Recv-Q / Send-Q,判断应用是否读得慢。

读法:

  • Recv-Q 持续大于 0,说明应用没来得及读;可能是线程阻塞,也可能是 AUTO_READ 类机制被关掉。
  • Send-Q 持续增大,说明对端读得慢或网络拥塞。
  • rhiwat / shiwat 是内核 socket 缓冲高水位,不是应用层写水位。

风险:lsof 遍历文件描述符表,连接数很大时耗时上升,不要放进每秒执行的心跳脚本。看自己的进程不需要 sudo。

macOS 没有 ss。Linux 上对应命令是 ss -tanp、ss -s,不要把本节的 netstat 参数直接搬到 Linux。

抓包

macOS 的 loopback 是 lo0,不是 Linux 的 lo。tcpdump 需要 sudo:

sudo tcpdump -i lo0 -nn -s 0 -c 50 port 9093
sudo tcpdump -i lo0 -nn -A -s 0 -c 20 port 9094
sudo tcpdump -i lo0 -nn -w /tmp/linkd.pcap -c 500 port 9093

用途:

  • -nn 不解析主机名与端口名。
  • -s 0 抓完整包,不截断 payload。
  • -c 限包数,避免忘了停。
  • -A 以 ASCII 显示 payload,看明文 HTTP;gRPC 是二进制,-A 只能看到帧头的一部分。
  • -w 落盘,后续用 Wireshark 或 tcpdump -r 分析。

读法:

  • 三次握手是 [S]、[S.]、[.];只有 [S] 没有 [S.],常见于 backlog 满或 accept 卡住。
  • [P.] 是带数据的包;length 是负载字节数。
  • win 0 是零窗口,表示接收缓冲满;对照 netstat 的 Recv-Q 区分「应用没读」和「内核没空间」。
  • 大量重复 seq 是重传;大量 [R] 是连接被重置。

风险:抓包需要 sudo,payload 可能含令牌、Cookie 与业务数据,pcap 按敏感文件管理。生产不要长期开,用 -c 限包数或 -w 落盘后立即停止。流量大时抓包本身会丢包,看到「少包」不等于链路真丢了。

压测:ab 可用,wrk/hey 未安装

本机 ab 可用;wrk 和 hey 未安装,不要写进脚本当作可用工具。

ab -V
ab -k -n 5000 -c 50 http://127.0.0.1:9094/healthz
ab -k -n 20000 -c 64 \
  -p /tmp/getbook.json \
  -T 'application/json' \
  http://127.0.0.1:9094/books/978-7-111-00001-1

用途与限制:

  • -k 复用连接;不加是每个请求一次握手。
  • -n 总请求数,样本太少时 p99 没有意义。
  • -c 并发数;ab 单进程,-c 太大时客户端自身可能先饱和。
  • -p / -T 用于 POST;GET 不需要。

ab 是闭环工具,存在协调缺失:服务端卡顿时客户端不发请求,卡顿时段不进入延迟统计。它也不支持 gRPC、流式响应和 HTTP/2。需要这些时用 14.2 的自写 gRPC 客户端,不要临时安装未经验证的工具。

风险:压测会真实占用下游资源。先在测试租户、独立限流配额下执行,并设置停止条件(p99、错误率、持续时间)。不要对生产入口直接跑 -n 1000000。

JVM 诊断:jcmd 与 JFR

jcmd -l
jcmd <pid> Thread.print > /tmp/threads-1.txt
sleep 5
jcmd <pid> Thread.print > /tmp/threads-2.txt
jcmd <pid> GC.heap_info
jcmd <pid> GC.class_histogram | head -20
jcmd <pid> VM.system_properties | grep -E 'linkd|grpc'
jcmd <pid> VM.command_line
jcmd <pid> VM.native_memory summary

用途:

  • jcmd -l 列出可附加的 JVM 与主类,先确认 pid。
  • Thread.print 抓线程栈;间隔 5 秒抓两次,同一个方法两次都在栈顶才是「卡住」。
  • GC.heap_info 看堆的 reserved / committed / used,不触发 full GC。
  • GC.class_histogram 看对象类型分布,默认会先做一次 full GC,停顿明显。
  • VM.system_properties 确认 linkd.fault.enabled 等参数是否生效。
  • VM.command_line 看启动参数。
  • VM.native_memory summary 需要启动时加 -XX:NativeMemoryTracking=summary,否则报未启用。

JFR 的 start/stop 用 jcmd,读文件用 jfr:

jcmd <pid> JFR.start name=linkd settings=profile \
  filename=/tmp/linkd.jfr duration=60s
jcmd <pid> JFR.check name=linkd
jcmd <pid> JFR.stop name=linkd
jfr summary /tmp/linkd.jfr
jfr print --events jdk.GarbageCollection,jdk.SocketRead,jdk.SocketWrite /tmp/linkd.jfr

读法:jfr summary 先看实际 Duration,再看 GC 暂停、SocketRead/SocketWrite 耗时分布,最后看 CPU 采样。duration 到了之后录制会自动结束,再按名字 stop 会失败;先 JFR.check 看状态。

也可以用 jstack <pid> 代替 Thread.print,输出格式基本一致。jcmd 与目标进程最好用同一个 JDK,版本差太远可能附加失败。

风险:线程栈需要走到安全点,进程有明显停顿时输出会延迟;对延迟敏感的服务不要连续高频抓。GC.class_histogram 触发 full GC,避开高峰。JFR 的 profile 配置开销低于 diagnostic,但不是零,必须配 duration 或 maxsize。

用 Java 封装命令与指标读取

诊断脚本经常需要「执行命令、限时、拿输出」。用 ProcessBuilder 包一层,避免脚本里无限等待:

package com.plume.linkd.v3.diag;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.TimeUnit;

public final class CommandRunner {

    public record Output(int exitCode, String stdout, String stderr, boolean timedOut) {
    }

    public static Output run(Duration timeout, String... command) throws Exception {
        Process process = new ProcessBuilder(command).start();
        boolean finished = process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS);
        if (!finished) {
            process.destroyForcibly();
            return new Output(-1, "", "timeout after " + timeout, true);
        }
        String stdout = new String(process.getInputStream().readAllBytes(),
                StandardCharsets.UTF_8);
        String stderr = new String(process.getErrorStream().readAllBytes(),
                StandardCharsets.UTF_8);
        return new Output(process.exitValue(), stdout, stderr, false);
    }

    public static void main(String[] args) throws Exception {
        Output output = run(Duration.ofSeconds(5), "lsof", "-nP", "-iTCP:9095");
        System.out.println("exit=" + output.exitCode() + " timedOut=" + output.timedOut());
        System.out.println(output.stdout());
    }
}

这个实现只适合输出较小的诊断命令。如果命令输出很大,waitFor 之后才读流可能因为管道缓冲满而卡住;生产脚本要么把输出重定向到临时文件,要么用两个线程分别读 stdout/stderr。

从 9095 读一个指标值,避免每次手写 grep:

package com.plume.linkd.v3.diag;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public final class MetricsProbe {

    private final HttpClient client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofMillis(300))
            .build();

    public double value(String baseUrl, String metricName) {
        HttpRequest request = HttpRequest.newBuilder(URI.create(baseUrl + "/metrics"))
                .timeout(Duration.ofSeconds(2))
                .GET()
                .build();
        try {
            String body = client.send(request, HttpResponse.BodyHandlers.ofString()).body();
            for (String line : body.split("\n")) {
                if (line.startsWith("#") || !line.startsWith(metricName)) {
                    continue;
                }
                String[] parts = line.trim().split("\\s+");
                if (parts.length >= 2) {
                    return Double.parseDouble(parts[parts.length - 1]);
                }
            }
            return Double.NaN;
        } catch (Exception e) {
            return Double.NaN;
        }
    }
}

这个解析器只适合「一个指标名对应一行样本」的场景。带 label 的指标会匹配到多行,应该按 label 精确匹配,或者直接用 Prometheus 查询接口。NaN 表示没找到或请求失败,调用方要单独处理,不要当成 0。

组合排查路径

单条命令只回答一个问题。按现象定顺序,前一步没有结论再走下一步:

现象第一条命令接着看判据
9094 连不上lsof -nP -iTCP:9094 -sTCP:LISTENnetstat -anv -p tcp、tcpdump -i lo0没有 LISTEN 是没启动;有 LISTEN 但 SYN 无响应看 backlog
gRPC 返回 UNAVAILABLElsof -nP -iTCP:8080 -sTCP:LISTENpgrep、curl 8080/healthzlibraryd 没听、进程退出或被防火墙拦
p99 升高、CPU 不高两次 jcmd Thread.print(间隔 5 秒)netstat 的 Recv-Q、JFR线程卡在锁/下游等待;Recv-Q 持续大于 0
429 增多查 ratelimit_rejected_total 维度入口 QPS、下游 QPS入口没涨而下游涨,是重试风暴
连接数只涨不降lsof -nP -iTCP:9093 -sTCP:ESTABLISHED | wc -lTIME_WAIT、连接池借出数请求停止后不回落,是泄漏
堆外内存上涨jcmd GC.heap_infoRSS、VM.native_memory summary堆内稳定而 RSS 上涨,查直接缓冲与连接
证书握手失败openssl x509 -enddates_client、truststorenotAfter 已过或 SAN 不匹配

固定顺序是 lsof → netstat → 两次 Thread.print → GC.heap_info → GC.class_histogram → JFR。原则是「先便宜、后昂贵;先现象、后根因」。lsof 与 netstat 几乎不影响服务,线程栈有短暂 safepoint,class_histogram 触发 full GC,JFR 持续写文件。不要在第一步就上最重的工具。

常见错误与边界

把 lsof 的 IPv6 通配当成只监听 IPv6。 *:9093 通常是双栈,IPv4 客户端也能连;判断协议族看 tcp4 / tcp46。

把 netstat 的 rhiwat 当成应用层写水位。 前者是内核 socket 缓冲高水位,后者是 Netty 的 WriteBufferWaterMark,观察位置和调整方式都不同。

不带 sudo 跑 tcpdump。 macOS 的 BPF 设备需要权限,报错是 You don't have permission to capture on that device,不是接口名写错。

在生产长期开 JFR profile。 开销低于 diagnostic,但不是零;必须配 duration 或 maxsize,录完即停。

用 JDK 8 的 jcmd 连 JDK 25 进程。 先对齐 java -version 与 jcmd -version;附加失败先怀疑版本,不要怀疑端口。

把 GC.class_histogram 当无侵入命令。 它默认统计存活对象,会先做 full GC;延迟敏感实例上这一下停顿足以触发超时。

假设 wrk、hey、grpcurl 可用。 本机未安装,脚本里要么用 ab,要么自写客户端;不要写「执行 wrk 得到以下结果」。

把 JDK 25 的 Unsafe 警告当成启动失败。 gRPC shaded Netty 会触发终结弃用警告,进程仍可正常服务;判断健康看 readyz 和实际调用,不看警告文本。

边界:本附录是 macOS 口径。 Linux 上的 ss、/proc、cgroup 指标不在其中;跨平台脚本要先判断 uname,不要混用参数。

边界:命令输出会随版本变化。 lsof 的列、netstat 的字段、jcmd 的子命令在不同版本可能不同;以本机 man 和 --help 为准。

小结

  • 先固定 JAVA_HOME,确认 java -version 是 25.0.5,再谈诊断。
  • 证书用 keytool -list、openssl x509 -dates、s_client -alpn h2 三条命令覆盖有效期、链和握手。
  • macOS 没有 ss;连接用 lsof、netstat,抓包用 sudo tcpdump -i lo0。
  • ab 可用但只适合 HTTP/1.x 闭环压测;wrk、hey、grpcurl 未安装,不要假设可用。
  • 线程栈至少抓两次、间隔 5 秒;JFR 用 jcmd 控制、jfr 读文件。
  • 排查顺序先便宜后昂贵:端口与连接 → 线程栈 → 堆与对象 → JFR;先现象后根因。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《Java 消息中间件·事件驱动架构》附录 B 架构决策记录模板
  2. 《Java 消息中间件·事件驱动架构》附录 A 术语表
  3. 《Java 消息中间件·事件驱动架构》15.3 全书验收