天天看点

远程接口设计经验分享远程接口设计经验分享

分布式架构是互联网应用的基础架构,很多新人入职以来就开始负责编写和调用阿里的各种远程接口。但如同结婚一般,用对一个正确的接口就如同嫁一个正确的人一样,往往难以那么顺利的实现,或多或少大家都会在这个上边吃亏。

每年双十一系统调用复盘的时候,我都会听到以下声音

你们调我的接口报错了竟然不会自己重试?

我的返回值应该从这里取

我返回issuccess() == true,不代表业务成功,你还需要判断error_code

这个error_code没说全部都要重试啊!

这个error_code必须要重试!

还有很多了,本文的目标就是帮助大家思考,如何设计自己的远程接口,让接口做到<code>健壮</code>、<code>易用</code>,节省大家在这块泥潭中所挣扎的时间。

ps:本例子的代码可以见 excavatore-demo

...

苍老师

上课!大家好,我是你们的苍老师。今天就由我来给大家讲讲如何编写一个健壮的远程接口。老师将在这里给大家设计一个集中式的日志系统。

虽然这个系统的存在不合理,但这是能找到的最简单例子,所以不要在课堂上就系统的合理性展开讨论,否则老师会生气的哟~

远程接口设计经验分享远程接口设计经验分享

一个集中性的日志服务器,要求应用通过日志系统提供的日志服务,将所有日志集中统一的输出到固定的文件中。

系统架构图

远程接口设计经验分享远程接口设计经验分享

小明

远程接口设计经验分享远程接口设计经验分享

这很简单嘛,根据系统的要求和架构特性,我很快就能写出接口定义,老师你看。“如果方法顺利无异常返回,则说明日志已经被成功写入了日志文件”

非常好,但这种接口只能用在单机版的程序中,如果遇到远程调用的场景就不适用了。要了解这个事实,首先大家就要知道远程调用的大概实现原理。

远程接口设计经验分享远程接口设计经验分享

rpc(remote procedure call)远程过程调用,一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的技术实现。

rpc采用c/s模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息的到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续进行。 以上信息摘录自百度百科
远程接口设计经验分享远程接口设计经验分享

请求过程

<code>客户端函数</code>将参数传递到<code>客户端句柄</code>。

<code>客户端句柄</code>将请求序号、远程方法、参数等信息封装到请求对象中,并完成请求对象序列化形成请求报文,通过<code>网络客户端</code>发送请求报文。

请求报文通过<code>网络客户端</code>与<code>网络服务端</code>所约定的协议(http、rmi或自定义)进行通讯。

<code>网络服务端</code>收到请求报文之后,通过反序列化,从请求对象中解析出远程方法、参数等信息,并根据这些信息找到<code>服务器句柄</code>。

通过<code>服务器句柄</code>完成<code>服务器函数</code>的本地调用过程

自此,整个请求流程完成。

应答过程

<code>服务器函数</code>执行的过程将结果返回<code>服务器句柄</code>,返回的结果可能是正常返回,也可能是以抛异常的形式返回。

<code>服务器句柄</code>根据返回的值与请求序号封装到应答对象中,并完成应答对象的序列化,形成应答报文,通过<code>网络服务端</code>发送应答报文。

应答报文通过<code>网络服户端</code>与<code>网络客务端</code>所约定的协议(http、rmi或自定义)进行通讯。

<code>网络客户端</code>收到应答报文之后,通过反序列化,从应答对象中解析出请求序号所挂钩的<code>客户端句柄</code>

<code>客户端句柄</code>将返回数据返回到<code>客户端函数</code>,以返回值或抛异常的形式将信息返回

自此,整个应答流程完成。

一次完整的rpc调用一共分10步,每一步都有可能出错,所以在设计一个远程接口的时候必须充分考虑到所有的出错可能,与客户端约定出错的应对方案。无论哪个环节出问题,都要求你的业务逻辑依旧保证不能错乱!

远程接口设计经验分享远程接口设计经验分享
远程接口设计经验分享远程接口设计经验分享

不愧是苍老师,果然 博 大精深。我明白了,因为增加了远程访问的因素,所以原本单机中非常小的出错概率就被放大了,这也不得不让程序被迫感知和处理这些通讯错误。

那请问遇到这些错误都应该怎样进行归纳和处理呢?

通讯框架错误根据发生环节分可以细分为

marshell &amp; unmarshell

c/s双方采用了不一致的序列化/反序列化算法,导致在通讯之前或之后无法正常取得通讯的对象。从而导致双方在编码、解码的过程中发生错误。

如果你的通讯框架使用了hessian那基本上你都有机会遇到过。至于序列化和反序列化的梗,都可以开个专题了。这里就不在啰嗦。

网络通讯错误

系统错误会导致无法预测的异常产生,具体取决于rpc的实现方式。对于这种错误,唯一的处理方式只有:另外找时间/机会重试。

业务系统错误分两种情况

业务错误

client传递了违背业务规则的参数,导致业务逻辑处理失败。这种错误无论重复多少次都会得到一样结局。

系统错误

server处理内部逻辑时出现了无法控制的错误,常见的有:

数据库访问失败

文件写入失败

网络通讯失败

一般遇到这种错误,可以通过重试解决。

出错情况

解决方案

是否重试

通讯框架错误

抛出框架异常

重试

抛出系统异常

返回明确的错误码

禁止重试

远程接口设计经验分享远程接口设计经验分享

