让 Java Agent 在 Dragonwell 上更好用

背景

Java Agent 技术能够动态修改 Java 应用程序行为,而不用重新修改代码。

正是因为这些特点,很多中间件团队、云厂商团队、开源产品,开始使用 Java Agent 技术来提供一些基础能力,比如 Apache Skywalking、OpenTelemetry 都提供了 Java Agent。

在早前,中间件团队通过SDK提供能力(比如可观测、微服务治理能力等);但中间件团队每次新增特性、修复缺陷,都需要各个业务方更新SDK版本、重新发布。

随着公司架构越来越复杂,随着云厂商开始提供中间件能力,这种逐个推动SDK使用方更新的方式越来越麻烦。

而用了 Java Agent 之后,业务同学只需要写业务代码;中间件能力通过设置环境变量来动态注入Java Agent来实现。Java Agent的更新,也只需要重启应用即可。

问题

我们以一个微服务demo为例。先在一个Kubernetes集群中部署demo,然后通过JAVA_TOOL_OPTIONS使用用Java Agent:

1
2
$ echo $JAVA_TOOL_OPTIONS
-javaagent:/home/admin/.opt/ArmsAgent/arms-bootstrap-1.7.0-SNAPSHOT.jar ...

我们登陆容器时,就能看到注入的 Java Agent:


Java Agent踩坑之appendToSystemClassLoaderSearch问题

从Java Agent报错开始,到JVM原理,到glibc线程安全,再到pthread tls,逐步探究Java Agent诡异报错。

背景

由于阿里云多个产品都提供了Java Agent给用户使用,在多个Java Agent一起使用的场景下,造成了总体Java Agent耗时增加,各个Agent各自存储,导致内存占用、资源消耗增加。

所以我们发起了one-java-agent项目,能够协同各个Java Agent;同时也支持更加高效、方便的字节码注入。

其中,各个Java Agent作为one-java-agent的plugin,在premain阶段是通过多线程启动的方式来加载,从而将启动速度由O(n)降低到O(1),降低了整体Java Agent整体的加载时间。

问题

但最近在新版Agent验证过程中,one-java-agent的premain阶段,发现有如下报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2022-06-16 09:51:09 [oneagent plugin a-java-agent start] ERROR c.a.o.plugin.PluginManagerImpl -start plugin error, name: a-java-agent
com.alibaba.oneagent.plugin.PluginException: start error, agent jar::/path/to/one-java-agent/plugins/a-java-agent/a-java-agent-1.7.0-SNAPSHOT.jar
at com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:113)
at com.alibaba.oneagent.plugin.PluginManagerImpl.startOnePlugin(PluginManagerImpl.java:294)
at com.alibaba.oneagent.plugin.PluginManagerImpl.access$200(PluginManagerImpl.java:22)
at com.alibaba.oneagent.plugin.PluginManagerImpl$2.run(PluginManagerImpl.java:325)
at java.lang.Thread.run(Thread.java:750)
Caused by: java.lang.InternalError: null
at sun.instrument.InstrumentationImpl.appendToClassLoaderSearch0(Native Method)
at sun.instrument.InstrumentationImpl.appendToSystemClassLoaderSearch(InstrumentationImpl.java:200)
at com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:100)
... 4 common frames omitted
2022-06-16 09:51:09 [oneagent plugin b-java-agent start] ERROR c.a.o.plugin.PluginManagerImpl -start plugin error, name: b-java-agent
com.alibaba.oneagent.plugin.PluginException: start error, agent jar::/path/to/one-java-agent/plugins/b-java-agent/b-java-agent.jar
at com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:113)
at com.alibaba.oneagent.plugin.PluginManagerImpl.startOnePlugin(PluginManagerImpl.java:294)
at com.alibaba.oneagent.plugin.PluginManagerImpl.access$200(PluginManagerImpl.java:22)
at com.alibaba.oneagent.plugin.PluginManagerImpl$2.run(PluginManagerImpl.java:325)
at java.lang.Thread.run(Thread.java:855)
Caused by: java.lang.IllegalArgumentException: null
at sun.instrument.InstrumentationImpl.appendToClassLoaderSearch0(Native Method)
at sun.instrument.InstrumentationImpl.appendToSystemClassLoaderSearch(InstrumentationImpl.java:200)
at com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:100)
... 4 common frames omitted

熟悉Java Agent的同学可能能注意到,这是调用Instrumentation.appendToSystemClassLoaderSearch报错了。

但首先appendToSystemClassLoaderSearch的路径是存在的;其次,这个报错的真实原因是在C++部分,比较难排查。


Spring Native 0.11发布,带来新的AOT引擎和性能优化

Spring Native 0.11已于2021年12月9日发布。

这个宏大的版本是Spring团队五个月辛勤工作的结果,他们一直在研究一个全新的架构,将让Spring使用GraalVM创建原生可执行文件的方式提升到一个新的水平。你目前已经可以已经在start.spring.io上试用了!

