Jvm-Sandbox源碼分析--啓動

前言

1.工作原因,使用jvm-sandbox比較多,遂進行源碼分析,做到知己知彼,個人能力有限,如有錯誤,歡迎指正。
2.關於jvm-sandbox 是什麼,如何安裝相關環境,可移步 官方文檔
3.源碼分析基於jvm-sandbox 最新的master代碼,tag-1.2.1。
4.暫定計劃通過啓動,加載模塊,刷新模塊,卸載模塊,激活模塊等方面入手,通過幾篇文章覆蓋jvm-sandbox關鍵流程。

啓動

attach方式啓動

sh sandbox/bin/sandbox.sh -p pid

簡單看一下啓動腳本sandbox.sh

# the sandbox main function
function main() {

    check_permission

    while getopts "hp:vFfRu:a:A:d:m:I:P:ClSn:X" ARG
    do
        case ${ARG} in
            h) usage;exit;;
            p) TARGET_JVM_PID=${OPTARG};;
            v) OP_VERSION=1;;
            l) OP_MODULE_LIST=1;;
            R) OP_MODULE_RESET=1;;
            F) OP_MODULE_FORCE_FLUSH=1;;
            f) OP_MODULE_FLUSH=1;;
            u) OP_MODULE_UNLOAD=1;ARG_MODULE_UNLOAD=${OPTARG};;
            a) OP_MODULE_ACTIVE=1;ARG_MODULE_ACTIVE=${OPTARG};;
            A) OP_MODULE_FROZEN=1;ARG_MODULE_FROZEN=${OPTARG};;
            d) OP_DEBUG=1;ARG_DEBUG=${OPTARG};;
            m) OP_MODULE_DETAIL=1;ARG_MODULE_DETAIL=${OPTARG};;
            I) TARGET_SERVER_IP=${OPTARG};;
            P) TARGET_SERVER_PORT=${OPTARG};;
            C) OP_CONNECT_ONLY=1;;
            S) OP_SHUTDOWN=1;;
            n) OP_NAMESPACE=1;ARG_NAMESPACE=${OPTARG};;
            X) set -x;;
            ?) usage;exit_on_err 1;;
        esac
    done

    reset_for_env

    # reset IP
    [ -z ${TARGET_SERVER_IP} ] && TARGET_SERVER_IP="${DEFAULT_TARGET_SERVER_IP}";

    # reset PORT
    [ -z ${TARGET_SERVER_PORT} ] && TARGET_SERVER_PORT=0;

    # reset NAMESPACE
    [[ ${OP_NAMESPACE} ]] \
        && TARGET_NAMESPACE=${ARG_NAMESPACE}
    [[ -z ${TARGET_NAMESPACE} ]] \
        && TARGET_NAMESPACE=${DEFAULT_NAMESPACE}

    if [[ ${OP_CONNECT_ONLY} ]]; then
        [[ 0 -eq ${TARGET_SERVER_PORT} ]] \
            && exit_on_err 1 "server appoint PORT (-P) was missing"
        SANDBOX_SERVER_NETWORK="${TARGET_SERVER_IP};${TARGET_SERVER_PORT}"
    else
        # -p was missing
        [[ -z ${TARGET_JVM_PID} ]] \
            && exit_on_err 1 "PID (-p) was missing.";
        attach_jvm
    fi

    # -v show version
    [[ ! -z ${OP_VERSION} ]] \
        && sandbox_curl_with_exit "sandbox-info/version"

    # -l list loaded modules
    [[ ! -z ${OP_MODULE_LIST} ]] \
        && sandbox_curl_with_exit "sandbox-module-mgr/list"

    # -F force flush module
    [[ ! -z ${OP_MODULE_FORCE_FLUSH} ]] \
        && sandbox_curl_with_exit "sandbox-module-mgr/flush" "&force=true"

    # -f flush module
    [[ ! -z ${OP_MODULE_FLUSH} ]] \
        && sandbox_curl_with_exit "sandbox-module-mgr/flush" "&force=false"

    # -R reset sandbox
    [[ ! -z ${OP_MODULE_RESET} ]] \
        && sandbox_curl_with_exit "sandbox-module-mgr/reset"

    # -u unload module
    [[ ! -z ${OP_MODULE_UNLOAD} ]] \
        && sandbox_curl_with_exit "sandbox-module-mgr/unload" "&action=unload&ids=${ARG_MODULE_UNLOAD}"

    # -a active module
    [[ ! -z ${OP_MODULE_ACTIVE} ]] \
        && sandbox_curl_with_exit "sandbox-module-mgr/active" "&ids=${ARG_MODULE_ACTIVE}"

    # -A frozen module
    [[ ! -z ${OP_MODULE_FROZEN} ]] \
        && sandbox_curl_with_exit "sandbox-module-mgr/frozen" "&ids=${ARG_MODULE_FROZEN}"

    # -m module detail
    [[ ! -z ${OP_MODULE_DETAIL} ]] \
        && sandbox_curl_with_exit "sandbox-module-mgr/detail" "&id=${ARG_MODULE_DETAIL}"

    # -S shutdown
    [[ ! -z ${OP_SHUTDOWN} ]] \
        && sandbox_curl_with_exit "sandbox-control/shutdown"

    # -d debug
    if [[ ! -z ${OP_DEBUG} ]]; then
        sandbox_debug_curl "module/http/${ARG_DEBUG}"
        exit
    fi

    # default
    sandbox_curl "sandbox-info/version"
    exit

}

