第1章 微服务介绍
随着用户需求个性化、产品生命周期变短,微服务架构是未来软件架构朝着灵活性、扩展性、伸缩性以及高可用性发展的必然方向。这里主要将对比传统的垂直应用与分布式微服务应用之间的区别。
1.1 什么是微服务架构
微服务是一种软件架构风格,目标是将一个复杂的应用拆分成多个服务模块,每个模块专注单一业务功能对外提供服务,并可以独立编译及部署,同时各模块间互相通信彼此协作,组合为整体对外提供完整服务。
微服务架构就像是活字印刷术,每个文字模都可以看成是一个微服务,它可以独立地提供印刷服务,又可以将模块之间组合,最终形成一篇完整文章提供更为复杂的印刷服务。
由于每个模块都独立部署,各自拥有互不干扰的内存空间,模块之间无法直接调用,所以需要借助RPC(远程过程调用协议)或HTTP协议让各个模块之间传递通信报文及交换数据,实现远程调用,整个通信管理的过程也是微服务架构重要的组成部分。
1.2 垂直应用与微服务
MVC模式构建的垂直应用非常适合项目初期,使用其能够方便地进行开发、部署、测试,但随着业务的发展与访问量的增加,垂直应用的问题也随之暴露出来,而微服务架构可以很好地解决这些问题。
代码维护
垂直应用里,大部分逻辑都部署在一个集中化、单一的环境或服务器中运行。垂直应用程序通常很大,由一个大型团队或多个团队维护。庞大的代码库可能给希望熟悉代码的开发人员增加学习成本,还会让应用程序开发过程中使用的开发环境工具和运行容器不堪重负,最终导致开发效率降低,可能会阻止对执行更改的尝试。
微服务架构将这个庞大并且复杂的应用拆分成多个逻辑简单且独立的小应用,每个小应用交由不同的团队或开发人员维护,彼此之间互不干扰,通过标准接口互相通信。对于希望熟悉代码的开发人员来说只需掌握他所负责的应用即可,这样做的好处是简单、快速、逻辑清晰。
部署
垂直应用需要处理一个庞大的应用程序,编译、部署需要花费很长时间,一个小的修改就可能导致重新构建整个项目。
微服务架构中对其中某一个服务进行修改,只需重新编译、部署被改动的服务模块。
资源控制
垂直应用里,当请求量过大导致单台服务器无法支撑时,一般会将垂直应用部署在多台服务器形成服务集群,并通过反向代理实现负载均衡。集群中的每个服务必须部署完整的应用,但在实际业务需求中仅有部分功能使用频繁,但这种架构必须为不常用的功能分配计算资源。
微服务将提供功能的各服务拆分为多个服务模块,它具有天生的集群属性,能够轻松地根据用量部署。
例如系统中的消息功能使用频率占了整个系统的90%,而密码找回功能则只占到2%。为了分解消息功能的压力,以传统负载均衡的方式进行集群化时,每个服务必须为使用量只有2%的密码找回功能分配资源,这无疑造成了浪费。
在微服务架构中,消息功能使用率占据90%,则将消息模块多部署几个实例形成集群,而密码找回功能所在的用户模块只部署一个就可以了。
稳定
垂直应用中如果有一个小的问题,就可能使整个系统崩溃。
微服务所拆分出的各个模块中,由于模块之间的耦合度很低,当发生问题时影响范围被固定在该模块本身,整个系统依然健全。
1.3 实现一个最简单的微服务框架
基本工作流程如下。
① 客户端发起调用请求。
② 将调用的内容序列化后通过网络发给服务端。
③ 服务端接收到调用请求,执行具体服务并获得结果。
④ 将结果序列化后通过网络返回给客户端。
1.3.1 公共接口
在发起远程调用时,需要基于接口(Interface)来约定客户端与服务端所调用服务的具体内容。为了方便管理依赖关系,这里使用Maven构建应用并编写一些接口,以提供给客户端与服务端使用。
当然也可以使用普通的Java应用来实现此简单微服务框架,只需将该应用编译后的jar包提供给后续的服务端与客户端即可。
Maven参数
groupId:org.book artifactId:rpc-interface version:0.0.1-SNAPSHOT packaging:jar
编写接口。
public interface HelloService { public String hello(String name); }
1.3.2 服务端
新建用于提供服务的Maven应用,并引入刚编写的接口应用依赖。
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(); } } } }); } } }
register()
提供一个数组保存所注册的服务接口及实现类。
start()
启动一个阻塞式的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); } }
1.3.3 客户端
新建用于调用服务的Maven应用,并引入刚编写的接口应用依赖。
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
本章示例代码详见异步社区网站本书页面。
1.3.4 完善框架
服务之间的调用已基本实现,但想将它投入正式开发使用还有很多细节需要完善。
通信
当请求过大后会发现,BIO(同步阻塞式)的通信方式会消耗过多的资源导致服务器变慢甚至崩溃。
序列化与反序列化
在发起网络请求前,将对象转换成二进制串便于网络传输;收到消息请求后,将二进制串反转换成对象便于后续处理。序列化及反序列化直接影响到整个RPC框架的效率及稳定性。
服务注册中心
发起服务调用时,都需要指定服务提供方的访问地址(ip+端口),如果当前服务提供方有多个或一个服务部署在多个机器上,调用时每次手动指定访问地址非常麻烦,这时就需要一个公共的注册中心去管理这些服务。
负载均衡
实施微服务的目的是为了让系统在进行横向扩展时能够拥有更多的计算资源,如果发现某一提供服务的机器负载较大,这就需要将新的需求转发到其他空闲的机器上。
服务监控
服务提供方有可能崩溃无法继续提供服务,在客户端进行调用时就需要将这些无法使用的服务排除掉。
异常处理
当服务端有异常发生导致无法返回正确的结果时,客户端并不知道该如何处理,只能等待并最终以超时结束此次远程调用请求。
以上所有的问题在后续将要介绍的Dubbo与Spring Cloud分布式框架中都得到了很好的解决,甚至基于Spring Boot构建的应用能让整个开发过程变得轻松愉快。
1.4 主流微服务框架介绍
1.4.1 Dubbo
阿里巴巴在2011年开源了Dubbo框架,虽然在2013年停止更新,但在2017年9月又重启维护并发布了新版本。目前已有很多的公司将自己的业务建立在Dubbo之上,同时阿里云也推出了企业级分布式应用服务EDAS,为Dubbo提供应用托管。
Dubbo采用Zookeeper作为注册中心,RPC作为服务调用方式,致力于提供高性能和透明化的RPC远程服务调用方案。它与Spring无缝集成,基于服务提供方(服务端)与服务调用方(客户端)角色构建简单模型,其优点是使用方便、学习成本低。
① 服务提供方发布服务到服务注册中心。
② 服务消费方从服务注册中心订阅服务。
③ 注册中心通知消息调用方服务已注册。
④ 服务消费方调用已经注册的可用服务。
⑤ 监控计数。
1.4.2 Spring Cloud
Spring Cloud基于Spring Boot实现,使用HTTP的RESTful风格API作为调用方式。它所包含的多个子项目共同构建了微服务架构体系。
Netflix Eureka
Spring Cloud的服务注册中心提供服务注册、服务发现、负载均衡等功能。
Netflix Hystrix
当某个服务发生故障之后,则触发熔断机制(Hystrix)向服务调用方返回结果标识错误,而不是一直等待服务提供方返回结果,这样就不会使得线程因调用故障服务而被长时间占用不释放,避免了故障在分布式系统中的蔓延。
Netflix Zuul
代理各模块提供的服务,统一暴露给第三方应用。提供动态路由、监控、弹性、全等的边缘服务。
Config Server
分布式架构下多微服务会产生非常多的配置文件,分布式配置中心(Config Server)将所有配置文件交由GIT或SVN进行统一管理,避免出错。
Spring Boot
在使用Spring开发时,通常需要完成Spring框架及其他第三方工具配置文件的编写,非常麻烦。Spring Boot通过牺牲项目的自由度来减少配置的复杂度,约定一套规则,把这些框架都自动配置集成好,从而达到“开箱即用”。