JvmSandbox原理分析01-Sandbox的启动

1,439 阅读2分钟

Jvm-Sandbox的启动(一):sandbox.sh脚本分析

Sandbox的启动是通过其内置的shell脚本 sandbox.sh 开始执行的,一切的开始皆可从该脚本中探寻出结果。脚本有一定的代码量,大概有400+行,这里将该脚本分为如下几个部分进行讲解:

image-20220417132825306.png

1、变量定义过程

这个过程首先预定义了接下来即将使用的一些变量。代码如下:

 # 定义sandbox的home目录,并为其赋值
 typeset SANDBOX_HOME_DIR
 [[ -z ${SANDBOX_HOME_DIR} ]] && SANDBOX_HOME_DIR=${PWD}/..
 ​
 # 定义 SANDBOX_USER,并为其赋值
 typeset SANDBOX_USER=${USER}
 [[ -z ${SANDBOX_USER} ]] && SANDBOX_USER=$(whoami)
 ​
 # 定义 SANDBOX_SERVER_NETWORK
 typeset SANDBOX_SERVER_NETWORK
 ​
 # 定义lib目录,这个目录下主要存放jar包
 typeset SANDBOX_LIB_DIR=${SANDBOX_HOME_DIR}/lib
 ​
 # 定义 SANDBOX_TOKEN_FILE
 typeset SANDBOX_TOKEN_FILE="${HOME}/.sandbox.token"
 ​
 # 定义JVM参数 SANDBOX_JVM_OPS
 typeset SANDBOX_JVM_OPS="-Xms128M -Xmx128M -Xnoclassgc -ea"
 ​
 # 定义目标JVM的进程号,后面的agent主要attach到该JVM进程上
 typeset TARGET_JVM_PID
 ​
 # 定义目标机器IP以及默认机器IP
 typeset TARGET_SERVER_IP
 typeset DEFAULT_TARGET_SERVER_IP="0.0.0.0"
 ​
 # 定义目标进程端口
 typeset TARGET_SERVER_PORT
 ​
 # 定义名称空间
 typeset TARGET_NAMESPACE
 typeset DEFAULT_NAMESPACE="default"

注释和变量命名已经描绘的非常清楚了,在看后面代码遇到忘记了的变量可以到这里来回顾下。

这里为其中一些变量补充说明:

  • SANDBOX_HOME_DIR:shell脚本中,-z表示检测紧跟的字符串长度是否为0,如果为0返回true。这里使用短路与,如果 ${SANDBOX_HOME_DIR} 为0,则使用 ${PWD}/.. 的目录作为sandbox的home目录。这种方式表示优先使用环境变量 SANDBOX_HOME_DIR,如果未定义环境变量SANDBOX_HOME_DIR,则使用当前目录。
  • SANDBOX_TOKEN_FILE:这个文件主要存放了sandbox attach记录,包括attach进程的host:port。
  • TARGET_SERVER_IP:一般情况下,我们都是将整个工程打包后上传至目标机器,然后在目标机器上执行该shell脚本,因此默认机器IP一般为localhost即可。

2、执行入口

执行入口就比较简单了,就一行代码,其中${@}会保存我们传递给该shell脚本的所有参数:

 main "${@}"

比方说,我们以如下命令启动脚本,则${@} 就包含了-p 12345 这个参数

 ./sandbox.sh -p 12345

3、main函数

main函数是该脚本的重要方法,也是脚本的执行入口,它主要完成了以下几件事:

image-20220417160624718.png