通過腳本源碼,我們可以看到在執行sandbox.sh的時候,會先執行reset_for_env方法
關鍵步驟:
1.使用默認環境變量 JAVA_HOME
2.或者通過TARGET_JVM_PID查找 設置sandbox環境變量
3.判斷 JVM 版本是否符合要求
4.若 ${JAVA_HOME}/lib/tools.jar 存在,則通過 -Xbootclasspath/a 這個配置,將它加入 classpath 末尾,爲執行 attach_jvm 方法做準備

reset_for_env()
{
    #使用默認環境變量 JAVA_HOME
    # use the env JAVA_HOME for default
    [[ ! -z ${JAVA_HOME} ]] \
        && SANDBOX_JAVA_HOME="${JAVA_HOME}"

    # 或者通過TARGET_JVM_PID查找 設置sandbox環境變量
    # use the target JVM for SANDBOX_JAVA_HOME
    [[ -z ${SANDBOX_JAVA_HOME} ]] \
        && SANDBOX_JAVA_HOME="$(\
            ps aux\
            |grep ${TARGET_JVM_PID}\
            |grep java\
            |awk '{print $11}'\
            |xargs ls -l\
            |awk '{if($1~/^l/){print $11}else{print $9}}'\
            |sed 's/\/bin\/java//g'\
        )"

    [[ ! -x "${SANDBOX_JAVA_HOME}" ]] \
        && exit_on_err 1 "permission denied, ${SANDBOX_JAVA_HOME} is not accessible! please set JAVA_HOME"

    [[ ! -x "${SANDBOX_JAVA_HOME}/bin/java" ]] \
        && exit_on_err 1 "permission denied, ${SANDBOX_JAVA_HOME}/bin/java is not executable!"
    #判斷 JVM 版本
    # check the jvm version, we need 6+
    local JAVA_VERSION=$("${SANDBOX_JAVA_HOME}/bin/java" -version 2>&1|awk -F '"' '/version/&&$2>"1.5"{print $2}')
    [[ -z ${JAVA_VERSION} ]] \
        && exit_on_err 1 "illegal java version: ${JAVA_VERSION}, please make sure target java process: ${TARGET_JVM_PID} run int JDK[6,11]"

    #若 ${JAVA_HOME}/lib/tools.jar 存在,則通過 -Xbootclasspath/a 這個配置,將它加入 classpath 末尾,爲執行 attach_jvm 方法做準備
    [[ -f "${SANDBOX_JAVA_HOME}"/lib/tools.jar ]] \
        && SANDBOX_JVM_OPS="${SANDBOX_JVM_OPS} -Xbootclasspath/a:${SANDBOX_JAVA_HOME}/lib/tools.jar"

}

然後再執行attach_jvm方法
關鍵步驟:
通過java -jar 命令啓動 sandbox-core.jar 並傳遞參數 1. TARGET_JVM_PID 2. sandbox-agent.jar 3. 啓動要用到的數據信息

# attach sandbox to target JVM
# return : attach jvm local info
function attach_jvm() {

    # got an token
    local token=`date |head|cksum|sed 's/ //g'`

    # attach target jvm
    # 通過java -jar 命令啓動 sandbox-core.jar 並傳遞參數 1. TARGET_JVM_PID 2. sandbox-agent.jar 3. 啓動要用到的數據信息
    "${SANDBOX_JAVA_HOME}/bin/java" \
        ${SANDBOX_JVM_OPS} \
        -jar ${SANDBOX_LIB_DIR}/sandbox-core.jar \
        ${TARGET_JVM_PID} \
        "${SANDBOX_LIB_DIR}/sandbox-agent.jar" \
        "home=${SANDBOX_HOME_DIR};token=${token};server.ip=${TARGET_SERVER_IP};server.port=${TARGET_SERVER_PORT};namespace=${TARGET_NAMESPACE}" \
    || exit_on_err 1 "attach JVM ${TARGET_JVM_PID} fail."

    # get network from attach result
    SANDBOX_SERVER_NETWORK=$(grep ${token} ${SANDBOX_TOKEN_FILE}|grep ${TARGET_NAMESPACE}|tail -1|awk -F ";" '{print $3";"$4}');
    [[ -z ${SANDBOX_SERVER_NETWORK} ]]  \
        && exit_on_err 1 "attach JVM ${TARGET_JVM_PID} fail, attach lose response."

}

