前言
dubbo是阿里开源的分布式rpc框架,在许多中小企业的微服务化过程中发挥着核心作用。但是想把dubbo运行起来也不是那么简单的,这几天我想搭个dubbo环境玩玩,一路受阻。
相信前来了解rpc的同学都有一定的java编程基础,也知道为什么用rpc,本文只教为什么用和怎么用。
本文的目标和大致文章内容是:
- 介绍rpc框架、rpc框架的特点以及使用rpc框架一般的编程步骤
- 给出demo,让大家对照着来了解dubbo的使用方法(【源码 demo 】 【docker-compose demo 可直接运行】)
- 从pom开始配置springboot,配置dubbo环境,编写dubbo的二方包、provider、comsumer
- 打包dubbo环境成为一个单独的jar包
- 将dubbo部署到docker中
- 将dubbo部署到k8s中(未完)
- 实现dubbo的mock(服务降级)功能(未完)
文章的内容除了rpc,还涉及到maven、springboot、docker、k8s等的使用,难免会有复读的情况。但是之所以要出一个系列,而不是一篇文章举个rpc的例子完事,是因为这是我进行学习的过程。在之前做项目之类的过程中,基本都是在别人的基础上进行修改的,对于maven的使用并不了解,因此自己需要引入依赖时也不知如何下手;同时,项目的运行基本上都得形成单独的jar包,不管是给别人引用,还是自己运行,自己在打包的过程中也踩了很多坑;此外,自己电脑开发的时候,也很讨厌装一大堆mysql、nginx的依赖(因为好装不好删,还会后台启动很多服务),到部署的时候,一堆配置文件和软件安装也麻烦得要死,项目交接一团糟,因此需要容器这一工具来解决这些痛点,一个配置、一次打包,小巧又全面,基本可以做到开发=部署,DevOps;最后就是在学习容器和实习工作的过程中,看到现在容器一般通过“容器编排”技术进行使用,来对容器进行自动管理,实现自动扩容、失败回滚、平滑升级等功能,因此顺便也学习了k8s。
在整个系列中,我会尽量解耦不同的内容,避免内容有过多交叉。此外,这个系列强调的是“应用”而不是“原理”,通过应用,可以对分布式的一些术语有更全面的了解,而原理则适合深入地挖掘某些知识点。
此外,一开始我还想着带读者一起从零开始构建这个项目,后来还是选择给一个demo代码大家跟着看。因为一些边边角角的知识太多了,解释起来很费劲,直接给出又很占篇幅,不如给一个demo大家对照着理解一下,能照顾到初学者。如果有一定的maven使用基础也有兴趣,可以从头开始搭建
RPC简介
这世上有许多简称,看起来相同,但是在不同的语境内表达不同的含义。rpc的全称是Remote Procedule Call,远程过程调用。当介绍完rpc使用的一般步骤之后,这个名词的每个字都很清晰了。
与远程过程调用相对的是本地过程调用。“过程”可以理解为“函数”或者“方法”(在一些面向对象的语言里喜欢这么叫)。本地过程调用,就是在代码里写一句调用函数的语句即可,比如java里(以下是一段废话代码),
public class RpcTest {
public static void main(String[] args) {
int kbytes = (int)Math.pow(2, 10);
}
}
我相信来了解rpc的同学都知道什么是函数名、函数参数、函数返回值。在本地调用方法时,其实也有一系列过程(只不过我们不太关心),比如
- 参数(的引用)入栈,调用方的字节码指针入栈
- jvm找到应该调用的函数版本(尤其对于虚函数)
- 执行函数,然后将函数的返回值入栈
- 调用方字节码指针出栈,回到调用方的代码下,并且把返回值赋值给接收变量
我们细品这个过程,实际上一句代码的背后,伴随着参数传递、代码定位、执行、返回值传递等过程。不管是什么语言,都是如此(因为家用pc的底层,从汇编就开始大致就是这样处理函数调用的流程的)
那我们再看看rpc进行函数调用的简化过程
- 1.根据函数的签名、接口名、类名、服务器空闲情况等选择合适的执行者(下面也称远端,也可以直接通过ip+端口直接指定一个执行者)
- 2.函数参数等信息打包传递到远端(这一步需要进行网络传输)
- 3.远端执行函数,得到返回值
- 4.远端将返回值等信息(还可以有异常等)打包传递回调用方
这边的步骤顺序略有变化,但是无伤大雅,起码这几个步骤是不能少的。
细品本地和远程调用的过程,是不是很类似(只不过远程调用的描述忽略了更多底层细节)?
我再举一个例子,相信大家都知道http有GET和POST请求吧?在前端页面内用js请求服务器的数据,本质上就是一种远程过程调用——将参数用json打包后通过http协议传递,这里的方法签名可以认为是域名、url路径等;后台服务器可能也具有负载均衡功能,将请求分发到不同的执行机器上;执行机器执行成功后将返回值通过json打包后通过http协议传递;前端页面收到远程调用的返回值,进行展示等后续操作。
那么使用rpc框架的大致步骤是什么呢?
- 过程调用方(下面也称为服务消费者)和过程执行方(下面也称为服务提供者)约定一个函数契约,包含函数名、调用参数、返回值(注意,这里会包含返回值的,因为远程调用时编译器无法检查返回值是否合法,所以双方必须保证返回值类型也相同,否则可能会出现类型转换错误)等,在java里,就是一个接口。一系列这种接口(或者说契约等)的集合,通常会单独打包,称为“二方包”
- 服务提供者实现契约里指定的函数,并且通过rpc框架的配置,将服务名、函数契约进行绑定
- 服务消费者通过rpc框架获取某个服务(可以通过服务名指定)的远程实现,进行调用
总而言之,一个契约,一个服务提供者(具有特定的服务名),一个服务消费者(通过服务名进行调用)。在spring的注入等技术帮助下,基本可以实现远端调用和本地调用的代码零差别。
接下来上三段代码展示一下一个简单的rpc调用代码(省略了背后默默付出的配置文件们),简单到初学者看到时一脸懵逼(是的,我就是这样)。
// 二方包,单独打包
interface IHelloworld {
String say(String name);
}
// 服务提供者,也需要单独打包
// 这个注解可以暂时理解,将这个类标识为dubbo的rpc实现类,服务名这里没有指定,可以认为就是接口的全限定名
@DubboService
public class HelloWorldImpl implements IHelloworld {
String say(String name) {
return "Hello, " + name;
}
}
// 服务消费者,也需要单独打包
public class HelloWorldConsumer {
// 这个注解可以理解为,这个类引用的是远端服务提供者。
@DubboReference
IHelloworld helloWorld;
void test() {
// 和本地调用无差别,会打印出“Hello, xxiaoming”
System.out.println(helloWorld.say("xiaoming"));
}
}
这里强调了三个“单独打包”,这是因为如果把服务消费者和提供者一起打包,一起部署,那还叫什么“远程过程调用?”通过网络进行调用,起码也得在两个jvm进程中吧。如果二方包不单独打包,那二方包一旦修改,就需要修改双方的代码(双方肯定都得引用这个契约接口吧?),很容易造成遗漏,所以将二方包单独打包,然后引入服务消费者和提供者的依赖中。
PRC的进一步介绍
刚刚描述的RPC使用方式只是最简单的,只能实现RPC调用而已。RPC框架需要提供更丰富的功能来实现分布式系统中的稳定性等额外要求。在上面所述的RPC调用过程中,细细拆开分析,会涉及到分布式系统的许多术语。
服务消费者执行函数时的同步/异步执行
世上没有真正的容易,所谓容易,只是其它人承担了本应由你承担的痛苦罢了 ——佚名
在本地调用的时候,并没有把这个过程单独拉出来讲,是因为大部分本地调用都是同步的(我指的是,在所有的本地调用中,异步调用只占很少的一部分,而不是说异步调用很少出现),而一旦变成远程调用,大概没有人愿意把一个1ms以内能执行完的超级简单的任务都通过远程执行——因为执行时间太短,网络传输等开销就难以忽略,整个系统的性能会下降很多。此外,还要考虑网络波动带来的影响。因此,采用RPC调用函数时,异步调用方式占比不小。
所谓同步调用,就是等待服务提供者回复调用结果,再接着处理下面的过程;而异步调用,只向服务提供者发起调用请求,请求传输、等待回复等过程都由框架在幕后完成(通常还会注册一个回调函数,便于对调用结果进行一定的处理)
dubbo支持同步或异步方式调用接口
服务提供者的选择:
这里涉及负载均衡(策略)、注册中心等内容。
服务调用方可以定期向注册中心请求可用服务列表,并且使用一定的负载均衡策略选择合适的服务提供者进行调用。这样就可以做到服务自动注册和发现,不需要服务消费者硬编码服务提供者的ip地址等信息,就可以自动找到可用的服务进行调用
函数参数的打包传输:
这里涉及到序列化技术、消息协议选择、传输方式选择。需要注意,参数的序列化和rpc信息的打包传输是两件事,前者只将一个参数变为一段字节序列,后者是将参数及函数名等额外信息打包形成一次调用请求的过程。而传输方式选择指的是用什么样的通信框架(如大名鼎鼎的netty),甚至是用tcp还是udp。
序列化指的是将“对象”变为字节流,这样程序中使用的代码对象就可以通过网络进行传输。接收者还可以根据序列化的逆过程将这个对象恢复出来。这样就实现了远端获取本地的对象
再进一步说,“对象”是什么?Java的对象内部可以存储基本类型、数组、其它对象的引用。基本类型不可拆,数组是同一类对象按顺序排列构成的一种对象,其它对象也是由这三种东西构成的。在不考虑循环引用的前提下,可以说,每个对象是由基本类型构建起来的一棵树,树的每个节点抽出来都是一个单独的对象。
相信大家都知道json,json内部只有两种基本类型,一种叫数字,一种叫字符串。此外,还具有数组和对象,分别用方括号[]、花括号{}来表示。json可以轻松地序列化java对象——对象本质不就是一堆数字构成的一棵树嘛!所以只需要把这些数字存下来,再把数字之间的关系存下来,对象自然就存下来了。具体来说,json的基本元素是数字和字符串,用花括号{}表示异构元素(即使元素类型相同,但是表达含义不同,那也算异构元素)的集合,用方括号[]表示同构元素的集合。
有点跑题了。在序列化方式的选择中,需要考虑编解码性能、传输性能、通用性等指标,dubbo支持多种序列化方式
说到这里,应该觉得对象序列化并不是那么神秘了,最起码json是可以做到的。
消息协议和传输方式的选择就不赘述了,在dubbo中都是可以定制的
调用失败的处理:
这里涉及到服务降级、mock等内容
这一条在本地调用中也没有提及,因为本地调用中,大部分情况下不太可能调用到不存在的函数,编译器也会辅助检查。但是在远程调用中,由于远程服务的不稳定性,很可能上一次调用正常,下一次调用时由于服务方宕机、网络拥塞、负载过高等原因造成调用失败。此时就需要执行服务降级等策略,调用备用服务。可以用一个简单的例子来说明:在浏览器的某个图片加载不出来时,会显示一个图片打个叉的图标,这就是某种意义上的服务降级——对于一些非核心功能,如果调用失败,也不要影响主要功能的实现。
除此之外,由于RPC需要依赖外部应用,如果想要进行单独的测试就比较麻烦——因此有了mock,也就是可以在只启动本地服务的前提下,模拟外部服务的响应来进行本地测试,提高测试效率并且降低测试难度。
dubbo支持mock,支持返回固定值和调用备用服务两种方式配置mock
后记
这里简要描述了系列文章的内容并且介绍了rpc框架的基本功能和进阶功能,并且关联解释了分布式系统中的一些术语。一方面希望大家可以快速知道rpc是什么,大概该怎么用;另一方面又结合rpc调用流程中的实现细节,更全面地介绍rpc技术。也希望大家带着问题来看后面的文章,或者为大家再看有关rpc框架原理的文章做一点铺垫。