书名:微服务分布式构架开发实战
ISBN:978-7-115-47558-9
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
• 著 龚 鹏
责任编辑 赵 轩
• 人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
• 读者服务热线:(010)81055410
反盗版热线:(010)81055315
随着第三方框架的逐渐完善,实施微服务架构的开发成本越来越低,分布式架构成为主流势不可挡。一个完善的架构或系统中包含了许多的知识点,而每一个知识点则又可以引出非常多的内容,过度地专注于细节反而会拖慢达成目标的步伐。为了更快地实施微服务,本书基于开源且稳定的第三方工具,介绍如何构建一个庞大且复杂的分布式系统,用于满足项目中的实际需求。
每一个工具库为了适应更丰富的使用场景,通常都会把部分参数以配置文件的方式暴露出来,同时提供用于开发环境的默认配置。本书基于快速使用为主线,尽可能多地讲解配置参数的意义及它们之间的关系,帮助读者在掌握足够多的知识点后,建立起对微服务分布式架构的认知,以便为探求更深层次的知识点做好铺垫。
本书适合Java工程师、初级架构师、大中专院校相关专业师生、Java培训班学员及独立开发者与自学读者使用。
龚鹏同学的这本书全面覆盖了微服务的细分领域,以一线实战视角详述了如何实现微服务。一方面,读者可以通过本书拓宽技术视野,另一方面,读者也可以从其中的一章深入了解一个细分领域的微服务实践。
在阿里巴巴的生态中,微服务逐渐成为主要的服务形态,伴随着容器化的日臻成熟,大量的分布式、领域驱动设计的微服务被快速开发和部署,服务间呈现出搭积木的能力,使不同的业务通过重新组合数个微服务,就能实现新的业务场景。借助成熟的底层集团中间件,天然地支持微服务所需的动态扩缩、服务发现、全链路日志分析等能力。以阿里巴巴达摩院语音对话平台为例,对话服务、理解服务、问答服务,以及对话管理平台都是基于SpringBoot和Docker技术栈的。
希望读者能够通过本书的指引,在自己的工作和学习领域,快速并逐渐深入地建立起自己的微服务。
韩陆
《Java RESTful Web Service实战》作者
达摩院语音对话平台负责人
在当下的互联网世界里,系统开发既简单又困难。说它简单,是因为各技术社区的贡献及各框架的完善,使整个生态越来越丰富,绝大多数的需求都可以使用现有的库实现(不用重新发明“轮子”),并且现在很多的库都非常注重使用体验,以快速实现为目标,通过少量的配置及代码便可集成使用。但合理地编排这些库以构建一个高效、稳健、灵活的系统,并不是一件容易的事,人们一般将专精此道的人称为架构师。
一个完善可靠的微服务分布式架构需要解决众多的问题,我们可以用多种方法去解决这些问题,但刚开始接触时很难确定哪种方法是最优方案。在不确定并且刚接触如此庞大的架构时,选择信任、成熟且活跃的框架及第三方库提供的解决方案显然是明智之举。当建立起对微服务分布式架构的认知后,再重新回归最初的问题逐步审视并深入,最终形成属于自己的方案。
以往的图书大多只针对微服务分布式架构自身的知识点讲解,周边的相关知识点并未涉及,在进行项目实践时,最终效果则根据读者自身的知识储备而定。
本书并没有过多地探讨理论性的内容,而是基于现有成熟框架,围绕实际项目中遇见的具体需求,以微服务分布式架构的角度去逐一分解并且实现这些需求。掌握这些知识的读者,完全有能力快速搭建出可靠、高效、灵活的微服务分布式架构。
借助于现有框架进行微服务分布式架构实践的成本越来越低,并且这种形式正在逐渐成为主流架构。在进行框架及第三方库的选择时,也同样紧跟行业动向。
本书中的每一个示例都尽量用最少的代码和最快的方式解决具体的问题,为读者呈现复杂系统中简单的一面,其目的是快速接受并理解各知识点在微服务分布式架构中所处的位置及其相互关系。
Java工程师
初级架构师
独立开发者与自学读者
高校相关专业师生
Java培训班学员
技术服务于商业,尽量避免陷入技术细节的漩涡中,不要为了技术而技术。在进入学习状态之前,根据书中的章节与自己的经验明确学习目标,不断地提出问题并验证,最终找出答案。
面对一个复杂架构体系,从简单的知识点入手逐个攻破,用小成就感驱动自己最终完成设定的计划。
学习从来都不是一件容易的事,从学习ABC到写出自己的第一篇文章,从建立账号到超越第一个BOSS或者赢得第一场胜利,这之间的过程都是学习,都需要投入大量的时间和精力。如果你想要做出心目中理想的网站,或者将其作为自己赖以谋生的技能,坚持下去,你才能够做到。
互联网技术的优势是,当你遇到问题时,往往可以在互联网上寻求答案。互联网行业的大牛同样活跃在互联网上,找到他们,向他们学习。订阅公众号或者相关博客,积极了解行业发展和最新动向。这是其他学科无法比拟的优势。
在本书写作出版过程中,无论是作者还是编辑,都希望本书能够尽善尽美。如果您发现了本书的不足或错漏之处,欢迎您不吝赐教,帮助我们改进提升本书的内容,您可以通过以下方式联系我们。
我们为本书专门提供了一个QQ交流群(群号259280854),读者不仅可以在此向本书作者反馈建议,还能和其他读者共同交流,一起成长。
如果您有任何问题,都可以直接与作者联系,电子邮件地址为gongroc@outlook.com。我会尽快与您联系,解答您的疑问。
在异步社区http://www.epubit.com.cn/中搜索到本书页面,您便可以下载本书相关素材,还可以提交本书勘误。勘误被确认后,我们会向您提供积分奖励,这些积分可以在社区使用,如购书优惠、换领样书。
1.1 什么是微服务架构
1.2 垂直应用与微服务
1.3 实现一个最简单的微服务框架
1.4 主流微服务框架介绍
随着用户需求个性化、产品生命周期变短,微服务架构是未来软件架构朝着灵活性、扩展性、伸缩性以及高可用性发展的必然方向。这里主要将对比传统的垂直应用与分布式微服务应用之间的区别。
微服务是一种软件架构风格,目标是将一个复杂的应用拆分成多个服务模块,每个模块专注单一业务功能对外提供服务,并可以独立编译及部署,同时各模块间互相通信彼此协作,组合为整体对外提供完整服务。
微服务架构就像是活字印刷术,每个文字模都可以看成是一个微服务,它可以独立地提供印刷服务,又可以将模块之间组合,最终形成一篇完整文章提供更为复杂的印刷服务。
由于每个模块都独立部署,各自拥有互不干扰的内存空间,模块之间无法直接调用,所以需要借助RPC(远程过程调用协议)或HTTP协议让各个模块之间传递通信报文及交换数据,实现远程调用,整个通信管理的过程也是微服务架构重要的组成部分。
MVC模式构建的垂直应用非常适合项目初期,使用其能够方便地进行开发、部署、测试,但随着业务的发展与访问量的增加,垂直应用的问题也随之暴露出来,而微服务架构可以很好地解决这些问题。
垂直应用里,大部分逻辑都部署在一个集中化、单一的环境或服务器中运行。垂直应用程序通常很大,由一个大型团队或多个团队维护。庞大的代码库可能给希望熟悉代码的开发人员增加学习成本,还会让应用程序开发过程中使用的开发环境工具和运行容器不堪重负,最终导致开发效率降低,可能会阻止对执行更改的尝试。
微服务架构将这个庞大并且复杂的应用拆分成多个逻辑简单且独立的小应用,每个小应用交由不同的团队或开发人员维护,彼此之间互不干扰,通过标准接口互相通信。对于希望熟悉代码的开发人员来说只需掌握他所负责的应用即可,这样做的好处是简单、快速、逻辑清晰。
垂直应用需要处理一个庞大的应用程序,编译、部署需要花费很长时间,一个小的修改就可能导致重新构建整个项目。
微服务架构中对其中某一个服务进行修改,只需重新编译、部署被改动的服务模块。
垂直应用里,当请求量过大导致单台服务器无法支撑时,一般会将垂直应用部署在多台服务器形成服务集群,并通过反向代理实现负载均衡。集群中的每个服务必须部署完整的应用,但在实际业务需求中仅有部分功能使用频繁,但这种架构必须为不常用的功能分配计算资源。
微服务将提供功能的各服务拆分为多个服务模块,它具有天生的集群属性,能够轻松地根据用量部署。
例如系统中的消息功能使用频率占了整个系统的90%,而密码找回功能则只占到2%。为了分解消息功能的压力,以传统负载均衡的方式进行集群化时,每个服务必须为使用量只有2%的密码找回功能分配资源,这无疑造成了浪费。
在微服务架构中,消息功能使用率占据90%,则将消息模块多部署几个实例形成集群,而密码找回功能所在的用户模块只部署一个就可以了。
垂直应用中如果有一个小的问题,就可能使整个系统崩溃。
微服务所拆分出的各个模块中,由于模块之间的耦合度很低,当发生问题时影响范围被固定在该模块本身,整个系统依然健全。
基本工作流程如下。
① 客户端发起调用请求。
② 将调用的内容序列化后通过网络发给服务端。
③ 服务端接收到调用请求,执行具体服务并获得结果。
④ 将结果序列化后通过网络返回给客户端。
在发起远程调用时,需要基于接口(Interface)来约定客户端与服务端所调用服务的具体内容。为了方便管理依赖关系,这里使用Maven构建应用并编写一些接口,以提供给客户端与服务端使用。
当然也可以使用普通的Java应用来实现此简单微服务框架,只需将该应用编译后的jar包提供给后续的服务端与客户端即可。
groupId:org.book
artifactId:rpc-interface
version:0.0.1-SNAPSHOT
packaging:jar
编写接口。
public interface HelloService {
public String hello(String name);
}
新建用于提供服务的Maven应用,并引入刚编写的接口应用依赖。
groupId:org.book
artifactId:rpc-server
version:0.0.1-SNAPSHOT
packaging:jar
① 在pom.xml文件中引入依赖。
<dependency>
<groupId>org.book</groupId>
<artifactId>rpc-interface</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
② 实现服务接口。
public class HelloServiceImple implements HelloService {
public String hello(String name) {
System.out.println("收到消息:" + name);
return "你好:" + name;
}
}
③ 编写监听服务类。
public class Server {
private static ExecutorService executor = Executors.newFixedThreadPool(10);
private static final HashMap<String, Class> serviceRegistry = new HashMap<String, Class>();
public void register(Class serviceInterface, Class impl) {
//注册服务
serviceRegistry.put(serviceInterface.getName(), impl);
}
public void start(int port) throws IOException {
final ServerSocket server = new ServerSocket();
server.bind(new InetSocketAddress(port));
System.out.println("服务已启动");
while (true) {
executor.execute(new Runnable() {
public void run() {
Socket socket = null;
ObjectInputStream input = null;
ObjectOutputStream output = null;
try {
socket = server.accept();
// 接收到服务调用请求,将码流反序列化定位具体服务
input = new ObjectInputStream(socket.getInputStream());
String serviceName = input.readUTF();
String methodName = input.readUTF();
Class<?>[] parameterTypes = (Class<?>[]) input.readObject();
Object[] arguments = (Object[]) input.readObject();
// 在服务注册表中根据调用的服务获取到具体的实现类
Class serviceClass = serviceRegistry.get(serviceName);
if (serviceClass == null) {
throw new ClassNotFoundException(serviceName + " 未找到");
}
Method method = serviceClass.getMethod(methodName, parameterTypes);
// 调用获取结果
Object result = method.invoke(serviceClass.newInstance(), arguments);
// 将结果序列化后发送回客户端
output = new ObjectOutputStream(socket.getOutputStream());
output.writeObject(result);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
try {
if (socket != null) socket.close();
if (input == null) input.close();
if (output == null) output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
}
}
提供一个数组保存所注册的服务接口及实现类。
启动一个阻塞式的Socket服务用于等待客户端发起的调用请求,当收到请求后将码流反序列化成对象,并根据接口从注册列表中寻找具体实现类,最终通过反射的方式调用该实现类返回结果。
④ 注册服务并启动服务端。
public class App {
public static void main(String[] args) throws IOException {
Server server = new Server();
// 注册服务
server.register(HelloService.class, HelloServiceImple.class);
// 启动并绑定端口
server.start(8020);
}
}
新建用于调用服务的Maven应用,并引入刚编写的接口应用依赖。
groupId:org.book
artifactId:rpc-client
version:0.0.1-SNAPSHOT
packaging:jar
① 在pom.xml文件中引入依赖。
<dependency>
<groupId>org.book</groupId>
<artifactId>rpc-interface</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
② 编写远程调用类。
public class Client<T> {
@SuppressWarnings("unchecked")
public static <T> T get(final Class<?> serviceInterface, final InetSocketAddress addr) {
T instance = (T) Proxy.newProxyInstance(serviceInterface. getClassLoader(), new Class<?>[]{serviceInterface},
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Socket socket = null;
ObjectOutputStream output = null;
ObjectInputStream input = null;
try {
// 连接服务端
socket = new Socket();
socket.connect(addr);
// 将调用的接口类、方法名、参数列表等序列后发送给服务提供者
output = new ObjectOutputStream(socket.getOutputStream());
output.writeUTF(serviceInterface.getName());
output.writeUTF(method.getName());
output.writeObject(method.getParameterTypes());
output.writeObject(args);
// 同步阻塞等待服务器返回应答,获取应答后返回
input = new ObjectInputStream(socket.getInputStream());
return input.readObject();
} finally {
if (socket != null) socket.close();
if (output != null) output.close();
if (input != null) input.close();
}
}
});
return instance;
}
}
使用JDK动态代理方式,根据提供的服务接口类将接口序列化成码流,向目标服务端发起Socket远程调用请求,获得服务端反馈的结果并反序列化成对象后返回。
③ 调用测试。
public class App {
public static void main(String[] args) throws IOException {
HelloService service = Client.get(HelloService.class, new InetSocketAddress ("localhost", 8020));
System.out.println(service.hello("RPC"));
}
}
运行结果如下所示:
// 客户端
hello : RPC
// 服务端
服务已启动
收到消息:RPC
本章示例代码详见异步社区网站本书页面。
服务之间的调用已基本实现,但想将它投入正式开发使用还有很多细节需要完善。
当请求过大后会发现,BIO(同步阻塞式)的通信方式会消耗过多的资源导致服务器变慢甚至崩溃。
在发起网络请求前,将对象转换成二进制串便于网络传输;收到消息请求后,将二进制串反转换成对象便于后续处理。序列化及反序列化直接影响到整个RPC框架的效率及稳定性。
发起服务调用时,都需要指定服务提供方的访问地址(ip + 端口),如果当前服务提供方有多个或一个服务部署在多个机器上,调用时每次手动指定访问地址非常麻烦,这时就需要一个公共的注册中心去管理这些服务。
实施微服务的目的是为了让系统在进行横向扩展时能够拥有更多的计算资源,如果发现某一提供服务的机器负载较大,这就需要将新的需求转发到其他空闲的机器上。
服务提供方有可能崩溃无法继续提供服务,在客户端进行调用时就需要将这些无法使用的服务排除掉。
当服务端有异常发生导致无法返回正确的结果时,客户端并不知道该如何处理,只能等待并最终以超时结束此次远程调用请求。
以上所有的问题在后续将要介绍的Dubbo与Spring Cloud分布式框架中都得到了很好的解决,甚至基于Spring Boot构建的应用能让整个开发过程变得轻松愉快。
阿里巴巴在2011年开源了Dubbo框架,虽然在2013年停止更新,但在2017年9月又重启维护并发布了新版本。目前已有很多的公司将自己的业务建立在Dubbo之上,同时阿里云也推出了企业级分布式应用服务EDAS,为Dubbo提供应用托管。
Dubbo采用Zookeeper作为注册中心,RPC作为服务调用方式,致力于提供高性能和透明化的RPC远程服务调用方案。它与Spring无缝集成,基于服务提供方(服务端)与服务调用方(客户端)角色构建简单模型,其优点是使用方便、学习成本低。
① 服务提供方发布服务到服务注册中心。
② 服务消费方从服务注册中心订阅服务。
③ 注册中心通知消息调用方服务已注册。
④ 服务消费方调用已经注册的可用服务。
⑤ 监控计数。
Spring Cloud基于Spring Boot实现,使用HTTP的RESTful风格API作为调用方式。它所包含的多个子项目共同构建了微服务架构体系。
Spring Cloud 的服务注册中心提供服务注册、服务发现、负载均衡等功能。
当某个服务发生故障之后,则触发熔断机制(Hystrix)向服务调用方返回结果标识错误,而不是一直等待服务提供方返回结果,这样就不会使得线程因调用故障服务而被长时间占用不释放,避免了故障在分布式系统中的蔓延。
代理各模块提供的服务,统一暴露给第三方应用。提供动态路由、监控、弹性、全等的边缘服务。
分布式架构下多微服务会产生非常多的配置文件,分布式配置中心(Config Server)将所有配置文件交由GIT或SVN进行统一管理,避免出错。
在使用Spring开发时,通常需要完成Spring框架及其他第三方工具配置文件的编写,非常麻烦。Spring Boot通过牺牲项目的自由度来减少配置的复杂度,约定一套规则,把这些框架都自动配置集成好,从而达到“开箱即用”。
2.1 拆分逻辑
2.2 单模块
2.3 基础模块
2.4 复杂模块
将完整地使用本地调用方式的垂直应用拆分成多个微小的服务,每个服务模块负责提供各自独立的服务接口,并通过网络调用的方式将各个服务模块组织起来形成完整的微服务系统。
这里介绍微服务架构中拆分模块的基本逻辑,更为完善的模块拆分可以基于领域驱动设计(Domain-Driven Design,DDD)进行。
模块拆分是分布式微服务实施时的困难之一,它将直接影响到系统的复杂度、团队协作、代码维护难度、硬件资源分配等方面。模块拆分得越细,则能够更灵活地分配硬件资源与更方便地进行团队协作,但这样也会增加系统复杂度与代码维护难度,在团队人数较少的情况下无疑增加了负担。拆分模块时需要以具体的业务需求与系统请求压力分布为出发点进行权衡取舍。
业务的复杂性决定了被拆分的模块之间必然存在一定的依赖,模块被拆分得越细就意味着会产生更多的依赖关系,在拆分解耦的同时必然增加了整个系统的复杂度。
随着业务的丰富不可避免地使系统越来越复杂,我们没有办法拒绝复杂但应通过规范、约定、框架等手段尽量做到结构及代码的清晰与整洁。
当垂直应用变得庞大且复杂需要更多的工程师维护时,一般会使用Maven的多模块依赖特性将单一的应用拆分成多个模块,每个模块分给不同的工程师维护,最终团队成员提交模块,根据依赖关系打包成一个应用发布。
微服务天生由多模块组成,各个模块交由具体的专人负责,团队之间通过模块所暴露的服务进行协作。拆分模块的同时也确定了团队协作的方式。
微服务能够解决在垂直应用中错综复杂的业务逻辑耦合在一起的维护困难问题,但也并不是将模块拆分得越细越好,过多的模块反而会增加工作量与代码维护难度。
每个模块都处理着属于自己的业务逻辑,它们提供服务并维护着与其他模块的依赖关系,如果服务提供方的返回结果发生了变动,则各个调用方均要修改自己的代码。
在实际开发中不可避免地要跨模块调试,调试过程中所涉及的模块数量越多,则整个过程就越麻烦。
系统中存在请求压力不均衡的情况,模块拆分得越细,则能更有针对性地为高压力模块分配更多的计算资源,避免浪费。
为了设计出低耦合、高内聚的系统,需要确保每个模块都具有一定的独立性,每个模块只完成它所负责的业务功能,并且模块之间做到最少联系及接口简单。模块之间的边界则是思考的重点。
单个模块内聚了相关性较强的功能,并且拥有独立的数据库(ORM)、单元测试、运行内存、业务逻辑处理等。可以将单个模块看成是一个完整的垂直应用,只是在输入(接受请求)、输出(结果输出)时有差别。
大部分请求都是同步的,即消费方发起请求后等待提供方完成计算并返回结果,但如果等待的过程过于漫长,则需要借助消息队列与回调将请求的过程变为异步,消费方向服务提供方的消息队列中发送请求消息,服务方计算完成后调用消费方的方法通知结果。
异步请求适合处理批量处理类的功能,例如群发邮件,一次性发送1000封邮件需要一定的发送时间,采用异步处理,则消费方只需向发送邮件的服务队列中增加收件列表,任务完成后调用消费方的接口告知发送结果即可。
一个复杂的业务功能需要依赖多个模块所提供的功能实现,在进行模块拆分时需要提前对业务所涉及的模块进行梳理,最终如图所示, 各个模块由上至下依次层级依赖。
各个模块提供的服务具有一定的原子性,保持独立不重叠。将不需要依赖其他模块或依赖较少的模块抽象为基础模块,为更为复杂的业务逻辑做准备,提高复用性与扩展性。
为实现复杂业务将基础模块进行聚合重组时,每个模块对自身数据库操作事务的管理比较简单,但基于网络跨模块的二阶事务管理则会将整个过程变得无比复杂,并且耗费更多的资源,所以在进行模块拆分时且尽量规避二阶事务的产生。在无法避免分布式事务的情况下,可以采用 TCC(Trying Confirming Canceling)补偿性事务解决方案实现对二阶事务的管理。
垂直应用在处理购物车或登录状态管理等功能时一般会基于Session实现,但在分布式架构下Session的共享与传递会增加整个系统的耦合度并且提高复杂性。模块在处理自身业务逻辑及服务调用时,尽量以无状态协议的角度进行设计,请求完立刻释放资源。维护状态的工作可由服务提供方增加状态检查的服务,但面对高查询、低修改的场景时可以基于约定交由公共缓存系统(Redis)维护。
复杂的订单模块为了产生一张订单,需要用户模块提供的买卖双方信息,产品模块提供的货物信息,财务模块提供的账户余额、消息模块提供的短信通知等基础服务。
3.1 目录结构
3.2 主要文件
3.3 编辑器集成
Spring Boot是Spring官方的顶级项目之一,基于Spring Platform 对 Spring 框架和第三方库进行处理,提供默认配置以降低使用复杂度,可轻松创建单独运行的、基于生产级的Spring应用程序。
后续将要介绍的Spring Cloud 分布式微服务框架也是在Spring Boot的基础上构建的,并且Dubbo框架的社区也提供了Spring Boot的支持。为了更加方便愉快地开发,后续所有例子都将基于Spring Boot进行讲解。
Spring Boot基于Maven构建,官网提供了快速初始化服务,只需提供相关Maven信息及需要引入的第三方依赖包,便可自动生成应用并打包成zip压缩包下载。
将下载好的zip压缩包解压后得到的目录如下:
用于存放源代码文件。
用于存放配置文件,如果在使用Spring Initializr创建应用时勾选了spring-boot-starter-web
依赖,则会在此目录自动创建static
目录用于存放静态文件及templates
目录用于存放界面模板文件。
用于存放测试文件。
用于存放Maven编译后的文件。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
通过注解@SpringBootApplication
指定该类为Spring Boot启动类,Spring Boot将根据该类所在包路径自动扫描子路径下所有的Spring Beans 并且完成自动配置。
Spring Boot的默认配置文件,根据文件名application
自动加载,并且支持以.yml
为后缀的配置文件。
application.properties 示例
spring.application.name=demo
application.yml 示例
spring:
application:
name: demo
Maven用于管理依赖的重要文件,Spring Boot 基于Maven的项目集成特性在parent
节点中配置了相关信息,同时指定了spring-boot-maven-plugin
插件用于编译Spring Boot应用。
对Maven的mvn命令的封装,mvnw.cmd
用于Windows系统,而mvnw
则用于Linux系统。
目前主流的编辑器如Eclipse、IntelliJ IDEA等都集成了Spring Initializr用于创建Spring Boot应用,并且默认支持Maven,使整个开发过程变得轻松愉快。
使用插件的方式集成Spring Initializr
插件名称:Spring Tools(aka Spring IDE and Spring Tool Suite)
安装方式:菜单栏 → Helps → Eclipse Marketplace → 搜索 STS
已默认集成Spring Initializr新建应用的方式。
4.1 注册中心
4.2 接口工程
4.3 服务端
4.4 消费方
4.5 网关
4.6 监控中心
4.7 服务管理
4.8 负载均衡
4.9 服务降级
4.10 集群容错
最基本的Dubbo工程由服务提供方、消费方、服务接口组成,接口工程中编写所提供服务的接口(Interface)由服务提供方实现具体业务逻辑并注册服务,消费方则基于接口工程中所规定的服务接口进行调用,各工程之间基于Maven管理依赖。
服务方与消费方都依赖于接口服务
Dubbo支持多种注册中心,其中Zookeeper最为可靠,并且官方也推荐使用。
Zookeeper是 Apache Hadoop 的子项目,主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
Zookeeper 的安装非常简单,从官网下载后解压,进入conf
目录中把zoo_sample.cfg
重命名为zoo.cfg
便可开始配置。
tickTime=2000
initLimit=10
syncLimit=5
dataDir=E:\\apache\\zookeeper-3.5.3-beta\\data
clientPort=2181
admin.serverPort=9090
服务器之间或客户端与服务器之间维持心跳的时间间隔。
配置在集群中与其他Zookeeper的连接最大心跳时间间隔数。
标识 Leader与Follower之间发送消息时请求和应答的时间长度,规定了在此期间最长不能超过多少个心跳数。
保存数据的目录。
客户端连接服务器的端口,zookeeper 会监听这个端口,接受客户端的访问请求。
Jetty服务的监听端口,默认是8080。
server.A:B:C:D,其中 A 表示服务器编号,B表示该服务器的IP地址,C 和 D 是两个TCP 端口号,分别用于仲裁和 Leader 选举。
server.1:192.169.1.22:2222:2223
server.2:192.169.1.23:2222:2223
示例表示当前的Zookeeper与IP为22和23的Zookeeper组成集群,如果IP相同,则用于仲裁和选举的端口号需要区分。
启动脚本存放在zookeeper的bin
目录中,根据你的操作系统选择。
Windows:由CMD或PowerShell命令进入Zookeeper的bin
目录中,并执行.\zkServer.cmd。
Linux:进入Zookeeper的bin
目录中,并执行zkServer.sh start。
与之前“实现一个最简单的微服务框架”中的公共接口一样,这里需要新建一个Maven应用,并服务于服务提供方与消费方。
groupId:org.book.rpc.dubbo
artifactId:dubbo-api
version:0.0.1-SNAPSHOT
packaging:jar
编写接口
public interface IHello {
public String say(String msg);
}
① 新建Spring Boot应用。
在Eclipse中可以使用STS插件新建,地址为:菜单栏 → File → New → Spring Starter Project
groupId:org.book.rpc.dubbo
artifactId:dubbo-service
version:0.0.1-SNAPSHOT
packaging:jar
② 在pom.xml
文件中添加Dubbo与接口应用的依赖。
<dependency>
<groupId>io.dubbo.springboot</groupId>
<artifactId>spring-boot-starter-dubbo</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.book.rpc.dubbo</groupId>
<artifactId>dubbo-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
③ 编写代码实现接口。
@Service
public class HelloImple implements IHello {
@Override
public String say(String msg) {
System.out.println("你好:" + msg);
return msg;
}
}
Dubbo的@Service
注解用于暴露服务,其可配置的属性如下。
指定该服务的版本。
服务所属分组,当一个接口有多种实现时,可以用group区分。
该服务的缓存策略。
lru 基于“最近最少使用”原则删除多余缓存,保持最热的数据被缓存。
threadlocal 当前线程缓存,比如一个页面渲染,用到很多portal,每个portal都要去查用户信息,通过线程缓存,可以减少这种多余访问。
基于NIO的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务的功能,相对多线程开销较小。
延迟暴露服务,如果你的服务需要warmup时间,比如初始化缓存、等待相关资源就位等,可以使用delay进行延迟暴露。为-1时,则表示延迟到Spring初始化完成后,再暴露服务(基于Spring的ContextRefreshedEvent事件触发暴露)。
调用服务时的超时时间,单位为秒。
设为true,表示使用缺省Mock类名,即接口名 + Mock后缀,服务接口调用Mock实现类,该Mock类必须有一个无参构造函数,与Local的区别在于,Local总是被执行,而Mock只在出现非业务异常(比如超时、网络异常等)时执行;Local在远程调用之前执行,Mock在远程调用后执行。
远程服务调用重试次数,不包括第一次调用,不需要重试设为0。
令牌验证。如果为空,表示不开启;如果为true,表示随机生成动态令牌;否则使用静态令牌。令牌的作用是防止消费者绕过注册中心直接访问,保证注册中心的授权功能有效,如果使用点对点调用,需关闭令牌功能。
服务是否动态注册,如果设为false,注册后将显示disable状态,需人工启用,并且服务提供者停止时,也不会自动取消册,需人工禁用。
该协议的服务是否注册到注册中心。
服务是否过时,如果设为true,消费方引用时将打印服务过时警告error日志。
设为true,将向logger中输出访问日志,也可填写访问日志文件路径,直接把访问日志输出到指定文件。
在暴露服务中的每个方法服务器端并发执行(或占用线程池中的线程数)。
在暴露服务中的每个方法客户端并发执行(或占用连接的请求数)。
负载均衡策略,可选值为random、roundrobin、leastactive,分别表示随机、轮循、最少活跃调用。
此参数均为小写。
限制客户端服务使用连接数(如果是长连接,比如Dubbo协议,connections表示该服务对每个提供者建立的长连接数)。
该服务所使用的协议,不同服务在性能上适用不同协议进行传输,比如大数据用短连接协议,小数据大并发用长连接协议,默认为Dubbo。
Dubbo协议,采用单一长连接和NIO异步通信,适合于“小数据量大并发”的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。
Hessian协议,传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件。
RMI协议,采用JDK标准的java.rmi实现,常规远程服务方法调用,与原生RMI服务互操作。
HTTP协议,可用浏览器查看,通过表单或URL传入参数。
WebService协议,SOAP文本序列化,多用于系统集成,跨语言调用。
Thrif协议,对 thrift(Facebook的RPC框架) 的原生协议的扩展。
④ 在appaction.properties
文件中配置Dubbo。
spring.dubbo.application.name=provider
spring.dubbo.registry.address=zookeeper://127.0.0.1:2181
spring.dubbo.protocol.name=dubbo
spring.dubbo.protocol.port=-1
spring.dubbo.scan=org.book
当前应用名称,用于注册中心计算应用间依赖关系。
当前应用的版本。
日志输出方式,可选:slf4j、jcl、log4j、jdk。
服务所使用的注册中心,A://B:C,其中A为注册中心类型,B为注册中心所在ip地址,C为注册中心的端口号。配置集群时候以逗号分隔。例如A://B1:C1,A://B2:C2
Zookeeper注册中心:使用zookeeper作为注册中心。
Multicast注册中心:以广播的方式在地址段224.0.0.0 - 239.255.255.255实现服务的注册与发现,适合小规模应用或开发阶段使用。
Redis注册中心:使用redis作为注册中心,通过心跳的方式检测脏数据,服务器时间必须相同,并且对服务器有一定压力。
Simple注册中心:注册中心本身就是一个普通的Dubbo服务,可以减少第三方依赖,使整体通信方式一致。
登录注册中心用户名,如果注册中心不需要验证可不填。
登录注册中心密码,如果注册中心不需要验证可不填。
注册中心请求超时时间(毫秒)。
是否向注册中心注册服务,如果设为false,将只订阅,不注册。
是否向注册中心订阅服务,如果设为false,将只注册,不订阅。
网络传输方式,可选mina、netty。
服务所使用的协议名称。
服务提供方所暴露的端口号,多个服务提供方不可重复。
Dubbo协议缺省端口为20880,RMI协议缺省端口为1099,HTTP和Hessian协议缺省端口为80。如果配置为-1 或者没有配置,则会分配一个没有被占用的端口。
扫描服务所在包路径。
线程池类型,可选:fixed、cached
服务线程池大小(固定大小)。
IO线程池大小(固定大小)。
服务提供方最大可接受连接数。
请求及响应数据包大小限制,单位为字节,默认为8838860B(8MB)。
协议序列化方式,当协议支持多种序列化方式时使用,比如: Dubbo、Hessian2、Java原生,以及HTTP协议的JSON等。
① 新建Spring Boot应用。
groupId:org.book.rpc.dubbo
artifactId:dubbo-client
version:0.0.1-SNAPSHOT
packaging:jar
② 在pom.xml
文件中添加Dubbo与接口工程的依赖。
<dependency>
<groupId>io.dubbo.springboot</groupId>
<artifactId>spring-boot-starter-dubbo</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.book.rpc.dubbo</groupId>
<artifactId>dubbo-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
③ 编写远程调用Dubbo服务。
@Component
public class InvokeService {
@Reference
public IHello hello;
}
Dubbo的@Reference
注解可以用于生成远程服务代理,其可配置的属性如下。
服务版本,与服务提供者的版本一致。
服务分组,当一个接口有多个实现时,可以用分组区分,必须和服务提供方一致。
服务方法调用超时时间(毫秒)。
远程服务调用重试次数,不包括第一次调用,不需要重试请设为0。
设置提供者的最大连接数。
负载均衡策略,可选值为random、roundrobin、leastactive,分别表示:随机、轮循、最少活跃调用。
此参数均为小写。
是否异步执行,不可靠异步,只是忽略返回值,不阻塞执行线程。
启动时检查提供者是否存在,true报错,false忽略。
是否等到有服务注入或引用该实例时再初始化。
只调用指定协议的服务提供方,其他协议忽略。
以调用参数为key,缓存返回结果,可选:lru、threadlocal、jcache等。
④ 在appaction.properties
文件中配置Dubbo。
spring.dubbo.application.name=consumer
spring.dubbo.registry.address=zookeeper://127.0.0.1:2181
spring.dubbo.scan=org.book
与服务提供方的配置一致,指定与服务提供方一致的注册中心地址,并提供不同的application.name
便可完成调用端的配置。
⑤ 测试调用。
在Spring Boot的入口处获得刚编写的用于调用Dubbo服务的InvokeService
,并依次启动Zookeeper、服务方、消费方工程完成远程调用。
@SpringBootApplication
public class DubboClientApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(DubboClient Application.class, args);
InvokeService service = run.getBean(InvokeService.class);
System.out.println("收到返回结果:"+service.hello.say("rpc"));
}
}
模块之间互相调用时,为了降低由网络波动带来的不确定性因素并提升系统安全性,生产环境中所有模块一般都运行在内网环境中,并单独提供一个工程作为网关服务,开放固定端口代理所有模块提供的服务,并通过拦截器验证所有外部请求以达到权限管理的目的。外部应用有可能是App、网站或桌面客户端,为了达到通用性,网关服务一般为Web服务,通过HTTP协议提供RESTful风格的API接口。
① 新建 Spring Boot 应用。
groupId:org.book.rpc.dubbo
artifactId:dubbo-getway
version:0.0.1-SNAPSHOT
packaging:jar
② 在pom.xml
文件中添加Dubbo与接口工程的依赖。
<dependency>
<groupId>io.dubbo.springboot</groupId>
<artifactId>spring-boot-starter-dubbo</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.book.rpc.dubbo</groupId>
<artifactId>dubbo-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
支持 Web 应用开发,包含 Tomcat 和 Spring-MVC。
③ 在appaction.properties
文件中配置。
spring.dubbo.application.name=geteway
spring.dubbo.registry.address=zookeeper://127.0.0.1:2181
spring.dubbo.registry.register=false
spring.dubbo.scan=org.book
server.port=8081
网关服务只负责调用模块的服务,通过spring.dubbo.registry.register=false
限制只调用服务,并不注册。
网关服务所访问端口号
④ 编写用于调用Dubbo服务的编写控制器。
@RestController
public class RpcController {
@Reference
private IHello hello;
@RequestMapping(value = "/")
public String say() {
return hello.say("rpc");
}
}
使用注解@RestController
标识当前类为一个Servlet,并通过注解@RequestMapping
映射请求,相当于Servlet在web.xml的配置。
@RestController
继承自@Controller,它
将自动把response的返回结果进行json序列化,并且可以为请求链接增加.json
或.xml
后缀来指定序列化方式。如果想使用@Controller
达到返回json结构的结果,则需要为每个@RequestMapping
增加@ResponseBody
注解。
⑤ 编写限验证逻辑。
Dubbo中所暴露的接口由网关服务转为HTTP协议的API调用,这些接口通常需要安全检查,所以这里新建HandlerInterceptor
拦截器的实现类用来模拟最简单的权限验证。
public class RequestInterceptor implements HandlerInterceptor {
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object obj,
Exception exception) throws Exception {
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object obj,
ModelAndView modelAndView) throws Exception {
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object obj) throws Exception {
String token = request.getParameter("token");
if (token != null && token.equals("1"))
return true;
response.getWriter().write("token error");
return false;
}
}
实现HandlerInterceptor
接口后,Spring提供了3个方法用于在不同阶段满足过滤的需求。
当前对应的Interceptor的preHandle方法的返回值为true 时执行,主要用于资源清理工作。
当前请求进行处理之后执行,主要用于日志记录、权限检查、性能监控、通用行为等。
在请求处理之前执行,主要用于权限验证、参数过滤等。
Spring允许多个拦截器同时存在,通过拦截器链管理。
返回值为true时执行下一个拦截器,直到所有拦截器执行完,再运行被拦截的Controller。
返回值为false时不再执行后续的拦截器链及被拦截的Controller。
⑥ 配置拦截器。
@Configuration
public class WebAppConfig extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RequestInterceptor()).addPathPatterns("/**");
super.addInterceptors(registry);
}
}
WebMvcConfigurerAdapter
用于进行WebMVC环境中的相关配置,通过对addInterceptors()
方法重写可以使之前编写好的拦截器生效。@Configuration
注解则用于标识当前为一个配置类,在Spring Boot启动时会被加载。
使用addPathPatterns
方法设置拦截规则,而excludePathPatterns
方法则可以设置排除的拦截规则。
此处设置的addPathPatterns("/**")
表示拦截所有请求,如果只拦截account目录下的请求,则写为/account/**。
⑦ 测试调用。
依次启动Zookeeper、服务提供方、网关应用。
访问http://localhost:8081
:由于缺少token参数,拦截器生效,请求被拦截则直接返回token error
信息。
访问http://localhost:8081?token=1
:调用服务并返回结果。
Dubbo官方提供了dubbo-admin子项目,主要用于路由规则、动态配置、服务降级、访问控制、权重调整、负载均衡等管理。
① 从官网下载Dubbo源码。
git clone https://github.com/alibaba/dubbo.git
② 编译Dubbo。
Dubbo也是基于Maven构建,所以这里需要先在开发环境中安装Maven。
将下载好的maven包解压后获得bin目录地址,添加进Windows环境变量中
右键我的电脑 → 属性 → 高级系统设置 → 高级 → 环境变量 → 系统变量 → 双击变量名为path的变量。
在末尾增加“;maven
的bin
目录路径”,其中;
为分隔符,用于区分多个环境变量。
添加完成后在命令行工具中使用mvn -v
命令查看Maven版本。
Apache Maven 3.5.0 (ff8f5e7444045639af65f6095c62210b5713f426; 2017-04-04T03:39:06+ 08:00)
Maven home: E:\apache\apache-maven-3.5.0\bin\..
Java version: 1.8.0_101, vendor: Oracle Corporation
Java home: C:\Program Files\Java\jdk1.8.0_101\jre
Default locale: zh_CN, platform encoding: GBK
OS name: "windows 7", version: "6.1", arch: "amd64", family: "windows"
可直接使用软件管理工具yum
或apt
进行安装。
yum install maven
apt-get install maven
安装完成后通过mvn -v
验证安装结果。
当Maven安装配置完成后便可进入已下载的Dubbo根目录编译源码,通过命令DskipTests
跳过测试阶段。
mvn package -DskipTests
③ 配置及启动监控中心。
当项目编译成功后,进入dubbo-admin子项目的target目录,将编译好的dubbo-admin-2.5.5-SNAPSHOT.war
解压后移入Tomcat的webapps/ROOT
目录,并修改dubbo-admin的WEB-INF
目录下dubbo.properties
文件来进行配置。
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.admin.root.password=root
dubbo.admin.guest.password=guest
注册中心地址。
root管理员的登录密码。
游客的登录密码。
依次启动Zookeeper、Tomcat后,便可在浏览器中访问监控界面。
启动服务提供方或服务消费方项目,便可在服务治理目录下的提供者与消费者页面中查看对应的项。
服务提供方中所暴露的具体服务,与注解@Service
相关。
服务提供方或消费方的名称,由appaction.properties
配置文件中的spring.dubbo.application. name
参数设置。
服务提供方或消费方所运行的环境,由ip地址与端口号进行区分。
一个机器可以部署多个应用,而一个应用可以提供多个服务。
当并发请求与计算的需求越来越大,一台机器难以处理时,将提供该服务的应用部署在多个机器上,共同向注册中心提交服务,用户发起请求调用服务时,则从注册中心根据负载策略寻找服务提供方,最终完成此次调用。
为了适应不同的场景,Dubbo提供了4种负载均衡策略,并可以通过实现com.alibaba.dubbo.rpc.cluster.LoadBalance
接口扩展自定义策略。
配置策略时可以在服务提供方(@Service
注解)与消费方(@Reference
注解)修改LoadBalance
参数,也可以在dubbo-admin中使用图形化界面完成配置。当同时配置了不同的策略时,dubbo-admin优先级大于@Reference
,最后为@Service
。
dubbo-admin配置负载均衡位于服务治理目录下的负载均衡页面。新增配置并提供服务名称、方法名称、负载策略,保存后便可完成配置。
随机,按权重设置随机概率。默认策略。
在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
启动服务提供方项目后,在dubbo-admin的服务治理目录下的提供者页面中,使用倍权与半权功能调节权重值。
例如将同一服务部署两次,会得到两个服务提供者,分别设置权重值为100与200,假设当前有3000个并发请求,权重值为100的服务将接受1000个请求,而权重值为200的则接受2000个请求。
轮循,按公约后的权重设置轮循比率。
存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没崩溃。当请求调到第二台时就卡在那里,久而久之,所有请求都卡在第二台机器上。
最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
一致性Hash,相同参数的请求总是发给同一提供者。
当某一台提供者崩溃时,原本发往该提供者的请求,基于虚拟节点被平摊给其他提供者,不会引起剧烈变动。
分布式环境下服务之间存在一定依赖关系,当整个依赖链中的某服务崩溃或网络不通时,远程服务无法调用成功并抛出RpcException异常。为了避免由一个服务崩溃而引起的连锁反应以及保持主业务的正常运行,可以对服务进行降级处理,在调用失败后返回默认数据。
在有部分服务崩溃时访问网站,页面中由崩溃的服务提供的数据为空会比整个网站无法打开要好得多。
Dubbo通过mock实现服务降级,并提供了两种配置方式。
设置服务消费方@Reference
注解的mock参数为return null
,表示当远程调用失败时直接返回null,或返回其他默认值(return something
)。
@Reference(mock = "return null")
private IHello hello;
mock可以指定一个具体的类,当调用失败时执行,以满足复杂的业务逻辑。mock类需要实现服务接口,类名规范为:接口名+Mock。
① 在接口工程中创建mock类。
public class IHelloMock implements IHello {
public String say(String msg) {
return "降级数据";
}
}
② 在服务提供方,配置注解@Service
指定mock类所在位置。
@Service(mock = "org.book.service.IHelloMock")
public class HelloImple implements IHello {
@Override
public String say(String msg) {
try {
// 模拟请求超时
Thread.sleep(10 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return msg;
}
}
③ 在服务调用方,配置注解@Reference
设置mock为true开启降级。
@Reference(mock = "true")
private IHello hello;
为了提高系统可用性,将服务提供方部署成多个组成服务集群,在消费方调用服务失败或超时,Dubbo可以配置多种策略重新调用集群中的其他服务提供方。
在提供方的@Service
和消费方的@Reference
注解中配置cluster
参数,或者用application. properties
中的spring.dubbo.service.cluster
参数来指定容错策略。
失败自动切换,当出现失败时,重试其他服务器。(默认)
通常用于读操作,但重试会带来更长延迟。
可以通过在服务提供方的@Service
或消费方的@Reference
注解中设置retries
参数来指定重试次数(不含第一次)。
快速失败,只发起一次调用,失败立即报错。
通常用于非幂等性的写操作,比如新增记录。
失败安全,出现异常时,直接忽略。
通常用于写入审计日志等操作。
失败自动恢复,后台记录失败请求,定时重发。
通常用于消息通知操作。
并行调用多个服务器,只要一个成功即返回。
通常用于实时性要求较高的读操作,但需要浪费更多服务资源。
广播调用所有提供者,逐个调用,任意一台报错则报错。
通常用于通知所有提供者更新缓存或日志等本地资源信息。
本章示例代码详见异步社区网站本书页面。
5.1 注册中心
5.2 注册服务
5.3 调用服务
5.4 Zuul网关
5.5 Hystrix 断路器
5.6 服务监控
5.7 应用监控
5.8 熔断器监控
5.9 统一管理配置文件
Spring Cloud是基于Spring Boot的一整套实现微服务的框架。它提供了微服务开发所需的配置管理、服务发现、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等组件。
Spring Cloud有众多的子项目,各自之间都有自己的版本号。为了方便统一由spring-cloud-dependencies
进行管理,使用时只需引入具体的依赖而不需要提供版本号。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
dependencyManagement是Maven用于管理依赖的一种方式,它只申明依赖但并不直接引入。
Spring Boot与Spring Cloud版本依赖关系如下。
Angel版本对应Spring Boot 1.2.x
Brixton版本对应Spring Boot 1.3.x
Camden版本对应Spring Boot 1.4.x
Dalston版本对应Spring Boot 1.5.x
后续示例均基于Dalston.RELEASE版本进行。
与Dubbo一样,Spring Cloud支持Zookeeper作为注册中心,但官方更推荐Spring Cloud Netflix的Eureka。
Spring Cloud Netflix是Spring Cloud的子项目之一,主要内容是对Netflix公司一系列开源产品的包装,它为Spring Boot应用提供了自配置的Netflix OSS整合。通过一些简单的注解,开发者就可以快速地在应用中配置一些常用模块并构建庞大的分布式系统。它主要提供的模块包括:服务发现(Eureka)、断路器(Hystrix)、智能路由(Zuul)、客户端负载均衡(Ribbon)等。
① 新建Spring Boot工程,并在创建时勾选Eureka Server
依赖。
groupId:org.book.rpc.cloud
artifactId:cloud-eureka
version:0.0.1-SNAPSHOT
packaging:jar
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
② Spring Boot入口开启Eureka服务。
@SpringBootApplication
@EnableEurekaServer
public class CloudEurekaApplication {
public static void main(String[] args) {
SpringApplication.run(CloudEurekaApplication.class, args);
}
}
Spring Boot封装了Eureka,通过该注解将本应用变为Eureka服务器。
③ 在application.properties
配置Eureka。
server.port=8082
eureka.instance.prefer-ip-address=true
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.service-url.defaultZone=http://localhost:${server.port}/eureka/
该服务访问端口。
使用主机名广播服务。
使用ip地址广播服务。
是否注册到eureka服务器。由于应用本身就是注册中心,所以设置为false。
是否从eureka服务器检索服务。由于本应用的职责就是维护服务实例,所以设置为false。
用于服务注册和服务检索的地址。其中service-url
后面需要的是一个Map<String,String>
类型参数,所以defaultZone
可以自己定义。
${server.port}
使用Spring EL来获取上面已配置的server.port
参数值。
可以通过设置http://username:password@localhost:${server.port}/eureka/
为注册与获取服务增加HTTP基础身份验证。
是否关闭eureka自我保护模式。当网络产生波动无法请求成功但服务提供方并未崩溃时,自我保护模式并不会删除已在Eureka注册表中注册的服务。在对应用频繁的开发环境中建议关闭,生产环境中开启。
服务清理间隔(单位:毫秒,默认:60*1000)。
设置服务租约时间(单位:秒,默认:30)。开发环境中可以降低此参数以加快服务注册过程,但生产环境中建议保持默认值。
多个Eureka Server之间通过eureka.client.service-url
互相注册便可实现集群部署。
Eureka Server 1 配置
server.port=8081
eureka.instance.prefer-ip-address=true
eureka.client.service-url.defaultZone=http://localhost:8081/eureka/
Eureka Server 2 配置
server.port=8082
eureka.instance.prefer-ip-address=true
eureka.client.service-url.defaultZone=http://localhost:8082/eureka/
如果有多个Eureka Server,则在注册时使用逗号(,)分隔,将除自身外的其他所有Eureka Server地址进行配置。
eureka.client.service-url.defaultZone=http://localhost:8081/eureka/,http://localhost:8082/eureka/
Spring Cloud基于HTTP协议的RESTful风格的API进行服务之间的通信,在注册服务时会明显地发现与编写Spring Controller没有太大区别。
① 新建Spring Boot工程,并在创建时勾选Eureka Discovery
依赖。
groupId:org.book.rpc.cloud
artifactId:cloud-service
version:0.0.1-SNAPSHOT
packaging:jar
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
② 在Spring Boot
入口开启服务发现与注册支持。
@EnableDiscoveryClient
@SpringBootApplication
public class CloudServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CloudServiceApplication.class, args);
}
}
开启DiscoveryClient的实例,与Eureka Server进行交互,负责注册服务、租约续期、检索服务、取消租约等功能。
③ 在application.properties
配置Eureka。
spring.application.name=SERVER-NAME
server.port=${PORT:${SERVER_PORT:0}}
eureka.client.service-url.defaultZone=http://localhost:8082/eureka
应用名称,在服务调用时将使用此参数做依赖计算。
应用访问端口号。${PORT:${SERVER_PORT:0}}
表示自动分配一个未使用的端口,如果需要直接访问该服务则直接提供一个具体的端口号便可。
注册中心所在地址,如果eureka为集群,则通过逗号分隔配置所有的注册中心地址。
④ 编写服务。
与服务注册的代码Spring MVC的Controller并无区别,这样做极大地方便了对原先使用Spring MVC的项目进行微服务化重构的工作。
@RestController
public class ServerController {
@RequestMapping(value = "hello")
public String hello(@RequestParam("param") String param) {
return "rpc:" + param;
}
}
各个微服务模块都是以HTTP协议暴露服务的,调用服务时只需使用类似JDK的URLConnection
或Spring的RestTemplate(
HTTP客户端)
便可,而Spring Cloud Netflix则提供了Ribbon
与Feign
工具来简化调用过程,并且支持客户端负载均衡、熔断等功能。
① 新建Spring Boot工程,并在创建时勾选Eureka Discovery
依赖。
groupId:org.book.rpc.cloud
artifactId:cloud-client
version:0.0.1-SNAPSHOT
packaging:jar
每一个Spring Cloud应用都需要引入Eureka Discovery来接入到Eureka运行环境中,所以需要在Spring Boot的入口处通过@EnableDiscoveryClient
注解添加发现服务能力,并且在application.properties
中配置Eureka地址、应用名称、访问端口等基本信息。
Ribbon是一个基于HTTP和TCP客户端的负载均衡器,通过客户端中配置的ribbonServerList服务端列表去轮询访问以达到均衡负载的作用。
Ribbon核心组件
Rule - 从服务列表中如何获取一个有效服务。
Ping - 后台运行线程用来判断服务是否可用。
ServerList - 服务列表。
② 在pom.xml
文件中引入ribbon依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
③ 在Spring Boot
入口处注入RestTemplate
实例。
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
通过Spring自带的Rest客户端,可以方便地发起HTTP请求并且将结果序列化。
该注解将开启Ribbon的负载均衡模式。开启后Ribbon将拦截RestTemplate发起的请求,并实现负载均衡。
④ 调用服务。
@Autowired
private RestTemplate restTemplate;
public String say() {
return restTemplate.getForObject("http://SERVER/hello?param=cloud", String.class);
}
http://SERVER/hello?param=cloud
是服务具体所在地址及传递的参数,与URL不同的是原本主机名+端口的地址变为服务名称SERVER
,由此可以推断出Ribbon客户端根据服务名称从Eureka注册中心寻找具体服务地址。在有多个服务提供者时由Eureka注册中心的服务列表与Ribbon配合完成负载均衡。
⑤ 在application.properties
文件中配置Ribbon。
当请求失败时,如下配置可以让Ribbon重试链接及更换其他服务提供方。
spring.cloud.loadbalancer.retry.enabled=true
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000
ribbon.ConnectTimeout=250
ribbon.ReadTimeout=1000
ribbon.OkToRetryOnAllOperations=true
ribbon.MaxAutoRetriesNextServer=2
ribbon.MaxAutoRetries=1
是否开启重试机制,默认为关闭。
断路器的超时时间,需要大于ribbon的超时时间,否则不会触发重试。
请求连接的超时时间。
请求处理的超时时间。
对所有操作请求都进行重试。
重试负载均衡其他实例的最大重试次数,不包括首次调用。
同一个服务提供方最大重试次数,不包括首次调用。
以ribbon开头的配置项是针对所有服务的调用设置,如果需指定某一具体服务,只需在前面增加具体的应用名称,如:SERVER.ribbon.ConnectTimeout=250。
⑥ 配置Ribbon负载均衡策略。
@Configuration
public class RibbonConfiguration {
@Bean
public IRule ribbonRule() {
return new BestAvailableRule();
}
}
Ribbon提供多种负载策略,由IRule进行管理。通过继承ClientConfigEnabledRoundRobinRule
可自定义负载策略。
最大可用策略,即先过滤出故障服务器,然后选择一个当前并发请求数最小的。
可用过滤策略,先过滤出故障或并发请求大于阈值的一部分服务实例,然后再以线性轮询的方式从过滤后的实例清单中选出一个。
带有加权的轮询策略,对各个服务器响应时间进行加权处理,然后再采用轮询的方式来获取相应的服务器。
在选定的负载均衡策略机上重试机制。
以轮询的方式依次将请求调度不同的服务器(默认策略)。
随机选择一个服务提供方。
区域感知轮询负载均衡。
使用@RibbonClients
注解对所有服务的负载策略配置生效。
@EnableDiscoveryClient
@RibbonClients(defaultConfiguration = RibbonConfiguration.class)
@SpringBootApplication
public class CloudClientApplication {
public static void main(String[] args) {
SpringApplication.run(CloudClientApplication.class, args);
}
}
defaultConfiguration为负载策略配置文件。
如果希望针对某一具体服务配置负载策略,可以使用@RibbonClient
注解单独配置。
@Configuration
@RibbonClient(name = "SERVER", configuration = RibbonConfiguration.class)
public class TestConfiguration {
}
name为服务名称,configuration为负载策略配置文件。
Feign是一个声明式的Web服务客户端,通过简单的注解便可像调用本地方法一样调用远程服务。Spring Cloud为Feign添加了Spring MVC的注解支持,并整合了Ribbon和Eureka来为Feign提供负载均衡功能。
② 在pom.xml
文件中引入Fegin依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
③ 在Spring Boot
入口处开启Fegin支持。
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class CloudClientApplication {
public static void main(String[] args) {
SpringApplication.run(CloudClientApplication.class, args);
}
}
将自动扫描所有@FeignClient
注解。
④ 编写调用接口。
通过接口告知Fegin调用信息,定义的方法(hello()
)则是来自于服务提供方暴露的具体服务方法(RestController
)。
@FeignClient(name = "SERVER")
public interface ServerClient {
@RequestMapping(value = "hello")
public String hello(@RequestParam("param") String param);
}
服务提供方的应用名称。
手动指定调用地址。
是否开启decoder解码。为true时,当请求发生404错误会调用decoder进行解码;否则抛出FeignException。
指定配置类,用于自定义Encoder、Decoder、LogLevel、Contract。
定义当前fegin调用时的统一前缀。
指定容错处理类,当调用发生错误时会调用该指定的类。
指定容错处理的工厂类,用于减少重复的容错处理类编写。
⑤ 在application.properties
中配置Fegin。
feign.compression.request.enabled=true
feign.compression.response.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048
开启请求压缩。
开启返回值压缩。
设置压缩类型。
设置压缩最小阈值。
⑥ 调用服务。
@Autowired
private ServerClient serverClient;
public String say() {
return serverClient.hello("cloud");
}
Spring Cloud应用启动时,Feign会扫描标有@FeignClient
注解的接口生成代理,为每个接口方法创建一个RequetTemplate对象,并封装发起http请求时所需的所有信息,最终注册到Spring容器中。
Spring Cloud提供了Zuul作为服务网关,与Dubbo服务网关不同的是,Zuul并不直接调用服务,而是通过动态路由提供代理服务,在具体开发过程中也更为简单方便。
① 新建Spring Boot工程,并在创建时勾选Eureka Discovery
依赖。
groupId:org.book.rpc.cloud
artifactId:cloud-getway
version:0.0.1-SNAPSHOT
packaging:jar
② 在pom.xml
文件中引入Zuul依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
③ 在Spring Boot
入口处开启Zuul支持。
@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class CloudGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(CloudGatewayApplication.class, args);
}
}
设置一个Zuul的服务端点,并开启反向代理过滤器,以达到将请求转发到后端的服务提供方的目的。
④ 在application.properties
配置Zuul。
spring.application.name=geteway
server.port=8083
eureka.client.service-url.defaultZone=http://localhost:8082/eureka
zuul.routes.SERVER.path=/server/**
zuul.routes.SERVER.service-id=SERVER
拦截请求的路径,其中星号(SERVER)为自定义的分组标记,用于路由配置。
根据分组标记(SERVER)将拦截到的请求转发到某服务提供方的名称。
根据分组标记(SERVER)将拦截到的请求转发到一个具体的URL地址。
⑤ 过滤器权限验证。
通过继承ZuulFilter
类实现过滤功能,与Spring MVC的HandlerInterceptor
类似。
public class RequestFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = request.getParameter("token");
if (token == null || !token.equals("1")) {
ctx.setResponseBody("token error");
ctx.setSendZuulResponse(false);
}
return null;
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
}
返回一个Boolean值,标识该过滤器是否开启。
filterType()
返回一个字符串,标识该过滤器在何时调用。
pre
:在请求被路由之前调用。
routing
:在路由请求时候被调用。
error
:处理请求时发生错误时被调用。
post
:在routing和error过滤器之后被调用。
返回一个int,标识该过滤器在过滤器链中执行的顺序。
过滤器的具体逻辑。
ctx.setResponseBody("token error");
设置请求返回值。
ctx.setSendZuulResponse(false);
设置不对该请求进行路由操作。
当过滤器配置完成后,便可以在程序入口处注入该过滤器使他生效。
@Bean
public RequestFilter logFilter(){
return new RequestFilter();
}
当远程请求失败时,Dubbo通过mock实现服务降级与容错,而Spring Cloud则提供Hystrix达到同样的目的。与之不同的是Hystrix是以框架级别角度解决该问题,而mock则是以功能角度出发。
Hystrix通过线程池来隔离资源,在使用时会根据调用的远程服务划分出多个线程池。例如调用产品服务的Command放入A线程池, 调用账户服务的Command放入B线程池。当调用服务的代码存在bug或者由于其他原因导致自己所在线程池被耗尽时, 不会对系统的其他服务造成影响。
Spring Cloud提供的Fegin与Ribbon都非常好地支持了Hystrix,只需简单的配置便可完成。
① 在pom.xml
中引入hystrix依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
② 在Spring Boot
入口处开启hystrix支持。
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class CloudClientApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(CloudClientApplication.class, args);
}
}
开启熔断器。
③ 调用服务。
Ribbon调用服务发生错误时,熔断机制生效,将调用由注解@HystrixCommand
的fallbackMethod
参数指定的方法,而不抛出服务异常,使整个系统保持正常运行。
@HystrixCommand(fallbackMethod = "fallback")
public String say() {
return restTemplate.getForObject("http://SERVER/hello?param=cloud", String.class);
}
public String fallback() {
return "容错数据";
}
指定一个处理回退逻辑的方法,回退方法应与需要熔断功能的方法具有相同返回值,并且要在同一个类中。
该熔断器的名称,默认为default。
与fallbackMethod参数功能一致。
服务所属分组
调用时发生异常,则触发熔断机制,此参数可设置排除的异常。
为该熔断器分配一个自定义名称,配置资源隔离策略时使用。
配置该熔断器的线程池参数。接受多个@HystrixProperty
参数以kv形式配置。
示例:threadPoolProperties = {@HystrixProperty(name = "coreSize", value = "30") }
coreSize:核心线程池大小和线程池最大值
maxQueueSize:线程池队列最大值
keepAliveTimeMinutes:线程池中空闲线程生存时间
queueSizeRejectionThreshold:限定当前队列大小
④ 在appaction.properties
配置熔断器。
spring.application.name=CLIENT
server.port=8889
eureka.client.service-url.defaultZone=http://localhost:8082/eureka
hystrix.command.default.execution.isolation.strategy=THREAD
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=1000
hystrix.command.default.execution.timeout.enabled=true
hystrix.command.default.execution.isolation.thread.interruptOnTimeout=true
hystrix.command.default.fallback.enabled=true
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=5000
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
hystrix.command.default.circuitBreaker.forceOpen=false
hystrix.command.default.circuitBreaker.forceClosed=false
hystrix.threadpool.default.coreSize=10
hystrix.command.*.execution.isolation.strategy
设置熔断器的资源隔离策略。
THREAD:并发请求受线程池中的线程数量的限制(默认及推荐项)。
SEMAPHORE:并发请求受到信号量计数的限制。
参数中command后的default为注解中@HystrixCommand
的commandKey
参数设置的值,默认为default。
超时时间。
是否开启超时。
是否打开超时线程中断。
设置fallback是否可用。
触发熔断的时间间隔。
错误比率阀值,当错误超过或等于该值时,后续的请求将直接调用fallback。
强制打开熔断器,打开后将强制执行fallback。
强制关闭熔断器。
核心线程池大小和线程池最大值。
与@HystrixCommand
注解中的threadPoolProperties
参数一致。
Fegin中已经集成了Ribbon与Hystrix依赖,在Dalston
版本中默认并未开启,只需简单配置便可实现熔断功能。
① 编写回调类。
Fegin通过接口确定调用信息,回调类只需实现接口便可,与Dubbo的mock类似,但不限制回调类的名称。
@Component
public class ServerClientFallback implements ServerClient {
@Override
public String hello(String param) {
return "容错数据";
}
}
另外还可以通过实现FallbackFactory
接口,在create方法中返回服务接口的实例以实现回调。
@Component
public class DefaultFallback implements FallbackFactory<Object> {
@Override
public ServerClient create(Throwable cause) {
return new ServerClient() {
@Override
public String hello(String param) {
return "容错数据";
}
};
}
}
② 配置回调类。
@FeignClient(name = "SERVER", fallback = ServerClientFallback.class,fallbackFactory = DefaultFallback.class)
public interface ServerClient {
@RequestMapping(value = "hello")
public String hello(@RequestParam("param") String param);
}
指定具体的回调类,即当前接口的实现类。
指定回调工厂的实现类,即实现FallbackFactory
接口的类。
③ 在appaction.properties
中开启熔断器。
feign.hystrix.enabled=true
将一个应用部署多次,以满足服务高可用需求,而监控每个应用的运行状态则是必不可少的事。因此Spring Cloud已经对每个应用的运行状态做了统计,并提供了简单的图形化界面管理工具。
同时活跃的Spring社区为Spring Cloud提供了Spring-Cloud-Admin
集成管理工具,低侵入的方式只需简单配置便可完成对分布式环境中各应用的监控。
主要功能如下:
显示应用信息
显示在线状态
日志级别管理
JMX beans管理
会话和线程管理
应用请求跟踪
应用运行参数信息
显示熔断器信息
spring cloud admin
可以通过eureka
注册中心中的数据或专门提供的客户端获取应用信息。在Spring Cloud环境下eureka
是最优选择,而客户端方式则常用于Spring Boot
应用中。
① 新建Spring Boot工程,并在创建时勾选Eureka Discovery
依赖。
groupId:org.book.rpc.cloud
artifactId:cloud-admin
version:0.0.1-SNAPSHOT
packaging:jar
② 在pom.xml
文件中引入admin依赖。
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-server</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-server-ui</artifactId>
<version>1.5.3</version>
</dependency>
③ 在Spring Boot
入口处开启admin支持。
@SpringBootApplication
@EnableAdminServer
@EnableDiscoveryClient
public class CloudAdminApplication {
public static void main(String[] args) {
SpringApplication.run(CloudAdminApplication.class, args);
}
}
标识当前应用是Spring Cloud监控应用。
④ 在application.properties
文件中配置admin。
spring.application.name=ADMIN
server.port=8899
eureka.client.service-url.defaultZone=http://localhost:8082/eureka
⑤ 配置客户端。
监控应用中的数据来自于eureka注册中心,所以要对所有eureka应用(在Spring Boot入口处添加过@EnableDiscoveryClient
注解的应用)进行相应的配置。
在各微服务应用的pom.xml
文件中设置暴露应用基本信息。
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
⑥ 测试。
依次启动注册中心、cloud-service、cloud-client、cloud-admin后,用浏览器访问http://localhost: 8899
便可看见Spring-Cloud-Admin
的管理界面。
虽然admin从注册中心的列表中获取已注册应用,但目前仅展示了应用的基础信息,需要继续对eureka中的各个应用进行配置以获取应用的运行时及熔断器数据。
① 在各微服务模块中的pom.xml
文件中添加监控依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.jolokia</groupId>
<artifactId>jolokia-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
Actuator是Spring Boot提供的监控应用系统数据和管理生产环境的模块,可以使用HTTP、JMX、SSH、Telnet等方式管理和监控应用。
以HTTP为例,可以通过浏览器轻松访问如下信息。
/autoconfig
:查看自动配置的使用情况。
/configprops
:查看配置属性,包括默认配置。
/beans
:bean及其关系列表, 显示一个应用中所有Spring Beans的完整列表。
/dump
:打印线程栈。
/env
:查看所有环境变量。
/env/{name}
:查看具体变量值。
/health
:查看应用健康指标。
/info
:查看应用信息。
/mappings
:查看所有url映射, 即所有@RequestMapping
路径的列表。
/metrics
:查看应用基本指标。
/shutdown
:关闭应用。
/trace
:查看基本追踪信息,默认为最新的HTTP请求。
Jolokia是一个JMX-HTTP桥,它提供了一种访问JMX beans的替代方法。为Spring-Cloud-Admin
提供beans相关信息。
② 在application.properties
文件中配置权限。
management.security.enabled=false
actuator中暴露了很多应用的敏感信息,所以默认进行了权限限制,通过security.enabled
将其关闭方便配置。
Hystrix记录了触发熔断时的数据,并提供了简单的图形化统计面板,Hystrix的数据来自各个应用,只能分别查看,管理起来比较麻烦,为了简化Spring Cloud的管理,提供了Turbine来聚合所有的Hystrix,同时Spring Cloud Admin也很好地支持了Turbine,只需简单的配置便可集成。
在每一个应用中配置熔断器以获取hystrix.stream数据。
① 在pom.xml
文件中添加依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
</dependency>
Fegin
中虽然已集成了Hystrix,但为了让监控数据生效还需再引用。
spring-cloud-starter-hystrix-dashboard
熔断器监控面板依赖。
② 在Spring Boot
入口处开启Hystrix面板。
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableHystrix
@EnableHystrixDashboard
public class CloudClientApplication {
public static void main(String[] args) {
SpringApplication.run(CloudClientApplication.class, args);
}
}
通过@EnableHystrixDashboard
开启Hystrix面板的支持,如果只想使用Spring Cloud Admin的Turbine展示数据,则可以不用依赖spring-cloud-starter-hystrix-dashboard
。
③ 测试。
启动应用后便可获取熔断器的实时数据。
hystrix数据地址: http://localhost:8888/hystrix.stream
hystrix dashboard地址:http://localhost:8888/hystrix
熔断器的实时数据,ping数据为空时,访问一个使用了熔断器的服务便可。
将hystrix.stream
地址提供给它,便可解析成可视化图表。启动应用后便可获得聚合了指定应用的hystrix.stream。
Turbine负责整合所有应用的hystrix.stream数据,为了分担压力所以将Turbine独立成一个新的应用。
① 新建Spring Boot工程,并在创建时勾选Eureka Discovery
依赖。
groupId:org.book.rpc.cloud
artifactId:cloud-turbine
version:0.0.1-SNAPSHOT
packaging:jar
② 在pom.xml
添加Turbine依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-turbine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-turbine</artifactId>
</dependency>
③ 在Spring Boot
入口处开启Turbine。
@EnableTurbine
@SpringBootApplication
public class CloudTurbineApplication {
public static void main(String[] args) {
SpringApplication.run(CloudTurbineApplication.class, args);
}
}
④ 在application.properties
配置Turbine。
spring.application.name=TURBINE
server.port=8892
eureka.client.service-url.defaultZone=http://localhost:8082/eureka
management.security.enabled=false
turbine.app-config=CLIENT
turbine.aggregator.clusterConfig=default
turbine.clusterNameExpression=new String("default")
指定要聚合熔断数据的应用名称,多个应用以逗号分隔。
指定需要聚合的集群名称。
获取集群名表达式。
⑤ 测试。
启动应用后便可获得聚合了指定应用的hystrix.stream。
通过http://localhost:8892/turbine.stream
地址便可访问所有的应用熔断数据。
: ping
data: {"reportingHostsLast10Seconds":1,"name":"meta","type":"meta","timestamp":1503519020000}
将turbine.stream地址传送给hystrix dashboard
便可以解析多个熔断监控图表。
在cloud-admin
应用中继续改造以让它能够解析Turbine的图表,完成统一监控。
① 在pom.xml
中添加Turbine依赖。
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-server-ui-turbine</artifactId>
<version>1.5.3</version>
</dependency>
② 在application.properties
中配置Turbine。
spring.boot.admin.turbine.clusters=default
spring.boot.admin.turbine.location=TURBINE
turbine应用的名称或地址,它将自动寻找并解析turbine.stream。
聚合的集群名称。
③ 测试。
启动应用后便可在cloud-admin的Turbine目录看到各个应用的统计图表。
在工作中一般会进行开发环境、测试环境、生产环境的区分,不同的环境由不同的配置文件管理,当拆分的模块数量较多时,无疑众多的配置文件会显得混乱并且不便于管理。为此Spring Cloud提供了spring-cloud-config-server
结合SVN或Git对这些配置文件进行集中管理。
① 新建Spring Boot工程,并在创建时勾选Eureka Discovery
依赖。
groupId:org.book.rpc.cloud
artifactId:cloud-config
version:0.0.1-SNAPSHOT
packaging:jar
② 在pom.xml
文件中引入Config依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.tmatesoft.svnkit</groupId>
<artifactId>svnkit</artifactId>
</dependency>
SVN客户端支持,此例使用SVN作为配置文件管理工具,如果使用Git,则不必引入该依赖。
③ 在application.properties
配置Config Server。
spring.application.name=CONFIG
server.port=8894
eureka.client.service-url.defaultZone=http://localhost:8082/eureka
spring.profiles.active=subversion
spring.cloud.config.server.svn.uri=https://localhost:443/svn/config
spring.cloud.config.server.svn.username=username
spring.cloud.config.server.svn.password=password
指定当前配置文件管理工具,如果为Git可不写,则将后续配置中的server.svn
变为server.git
便可完成对Git工具的配置。
svn仓库地址。
svn访问用户名。
svn访问密码。
④ 上传配置文件到SVN。
// 文件名:'client-dev.properties'
config=hello config
⑤ 测试。
启动工程后用浏览器访问http://localhost:8894/client/dev/
便可获得client-dev.properties
的配置文件内容。
{
name: "client",
profiles: [
"dev"
],
label: null,
version: null,
state: null,
propertySources: [{
name: "https://localhost:443/svn/config/trunk/client-dev.properties",
source: {
config: "hello config"
}
}
]
}
不难发现,访问的url路径与配置文件名存在一定的映射关系。
配置文件名称格式为{application}-{profile}.properties
,对应url如下:
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
其中label
为分支名称,Git默认为master
而SVN默认为trunk
。
⑥ 应用端配置。
在各微服务模块的pom.xml
文件中引入依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
在src/main/resources
目录中新建bootstrap.properties
并配置Config Server。
spring.cloud.config.name=client
spring.cloud.config.profile=dev
spring.cloud.config.label=trunk
spring.cloud.config.uri=http://localhost:8894/
对应SVN配置文件名称中{application}
部分。
对应SVN配置文件名称中{profile}
部分。
对应的分支。
Config Server地址。
直接提供config server的访问地址意味着普通的Sring Boot应用也可以使用Config Server配置,如果基于Eureka注册中心使用的话,需要配置eureka.client.service-url.defaultZone
参数以提供注册中心的地址与spring.cloud.config.discovery.service-id
参数提供config server的应用名称。
基于Spring Boot的Dubbo应用也可以使用。
本章示例代码详见异步社区网站本书页面。
18.1 Jenkins 安装配置
18.2 构建任务
与单应用相比分布式架构会编译出多个jAR包,在快速版本迭代的开发环境中,需要频繁地将这些jar包部署到多台服务器上,这显然是一件枯燥且繁琐的事情。Jenkins 可以很好地解决这一问题,它可以自动将代码从Git或SVN仓库中检出并编译,最后将编好的jar包发送到目标服务器(Linux)中完成部署,同时还可以指定触发编译规则与记录通知结果,使整个构建过程全自动化,最终实现系统持续集成。
持续集成:Continuous Integration(CI)是一种软件开发实践,即团队开发成员经常集成他们的工作,通常每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽快地发现集成错误。许多团队发现这个过程可以大大减少集成的问题,让团队能够更快地开发内聚的软件。
根据操作系统选择Jenkins版本或者直接选择War包部署在Tomcat中。完成部署后便可通过浏览器访问 http://localhost:8080 完成安装。
第一步:提交指定路径文件中的密码完成权限验证。
第二步:安装插件。
第三步:设定管理员账户密码。
Spring Boot 基于Maven进行构建,并最终将编译好的jar包发送至Linux服务器中进行部署。
所以需要在 Jenkins
→ 系统管理
→ 插件管理
中安装以下两个插件。
Maven Integration plugin
Publish Over SSH
安装完Publish Over SSH
后便可在 Jenkins
→系统设置
→Publish over SSH
中配置用于部署应用的服务器账号密码相关信息。
Passphrase:密码
Name:名称
Hostname:访问地址
Username:用户名
Remote Directory:指定上传编号的jar包所在地址
完成配置后点击Test Configuartion
按钮测试,返回Success
时表示服务器连接成功。
编译的过程由Jenkins管理,所以需要在Jenkins
→ 系统管理
→Global Tool Configuration
中指定JDK与Maven。
取消勾选自动安装的复选框后,便可在JAVA_HOME
中指定JDK所在地址。
与配置JDK一样,在Maven选项中通过MAVEN_HOME
指定Maven所在地址。
在创建构建任务之前,需要先准备一个Spring Boot应用,并提交到SVN或Git中,当一切准备工作就绪后,便可在Jenkins中选择“构建一个maven项目”开始进行任务的配置工作。
“构建一个maven项目”的选项来自之前安装的 Maven Integration plugin 插件。
新建完构建任务后便可以开始配置,使其满足使用maven构建并部署到指定的linux服务器中。
Jenkins会自动从代码仓库中检出代码,所以这里需要指定所使用的代码仓库及地址信息。
Repository URL:仓库所在地址。
Credentials:访问SVN所使用的权限配置信息,点击add按钮添加用户名与密码,并选择刚才所配置的即可。
Local module directory:需要构建的项目名称,由于在仓库地址中直接指定到了项目名称,所以此处可不填。
Repository depth:指定从仓库检出代码时的深度策略,infinity 表示所有文件及目录。
Ignore externals:忽略外部环境。
Build whenever a SNAPSHOT dependency is built:根据pom文件确定依赖关系,如果该构建任务中所依赖的其他应用构建,则执行该构建任务。
Build after other projects are built:如果检测到其他构建任务执行,则执行该构建任务。
Build periodically:根据指定的时间执行构建任务。
Poll SCM:根据指定的时间点检查代码,如果代码被修改则执行构建任务。
Build periodically
与Poll SCM
指定时间的表达式为:* * * * *
位置 |
描述 |
取值范围 |
---|---|---|
1 |
分钟 |
0至59 |
2 |
小时 |
0至23 |
3 |
天 |
1至31 |
4 |
月 |
1至12 |
5 |
周 |
0至7,0与7都代表星期天 |
特殊格式。
*
:所有有效值。
M-N:指定值的范围。
M-N/X、*/X:以X为步长,指定的区域或整个有效范围区间的值。
,
:分隔符,用于指定多个值。
示例。
H/15 :每15分钟执行一次。
H(0-29)/10 :每小时的0-29分内每隔10分钟执行一次。
H 9-16/2 1-5:每个工作日的9-16点内每隔两小时执行一次。
H H 1,15 1-11 * :1至11月的每月1日和15日各执行一次。
Delete workspace before build starts:构建之前删除工作区。
Send files or execute commands over SSH before the build starts:在代码检出工作区,并在构建运行之前向目标服务器发送文件或命令。
Send files or execute commands over SSH after the build runs:在构建完成之后向目标服务器发送文件或命令。
Abort the build if it’s stuck:如果构建出现问题则终止。
Add timestamps to the Console Output:控制台输出增加时间戳。
Use secret text(s) or file(s):使用加密文本或文件。
Pre Setps:添加编译应用之前执行的步骤。
Build:使用Maven编译的配置。
Root POM: 指定POM文件所在位置,默认为根目录,如果在子目录则需要标明,如:config/pom.xml。
Goals and options:指定编译命令,如果不指定则使用默认命令,-Dmaven.test.skip=true 为编译时跳过测试阶段。
Post Steps:添加在编译应用之后执行的步骤。
添加构建完成后的操作,这里将构建后的jar包通过 Publish Over SSH
发送到目标服务器中,并通过shell命令关闭之前运行的应用,运行新编译后的应用以完成部署。
使用Maven编译应用后,会将编译结果存放在工程目录下的/target/
目录中,默认jar包名称格式为:artifactId-version.jar
,例如boot-ci-0.0.1-SNAPSHOT.jar
。
Source files:需要上传的文件。
Remove prefix:移除目录。
Remote directory:远程目录,在系统配置Pushish Over SSH
时配置了/opt
目录作为上传的默认目录。
Exec command:上传完成之后执行的shell命令。
命令介绍:
kill -9 `ps -ef | grep boot-ci | grep -v grep | awk '{print $2}'`
ps -ef:查找线程信息。
grep boot-ci:在查找到的线程结果中过滤包含boot-ci
的线程信息。
grep -v grep:从过滤查找到的线程结果中排除来自 grep 命令的信息。
awk 'print $2':使用awk工具提取ps -ef查找到的线程信息中的第二个参数,即线程ID。
kill -9:关闭线程ID。
java -jar /opt/boot-ci*.jar&
使用java命令运行jar包,默认上传目录为/opt
,其中&
表示在后台运行。
至此构建任务已全部配置完成,保存后点击立即构建便可执行该构建任务,并且可以在构建历史列表中点击构建编号查看构建的详细信息。当构建任务完成后会发现,编译好的jar包已上传到目标服务器的/opt
目录中,并且已经启动了应用。
目前已经介绍了基于Dubbo与Spring Cloud框架构建的分布式微服务应用,并且基于Spring Boot Starter集成的第三方工具解决了项目中遇见的绝大部分需求,现在将这些全部整合起来,绘制出完整的架构图谱以便于更加清晰地理解各工具之间的关系。
无论是Dubbo的Zookeeper还是Spring Cloud的Eureka都负责所有服务模块的自动注册与发现,是整个分布式架构最为核心的部分,所以在生产环境中通常部署多个注册中心实现集群化以提高可用性。
大部分的企业级应用都是围绕着数据库(MySQL)的CURD操作编写,当数据库达到I/O瓶颈后,一般会对其进行分库分表,使用主从库的方式部署在多台机器上以获得更大的计算量。分布式架构中的各业务模块天生就支持这一特性,各业务模块部署在不同的计算机上,只需让每个模块拥有各自独立的数据源便可。
分布式架构师基于网络进行远程调用,为了更高的可用性甚至可以将服务模块部署在异地的多个机房,当其中一个机器发生故障后依然可以由远程的机房中的应用实例提供服务,并且可以与CDN配合起来组建网络。
消息队列(RabbitMQ)、缓存服务(Redis)、索引服务(ElasticSearch)并不直接参与到业务逻辑中,而是为各个独立业务模块提供公共服务,为了更好的稳定性也可以将各独立服务进行集群化部署。
Dubbo的Mock与Spring Cloud的熔断器都可以反映出服务之间的调用情况,并且各自的监控平台都能够反映出各业务模块的健康状况。它极大地减轻了运维的工作,在整个架构中属于必不可少的一块。
所有业务模块产出的服务接口由网关模块代理并向外提供服务,每次接口的请求都要经过网关模块,为了避免因请求量过大而造成网关模块崩溃,可以在Dubbo架构中为网关模块配置Nginx负载均衡,而Spring Cloud则可以直接在eureka中注册多个zuul网关以达到负载均衡的目的。
为了确保每个业务模块的独立性,开发人员更倾向于产出无状态的接口,而不是基于Session实现权限验证。所有的权限验证应交由基于OAuth2.0协议实现的授权模块,并配合网关模块统一对所有接口进行权限管理。
客户端是最终面向业务逻辑的应用,通过调研多个接口将其组装,最终实现业务逻辑。
分布式架构总结
异步社区(www.epubit.com.cn)是人民邮电出版社旗下IT专业图书旗舰社区,于2015年8月上线运营。
异步社区依托于人民邮电出版社20余年的IT专业优质出版资源和编辑策划团队,打造传统出版与电子出版和自出版结合、纸质书与电子书结合、传统印刷与POD按需印刷结合的出版平台,提供最新技术资讯,为作者和读者打造交流互动的平台。
我们出版的图书涵盖主流IT技术,在编程语言、Web技术、数据科学等领域有众多经典畅销图书。社区现已上线图书1000余种,电子书400多种,部分新书实现纸书、电子书同步出版。我们还会定期发布新书书讯。
社区内提供随书附赠的资源,如书中的案例或程序源代码。
另外,社区还提供了大量的免费电子书,只要注册成为社区用户就可以免费下载。
很多图书的作译者已经入驻社区,您可以关注他们,咨询技术问题;可以阅读不断更新的技术文章,听作译者和编辑畅聊好书背后有趣的故事;还可以参与社区的作者访谈栏目,向您关注的作者提出采访题目。
您可以方便地下单购买纸质图书或电子图书,纸质图书直接从人民邮电出版社书库发货,电子书提供多种阅读格式。
对于重磅新书,社区提供预售和新书首发服务,用户可以第一时间买到心仪的新书。
用户帐户中的积分可以用于购书优惠。100积分=1元,购买图书时,在里填入可使用的积分数值,即可扣减相应金额。
特别优惠
购买本电子书的读者专享异步社区优惠券。 使用方法:注册成为社区用户,在下单购书时输入“57AWG”,然后点击“使用优惠码”,即可享受电子书8折优惠(本优惠券只可使用一次)。
社区独家提供纸质图书和电子书组合购买方式,价格优惠,一次购买,多种阅读选择。
您可以在图书页面下方提交勘误,每条勘误被确认后可以获得100积分。热心勘误的读者还有机会参与书稿的审校和翻译工作。
社区提供基于Markdown的写作环境,喜欢写作的您可以在此一试身手,在社区里分享您的技术心得和读书体会,更可以体验自出版的乐趣,轻松实现出版的梦想。
如果成为社区认证作译者,还可以享受异步社区提供的作者专享特色服务。
您可以掌握IT圈的技术会议资讯,更有机会免费获赠大会门票。
扫描任意二维码都能找到我们:
异步社区
微信订阅号
微信服务号
官方微博
QQ群:436746675
社区网址:www.epubit.com.cn
官方微信:异步社区
官方微博:@人邮异步社区,@人民邮电出版社-信息技术分社
投稿&咨询:contact@epubit.com.cn