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查询系统中是否存在这样的进程,没有则删除之,启动应用服务,有则跳过提示服务已经启动,由此服务的顺序启动依赖问题解决了。