一,为什么写JMicro
印象中初次接触微服务大概是2011年,那会做Eclpise插件开发,网上查看好多关于OSGI的技术文章,发现Spring新出了一个叫Spring-boot的框架,那会没太上心,只是了解了点皮毛,工作又太忙,之后就没下文了。
直到大概2015年的某天,碰到一个小项目,没什么难度,都用老套路去玩,没什么意思,得玩点新东西才行,也不枉一翻付出,于是选择用GO语言实现,选择GO主要是想体验一下GO,看是不是真如传说中的那样无敌。经过一翻折腾,最终确定GOGIN+GOMICRO实现。是的,从那会开始,通过学心和使用GOMICRO,从此迷上微服务。后来因为工作需要,再没什么机会在项目中接触GO。
后面也曾试图去用Dubbo和Spring-Cloud做项目,但也止于浅尝则止,没能深入。一方面项目时间太紧,折腾不起。另一方面,也是最重要的,项目组成员根本不愿意去学新东西,在很多成员心中,微服务,Spring-Cloud太深奥,玩不起,没时间,至于Dubbo,写个服务,做个RPC也是从百度复制下来的,跑起来就完事了,没人关心个中的原理,经常碰到问题就抓狂!
从那时候起,就一直琢磨着能不能用Java写一个像GoMicro一样简单的微服务框架,这个框架要确保足够简单,入门和使用成本要底,就像写HelloWord一样,但微服务的基本功能要全。因此项目依赖不能多,打出来的运行包也要小,能不用的第三方包坚决不用,最好使用JDK库就行!
二.从RPC开始
微服务的基础是RPC,而反过来单单有RPC却不能叫微服务,微服务是在RPC基础上,实现一系列的基础性功能支持后才能叫微服务。没有超时,限流,熔断这些基础支持,服务“微”了之后,随时都可能挂机,这就是为什么微服务之前存在很多RPC框架,但却没人说那是微服务。
坚持从JDK库入手,使用原生NIO SOCKET做了个简单的RPC,并且兼容了HTTP方式,完后做压力测试发现性能太差,然后退一步吧,选择Mina做底层通信重新再折腾一遍,结果还是不尽如人间,性能还可以,就是自觉得太繁琐臃肿,还是不满意,最终又改为Netty才完成RPC基本功能开发。很多时候,路就是一步一个脚印走出来的,如果前面碰到大山大河,实在跨不过去,回头绕个湾也是必须的,否则一步没跨过去就OVER了。最优的不是选择最好的,而是选择合适的。
接着做IOC,超时,熔断,限速,配置管理,服务注理,监控服务,负载均衡,服务路由,API网关,HTTP,WEB Socket,全局ID,链路追踪,消息服务,异步RPC,基于VUE做的后台管理,服务编排。。。。,到现下图功能基本上都有实现,并且正常可用,后期肯定会不断优化。
图中最底层是JMicro依赖服务,没做任何修改,Redis用于做高速一至性ID生成及消息缓存,Zookeeper做配置管理及服务注册中心,应用可以通过JMicro IOC容器直接获得Redis和Zookeeper实例组件并使用,非常方便;Netty做RPC底层数据通信,同时支持HTTP及Websocket,应此支持Web端直接访问微服务;Mongodb是一个可选依赖,用于存储RPC监控日志和事件信息;Vue+IView实现后台管理的前端页面。
智能控制:目前没有实现,之所以列出来,是因为一直把他当作JMicro的核心功能,目的是通过监控获取到的系统数据训练一个人工智能大脑,让其自动完成JMicro系统的日常维护。
IOC容器:之所以没用现成的Spring等容器实现,就如一开始所说的,他们太复杂,不够轻量(即使他们一直坚持认为他们是轻量级容器),对于JMicro来说,他们还是大重了,也不利于JMicro对微服务的量身定制。JMIcro IOC使用非常简单,在99%情况,通过Component注解组件,通过Inject注解字段就够了,就这么简单。
编码解码:如果说RPC是微服务的基础,那么编码解码则是微服务的基石,基石不牢,服务的大厦随时都可能倒塌。我将JMicro实现的编码解码实命名为前缀类型编码解码器,首先对类对进行编码,然后将类的编码信息和要传输的数据一起序列化到二进制数据中,解码时则将编码转为类信息,然后反序列化二进制数据为类对象,当然,编码考虑了高频使用的类并确保其编码最短,从而达到序列化的数据冗余最底。测试效果还是相当不错的,对比ProtoBuffer性能不相上下,某此情况差此,但某些情况比ProtoBuffer还好,甩JDK原生序列化及Kryo序列化几条街。
别的功能待以后再细说。
实现以上功能的全部第三方依赖如下:
其中
javapoet是编译时做代码生成用的,在最终运行包中并不需要
guava前期实现限流试验性用到,最后限速并不理想,实际上已经排除
Curator后期打算去除,只保留Zookeeper.
三,目前最满意的一个特性:异步RPC
大家都知道,NodeJS单线程,但其性能却很强劲,因为异步;Redis性能也很强劲,因为异步;无数的库或服务都强调通过异步提高性能。
在对JMIcro做压力测试过程中,我深深地体会到一个同步的代价(性能底还不说,高并发时还会死锁)而异步的必要性。在JMicro中,异步体现在很多模块中。其中主要以下几个:
- 消息服务,一个针对JMicro量身定制的异步消息中间件,服务可以通过其实现消息发布/消息订阅; 还可以做异步RPC,比如发布一个RPC服务方法,使其自动订阅特定主题的消息,消息中间件能识别此种类型的服务方法,并将匹配的消息发送给此服务的服务方法。
- 以1作为基础,可以在客户端及服务端做异步RPC,比如在客户端过直接将消息发送到消息中间件,让消息中间件转发给目标服务;也可以在服务端收到消息后(比如API网关,同步转异步,绡峰限流),将消息发送到消息服务器,让消息服务器转发给目标方法。
- 代码级别的异步RPC调用,让Java代码像Scala,NodeJS等语言一样做异步编程,以下对此做详细的Demo
下面片段代码摘自
ISimpleRpcAsyncClient src = (ISimpleRpcAsyncClient)of.get(ISimpleRpc.class); //最外层异步RPC调用 src.helloAsync("Hello JMicro").then((rst, fail)->{ //第一次异步返回结果 System.out.println(rst); //做一次同步调用 String r = src.hello("Hello two"); //同步返回结果 System.out.println(r); //再做一次异步调用 src.helloAsync("Hello two").then((rst1, fail1)->{ //异步返回结果 System.out.println(rst); }); });
我觉得这种异步RPC风格是最有杀伤力的,这使得我们的远程RPC调用不再阻塞线程,包括工作线程和网络IO线程,只要我们的线程占用CPU,他都是在做有用的工作!
四,体验JMiro异步RPC
前面吹了这么多,不来点实际可见的操作,肯定不能让人信服,现在手把手教你体验一把JMicro是不是真的做了远程RPC调用,是不是真的是异步调用。
1. 环境准备
首先确保本机安装了Java,Maven,Zookeeper监听在2181端口,Redis工作在6379端口,保持默认,所有配置都省了。
2. 下载JMicro代码
到https://github.com/mynewworldyyl/jmicro下载代码到特定目录,下面以${basedir}指代此目录
3. 打开一个命令行窗口,cd进入到${basedir}codegenerator目录,执行
mvn clean install -Dmaven.test.skip=true
4. 上面命令执行成功后,cd进入到${basedir}目录,执行mvn clean install -Dmaven.test.skip=true
5. 确保3和4成功后,cd进入到${basedir}example目录, 执行mvn clean install-Dmaven.test.skip=true
6. 通过3,4,5步确保相关依赖包都已经安装在Maven本地仓库中,打包可执行的服务Jar包,cd进入到${basedir}exampleexample.provider目录, 执行mvn mvn clean install -Pbuild-main -Dmaven.test.skip=true
7. 运行服务端
java -jar target/jmicro-example.provider-0.0.1-SNAPSHOT-jar-with-dependencies.jar -javaagent: ${basedir} argetjmicro-agent-0.0.1-SNAPSHOT.jar
看到以下输出说明服务启动成功
8. 打包可执行的测试客户端Jar包,cd进入到${basedir}exampleexample.comsumer目录, 执行mvn clean install -Pbuild-main -Dmaven.test.skip=true
9. 运行客户端
java -jar target/jmicro-example.comsumer-0.0.1-SNAPSHOT-jar-with-dependencies.jar -javaagent: ${basedir} argetjmicro-agent-0.0.1-SNAPSHOT.jar
以下是客户端两次异步调用,一次同步调用返回的结果
对应服务器的3个被调用的输出
以上样例对应的服务接口及实现类分别为:
@Service //指定此接口是一个远程接口,对外提供RPC服务 @AsyncClientProxy //编译时注解,Javac编译器调用特定注解处理器生成客户端访问代码 public interface ISimpleRpc { //我们测试用的方法 String hello(String name); //IPromise<String> helloAsync(String name); //POJO对象为参数的RPC方法 String hi(Person p); //测试RPC链路 String linkRpc(String msg); }
服务实现类代码有点多,在此只贴我们调用的方法,别的请查看源代码
public String hello(String name) { if(SF.isLoggable(MC.LOG_DEBUG)) { //向监控服务发送一条日志事件,请放心,调用此方法只是把日志存于本地并立即返回,不影响正常的性能 SF.eventLog(MC.MT_PLATFORM_LOG,MC.LOG_DEBUG,SimpleRpcImpl.class, name); } System.out.println("Server hello: " +name); //返回一条信息给客户端 return "Server say hello to: "+name; }
客户端测试类
完整类图
简化类图
五,实现原理
建议适当修改上面代码然后编译运行测试,比如修改服务端返回值,修改客户端调用参数,服务端返回改为同步,或再调用别的服务(可以同步调用,也可异步调用)。
启动多个example.provider实例(理论上,你可以启动无数个服务实例,那怕都在同一台机器上)多次运行客户端调用看看调用了那个服务实例(服务路由及负载均衡)
如果你对微服务有兴趣,并且有足够的耐心,你应该能从中获得乐趣。
如果认真查看了原代码,应该看到以下几个关键的注解,而使用JMIcro,基本上使用这几个注解就够了:
1. Service:如果注解在接口上,意思是告诉JMicro容器,这个一个服务接实口,客户端使用接口类为参数get实例时,请返回服务代理实例;如果注解在实现类上,告诉JMicro容器,这是一个RPC服务,请将服务信息发布到服务注册中心,让客户端知道我的存在。
2. AsyncClientProxy:注解服务接口,告诉编译器在编译全部源代码前,请调用注解处理器生成服务客户端代理接口及实现类,供客户端直接使用,让客户端觉得自己就好像获得了服务实现类的直接引用一样,完全不知道是跨JVM的远程调用。
3. Component:JMIcro容器组件,JMicro容器启动时实例化,供容器中的其他组件使用,是的,服务本身也是一个组件,只不过其实现了服务接口,并向注册中心注册自己才变成了远程服务,同时其本身也是一个组件,可以被同一个容器中的其他组件依赖并被自动注入。
4. Reference:注解在组件的字段上,告诉JMicro容器,我需要一个远程服务代理实例,请将这个实例值赋给我注解的字段。
5. SMethod注解在方法上,告诉JMicro容器,我是一个服务方法,并指定服务参数,比如超时时间,熔断策略,是否可监控,日志级别等。
6. 每个服务都由三个值唯一确定,分别是服务接口全称(serviceName),名称空间(namespace),版本(version),并使用在Service及Reference注解上。
后面有时间会对JMicro的细节做更多介绍,以实战为主,微服务概念为铺。
附一个后台管理的前端链路日志查询页面,下面链接可以打开看,没登陆只能查看部份功能的权限,要登陆的同学可以私信。华为云送的服务器,性能很差,并且只有30天使用期!
大家有任何问题欢迎评论。
如果你对这个项目感兴趣,也欢迎参与开发,共同完善。
后面资金允许后,买个服务器在公网上部署一套,让大家体验!