想了解有关Spring Native 0.11的更多信息,可以查看来自Spring布道师的新一期的Spring Tips视频(在YouTube上)。

新的AOT引擎

这个版本最大的变化无疑是引入了新的AOT引擎,该引擎在构建时对Spring程序进行深入的转化和分析,并生成所需的GraalVM Native配置。这些转换由Maven和Gradle Spring AOT插件执行。

spring boot native

更深入地说,AOT引擎在构建时评估构建环境,以便生成专门为您的应用程序优化后的 application context 和 Spring factories(Spring Boot背后的插件系统)。在实践中,这意味着:

  • 在运行时执行的 Spring 基础结构更少
  • 在运行时要判断的条件更少
  • 减少反射,因为使用的是编程式bean注册

AOT 引擎根据标记为活动的 Bean、Spring 编程模型的知识以及与 Spring Native 捆绑在一起或由应用程序本身提供的native hint,来推断出将应用程序编译为本机可执行文件所需的native configuration。

aot architecture


在Java的类型系统中,数组有什么缺陷吗?

2020年2月,王垠吐槽了下Java的类型系统,说:

关于程序员对 Java 类型系统的理解,比较高级的一个面试问题是这样:

王垠原版的代码
1
2
3
4
5
6
public static void f() {
String[] a = new String[2];
Object[] b = a;
a[0] = "hi";
b[1] = Integer.valueOf(42);
}

这段代码里面到底哪一行错了?为什么?如果某个 Java 版本能顺利运行这段代码,那么如何让这个错误暴露得更致命一些?
注意这里所谓的「错了」是本质上,原理上的。

那么这儿的“错误”是指什么呢?

TL;DR

如果只能用一句话回答这个问题的话,那么就是:

Java数组不支持泛型,破坏了Java的类型安全性

类型系统的一些前提

一个好的类型系统,能够尽可能早的检测出错误,比如你将一个String赋值给int变量的时候,编译器就会报错,而不是等程序跑起来再报错。


尝鲜Go 1.18中范型版本的map和slice

大家最近都关注到了Go 1.18会支持范型的消息了吧。

作为Golang的内置类型,大家都期待map和slice支持范型后,可以简化很多的判断逻辑,比如Equal逻辑等等。

几天前,Go范型的标准库已经提交了,且可以试用了:

大家也可以读一下对应的代码:https://cs.opensource.google/go/x/exp/+/master:maps/maps.go

废话不多说,我们看下如何尝试范型版本的map和slice吧!

如何使用Go 1.18?

Golang 官网链接只有1.17版本的下载,那么我们如何才能使用1.18版本的Golang呢?

网上翻了翻,有人提供了Golang 1.18版本的Docker镜像 seongwoohong/golang-nightly:1.18,而且保证维护到1.18版本正式发布:


如何通过Kubernetes事件来报告错误

组内有维护一个Kubernetes Webhook,可以拦截pod的创建请求,并做一些修改(比如添加环境变量、添加init-container等)。

业务逻辑本身很简单,但是如果过程中产生错误,就很难处理。要不直接阻止pod创建,那么就有可能导致应用无法启动。要么忽略业务逻辑,那么就会导致静默失败,谁也不知道这儿出现了一个错误。

于是,朴素的想法就是接入告警系统,但这会导致当前组件和具体的告警系统耦合起来。

在Kubernetes中,有Event机制,可以做到把一些事件,比如警告、错误等信息记录下来,就比较适合这个场景。

什么是Kubernetes中的事件/Event?

事件(Event)是 Kubernetes 中众多资源对象中的一员,通常用来记录集群内发生的状态变更,大到集群节点异常,小到 Pod 启动、调度成功等等。

比如我们Describe一个pod,就能看到这个pod对应的事件:

kubectl describe pod sc-b-68867c5dcb-sf9hn

可以看到,从调度、到启动、再到这个pod最终拉取镜像失败,都会通过event的方式记录下来。


如何优雅关闭maven-default-http-blocker?

最近升级Maven到3.8.1后,mvn编译的时候总是提示拉不到依赖,报错如下:

Could not validate integrity of download from http://0.0.0.0/...

