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已经准备好了呢?有同仁可能会想到
通过maven生成可执行jar包,这样的jar包是linux操作系统服务命令直接在操作系统上运行,且在运行后会自动生成 pid文件,好像很方便的生成pid文件了,但它生成的pid文件的时机在jvm虚拟机启动响应业务服务开始就产生了,可能spring 还在初始化业务bean,造成另外服务启动访问依赖服务接口报错。所以这种生成pid文件的方式pass掉了,因此要自己添添加少量代码。org.springframework.boot spring-boot-maven-plugin true
首先我们得知道什么时候大多数启动解决初始化的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 [email protected] * http://git.oschina.net/zhou666/spring-cloud-7simple */ 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查询系统中是否存在这样的进程,没有则删除之,启动应用服务,有则跳过提示服务已经启动,由此服务的顺序启动依赖问题解决了。