spring boot/cloud 多服务部署单机启动顺序有依赖的解决办法

来源:互联网 发布:中级软件设计师 编辑:程序博客网 时间:2024/06/05 07:26

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/../tmprm -rf   $tmprunmkdir -p $tmprunchmod 777 $tmprunexport 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. */@Log4jpublic 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 * 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 INFOexport JAVA_HOME=/usr/java/jdkexport LANG=zh_CN.UTF-8export TZ='Asia/Shanghai'source /etc/profileexport ASS_HOME=/apps/pekall/ass/service#每个服务启动最多等待1200秒产生pid文件wait_time_out=1200eureka_app_name=eurekaeureka_home=$ASS_HOME/$eureka_app_nameeureka_pid_file=$eureka_home/${eureka_app_name}.pideureka_java_ops="-Xms256M -Xmx256M"config_app_name=configconfig_home=$ASS_HOME/$config_app_nameconfig_pid_file=$config_home/${config_app_name}.pidconfig_java_ops="-Xms256M -Xmx256M"# ANSI ColorsechoRed() { 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 nameawait_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_filedo_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_filedo_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_OPTIONSdo_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 functioncase "$1" in  start)    start "$@"; exit $?;;  stop)    stop "$@"; exit $?;;  restart)    restart "$@"; exit $?;;  status)    status "$@"; exit $?;;  *)    echo "Usage: $0 {start|stop|restart|status}"; exit 1;esacexit 0

操作系统中运行

cp  ./etc/init.d/assService /etc/init.d/chmod 700 /etc/init.d/sampleServicechkconfig --add sampleService

总结:

   多服务之间的顺序启动java服务由shell脚本来串联,脚本检测到依赖前一个服务产生pid文件之后在启动后一个服务,服务停止删除pid文件,当非正常关机,pid文件还在的时候,脚本用pid进程中的进程id查询系统中是否存在这样的进程,没有则删除之,启动应用服务,有则跳过提示服务已经启动,由此服务的顺序启动依赖问题解决了。


阅读全文
0 0