spring boot/cloud 多服务部署单机启动顺序有依赖的解决办法
spring cloud 做多服务是很方便的,但为了方便伸缩和计算资源的限制,我们需要在一台主机上部署多个业务实例,也需要这些业务实例开机自启动,我们知道,spring cloud服务或者根据业务需要,各业务服务启动顺序是有依赖关系的。那么我们如何得知一个被依赖的服务已经启动成功了呢,我们就需要代码的简单注入和配合脚本(如shell)来进行。
我们知道一般linxu的标准服务会放在/etc/init.d目录下,然后chkconfig add xxxService 来完成开机自启动,linxu操作系统能保证按照顺序启动,但应用服务启动成功后是否真正准备好服务了,得由应用决定了,一般服务启动后会生成/var/run/xxxx.pid文件(这是linux应用启动成功外服务的一般做法),脚本判断前个服务的pid文件生成后再启动后续依赖的服务。那么java业务服务业务也可以采用这种类似的方式。特别是以spring boot 的java服务,外部脚本通过类似于
base_dir=$(dirname $0)
tmprun=$base_dir/../tmp
rm -rf $tmprun
mkdir -p $tmprun
chmod 777 $tmprun
export LOGS_DIR="$base_dir/../logs"
nohup java -Djava.io.tmpdir=$tmprun -jar ${base_dir}/../libs/xxxx.jar \
--config.profile=production \
--spring.profiles.active=production \
--spring.config.location=file:${base_dir}/../config/ \
--logging.config=${base_dir}/../config/logback.xml \
>/dev/null 2>&1 &
方式来启动,由于脚本是推到后台运行,如何判断服务的所有bean已经准备好了呢?有同仁可能会想到
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
通过maven生成可执行jar包,这样的jar包是linux操作系统服务命令直接在操作系统上运行,且在运行后会自动生成 pid文件,好像很方便的生成pid文件了,但它生成的pid文件的时机在jvm虚拟机启动响应业务服务开始就产生了,可能spring 还在初始化业务bean,造成另外服务启动访问依赖服务接口报错。所以这种生成pid文件的方式pass掉了,因此要自己添添加少量代码。
首先我们得知道什么时候大多数启动解决初始化的bean已经初始化完成,这就要监听org.springframework.context.ApplicationListener的ApplicationReadyEvent事件来社工弄成pid文件,不废话直接贴代码
package com.pekall.ass.common;
import lombok.extern.log4j.Log4j;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.ContextStartedEvent;
import org.springframework.context.event.ContextStoppedEvent;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Created by maxl on 17-9-13.
*/
@Log4j
public class ApplicationEventListener implements ApplicationListener {
private String appName;
private String mainPath = "";
public String getMainPath() { return mainPath;}
public ApplicationEventListener(Class mainClass,String appName)
{
this.appName = appName;
String url = mainClass.getProtectionDomain().getCodeSource().getLocation().toString();
Path dir = Paths.get(url);
Path parentDir = dir.getParent();
if(!parentDir.endsWith("target")) { //测试,生产运行环境
//url jar:file:/apps/pekall/ass/service/depmon/libs/depmon.jar!/BOOT-INF/classes!/
mainPath = parentDir.getParent().getParent().getParent().toString();
//jar包中的路径字符串包含jar:file:前缀的9个字符,所以要去掉
mainPath = mainPath.substring(9);
}
else { //本地调试开发环境
//url file:/home/maxl/pekall_work/mdm_ass/server/eureka/target/classes/
mainPath = parentDir.getParent().toString();
}
log.info("class路径:"+url);
log.info("mainPath:"+mainPath);
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
// 在这里可以监听到Spring Boot的生命周期
if (event instanceof ApplicationEnvironmentPreparedEvent) {
log.info("初始化环境变量完成");
} else if (event instanceof ApplicationPreparedEvent) {
log.info("初始化完成");
} else if (event instanceof ContextRefreshedEvent) {
log.info("应用刷新完成");
} else if (event instanceof ApplicationReadyEvent) {
//应用名从构造参数传递,不自动获取,因为spring.application.name可能与最终打包的appName.jar包名不一样
//ConfigurableApplicationContext configContext = ((ApplicationReadyEvent) event).getApplicationContext();
//ConfigurableEnvironment env =configContext.getEnvironment();
//String appName = env.getProperty("spring.application.name");
String pidFile = getPidFileFullPath();
String pid = CreatePidFile(pidFile);
if(!pidFile.equals("")) {
log.info("应用启动完成,写进程id文件:"+pidFile+" 进程id:"+pid);
}
else {
log.info("应用启动完成,写进程id文件失败");
}
} else if (event instanceof ContextStartedEvent) {
log.info("应用启动完成,需要在代码动态添加监听器才可捕获");
} else if (event instanceof ContextStoppedEvent) {
log.info("应用停止完成");
} else if (event instanceof ContextClosedEvent) {
//删除进程ID文件,由于是异步动作,外部脚本不好估算时间,
// 加上kafka-client开了另外的线程,不好估计时间,所以不删除pid文件
//deletePidFile();
//单独的关闭应用端口去掉
log.info("应用关闭完成");
}
}
private String getPidFileFullPath() {
// URL url = this.getClass().getResource("/application.properties");
// Path dir = Paths.get(url.getPath());
// Path parentDir = dir.getParent().getParent();
//
// if(!parentDir.endsWith("target")) { //测试,生产运行环境
// //url:/file:/apps/pekall/ass/service/eureka/libs/eureka.jar!/BOOT-INF/classes!/application.properties
// mainPath = parentDir.getParent().getParent().getParent().toString();
// //jar包中的路径字符串包含file:前缀的5个字符,所以要去掉
// mainPath = mainPath.substring(5);
// }
// else { //本地调试开发环境
// //url:file:/home/maxl/pekall_work/mdm_ass/server/eureka/target/classes/application.properties
// mainPath = parentDir.getParent().toString();
// }
return mainPath+File.separator+ appName+".pid";
}
private String CreatePidFile(String pidFileFullPath) {
// get name representing the running Java virtual machine.
//25107@hostname
String name = ManagementFactory.getRuntimeMXBean().getName();
// get pid
String pid = name.split("@")[0];
Path path = Paths.get(pidFileFullPath);
try {
//Files.createDirectories(path.getParent());
Files.write(path, pid.getBytes());
return pid;
}
catch(Exception e) {
log.info(e.toString());
return "";
}
}
private void deletePidFile() {
String pidFile = getPidFileFullPath();
Path filePath = Paths.get(pidFile);
try {
Files.deleteIfExists(filePath);
}
catch(IOException e) {
log.info(e.toString());
}
}
public boolean isRunning() {
String pidFile = getPidFileFullPath();
Path filePath = Paths.get(pidFile);
return Files.exists(filePath);
}
}
main入口函数进行集成
/**
* eureka server
* @author 52395090@qq.com
*
*/
package com.pekall.ass.eureka;
import com.pekall.ass.common.ApplicationEventListener;
import lombok.extern.log4j.Log4j;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
import org.springframework.boot.SpringApplication;
@Log4j
@SpringBootApplication
@EnableEurekaServer
//@EnableAutoConfiguration(
// exclude={DataSourceAutoConfiguration.class,
// HibernateJpaAutoConfiguration.class,
// SpringBootWebSecurityConfiguration.class})
public class EurekaServer {
public static void main(String[] args) {
ApplicationEventListener listener = new ApplicationEventListener(EurekaServer.class,"eureka");
if(listener.isRunning()) {
log.info("eureka 进程文件已经存在,终止运行!!!");
return;
}
SpringApplication springApplication =new SpringApplication(EurekaServer.class);
springApplication.addListeners(listener);
springApplication.run(args);
//SpringApplication.run(EurekaServer.class, args);
}
}
然后启动过程中循环定时判断pid文件是否生成用脚本来启动下一个服务
开机自动启动sampleService的内容基本内容:
#!/bin/bash
### BEGIN INIT INFO
# Provides: sampleService
# Required-Start: $remote_fs $syslog $network
# Required-Stop: $remote_fs $syslog $network
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: sampleService
# Description: sampleService
# chkconfig: 2345 99 01
### END INIT INFO
export JAVA_HOME=/usr/java/jdk
export LANG=zh_CN.UTF-8
export TZ='Asia/Shanghai'
source /etc/profile
export ASS_HOME=/apps/pekall/ass/service
#每个服务启动最多等待1200秒产生pid文件
wait_time_out=1200
eureka_app_name=eureka
eureka_home=$ASS_HOME/$eureka_app_name
eureka_pid_file=$eureka_home/${eureka_app_name}.pid
eureka_java_ops="-Xms256M -Xmx256M"
config_app_name=config
config_home=$ASS_HOME/$config_app_name
config_pid_file=$config_home/${config_app_name}.pid
config_java_ops="-Xms256M -Xmx256M"
# ANSI Colors
echoRed() { echo $'\e[0;31m'"$1"$'\e[0m'; }
echoRed2() { echo -ne $'\e[0;31m'"$1"$'\e[0m'; }
echoGreen() { echo $'\e[0;32m'"$1"$'\e[0m'; }
echoGreen2() { echo -ne $'\e[0;32m'"$1"$'\e[0m'; }
echoYellow() { echo $'\e[0;33m'"$1"$'\e[0m'; }
echoYellow2() { echo -ne $'\e[0;33m'"$1"$'\e[0m'; }
is_running() {
ps -p "$1" &> /dev/null
}
# $1:pid dile $2:tiemout time $3:app name
await_file() {
end=$(date +%s)
let "end+=$2"
while [[ ! -s "$1" ]]
do
now=$(date +%s)
remain=`expr $end - $now`
echoYellow2 "Starting [$3] ... overtime countdown $remain second \r"
if [[ $remain -le 0 ]]; then
break
fi
sleep 1
done
echo ""
}
start() {
do_start_wap $eureka_home $eureka_app_name $eureka_pid_file $wait_time_out "$eureka_java_ops"
do_start_wap $config_home $config_app_name $config_pid_file $wait_time_out "$config_java_ops"
do_start_wap $base_home $base_app_name $base_pid_file $wait_time_out "$base_java_ops"
do_start_wap $dev_home $dev_app_name $dev_pid_file $wait_time_out "$dev_java_ops"
do_start_wap $appmanage_home $appmanage_app_name $appmanage_pid_file $wait_time_out "$appmanage_java_ops"
do_start_wap $appmarket_home $appmarket_app_name $appmarket_pid_file $wait_time_out "$appmarket_java_ops"
do_start_wap $devres_home $devres_app_name $devres_pid_file $wait_time_out "$devres_java_ops"
do_start_wap $depmon_home $depmon_app_name $depmon_pid_file $wait_time_out "$depmon_java_ops"
}
stop() {
do_stop_wap $depmon_home $depmon_app_name $depmon_pid_file
do_stop_wap $devres_home $devres_app_name $devres_pid_file
do_stop_wap $appmarket_home $appmarket_app_name $appmarket_pid_file
do_stop_wap $appmanage_home $appmanage_app_name $appmanage_pid_file
do_stop_wap $dev_home $dev_app_name $dev_pid_file
do_stop_wap $base_home $base_app_name $base_pid_file
do_stop_wap $config_home $config_app_name $config_pid_file
do_stop_wap $eureka_home $eureka_app_name $eureka_pid_file
}
status() {
do_status_wap $eureka_home $eureka_app_name $eureka_pid_file
do_status_wap $config_home $config_app_name $config_pid_file
do_status_wap $base_home $base_app_name $base_pid_file
do_status_wap $dev_home $dev_app_name $dev_pid_file
do_status_wap $appmanage_home $appmanage_app_name $appmanage_pid_file
do_status_wap $appmarket_home $appmarket_app_name $appmarket_pid_file
do_status_wap $devres__home $devres__app_name $devres__pid_file
do_status_wap $depmon_home $depmon_app_name $depmon_pid_file
}
# $1:app_home $2:app_name $3:pid_file
do_status_wap() {
if [[ -d "$1" ]]; then
if [[ -f "$3" ]]; then
pid=$(cat "$3")
is_running "$pid"
if [ X"$?" == X"0" ] ; then
echoGreen "Already running [$2]";
else
echoYellow "Already stop [$2]"
fi
else
echoYellow "Already stop [$2]"
fi
fi
}
# $1:app_home $2:app_name $3:pid_file
do_stop_wap() {
if [[ -d "$1" ]]; then
if [[ -f "$3" ]]; then
pid=$(cat "$3")
is_running "$pid"
if [ X"$?" == X"0" ] ; then
do_stop $3 $2
else
echoYellow "Already stop [$2]"
fi
else
echoYellow "Already stop [$2]"
fi
fi
}
do_stop() {
echoYellow "Stopping [$2] ... "
pid=$(cat "$1")
kill "$pid" &> /dev/null || { echoRed ""; echoRed "Unable to kill [$2]"; return 1; }
count=30
for i in $(seq 1 $count); do
is_running "$pid" || { rm $1; echoGreen ""; echoGreen "Stoped [$2]"; return 0; }
([[ $i -eq 5 ]] || [[ $i -eq 10 ]] || [[ $i -eq 15 ]] || [[ $i -eq 20 ]] || [[ $i -eq 25 ]] || [[ $i -eq 30 ]]) && kill "$pid" &> /dev/null
sleep 1
remain=`expr $count - $i`
echoYellow2 "wait overtime $remain second... \r"
done
echoRed ""; "Unable to kill [$2]";
return 1;
}
restart() {
stop
start
}
# $1:app_home $2:app_name $3:pid_file $4:time_tout $5:JAVA_OPTIONS
do_start_wap() {
if [[ -d "$1" ]]; then
if [[ -f "$3" ]]; then
pid=$(cat "$3")
is_running "$pid"
if [ X"$?" == X"0" ] ; then
echoYellow "Already running [$2]"
else
do_start $2 $3 $4 "$5"
fi
else
do_start $2 $3 $4 "$5"
fi
fi
}
do_start() {
rm $2 &> /dev/null
base_dir=$ASS_HOME/$1
tmp_run=$base_dir/tmp
rm -rf $tmp_run
mkdir -p $tmp_run
chmod 777 $tmp_run
log_var=$(echo $1 | tr '[a-z]' '[A-Z]')_LOGS_DIR="$base_dir/logs"
export $log_var
nohup java $4 -Djava.io.tmpdir=$tmp_run -jar ${base_dir}/libs/$1.jar \
--config.profile=production \
--spring.profiles.active=production \
--spring.config.location=file:${base_dir}/config/ \
--logging.config=${base_dir}/config/logback.xml \ \
>/dev/null 2>&1 &
await_file "$2" $3 "$1"
pid=$(cat "$2")
[[ -z $pid ]] && { echoRed "Failed to start $1"; return 1; }
echoGreen "Started [$1]"
}
# Call the appropriate function
case "$1" in
start)
start "$@"; exit $?;;
stop)
stop "$@"; exit $?;;
restart)
restart "$@"; exit $?;;
status)
status "$@"; exit $?;;
*)
echo "Usage: $0 {start|stop|restart|status}"; exit 1;
esac
exit 0
操作系统中运行
cp ./etc/init.d/assService /etc/init.d/
chmod 700 /etc/init.d/sampleService
chkconfig --add sampleService
总结:
多服务之间的顺序启动java服务由shell脚本来串联,脚本检测到依赖前一个服务产生pid文件之后在启动后一个服务,服务停止删除pid文件,当非正常关机,pid文件还在的时候,脚本用pid进程中的进程id查询系统中是否存在这样的进程,没有则删除之,启动应用服务,有则跳过提示服务已经启动,由此服务的顺序启动依赖问题解决了。