cjuexuan / mynote

237 stars 34 forks source link

重回sbt之路 #38

Open cjuexuan opened 7 years ago

cjuexuan commented 7 years ago

重回sbt之路

背景

当年考虑团队统一,我们在我们的两个纯scala项目中选择使用了maven而不是sbt作为默认的构建工具,现在主要存在以下几个问题

  1. maven写play项目的话,涉及到view的修改都需要重新打包,这点使得开发效率非常的低
  2. 正常打包的情况下,使用maven也比用sbt慢很多,导致每天花了大量时间在构建上
  3. 最近我们测试环境docker化了,但common的解决方案只是对使用tomcat的war项目,对于我们使用play和akka http的构建用户,还是需要自力更生的

当初弃坑sbt主要还考虑到java用户通常在不同env上使用不同的profile,其他地方都是同名调用,比如可能在test这个profile下有一个url.properties,在prod上有一个同样的文件,而sbt通过Config去隔离env用起来还是很难用的,另外一个问题是当初没找到类似maven中copy dependency这样的插件,而我们spark项目中使用assembly jar的话,其实很不好排查jar的冲突问题,所以我们的做法都是平铺在一个lib目录中,如果能解决这两个问题,及格分就达到了,另外docker的支持首先想到的就是sbt调用shell,然后docker build,后来想了下,我们大sbt肯定会有类似的打包工具的插件,找了下也找到了,所以几个问题都基本解决了,下面详细说下我们对于这几个问题的解决思路

解决profile的问题

在scala用户中,可能的一个做法就是我提供dev.conf,test.conf,prod.conf,并且在打包的时候全变成application.conf,目录结构看起来像这样

.
├── README.md
├── build.sbt
├── project
│   ├── BuildEnv.scala
│   ├── build.properties
│   ├── plugins.sbt
│   └── project
│       └── target
│           └── config-classes
├── src
│   └── main
│       ├── resources
│       │   ├── dev.conf
│       │   ├── prod.conf
│       │   ├── stage.conf
│       │   └── test.conf
│       └── scala
│           └── Main.scala
└── target

project里面会放一个BuildEnv的插件