嗯,我了解了,一个好的远程方法定义必须考虑到上边所罗列的异常场景,要求做到<code>明确的错误处理约定</code>。那请问苍老师这个接口应该如何写呢?

先别着急,要写出健壮的接口,你还有几个概念要理解。首先我们先来看这个接口的声明。我的比你多了两个重要的信息<code>resultdo&lt;void&gt;</code>与<code>logexception</code>,接下来我会讲解这定义这两个类的作用

远程接口设计经验分享远程接口设计经验分享

如果你有机会重新搭建一个应用,推荐大家采用分包的策略来考虑自己的模块组织。

远程接口设计经验分享远程接口设计经验分享

common:定义core和client所共用的内容

业务接口声明

logservice

domain对象(这里为了简单,所有的do、to、dto都统一命名为do)

resultdo&lt;t&gt;

业务异常

logexception

client:富客户端,在这一层可以组织cache、业务无关的通用校验,这一次层并非必须。

服务客户端实现

logserviceclient

asynclogserviceclient

core:业务服务的实现,这一层的代码运行在服务端。

服务业务逻辑实现,同时内部按照习惯可以再次分层为(<code>service</code>、<code>manager</code>、<code>dao</code>)

logserviceimpl

远程接口设计经验分享远程接口设计经验分享

这套rpc接口声明的理念在于:如何通过约定区分出系统异常与业务异常。区分的关键就在于<code>resultdo&lt;?&gt;</code>与<code>logexception</code>上

<code>info</code>方法不需要返值,但服务端需要在业务出错的时候,将错误码返回给客户端,以便友好的错误提示。所以在result对象中有两个方法:

<code>public boolean issuccess();</code>

<code>issuccess</code>为<code>true</code>时表明业务处理成功:当客户端获取到这个值时,表明服务端已正确经接请求到并且成功的处理了这个请求,业务完成。这是最好的情况。

<code>issuccess</code>为<code>false</code>时表明业务处理失败:当客户端获取到时,表明服务端已经正确接到请求,但业务处理失败,失败原因在错误码<code>errorcode</code>中体现。

<code>public string geterrorcode();</code>

当服务端正确接到请求,但业务处理失败时,失败的原因以错误码形式返回。

这个异常主要用于收缩和屏蔽服务层的具体错误信息,当服务端遇到无法处理的错误情况时,需要继续向客户端外抛,让客户端来择机进行重试。客户端亦可通过logexception快速判断当前业务中断的原因来自于logservice的失败。

客户端处理逻辑表

调用情况

issuccess

errorcode

throw logexception

throw exception

客户端处理

框架错误

/

true

false

不重试

成功返回

所有情况也不是一层不变。比如<code>业务错误</code>返回错误码,但有时处于性能考虑(抛异常非常消耗jvm性能),可以在接口声明中约定部分错误码也必须要进入重试。但这种场景越少越好,而且一旦做出约定,出于接口向下兼容的考虑,这种需要重试的错误码自声明以来,只能减少不能增加,否则会引起兼容问题。

老师也见过有系统在resultdo中声明了<code>public boolean isretry();</code>方法,这样当系统发生业务错误的时候,是否重试的判断就交由<code>isretry()</code>来进行判断,这也是不错的选择。

远程接口设计经验分享远程接口设计经验分享

增加isretry后的客户端处理逻辑表

isretry

老实说,这一层不是必须的,很多情况下客户端直接使用服务端声明的service接口足矣。但若遇到在客户端容灾、增强的场景,则serviceclient的优势就体现出来。

远程接口设计经验分享远程接口设计经验分享
远程接口设计经验分享远程接口设计经验分享

一个好的系统约定能减少很多不必要的错误,但毕竟不是所有系统都是新的系统,在面临各种<code>先人的智慧</code>时,如何让不符合约定的远程接口也纳入约定来?

在面对<code>先人的智慧</code>时,改变现有被大量调用的接口声明是不可能的,在这种情况下<code>存在即合理</code>,哪怕明知接口声明或实现存在问题,你也不能去变更这个接口。接口维护原则请听下堂课《远程接口维护经验分享》。

当遇到这种不在约定的接口时,需要用<code>装饰模式</code>将不规范的接口包装成为规范的接口。

远程接口设计经验分享远程接口设计经验分享

几乎可以肯定的,在公司中你肯定不是第一个声明接口的人。所以当你定出了远程接口设计规范之后,如何面对老接口则成了一个头疼的问题。

<code>先人的智慧</code>是无穷的,现在我们讨论的问题,我们的前辈都已经面临并解决了(运气不好你可能还会遇到新手练手写的接口),只是解决的方法各种各样,没有形成约定。何解?

此时可以考虑使用<code>装饰模式</code>将不规范的接口重新包装成符合设计规范的接口,这样做有两个好处:

解决老接口不规范问题

减小老接口暴露到业务代码中的概率

这里需要解释下。外部接口的定义不受控制,如果此时一个service需要升级,则改动、回归、代码review范围仅限于wrapper类即可,若将所有业务代码直接引用外部的service/serviceclient类,则升级的回归面将被放大。

所以无论对方声明的接口是否符合约定,我都会建议客户端不要直接使用service/serviceclient,而是wrapper一层。

远程接口设计经验分享远程接口设计经验分享

太好了,经过老师提点,我终于写出了一个健壮的远程接口,并知道如何与客户端约定重试的关系。

不过我还是想问问,这种远程的日志系统存在是否不是太合理,老师你举这个例子是不是不太恰当?

远程接口设计经验分享远程接口设计经验分享