groovy program 运行 as kubernetes job dumping threads during execution 的可能原因

Possible reasons for groovy program running as kubernetes job dumping threads during execution

我有一个简单的 groovy 脚本,它利用 GPars 库的 withPool 功能向两个内部 API 端点并行发起 HTTP GET 请求。

脚本 运行 在本地和 docker 容器都很好。

当我将它部署为 Kubernetes Job(在我们的内部 EKS 集群中:1.20)时,它 运行 也在那里,但是当它到达第一个 withPool 调用,我看到一个巨大的线程转储,但执行继续,并成功完成。

注意:我们集群中的容器 运行 具有以下 pod 安全上下文:

      securityContext:
        fsGroup: 2000
        runAsNonRoot: true
        runAsUser: 1000

环境

# From the k8s job container
groovy@app-271df1d7-15848624-mzhhj:/app$ groovy --version
WARNING: Using incubator modules: jdk.incubator.foreign, jdk.incubator.vector
Groovy Version: 4.0.0 JVM: 17.0.2 Vendor: Eclipse Adoptium OS: Linux


groovy@app-271df1d7-15848624-mzhhj:/app$ ps -ef 
UID        PID  PPID  C STIME TTY          TIME CMD
groovy       1     0  0 21:04 ?        00:00:00 /bin/bash bin/run-script.sh
groovy      12     1 42 21:04 ?        00:00:17 /opt/java/openjdk/bin/java -Xms3g -Xmx3g --add-modules=ALL-SYSTEM -classpath /opt/groovy/lib/groovy-4.0.0.jar -Dscript.name=/usr/bin/groovy -Dprogram.name=groovy -Dgroovy.starter.conf=/opt/groovy/conf/groovy-starter.conf -Dgroovy.home=/opt/groovy -Dtools.jar=/opt/java/openjdk/lib/tools.jar org.codehaus.groovy.tools.GroovyStarter --main groovy.ui.GroovyMain --conf /opt/groovy/conf/groovy-starter.conf --classpath . /tmp/script.groovy
groovy     116     0  0 21:05 pts/0    00:00:00 bash
groovy     160   116  0 21:05 pts/0    00:00:00 ps -ef

脚本(相关部分)

@Grab('org.codehaus.gpars:gpars:1.2.1')
import static groovyx.gpars.GParsPool.withPool

import groovy.json.JsonSlurper
final def jsl = new JsonSlurper()


//...

while (!(nextBatch = getBatch(batchSize)).isEmpty()) {
    def devThread = Thread.start {
        withPool(poolSize) {
            nextBatch.eachParallel { kw ->
                String url = dev + "&" + "query=$kw"
                try {
                    def response = jsl.parseText(url.toURL().getText(connectTimeout: 10.seconds, readTimeout: 10.seconds,
                            useCaches: true, allowUserInteraction: false))
                    devResponses[kw] = response
                } catch (e) {
                    println("\tFailed to fetch: $url | error: $e")
                }
            }
        }
    }

    def stgThread = Thread.start {
        withPool(poolSize) {
            nextBatch.eachParallel { kw ->
                String url = stg + "&" + "query=$kw"
                try {
                    def response = jsl.parseText(url.toURL().getText(connectTimeout: 10.seconds, readTimeout: 10.seconds,
                            useCaches: true, allowUserInteraction: false))
                    stgResponses[kw] = response
                } catch (e) {
                    println("\tFailed to fetch: $url | error: $e")
                }
            }
        }
    }
    devThread.join()
    stgThread.join()
}

Dockerfile

FROM groovy:4.0.0-jdk17 as builder

USER root
RUN apt-get update && apt-get install -yq bash curl wget jq

WORKDIR /app

COPY bin /app/bin

RUN chmod +x /app/bin/*

USER groovy
ENTRYPOINT ["/bin/bash"]
CMD ["bin/run-script.sh"]

bin/run-script.sh只是在运行时下载上面的groovy脚本并执行。

wget "$GROOVY_SCRIPT" -O "$LOCAL_FILE"

...

groovy "$LOCAL_FILE"

一旦执行到达 第一次 withPool(poolSize) 的调用,就会有一个巨大的线程转储,但执行会继续。

我正在尝试找出 什么 可能导致此行为。有什么想法‍♂️吗?

Thread dump

为了后代,在这里回答我自己的问题。

问题原来是 this log4j2 JVM hot-patch 我们目前正在利用它来修复最近的 log4j2 漏洞。此代理(运行 作为 DaemonSet)修补我们所有 k8s 集群中的所有 运行 JVM。

不知何故,这导致我基于 OpenJDK 17 的应用程序出现线程转储。我在 ElasticSearch 8.1.0 部署中也发现了同样的问题(也使用 pre-packaged OpenJDK 17)。这是一项服务,所以我几乎每半小时就会看到一次线程转储!有趣的是,还有其他 JVM 服务(和一些 SOLR 8 部署)没有 有这个问题‍♂️。

无论如何,我与我们的 devops 团队合作暂时排除 部署 运行 的节点,你瞧,线程转储消失了!

宇宙的平衡已经恢复‍♂️。