接下來就進入代碼分析階段,我們來看sandbox-core 這個moudle

在pom文件中存在插件配置如下,通過mainClass 指定了這個主函數,所以我們通過java -jar sandbox-core.jar命令會執行這個函數

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>attached</goal>
                        </goals>
                        <phase>package</phase>
                        <configuration>
                            <descriptorRefs>
                                <descriptorRef>jar-with-dependencies</descriptorRef>
                            </descriptorRefs>
                            <archive>
                                <manifest>
                                    <mainClass>com.alibaba.jvm.sandbox.core.CoreLauncher</mainClass>
                                </manifest>
                            </archive>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            

我們再看這個CoreLauncher這個類的main方法
關鍵步驟:
1.attach pid
2.load sandbox-angent.jar

/**
     * 內核啓動程序
     *
     * @param args 參數
     *             [0] : PID
     *             [1] : agent.jar's value
     *             [2] : token
     */
    public static void main(String[] args) {
        try {

            // check args
            if (args.length != 3
                    || StringUtils.isBlank(args[0])
                    || StringUtils.isBlank(args[1])
                    || StringUtils.isBlank(args[2])) {
                throw new IllegalArgumentException("illegal args");
            }

            new CoreLauncher(args[0], args[1], args[2]);
        } catch (Throwable t) {
            t.printStackTrace(System.err);
            System.err.println("sandbox load jvm failed : " + getCauseMessage(t));
            System.exit(-1);
        }
    }
    
    public CoreLauncher(final String targetJvmPid,
                        final String agentJarPath,
                        final String token) throws Exception {

        // 加載agent
        attachAgent(targetJvmPid, agentJarPath, token);

    }
    
    // 加載Agent
    private void attachAgent(final String targetJvmPid,
                             final String agentJarPath,
                             final String cfg) throws Exception {

        VirtualMachine vmObj = null;
        try {
            //attach 目標 pid
            vmObj = VirtualMachine.attach(targetJvmPid);
            if (vmObj != null) {
                //通過vm類 加載sandbox-agent.jar
                vmObj.loadAgent(agentJarPath, cfg);
            }

        } finally {
            if (null != vmObj) {
                vmObj.detach();
            }
        }

    }

我們可以看到在attach pid 之後加載了sandbox-agent.jar
接下來我們看一下sandbox-agent.jar
和sandbox-core.jar的pom文件類似,agent這個模塊也通過maven插件配置了Premain-Class和Agent-Class兩個參數,並且都指向AgentLauncher這個類

<plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>attached</goal>
                        </goals>
                        <phase>package</phase>
                        <configuration>
                            <descriptorRefs>
                                <descriptorRef>jar-with-dependencies</descriptorRef>
                            </descriptorRefs>
                            <archive>
                                <manifestEntries>
                                    <Premain-Class>com.alibaba.jvm.sandbox.agent.AgentLauncher</Premain-Class>
                                    <Agent-Class>com.alibaba.jvm.sandbox.agent.AgentLauncher</Agent-Class>
                                    <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                    <Can-Retransform-Classes>true</Can-Retransform-Classes>
                                </manifestEntries>
                            </archive>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

接下來我們看一下AgentLauncher
通過attach pid方式執行,會調用這個類中的agentmain方法,
如果大家不明白爲何通過maven插件配置後,便會和指定類中的某個方法進行關聯,可以參考一下這篇文章

 /**
     * 動態加載
     *
     * @param featureString 啓動參數
     *                      [namespace,token,ip,port,prop]
     * @param inst          inst
     */
    public static void agentmain(String featureString, Instrumentation inst) {
        LAUNCH_MODE = LAUNCH_MODE_ATTACH;
        final Map<String, String> featureMap = toFeatureMap(featureString);
        writeAttachResult(
                getNamespace(featureMap),
                getToken(featureMap),
                install(featureMap, inst)
        );
    }

java agent方式啓動

在應用服務啓動腳本中添加:

java -javaagent:/yourpath/sandbox/lib/sandbox-agent.jar

