给遗留 Java 系统增加指标埋点而不改变业务代码的尝试
先交代一下背景。手上有一套老 Java 系统,Java 8、Spring MVC、Tomcat,业务代码年头不短了,能跑,但谁都不太想动。最近想给它加一点基础指标,至少能看到请求量、耗时和异常数。问题就在这儿:业务代码不能改,连 pom 都不太想动,改一行就得回归一堆东西,没人愿意背这个锅。
最开始想的是 Spring Boot Actuator,结果一看,这系统根本不是 Spring Boot。又想加 Micrometer,直接在 Controller 里包一层 Timer,但这就回到改业务代码了。后来想用 Filter,在 web.xml 里加一个统一过滤器,理论上能少改点。结果发现这套系统有好几个入口,有的走 Spring MVC,有的直接是 Servlet,还有的静态资源也混在一起,过滤器加下去,指标口径反而不好统一。最重要的是,还是得改配置、改打包,跟“尽量不碰业务”的初衷有点偏。
于是换了个思路:能不能用 Java Agent,在类加载的时候把埋点织进去?业务代码一行不动,只在启动脚本里加一个 -javaagent 参数。听起来有点重,但试下来,对于这种遗留系统反而挺合适。
用 Byte Buddy 做方法级增强
我选的是 Byte Buddy,版本用的 1.12.x,主要是它对 Java 8 兼容比较稳。Agent 的入口还是老一套 premain:
public class MetricsAgent {
public static void premain(String args, Instrumentation inst) {
int port = parsePort(args, 9464);
Metrics.init(port);
new AgentBuilder.Default()
.ignore(nameStartsWith("net.bytebuddy.")
.or(nameStartsWith("com.sun."))
.or(nameStartsWith("java.")))
.type(named("javax.servlet.http.HttpServlet"))
.transform((builder, typeDescription, classLoader, module) ->
builder.visit(Advice.to(ServletAdvice.class).on(named("service"))))
.installOn(inst);
}
}
这里我一开始想拦截所有 HttpServlet 子类,后来发现没必要,直接增强 HttpServlet.service 就行。大部分 Servlet 不会重写 service,Spring 的 DispatcherServlet 也是继承下来的,请求最终会走到这里。少增强一点,少一点重复计数的风险。
Advice 里只做两件事:进方法记开始时间,出方法记耗时和异常。异常一定要 suppress = Throwable.class,埋点自己出问题不能影响业务,这个底线得守住。
public class ServletAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static long enter() {
return System.nanoTime();
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void exit(@Advice.Enter long start,
@Advice.Argument(0) Object request,
@Advice.Argument(1) Object response,
@Advice.Thrown Throwable throwable) {
Metrics.record(request, response, throwable, System.nanoTime() - start);
}
}
指标怎么暴露
指标注册用的是 Micrometer 的 PrometheusMeterRegistry,HTTP 端点没用 Spring,直接拿 JDK 自带的 com.sun.net.httpserver.HttpServer 起了一个小服务,监听 9464 端口。Agent 启动时顺手拉起来,Prometheus 来抓 /metrics 就行。
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext("/metrics", exchange -> {
byte[] body = registry.scrape().getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "text/plain; version=0.0.4");
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
server.setExecutor(Executors.newSingleThreadExecutor());
server.start();
指标名我起得很朴素,legacy_http_server_requests,标签有 method、uri、status、exception。uri 这里千万别直接用原始 requestURI,老系统里路径参数、ID 一堆,标签基数能炸。我的做法是只取 servletPath,再把纯数字和长 hash 段替换成 {id},够用就行,不追求特别精确。
String uri = path.replaceAll("/\\d+", "/{id}")
.replaceAll("/[0-9a-fA-F-]{8,}", "/{id}");
启动脚本里加一行,业务代码和打包完全不用动:
JAVA_OPTS="$JAVA_OPTS -javaagent:/opt/metrics-agent.jar=port=9464"
Prometheus 配置也很简单:
scrape_configs:
- job_name: 'legacy-java'
static_configs:
- targets: ['127.0.0.1:9464']
踩过的坑
第一个坑是类加载器。Agent 的类在系统类加载器里,业务 WebApp 有自己的一套类加载器,Advice 里如果直接引用 HttpServletRequest,有时候会碰到找不到类的问题。我后来在 Advice 里尽量用 Object 接参数,具体取 method、path、status 的时候再做判断,能拿到就拿,拿不到就标 unknown,不跟类加载器较劲。
第二个坑是 Agent jar 的 Manifest。用 Maven Shade 打 fat jar 的时候,别忘了加 Premain-Class,不然启动直接告诉你找不到 agent。还有 Can-Retransform-Classes 也顺手加上,后面想调增强范围会方便点。
第三个坑是指标标签。最开始把异常类名全塞进 exception 标签,结果组合数比想象中多。后来改成只区分 none 和 exception,具体异常还是看日志,指标只负责告诉你“有异常”。
最后效果
上线的时候只改了启动参数,业务代码一行没动。Prometheus 抓上以后,至少能看到每个 Servlet 路径的 QPS、P95 耗时和异常计数。虽然比不上完整 APM,但对这套遗留系统来说,已经够用了。后面如果还想加 JDBC 耗时或者线程池指标,也可以沿着 Agent 这条路继续织,不用再求着业务代码开口子。
不出问题的话就没有问题了。当然,Agent 这东西还是得控制范围,别一上来就全量增强,先挑一个入口试,稳了再扩,不然老系统真能给你整出点新花样。
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。