Jvm-Sandbox的启动(一):sandbox.sh脚本分析
Sandbox的启动是通过其内置的shell脚本 sandbox.sh
开始执行的,一切的开始皆可从该脚本中探寻出结果。脚本有一定的代码量,大概有400+行,这里将该脚本分为如下几个部分进行讲解:
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函数是该脚本的重要方法,也是脚本的执行入口,它主要完成了以下几件事:
其代码如下所示:
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包。
至此,sandbox.sh的职责基本完成,Sandbox整体的启动来到了我们熟悉的java工程当中,后面的章节将继续对其深入分析。