Alpine Docker配置LD_PRELOAD防JVM崩溃但执行外部命令报错问题
Alpine 3.15.4容器运行Quarkus调用Firestore的配置冲突解决方案
问题现象
基于Alpine 3.15.4基础镜像构建Docker容器运行Quarkus Java服务时,调用com.google.firebase.cloud.FirestoreClient.getFirestore()方法存在配置冲突,两类常规Dockerfile配置均无法同时满足Firestore初始化、外部命令调用两个场景:
- 配置1:容器内通过apk安装OpenJDK11、curl、ca-certificates依赖
运行表现:Firestore初始化流程正常,但通过ProcessBuilder调用RUN apk update RUN apk --no-cache add curl ca-certificates openjdk11-jre-headlessls等系统命令时抛出No such file or directory异常 - 配置2:注释上述apk安装OpenJDK的步骤
运行表现:Firestore初始化触发JVM SIGSEGV崩溃,崩溃点位于JVM外部原生代码层,但#RUN apk update #RUN apk --no-cache add curl ca-certificates openjdk11-jre-headlessls等系统命令可正常执行
错误日志
JVM崩溃日志
[de.ibe.DemoResource] (executor-thread-0) getFirestore # # A fatal error has been detected by the Java Runtime Environment: # # SIGSEGV (0xb) at pc=0x0000000000003efe, pid=1, tid=142 # # JRE version: OpenJDK Runtime Environment (11.0.15+10) (build 11.0.15+10-alpine-r0) # Java VM: OpenJDK 64-Bit Server VM (11.0.15+10-alpine-r0, mixed mode, tiered, compressed oops, g1 gc, linux-amd64) # Problematic frame: # C 0x0000000000003efe # # No core dump will be written. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again # # An error report file with more information is saved as: # /deployments/hs_err_pid1.log # # The crash happened outside the Java Virtual Machine in native code. # See problematic frame for where to report the bug.
外部命令调用失败日志
java.io.IOException: Cannot run program "ls": error=2, No such file or directory at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1128) at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1071) at de.ibe.DemoResource.shell(DemoResource.java:72) at de.ibe.DemoResource.demo(DemoResource.java:37)
根因分析
- Firestore底层依赖gRPC Java的原生库,该库基于glibc编译,而Alpine默认使用musl libc,直接运行会触发原生层内存访问错误导致JVM崩溃,需要安装gcompat兼容层做二进制兼容
- 常规方案中全局配置
LD_PRELOAD=/lib/libgcompat.so.0会让该环境变量被所有ProcessBuilder fork出的子进程继承,gcompat的初始化逻辑会干扰musl libc的可执行文件路径搜索流程,导致系统命令无法被找到
单镜像解决方案
不需要构建多个适配镜像,通过调整gcompat加载范围即可同时解决两个问题:
- 安装gcompat依赖,但不要配置全局LD_PRELOAD环境变量
- 仅在JVM主进程启动时临时加载gcompat库,保证该配置不会被子进程继承
- 调整JVM进程启动机制,避免fork过程中兼容层引发的异常
修正后的Dockerfile配置
FROM alpine:3.15.4 ARG RUN_JAVA_VERSION=1.3.8 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' # 合并安装步骤,同时安装openjdk和gcompat兼容层,减少镜像层数 RUN apk update && \ apk --no-cache add curl ca-certificates openjdk11-jre-headless gcompat && \ mkdir /deployments && \ chmod "g+rwX" /deployments && \ curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh && \ chmod 540 /deployments/run-java.sh && \ # 修改启动脚本,仅在执行java命令时临时加载gcompat,不设置全局环境变量 sed -i 's|exec.*java|LD_PRELOAD=/lib/libgcompat.so.0 exec java|g' /deployments/run-java.sh # JVM参数增加POSIX_SPAWN进程启动机制,规避vfork模式下的兼容问题 ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager -Djdk.lang.Process.launchMechanism=POSIX_SPAWN" # 应用分层拷贝逻辑保持不变 COPY target/quarkus-app/lib/ /deployments/lib/ COPY target/quarkus-app/*.jar /deployments/ COPY target/quarkus-app/app/ /deployments/app/ COPY target/quarkus-app/quarkus/ /deployments/quarkus/ EXPOSE 8080 ENTRYPOINT [ "/deployments/run-java.sh" ]
验证说明
该配置下:
- JVM主进程启动时会加载gcompat兼容层,gRPC原生库可正常运行,Firestore初始化不会触发SIGSEGV崩溃
- ProcessBuilder启动外部命令时,子进程不会继承LD_PRELOAD配置,系统命令路径搜索逻辑正常,
ls等命令可正常执行
附复现Demo代码
package de.ibe; import java.io.IOException; import java.io.InputStream; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.jaxrs.PathParam; import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.cloud.FirestoreClient; @Path("/demo") public class DemoResource { private static final Logger LOG = Logger.getLogger(DemoResource.class); @GET @Produces(MediaType.TEXT_PLAIN) @Path("/{command}") public String demo(@PathParam String command) { try { if (command == null || command.equals("")) { return "command null or empty"; } else if (command.equals("init")) { LOG.info("run init"); return this.init(); } else if (command.equals("shell")) { LOG.info("run shell"); return this.shell(); } else { return "unkown command"; } } catch (Exception e) { LOG.error(e); return e.getMessage(); } } private String init() throws IOException { try (InputStream in = this.getClass().getResourceAsStream("/demo-b9aa8a6.json")) { if (in == null) { return "init in is null"; } GoogleCredentials credentials = GoogleCredentials.fromStream(in); LOG.info("options"); FirebaseOptions options = new FirebaseOptions.Builder().setCredentials(credentials).build(); LOG.info("initializeApp"); FirebaseApp.initializeApp(options); LOG.info("getFirestore"); FirestoreClient.getFirestore(); } return "init ok"; } private String shell() { try { final ProcessBuilder builder = new ProcessBuilder("ls"); builder.start(); } catch (IOException e) { e.printStackTrace(); return "shell error"; } return "shell ok"; } }
内容的提问来源于stack exchange,提问作者Michael S
相关产品推荐
相关产品推荐

