作者:刘刚,叩丁狼高级讲师。原创文章,转载请注明出处。
Dubbo 服务部署解决方案:基于 Assembly 拆包部署
本文主要以 SpringBoot + Dubbo 为基础框架,为你提供一套比较通用的
Dubbo 服务部署解决方案。
I. 前了个言
首先,直接百度可以搜索到一大堆部署方案,包括我现在推荐的这一种,这里不去比较任何一种的好坏,我只是以我认为更好的方式去写下这篇文章
II. 简述下技术
在写具体部署步骤前,我们先简单了解下会涉及到的这几项技术:
Dubbo:这个在现在来讲应该不用多介绍了,阿里开源的 RPC 框架,目前已提交 Apache 组织,现在还处于孵化中。我们使用 Dubbo 来作为 RPC 服务的开发框架,选择的原因主要有以下几点
- 使用简单,代码侵入性低(使用 @Service 与 @Reference 注解即可完成 RPC 调用,几乎与本地开发没有啥区别)
- 支持切换 RPC 协议实现,如 Hessian、Thrift、WebService 等,可拓展性强(考虑支持其他语言,可以采用 Thrift 协议)
- 友好的服务治理功能(微服务开发,较为头疼的就是服务治理问题了,Dubbo 提供了比较全面的一套服务治理方案)
SpringBoot:SpringBoot 的火爆程度就不用多说了,它几乎帮你完成了一切事情,你几乎可以不用考虑太多各种依赖的管理,一大堆的配置文件,是的,统统没有,你只需要认真写代码就好了(如果你现在对 SpringBoot 还不是很了解,那一定要去看看这个视频了 叩丁狼:SpringBoot 高级实战 免费的)
Assembly:Maven 提供的一款插件,可以通过一个简单的配置,让你的项目打包后变成你想要的格式。微服务架构下,统一的打包方式与部署流程显得尤其重要,可以让我们较方便的实现持续集成与自动化运维
Maven-Jar-Plugin:同样是 Maven 为我们提供的一款插件,它的主要功能是让 Maven 将我们的项目打成一个可执行的 jar 包,这样我们可以直接使用命令将应用跑起来
III. 搭建项目
好了,废话不多说,以上技术先介绍到这里,接下来就开始搭建我们的项目吧,项目的创建可以直接选用 IntelJ IDEA / Eclipse / STS 等任意一种开发工具创建就行
项目结构
本文只涉及到 Dubbo 提供者的服务部署,客户端的选择方案很多就不提了,预期的打包后的结构如下图项目结构稍微再增加了一点内容,也就是 Assembly 相关的配置以及打包后的启动脚本的管理,如下图
引入相关依赖 pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.wolfcode</groupId> <artifactId>dubbo-demo-server</artifactId> <version>1.0.0</version> <!-- 继承 SpringBoot 管理通用依赖 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.RELEASE</version> </parent> <properties> <!-- 使用 jdk 1.8 --> <java.version>1.8</java.version> </properties> <dependencies> <!-- 日志依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency> <!-- dubbo2springboot 桥接包 --> <dependency> <groupId>com.gitee.reger</groupId> <artifactId>spring-boot-starter-dubbo</artifactId> <version>1.0.10</version> </dependency> </dependencies> <build> <plugins> <!-- 指定启动 jar 的 mainfest --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.3.1</version> <configuration> <archive> <manifest> <!-- Dubbo 自身的启动类 <mainClass>com.alibaba.dubbo.container.Main</mainClass> --> <!--运行jar包时运行的主类,要求类全名--> <mainClass>cn.wolfcode.dubbo.main.DubboDemoServer</mainClass> <!-- 是否指定项目classpath下的依赖 --> <addClasspath>true</addClasspath> <!-- 指定依赖的时候声明前缀 --> <classpathPrefix>./</classpathPrefix> </manifest> </archive> <!-- 排除 jar 包中的配置文件 因为要使用 Assembly 将配置文件输出到 /project/conf 目录中,则将输出的配置文件从 jar 包中剔除 --> <excludes> <!-- 此处为排除所有 properties 文件,具体可根据实际情况排除 --> <exclude>**/*.properties</exclude> <!-- 排除 xml 配置文件 此处需要注意,如果使用 MyBatis 有 Mapper.xml 文件,不能排除,可改为指定排除某些配置文件的方式,或在上级使用 includes 标签将其重新包含进来 --> <exclude>**/*.xml</exclude> </excludes> </configuration> </plugin> <!-- assembly 插件 --> <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <!-- assembly 分包配置文件 --> <descriptor>src/main/assembly/assembly.xml</descriptor> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <!-- 打包只执行一次 --> <goal>single</goal> </goals> </execution> </executions> </plugin> <plugin> <!-- 启用编译插件 --> <artifactId>maven-compiler-plugin</artifactId> </plugin> </plugins> </build> <profiles> <!-- 开发环境配置 --> <profile> <id>dev</id> <properties> <active.profile>dev</active.profile> <application.name>dubbo-demo-server-dev</application.name> <registry.address>N/A</registry.address> <protocol.name>dubbo</protocol.name> <protocol.port>20880</protocol.port> <scan.basepackage>cn.wolfcode.dubbo.service</scan.basepackage> </properties> <activation> <!-- 默认使用开发环境 --> <activeByDefault>true</activeByDefault> </activation> </profile> <!-- 测试环境配置 --> <profile> <id>test</id> <properties> <active.profile>test</active.profile> <application.name>dubbo-demo-server-test</application.name> <registry.address>N/A</registry.address> <protocol.name>dubbo</protocol.name> <protocol.port>20880</protocol.port> <scan.basepackage>cn.wolfcode.dubbo.service</scan.basepackage> </properties> </profile> <!-- 生产环境配置 --> <profile> <id>prd</id> <properties> <active.profile>prd</active.profile> <application.name>dubbo-demo-server</application.name> <registry.address>zookeeper://192.168.56.101:2181</registry.address> <protocol.name>dubbo</protocol.name> <protocol.port>20880</protocol.port> <scan.basepackage>cn.wolfcode.dubbo.service</scan.basepackage> </properties> </profile> </profiles> </project>
Dubbo 服务配置 application.properties,配置内容配合 Maven 的 Profile 实现在打包时根据不同环境的切换,具体配置内容可查看 pom.xml 中 <profiles> 中的内容,依赖于 spring-boot-parent 中的 <resources> 中的文件置换功能(默认开启)
# dubbo 服务名 spring.dubbo.application.name=@application.name@ # 注册中心地址(N/A表示为不启用) spring.dubbo.registry.address=@registry.address@ # rpc 协议实现使用 dubbo 协议 spring.dubbo.protocol.name=@protocol.name@ # 服务暴露端口 spring.dubbo.protocol.port=@protocol.port@ # 基础包扫描 spring.dubbo.base-package=@scan.basepackage@
PS:SpringBoot 中引用 profile 的值使用 @propertyName@,传统 Spring 项目使用 ${propertyName} 引用
Assembly 分包配置 assembly.xml
打包后的项目结构,主要就是依赖该配置文件来指定了,你可以修改以下配置信息,更改为你自己想要的结构<assembly> <id>dev</id> <formats> <!-- 打包支持的格式 --> <!-- zip,tar,tar.gz,tar.bz2,jar,dir,war --> <format>tar.gz</format> </formats> <!-- tar.gz 压缩包下是否生成和项目名相同的根目录 example: 1. 设置为 true 压缩包:dubbo-demo-server-1.0.0-dev.tar.gz 解压后: |-- dubbo-demo-server/ |---- bin/ |---- conf/ |---- lib/ 2. 设置为 false 压缩包:dubbo-demo-server-1.0.0-dev.tar.gz 解压后: |-- bin/ |-- conf/ |-- lib/ --> <includeBaseDirectory>true</includeBaseDirectory> <dependencySets> <!-- 依赖包的输出路径,将 jars 输出到 lib 目录中 --> <dependencySet> <useProjectArtifact>true</useProjectArtifact> <outputDirectory>/lib</outputDirectory> </dependencySet> </dependencySets> <!-- 需要打包的资源文件,将会被修改的配置文件输出到 conf 目录中 --> <fileSets> <fileSet> <directory>src/main/resources</directory> <outputDirectory>/conf</outputDirectory> <includes> <include>**/*.xml</include> <include>**/*.properties</include> </includes> <!-- 打包时是否进行文件置换(将 maven profile 中的 properties 与配置文件引用置换) --> <filtered>true</filtered> </fileSet> <fileSet> <directory>src/main/assembly/bin</directory> <outputDirectory>/bin</outputDirectory> <includes> <include>*.sh</include> </includes> <!-- 分配脚本文件可执行权限 --> <fileMode>0755</fileMode> </fileSet> </fileSets> </assembly>
对外暴露服务
此处没有另建 API 项目,仅作为部署演示项目接口文件 IUserinfoService.java
/** * @author hox */ public interface IUserinfoService { /** * 注册接口 * * @param username * @param password */ void register(String username, String password); }
服务实现 UserinfoServiceImpl.java
import cn.wolfcode.dubbo.service.IUserinfoService; import com.alibaba.dubbo.config.annotation.Service; /** * @author hox */ // 注意要使用 dubbo 的 Service 注解 @Service public class UserinfoServiceImpl implements IUserinfoService { @Override public void register(String username, String password) { System.out.println("用户注册:" + username + "\t" + password); } }
启动服务主类
我们使用 jar 包启动应用,需要指定一个程序入口,而在以 SpringBoot 作为基础框架的架构下,我们需要先将 SpringBoot 容器启动起来,不能再直接使用 Dubbo 为我们提供的启动类com.alibaba.dubbo.container.Main
,那么此时我们则需要自己新建一个服务启动类import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; /** * @author hox */ @SpringBootApplication public class DubboDemoServer { public static void main(String[] args) { // 启动容器 SpringApplication.run(DubboDemoServer.class, args); } }
但如果仅仅这样,则会有一个问题,当主线程跑完后,容器会立即关闭。为了避免这个问题,我们需要使用一种方式来阻塞主线程不退出。大概百度看了下,有些方式有点惊呆了,比如:
- 死循环(cpu 在哭泣): 确实可行,不过缺点也太明显了,即使不考虑这点性能损失但这种方案实在。。。
System.in.read()
: 这行代码的作用是读取一行控制台的输入,本身带有阻塞线程的功能,在绝大部分简单例子当中也可以使用(因为简单),但同样存在缺陷,比如一旦接受到输入后,线程会立即往下执行,同样会导致主线程执行完毕并退出以上两种方式都有着些许问题,不能用。突然想到貌似使用 Dubbo 提供的
com.alibaba.dubbo.container.Main
启动时并不会存在线程退出的问题。看源码,恍然大悟,用锁,以下为修改后的启动类```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;import java.io.IOException;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;/**
@author hox
/
@SpringBootApplication
public class DubboDemoServer {
/*控制主线程状态,应用正常启动后,使其进入等待
*/
private static final ReentrantLock LOCK = new ReentrantLock();
private static final Condition STOP = LOCK.newCondition();
private static final Logger logger = LoggerFactory.getLogger(DubboDemoServer.class);public static void main(String[] args) throws IOException, InterruptedException {
// 启动 SpringBoot 容器
ConfigurableApplicationContext ctx = SpringApplication.run(DubboDemoServer.class, args);
// 添加停止容器回调,当 JVM 退出时,会执行该线程
addJVMShutdownHook(ctx);
try {LOCK.lock(); // 修改主线程为等待状态,当 JVM 退出时唤醒主线程正常退出 STOP.await();
} catch (InterruptedException e) {
logger.warn("Dubbo service server stopped, interrupted by other thread!", e);
} finally {
LOCK.unlock();
}
}private static void addJVMShutdownHook(ConfigurableApplicationContext ctx) {
// 添加 JVM 退出时的回调线程
Runtime.getRuntime().addShutdownHook(new Thread(() -> {try { // 容器退出,可执行销毁操作 ctx.stop(); logger.info("Service " + DubboDemoServer.class.getSimpleName() + " stopped!"); } catch (Exception e) { logger.error("springboot continer shutdown exception.", e); } try { LOCK.lock(); // 唤醒主线程,使其正常退出 STOP.signal(); } finally { LOCK.unlock(); }
}, “DubboDemoServer-Thread-Shutdown-Hook”));
}
}
```
IV. 项目部署
以上,我们的服务就算构建完成了,接下来便可以进行打包并部署服务了
项目打包
这个相信大家都比较熟悉了,如果你使用的是 IDEA,你可以直接在右侧的 Maven Projects 中双击 Lifecycle 中的 package 进行打包(打包前要先选定 profiles 中对应的环境配置)
当然,你也可以直接在项目根目录输入 maven 的打包命令# -P 参数指定使用哪个环境(Maven 中的 profile) mvn package -P dev
打包完成后,在 target 目录下会出现
dubbo-demo-server-1.0.0-prd.tar.gz
文件,这便是按照指定配置后打包的文件了将项目上传至服务器
上传的方式有很多种,一般用的比较多的工具有 FileZilla、Xftp 等,这些工具的使用都比较简单,就不多介绍了,我使用一种命令的上传方式scp
,当然如果你也要使用这种方式,你的电脑上必须要安装 ssh server 才行# 第一个参数为需要上传的文件 [email protected].101:~/Downloads 为使用 root 用户连接到 `192.168.56.101` 这个机器,并将文件上传到 home 目录下的 Downloads 文件夹中 scp ./dubbo-demo-server-1.0.0-prd.tar.gz [email protected].101:~/Downloads
启动服务
# 登陆服务器,并进入到 Downloads 目录 root@wolfcode>cd ~/Downloads # 查看当前目录下是否有刚刚上传的文件 root@wolfcode>ls -lh total 12M -rw-r--r--. 1 root root 12M 5月 8 00:42 dubbo-demo-server-1.0.0-prd.tar.gz # 解压该文件 root@wolfcode>tar -zxf dubbo-demo-server-1.0.0-prd.tar.gz # 再次查看文件夹下的内容 root@wolfcode>ls -lh total 12M drwxr-xr-x. 5 root root 40 5月 8 00:44 dubbo-demo-server-1.0.0 -rw-r--r--. 1 root root 12M 5月 8 00:42 dubbo-demo-server-1.0.0-prd.tar.gz # 进入到 dubbo-demo-server-1.0.0 目录(服务器部署可以移动到指定位置后启动) root@wolfcode>cd dubbo-demo-server-1.0.0 # 查看解压后的目录结构是否符合预期 root@wolfcode>ls -lh total 4.0K drwxr-xr-x. 2 root root 52 5月 8 00:44 bin drwxr-xr-x. 2 root root 62 5月 8 00:44 conf drwxr-xr-x. 2 root root 4.0K 5月 8 00:44 lib # 此时并没有 logs 目录,因为项目还没启动,执行启动命令启动服务 root@wolfcode>bin/start.sh /root/Downloads/dubbo-demo-server-1.0.0 SERVER_NAME: dubbo-demo-server-dev SERVER_PROTOCOL_NAME: dubbo SERVER_PROTOCOL_PORT: 20880 APP_PID: LOGS_DIR :/root/Downloads/dubbo-demo-server-1.0.0/logs Starting the dubbo-demo-server-dev ... START SUCCESSED APP_PID: 1664 STDOUT: /root/Downloads/dubbo-demo-server-1.0.0/logs/stdout.log # 可以看到,日志输出到了 logs 下面的 stdout.log 文件中,我们可以查看该文件,检查服务启动过程中时候有出错 root@wolfcode>tail -n200 logs/stdout.log . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.5.6.RELEASE) 2018-05-08 00:51:31.215 [background-preinit] INFO org.hibernate.validator.internal.util.Version - HV000001: Hibernate Validator 5.3.5.Final 2018-05-08 00:51:31.276 [main] INFO cn.wolfcode.dubbo.main.DubboDemoServer - Starting DubboDemoServer v1.0.0 on local-vm with PID 1664 (/root/Downloads/dubbo-demo-server-1.0.0/lib/dubbo-demo-server-1.0.0.jar started by root in /root/Downloads/dubbo-demo-server-1.0.0) 2018-05-08 00:51:31.276 [main] DEBUG cn.wolfcode.dubbo.main.DubboDemoServer - Running with Spring Boot v1.5.6.RELEASE, Spring v4.3.10.RELEASE 2018-05-08 00:51:31.276 [main] INFO cn.wolfcode.dubbo.main.DubboDemoServer - The following profiles are active: dev 2018-05-08 00:51:31.398 [main] INFO o.s.c.a.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@5d47c63f: startup date [Tue May 08 00:51:31 CST 2018]; root of context hierarchy 2018-05-08 00:51:32.153 [main] INFO com.alibaba.dubbo.common.logger.LoggerFactory - using logger: com.alibaba.dubbo.common.logger.log4j.Log4jLoggerAdapter 2018-05-08 00:51:32.239 [main] INFO com.reger.dubbo.config.DubboAutoConfiguration - dubbo开始扫描: cn.wolfcode.dubbo.service 2018-05-08 00:51:33.226 [main] INFO o.s.jmx.export.annotation.AnnotationMBeanExporter - Registering beans for JMX exposure on startup 2018-05-08 00:51:33.254 [main] INFO com.alibaba.dubbo.config.AbstractConfig - [DUBBO] The service ready on spring started. service: cn.wolfcode.dubbo.service.IUserinfoService, dubbo version: 2.5.8, current host: 127.0.0.1 2018-05-08 00:51:33.393 [main] INFO com.alibaba.dubbo.config.AbstractConfig - [DUBBO] Export dubbo service cn.wolfcode.dubbo.service.IUserinfoService to local registry, dubbo version: 2.5.8, current host: 127.0.0.1 2018-05-08 00:51:33.393 [main] INFO com.alibaba.dubbo.config.AbstractConfig - [DUBBO] Export dubbo service cn.wolfcode.dubbo.service.IUserinfoService to url dubbo://10.0.2.15:20880/cn.wolfcode.dubbo.service.IUserinfoService?anyhost=true&application=dubbo-demo-server-dev&bind.ip=10.0.2.15&bind.port=20880&default.service.filter=regerProviderFilter&dubbo=2.5.8&generic=false&interface=cn.wolfcode.dubbo.service.IUserinfoService&methods=register&pid=1664&revision=1.0.0&side=provider×tamp=1525711893257, dubbo version: 2.5.8, current host: 127.0.0.1 2018-05-08 00:51:33.729 [main] INFO c.alibaba.dubbo.remoting.transport.AbstractServer - [DUBBO] Start NettyServer bind /0.0.0.0:20880, export /10.0.2.15:20880, dubbo version: 2.5.8, current host: 127.0.0.1 2018-05-08 00:51:33.752 [main] INFO cn.wolfcode.dubbo.main.DubboDemoServer - Started DubboDemoServer in 3.251 seconds (JVM running for 3.746) # 看到以上输出,你的服务就启动成功啦,当然还是不放心的话你还可以使用以下命令检查 # 查看进程是否存在(dubbo-demo-server 可修改为你自己的服务名) root@wolfcode>ps -ef | grep dubbo-demo-server # 使用 telnet 测试能否连接上 dubbo 服务,看到输出 connected 字样后再回车便会出现 dubbo 服务实现的命令行 root@wolfcode>telnet localhost 20880 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. # 出现以上字样,再次按回车则进入客户端 # 使用 dubbo 实现的 telnet 协议命令便可查看我们所提供的服务 dubbo>ls -l cn.wolfcode.dubbo.service.IUserinfoService -> dubbo://10.0.2.15:20880/cn.wolfcode.dubbo.service.IUserinfoService?anyhost=true&application=dubbo-demo-server-dev&bind.ip=10.0.2.15&bind.port=20880&default.service.filter=regerProviderFilter&dubbo=2.5.8&generic=false&interface=cn.wolfcode.dubbo.service.IUserinfoService&methods=register&pid=1664&revision=1.0.0&side=provider×tamp=1525711893257 # 到此便表示 dubbo 服务部署成功了,其他命令可以查看 dubbo 官方文档噢 dubbo>exit
V. 小结
到此,dubbo 服务的部署就结束了,完整的项目以及脚本文件等可以戳这里。
本文主要还是使用了 Maven 为我们提供的两个插件,可以让我们很方便的实现对应用打包结构的自定义,从而可以让我们的部署拥有更高的统一性。但当我们的服务多了之后,人为的部署难度则会逐步上升,要解决这个问题,敬请期待我的下一篇文章:Dubbo 服务部署解决方案:使用 Docker + Jenkins 实现自动化部署