其代码如下所示:

 function main() {
   # 遍历脚本参数
   while getopts "hp:vFfRu:a:A:d:m:I:P:ClSn:X" ARG; do
     case ${ARG} in
     h)
       # 帮助手册函数,大家可以自行翻阅源码查看
       usage
       exit
       ;;
     # 赋值PID
     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}
       ;;
     # 赋值IP
     I) TARGET_SERVER_IP=${OPTARG} ;;
     # 赋值PORT
     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
   # 校验权限
   check_permission
 ​
   # 根据不同的参数,进行相应处理
   # 如果没有指定IP,则使用默认值
   [ -z "${TARGET_SERVER_IP}" ] && TARGET_SERVER_IP="${DEFAULT_TARGET_SERVER_IP}"
 ​
   # 如果没有指定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的核心方法
     attach_jvm
   fi
 ​
   # -v show version
   [[ -n ${OP_VERSION} ]] &&
     sandbox_curl_with_exit "sandbox-info/version"
 ​
   # -l list loaded modules
   [[ -n ${OP_MODULE_LIST} ]] &&
     sandbox_curl_with_exit "sandbox-module-mgr/list"
 ​
   # -F force flush module
   [[ -n ${OP_MODULE_FORCE_FLUSH} ]] &&
     sandbox_curl_with_exit "sandbox-module-mgr/flush" "&force=true"
 ​
   # -f flush module
   [[ -n ${OP_MODULE_FLUSH} ]] &&
     sandbox_curl_with_exit "sandbox-module-mgr/flush" "&force=false"
 ​
   # -R reset sandbox
   [[ -n ${OP_MODULE_RESET} ]] &&
     sandbox_curl_with_exit "sandbox-module-mgr/reset"
 ​
   # -u unload module
   [[ -n ${OP_MODULE_UNLOAD} ]] &&
     sandbox_curl_with_exit "sandbox-module-mgr/unload" "&action=unload&ids=${ARG_MODULE_UNLOAD}"
 ​
   # -a active module
   [[ -n ${OP_MODULE_ACTIVE} ]] &&
     sandbox_curl_with_exit "sandbox-module-mgr/active" "&ids=${ARG_MODULE_ACTIVE}"
 ​
   # -A frozen module
   [[ -n ${OP_MODULE_FROZEN} ]] &&
     sandbox_curl_with_exit "sandbox-module-mgr/frozen" "&ids=${ARG_MODULE_FROZEN}"
 ​
   # -m module detail
   [[ -n ${OP_MODULE_DETAIL} ]] &&
     sandbox_curl_with_exit "sandbox-module-mgr/detail" "&id=${ARG_MODULE_DETAIL}"
 ​
   # -S shutdown
   [[ -n ${OP_SHUTDOWN} ]] &&
     sandbox_curl_with_exit "sandbox-control/shutdown"
 ​
   # -d debug
   if [[ -n ${OP_DEBUG} ]]; then
     sandbox_debug_curl "module/http/${ARG_DEBUG}"
     exit
   fi
 ​
   # default
   sandbox_curl "sandbox-info/version"
   exit
 ​
 }

整体看下来,main函数的逻辑还是比较清晰的:

它首先会遍历执行shell脚本的所有参数,为对应的参数设置标志位(OP_MODULE_xxx=1),如果参数有携带变量(参数有携带冒号的u:a:A:d:m:I:P:)的,再保存该变量(ARG_xxx=${OPTARG})。

然后执行reset_for_env函数,见名即知重置环境设置。

 reset_for_env() {
 ​
   # 如果JAVA_HOME的字符串长度不为0,则令SANDBOX_JAVA_HOME为JAVA_HOME
   [[ -n "${JAVA_HOME}" ]] && SANDBOX_JAVA_HOME="${JAVA_HOME}"
 ​
   # 如果SANDBOX_JAVA_HOME为空,通过lsof命令从指定进程中提取Java_home信息
   [[ -z "${SANDBOX_JAVA_HOME}" ]] &&
     SANDBOX_JAVA_HOME="$(
       lsof -p "${TARGET_JVM_PID}" |
         grep "/bin/java" |
         awk '{print $9}' |
         xargs ls -l |
         awk '{if($1~/^l/){print $11}else{print $9}}' |
         xargs ls -l |
         awk '{if($1~/^l/){print $11}else{print $9}}' |
         sed 's//bin/java//g'
     )"
 ​
   # 如果tools.jar存在且为普通文件,则为SANDBOX_JVM_OPS增加一些虚拟机参数“-Xbootclasspath/a”,即改变Bootstrap ClassLoader的类加载路径
   [[ -f "${SANDBOX_JAVA_HOME}"/lib/tools.jar ]] &&
     SANDBOX_JVM_OPS="${SANDBOX_JVM_OPS} -Xbootclasspath/a:${SANDBOX_JAVA_HOME}/lib/tools.jar"
 ​
   # 修改windows的问题,shell $HOME与user.home存在差异
   test -n "${USERPROFILE}" -a -z "$(cat "${SANDBOX_TOKEN_FILE}")" && SANDBOX_TOKEN_FILE=${USERPROFILE}/.sandbox.token
 ​
 }