object BuildEnvPlugin extends AutoPlugin {
...
  override def projectSettings: Seq[Setting[_]] = Seq(
    buildEnv := {
      sys.props.get("env")
         .orElse(sys.env.get("BUILD_ENV"))
         .flatMap {
           case "prod" => Some(BuildEnv.Production)
           case "stage" => Some(BuildEnv.Stage)
           case "test" => Some(BuildEnv.Test)
           case "dev" => Some(BuildEnv.Developement)
           case unkown => None
         }
         .getOrElse(BuildEnv.Developement)
    }
    ...

此时的 build.sbt会写成

libraryDependencies += "com.typesafe" % "config" % "1.3.0"

enablePlugins(JavaAppPackaging)

mappings in Universal += {
  // logic like this belongs into an AutoPlugin
  val confFile = buildEnv.value match {
    case BuildEnv.Developement => "dev.conf"
    case BuildEnv.Test => "test.conf"
    case BuildEnv.Stage => "stage.conf"
    case BuildEnv.Production => "prod.conf"
  }
  ((resourceDirectory in Compile).value / confFile) -> "conf/application.conf"
}

这样来解决多环境问题,不过公司项目底层的一些库基于spring那一套弄的,所以我们只能解决历史遗留问题

我们的项目结构看起来像这样

├── main
│   ├── resources
│   │   ├── application-context.xml
│   │   └── log4j.properties
│   └── scala
│       ├── com
│       └── org
├── profile
│   ├── dev
│   │   ├── logback.xml
│   │   ├── spark.properties
│   │   ├── spoor.conf
│   │   ├── spoorStream.conf
│   │   └── url.properties
│   ├── prod
│   │   ├── logback.xml
│   │   ├── spark.properties
│   │   ├── spoor.conf
│   │   ├── spoorStream.conf
│   │   └── url.properties
│   └── test
│       ├── logback.xml
│       ├── spark.properties
│       ├── spoor.conf
│       ├── spoorStream.conf
│       └── url.properties
└── test
    ├── resources
    │   ├── collect.conf
    │   ├── log4j.properties
    │   ├── metrics.properties
    │   └── spark.properties
    └── scala
        └── com

而spring的xml长这样

    <bean id="propertyConfigurer"
          class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath:url.properties</value>
                <value>classpath:spark.properties</value>
            </list>
        </property>
    </bean>

此时就要用到

sbt native packager

由于我们内部的发布系统依赖一个run.sh的文件,所以刚好可以用来做启动脚本

所以我们的生成project的方法可以这样


  def generateProject(name: String, path: Option[String] = None): Project = {
    Project(name, file(path.getOrElse(name))).
      enablePlugins(JvmPlugin).
      enablePlugins(JavaAppPackaging).
      enablePlugins(BuildEnvPlugin).
      enablePlugins(sbtdocker.DockerPlugin).
      settings(basicSettings)
  }

  def generateProjectWithProfile(name: String, path: Option[String] = None, profilePrefix: String = "src/profile"): Project = {
    val suffix = sys.props.get("env").getOrElse("dev")
    //get path from env
    val realPath = path.getOrElse(name)
    val confFiles = IO.listFiles(file(s"$realPath/$profilePrefix/$suffix")).map { confFile ⇒
      confFile → s"conf/${confFile.getName}"
    }.toSeq
    val binFile = IO.listFiles(file(s"$realPath")).find(_.getName == "run.sh")
      .map(_ → "run.sh")
    val allFiles = binFile.fold(confFiles) { shellMapping ⇒
      confFiles :+ shellMapping
    }
    generateProject(name, path)
      .settings(mappings in Universal ++= allFiles)
  }

我们目标是将profile里面的文件同名的搬运到打包出来的conf文件夹下面去,这样就可以通过sbt的启动的环境变量去区分对应的profile文件夹

调用逻辑如下


lazy val spoorDashboard = Packaging.generateProjectWithProfile("spoor-dashboard")
  .settings(libraryDependencies ++= Seq(
    Dependencies.scalaXML, Dependencies.scalaReflect,
    Dependencies.akkaHttp, Dependencies.logback
  )
  ).dependsOn(spoorDb, spoorConfig)
  .settings(excludeDependencies += "org.slf4j" % "slf4j-log4j12")
        .settings(Packaging.dockerSettings)

run.sh如下

➜  spoor-stream git:(docker) cat run.sh
#!/bin/sh

export JAVA_HOME=/usr/local/jdk8
currentdir=`dirname $0`
EXEC_COMMAND="${JAVA_HOME}/bin/java -classpath ${currentdir}/lib/*:${currentdir}/conf com.ximalaya.spoor.stream.StreamBoot"

usage() {
   echo "$0 start|stop|restart"
   exit 1
}

[ $# -ne 1 ] && usage

start() {
    if [ -f ${currentdir}/lib/*spoor-stream*.jar ]
    then
        echo 'start spoor-stream'
        nohup $EXEC_COMMAND > $currentdir/spoor-stream-out.log 2>&1 &
        echo 'end of start spoor-stream'
    else
      echo 'spoor-stream.jar is not exists!'
    fi
    RETVAL=$?
    echo
    return $RETVAL
}

stop() {
    pid=`ps -ef | grep spoor-stream | grep -v grep | awk '{print $2}'`
    echo "stopping..."
    kill -9 $pid || failure
    echo "done"
    RETVAL=$?
    echo
    return $RETVAL
}

restart(){
    stop
    start
}

case $1 in
    start)
    start
    ;;
    stop)
    stop
    ;;
    restart)
    restart
    ;;
    *)
    usage
    ;;
esac

打完包之后就目录结构如下了

➜  stage git:(docker) tree -L 1
.
├── conf
├── lib
└── run.sh

2 directories, 1 file
➜  stage git:(docker) ls conf
logback.xml      spark.properties spoor.conf       spoorStream.conf url.properties
➜  stage git:(docker) pwd
/Users/cjuexuan/IdeaProjects/spoor/spoor-stream/target/universal/stage
➜  stage git:(docker)

把这个目录在jenkins同步出去就比较完美解决了

docker 的问题

docker的问题更简单了

sbt docker

  lazy val dockerSettings = Seq(
    docker := (docker dependsOn stage.in(Universal)).value,
    buildOptions in docker := BuildOptions(
      cache = false,
      removeIntermediateContainers = BuildOptions.Remove.Always,
      pullBaseImage = BuildOptions.Pull.Always
    ),
    dockerfile in docker := {
      val targetDir = s"/opt/${name.value}"
      val mainclass = mainClass.in(Compile, packageBin).value.getOrElse(sys.error("Expected exactly one main class"))
      new Dockerfile {
        from("harbor.test.ximalaya.com/test/jdk8").entryPoint("java", "-cp", s"$targetDir/lib/*:$targetDir/conf", mainclass)
        copy(baseDirectory(_ / "target/universal/stage").value, file(targetDir))
      }
    },
    imageNames in docker := Seq(
      sbtdocker.ImageName(
        namespace = Some(s"harbor.test.ximalaya.com/test"),
        repository = s"${name.value}",
        tag = Some("latest")
      )
    ),
    dockerPush in docker := {
      s"docker push harbor.test.ximalaya.com/test/${name.value}:latest" !
    },
    dockerPush := (dockerPush dependsOn docker).value
  )

把docker这个命令的以及dockerPush,stage挂上依赖关系,最后只需要执行dockerPush就可以了

sbt shell

最后jenkins配置下

image image

copy dependency

用了sbt native packager之后自动解决了这个问题,所以整个过程还是非常完美的

总结

这次切换首先让编译速度飞起来了,第二个顺便解决了docker的问题,总体来说还是比较完美

cjuexuan commented 7 years ago

ps:我们正常项目打包也不会将prod.conf之类的扔到resouce里面去,应该是放到了和main同级的universal中去,这里是因为直接用的sbt native demo的截图

timzaak commented 7 years ago

问下, 在使用 sbt docker 插件和使用 shell 脚本相比,为什么会使用 sbt docker 插件?

cjuexuan commented 7 years ago

@timzaak 这个主要考虑到sbt的docker 插件可以帮你生成docker file,也可以和已有的task挂上依赖关系,比如我们将stage和docker build以及docker push挂在一起,只需要执行一个命令就可以了

timzaak commented 7 years ago

嗯。 thanks.

Mvpanswer7 commented 5 years ago

请教一下如果自己写一个sbt-profile的插件应该从哪里入手

cjuexuan commented 5 years ago

@Mvpanswer7 稍微封装下sbt-native-packager就可以了