全部报错如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[WARNING] Could not validate integrity of download from http://0.0.0.0/com/alibaba/nacos/nacos-client-mse-extension/1.4.2-SNAPSHOT/maven-metadata.xml
org.eclipse.aether.transfer.ChecksumFailureException: Checksum validation failed, expected <!doctype but is 18420d7f1430a348837b97a31a80e374e3b00254
at org.eclipse.aether.connector.basic.ChecksumValidator.validateExternalChecksums (ChecksumValidator.java:174)
at org.eclipse.aether.connector.basic.ChecksumValidator.validate (ChecksumValidator.java:103)
at org.eclipse.aether.connector.basic.BasicRepositoryConnector$GetTaskRunner.runTask (BasicRepositoryConnector.java:460)
at org.eclipse.aether.connector.basic.BasicRepositoryConnector$TaskRunner.run (BasicRepositoryConnector.java:364)
at org.eclipse.aether.util.concurrency.RunnableErrorForwarder$1.run (RunnableErrorForwarder.java:75)
at org.eclipse.aether.connector.basic.BasicRepositoryConnector$DirectExecutor.execute (BasicRepositoryConnector.java:628)
at org.eclipse.aether.connector.basic.BasicRepositoryConnector.get (BasicRepositoryConnector.java:235)
at org.eclipse.aether.internal.impl.DefaultMetadataResolver$ResolveTask.run (DefaultMetadataResolver.java:573)
at org.eclipse.aether.util.concurrency.RunnableErrorForwarder$1.run (RunnableErrorForwarder.java:75)
at java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1130)
at java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:630)
at java.lang.Thread.run (Thread.java:832)
[WARNING] Checksum validation failed, expected <!doctype but is 18420d7f1430a348837b97a31a80e374e3b00254 from maven-default-http-blocker for http://0.0.0.0/com/alibaba/nacos/nacos-client-mse-extension/1.4.2-SNAPSHOT/maven-metadata.xml
Downloaded from maven-default-http-blocker: http://0.0.0.0/com/alibaba/nacos/nacos-client-mse-extension/1.4.2-SNAPSHOT/maven-metadata.xml (63 kB at 19 kB/s)

从关键字maven-default-http-blocker可以找到相关资料。

简而言之,如果使用HTTP协议下载依赖,可能会导致中间人攻击。比如,本来想下载一个nacos-client的,结果下载的结果中被插入了恶意代码,然后开发人员运行了一下,黑客就能获得开发人员的计算机控制权了。

所以Maven 3.8.1就禁止了所有HTTP协议的Maven仓库。

详情见Maven 3.8.1的发布日志

问题是在日常开发中,我们经常会用到公司内部的maven仓库。这些仓库一般都是http协议,Maven 3.8.1禁止了http协议,那么就会导致开头的报错。

于是查了下,可以按照如下方式关闭:


虚引用真的不影响对象的生命周期吗?

Java的四大引用,大家都很熟悉吧:

  • 强应用:正常代码中的引用。一个对象能通过强应用访问到,那它就永远不会被回收
  • 软引用:比强引用弱一级的引用,内存不足时引用指向的对象会被回收
  • 弱引用:比软引用弱一级的引用,下一次GC时指向对象会被回收
  • 虚引用

最后一个虚应用是今天要讨论的。很多文章都是这么写的:

一个对象是否有虚引用存在,对其生存不会产生任何影响。

事实上,这个是错的。正确的表述是:

在Java 8以及之前的版本中,在虚引用回收后,虚引用指向的对象才会回收。在Java 9以及更新的版本中,虚引用不会对对象的生存产生任何影响。

一个示例

首先用Java 8,带上-Xmx10m -XX:+HeapDumpOnOutOfMemoryError参数运行如下代码:

Main.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public final class Main {

public static void main(String[] args) throws InterruptedException {
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
PhantomReference<byte[]> ref = new PhantomReference<>(new byte[1024 * 1024 * 5], queue);

System.out.println(queue.poll());
System.out.println("第一次gc");
System.gc();
Thread.sleep(300L);
System.out.println(queue.poll());
System.out.println("第二次gc");
System.gc();
byte[] bytes1 = new byte[1024 * 1024 * 6];
System.out.println("ending");
}
}

你猜猜结果是什么?


如何从Spring Cloud Config迁移到阿里云ACM

从零开始使用Spring Cloud Config中,我们简单了解了下Spring Cloud Config,它提供了一套配置管理的解决方案。

但是Spring Cloud Config需要自己搭建config-server,还需要结合eureka来实现高可用;如果需要实时更新配置,还需要Spring Cloud Bus。实在是过于繁琐。

而阿里云ACM(应用配置管理)则提供了一整套完整的解决方案:

  1. 与Spring生态紧密集成
    和Spring Cloud Config一样,ACM通过Envirment和PropertySource与Spring结合。
  2. 配置简单
    只需要在ACM上开一个namespace,然后配置client,就能享受到获取配置、自动更新配置功能。
  3. 免费

阿里云ACM相比与Spring Cloud Config,提供了一揽子配置解决方案,不需要在借助Spring Cloud Bus和eureka了。

我们以上一篇文章中的spring-cloud-config-client作为示例。

迁移配置文件

在使用ACM之前,我们需要将现有的配置迁移到ACM中,要不然直接切换过去没法获取任何配置,会导致应用无法正常运行。

为每个环境(此处的环境就是Spring中的profile)创建一个ACM的命名空间。

比如dev对应namespace dev,test对应test命名空间。


Robert Lu

关注我的公众号