然后执行check_permission函数,进行一些校验。

 check_permission()
 {
   # 如果HOME目录不可写,则直接报错,错误码为1
     [[ ! -w ${HOME} ]] \
         && exit_on_err 1 "permission denied, ${HOME} is not writable."
         
     # 如果SANDBOX_LIB_DIR目录不可读,则直接报错,错误码为1,这个目录包含了接下来需要使用的JAR包,需要具有读权限
     [[ ! -r ${SANDBOX_LIB_DIR} ]] \
         && exit_on_err 1 "permission denied, ${SANDBOX_LIB_DIR} is not readable."
         
     # 尝试创建SANDBOX_TOKEN_FILE,创建失败报错,错误码为1
     touch ${SANDBOX_TOKEN_FILE} \
         || exit_on_err 1 "permission denied, ${SANDBOX_TOKEN_FILE} is not readable."
 }

最后是根据前面轮询的环境变量参数执行一些处理,这些处理大致就是调用了两个函数

其中一个是sandbox_curl_with_exit:这个函数的调用链路为sandbox_curl_with_exit -> sandbox_curl -> sandbox_debug_curl,最后的debug这个函数源码如下所示,比较简单,就是组装了curl命令,向sandbox发起了http请求。

 function sandbox_debug_curl() {
   local host=${SANDBOX_SERVER_NETWORK%;**}
   local port=${SANDBOX_SERVER_NETWORK#**;}
   if [[ "$host" == "0.0.0.0" ]]; then
     host="127.0.0.1"
   fi
   curl -N -s "http://${host}:${port}/sandbox/${TARGET_NAMESPACE}/${1}" ||
     exit_on_err 1 "target JVM ${TARGET_JVM_PID} lose response."
 }

另外一个函数是attach_jvm,agent如何attach至目标JVM上的逻辑都在该函数中了。

4、attach jvm

这个函数也比较简单,就是组装了java执行命令,拉起 sandbox-core,并将一系列参数传递给拉起的java工程当中。

 function attach_jvm() {
   # got an token
   local token
   token="$(date | head | cksum | sed 's/ //g')"
 ​
   # 通过java指令启动核心jar包,并添加上所需虚拟机参数
   "${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."
 ​
   # 判断SANDBOX_SERVER_NETWORK是否为空,为空则报错
   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."
 ​
 }

一般而言,执行Java命令的参数如下所示:

 .../java -Xms128M -Xmx128M -Xnoclassgc -ea -jar .../sandbox-core.jar JVM_PID ".../sandbox-agent.jar" "home=${SANDBOX_HOME_DIR};token=${token};server.ip=${TARGET_SERVER_IP};server.port=${TARGET_SERVER_PORT};namespace=${TARGET_NAMESPACE}"

这里通过java -jar命令启动了sandbox-core.jar的核心jar包,并为其添加上三个参数:

  • JVM_PID
  • sandbox-agent.jar包的绝对路径字符串
  • home/token/ip/port/namespace信息字符串

5、总结

本篇文章分析了jvm-sandbox启动脚本sandbox.sh核心执行流程,描述了执行过程中的各个关键节点,并得知该脚本最后是使用java -jar命令拉起了sandbox-core.jar这个jar包。

image-20220417172353759.png

至此,sandbox.sh的职责基本完成,Sandbox整体的启动来到了我们熟悉的java工程当中,后面的章节将继续对其深入分析。