通過javaagent方式啓動會調用AgentLauncher類中的premain方法

 /**
     * 啓動加載
     *
     * @param featureString 啓動參數
     *                      [namespace,prop]
     * @param inst          inst
     */
    public static void premain(String featureString, Instrumentation inst) {
        LAUNCH_MODE = LAUNCH_MODE_AGENT;
        install(toFeatureMap(featureString), inst);
    }

到這一步我們可以很清楚的看到,不管是通過attach pid的方式還是通過javaagent的方式進行啓動,最終都會執行install這個方法,
接下來我們一起看一下這個方法做了什麼事情。

/**
     * 在當前JVM安裝jvm-sandbox
     *
     * @param featureMap 啓動參數配置
     * @param inst       inst
     * @return 服務器IP:PORT
     */
    private static synchronized InetSocketAddress install(final Map<String, String> featureMap,
                                                          final Instrumentation inst) {

        final String namespace = getNamespace(featureMap);
        final String propertiesFilePath = getPropertiesFilePath(featureMap);
        final String coreFeatureString = toFeatureString(featureMap);

        try {

            // 將Spy注入到BootstrapClassLoader
            inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(
                    getSandboxSpyJarPath(getSandboxHome(featureMap))
                    // SANDBOX_SPY_JAR_PATH
            )));

            // 構造自定義的類加載器,儘量減少Sandbox對現有工程的侵蝕
            final ClassLoader sandboxClassLoader = loadOrDefineClassLoader(
                    namespace,
                    getSandboxCoreJarPath(getSandboxHome(featureMap))
                    // SANDBOX_CORE_JAR_PATH
            );

            // CoreConfigure類定義
            final Class<?> classOfConfigure = sandboxClassLoader.loadClass(CLASS_OF_CORE_CONFIGURE);

            // 反序列化成CoreConfigure類實例
            final Object objectOfCoreConfigure = classOfConfigure.getMethod("toConfigure", String.class, String.class)
                    .invoke(null, coreFeatureString, propertiesFilePath);

            // CoreServer類定義
            final Class<?> classOfProxyServer = sandboxClassLoader.loadClass(CLASS_OF_PROXY_CORE_SERVER);

            // 獲取CoreServer單例
            final Object objectOfProxyServer = classOfProxyServer
                    .getMethod("getInstance")
                    .invoke(null);

            // CoreServer.isBind()
            final boolean isBind = (Boolean) classOfProxyServer.getMethod("isBind").invoke(objectOfProxyServer);


            // 如果未綁定,則需要綁定一個地址
            if (!isBind) {
                try {
                    classOfProxyServer
                            .getMethod("bind", classOfConfigure, Instrumentation.class)
                            .invoke(objectOfProxyServer, objectOfCoreConfigure, inst);
                } catch (Throwable t) {
                    classOfProxyServer.getMethod("destroy").invoke(objectOfProxyServer);
                    throw t;
                }

            }

            // 返回服務器綁定的地址
            return (InetSocketAddress) classOfProxyServer
                    .getMethod("getLocal")
                    .invoke(objectOfProxyServer);


        } catch (Throwable cause) {
            throw new RuntimeException("sandbox attach failed.", cause);
        }

    }

源碼本身的註釋已經非常清晰,在後續文章中,我們會繼續深入分析各個步驟,現在只要關注下面這部分代碼,其實就是又回到sandbox-core模塊,通過調用JettyCoreServer的bind方法啓動了一個jetty服務,後續我們的加載,卸載,等命令操作都會通過http請求的方式進行。

private static final String CLASS_OF_PROXY_CORE_SERVER = "com.alibaba.jvm.sandbox.core.server.ProxyCoreServer";

...省略代碼...

// CoreServer類定義
            final Class<?> classOfProxyServer = sandboxClassLoader.loadClass(CLASS_OF_PROXY_CORE_SERVER);

            // 獲取CoreServer單例
            final Object objectOfProxyServer = classOfProxyServer
                    .getMethod("getInstance")
                    .invoke(null);

            // CoreServer.isBind()
            final boolean isBind = (Boolean) classOfProxyServer.getMethod("isBind").invoke(objectOfProxyServer);


            // 如果未綁定,則需要綁定一個地址
            if (!isBind) {
                try {
                    classOfProxyServer
                            .getMethod("bind", classOfConfigure, Instrumentation.class)
                            .invoke(objectOfProxyServer, objectOfCoreConfigure, inst);
                } catch (Throwable t) {
                    classOfProxyServer.getMethod("destroy").invoke(objectOfProxyServer);
                    throw t;
                }

            }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章