本帖最后由 eying 于 2016-1-18 16:05 编辑
问题导读:
1.远程接口的系统架构是什么?
2.什么是RPC调用?
3.为什么要有Client层?
摘要
远程接口设计经验分享 写在前边 分布式架构是互联网应用的基础架构,很多新人入职以来就开始负责编写和调用阿里的各种远程接口。但如同结婚一般,用对一个正确的接口就如同嫁一个正确的人一样,往往难以那么顺利的实现,或多或少大家都会在这个上边吃亏。 每年双十一系统调用复盘的时候,我都会听到以下声音你们调我...
写在前边
分布式架构是互联网应用的基础架构,很多新人入职以来就开始负责编写和调用阿里的各种远程接口。但如同结婚一般,用对一个正确的接口就如同嫁一个正确的人一样,往往难以那么顺利的实现,或多或少大家都会在这个上边吃亏。
每年双十一系统调用复盘的时候,我都会听到以下声音
- 你们调我的接口报错了竟然不会自己重试?
- 我的返回值应该从这里取
- 我返回isSuccess() == true,不代表业务成功,你还需要判断ERROR_CODE
- 这个ERROR_CODE没说全部都要重试啊!
- 这个ERROR_CODE必须要重试!
还有很多了,本文的目标就是帮助大家思考,如何设计自己的远程接口,让接口做到健壮、易用,节省大家在这块泥潭中所挣扎的时间。
一个日志服务LogService
PS:本例子的代码可以见 Excavatore-DEMO
系统架构
一个集中性的日志服务器,要求应用通过日志系统提供的日志服务,将所有日志集中统一的输出到固定的文件中。
系统架构图
、、
接口v0.1版
[mw_shl_code=applescript,true]/**
* 日志服务
* @author : xiaoming.xm@cainiao.com
* @version: 0.1
*/
public interface LogService {
/**
* 记录INFO级别日志
*
* @param format 日志模版(同String.format())
* @param args 日志参数
*/
void info(String format, Serializable... args);
}[/mw_shl_code]
RPC调用
什么是RPC调用
RPC(Remote Procedure Call)远程过程调用,一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的技术实现。
RPC采用C/S模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息的到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续进行。
一次完整的RPC调用过程
- 客户端函数将参数传递到客户端句柄。
- 客户端句柄将请求序号、远程方法、参数等信息封装到请求对象中,并完成请求对象序列化形成请求报文,通过网络客户端发送请求报文。
- 请求报文通过网络客户端与网络服务端所约定的协议(HTTP、RMI或自定义)进行通讯。
- 网络服务端收到请求报文之后,通过反序列化,从请求对象中解析出远程方法、参数等信息,并根据这些信息找到服务器句柄。
通过服务器句柄完成服务器函数的本地调用过程 自此,整个请求流程完成。
一次远程调用出错的可能
通讯框架错误
通讯框架错误根据发生环节分可以细分为
Marshell & UnMarshell C/S双方采用了不一致的序列化/反序列化算法,导致在通讯之前或之后无法正常取得通讯的对象。从而导致双方在编码、解码的过程中发生错误。 如果你的通讯框架使用了Hessian那基本上你都有机会遇到过。至于序列化和反序列化的梗,都可以开个专题了。这里就不在啰嗦。 网络通讯错误 系统错误会导致无法预测的异常产生,具体取决于RPC的实现方式。对于这种错误,唯一的处理方式只有:另外找时间/机会重试。
业务系统错误
业务系统错误分两种情况
- 数据库访问失败
- 文件写入失败
网络通讯失败 一般遇到这种错误,可以通过重试解决。
各种出错场景&解决方案梳理
| 出错情况 | 解决方案 | 是否重试 | 通讯框架错误 | 抛出框架异常 | 重试 | 系统错误 | 抛出系统异常 | 重试 | 业务错误 | 返回明确的错误码 | 禁止重试 | 代码组织
如果你有机会重新搭建一个应用,推荐大家采用分包的策略来考虑自己的模块组织。
正确处理返回值
这套RPC接口声明的理念在于:如何通过约定区分出系统异常与业务异常。区分的关键就在于ResultDO<?>与LogException上
ResultDO<T> info方法不需要返值,但服务端需要在业务出错的时候,将错误码返回给客户端,以便友好的错误提示。所以在Result对象中有两个方法:
- public boolean isSuccess();
isSuccess为true时表明业务处理成功:当客户端获取到这个值时,表明服务端已正确经接请求到并且成功的处理了这个请求,业务完成。这是最好的情况。 isSuccess为false时表明业务处理失败:当客户端获取到时,表明服务端已经正确接到请求,但业务处理失败,失败原因在错误码errorCode中体现。 - public String getErrorCode();
当服务端正确接到请求,但业务处理失败时,失败的原因以错误码形式返回。
客户端对返回值的处理总结
客户端处理逻辑表
调用情况 | isSuccess | errorCode | throw LogException | throw Exception | 客户端处理 | 框架错误 | / | / | / | true | 重试 | 系统错误 | / | / | true | / | 重试 | 业务错误 | false | true | / | / | 不重试 | 成功返回 | true | true | / | / | 不重试 |
所有情况也不是一层不变。比如业务错误返回错误码,但有时处于性能考虑(抛异常非常消耗JVM性能),可以在接口声明中约定部分错误码也必须要进入重试。但这种场景越少越好,而且一旦做出约定,出于接口向下兼容的考虑,这种需要重试的错误码自声明以来,只能减少不能增加,否则会引起兼容问题。
增加isReTry后的客户端处理逻辑表
调用情况 | isSuccess | isReTry | errorCode | throw LogException | throw Exception | 客户端处理 | 框架错误 | / | / | / | / | true | 重试 | 系统错误 | / | / | / | true | / | 重试 | 业务错误 | false | true | true | / | / | 重试 | 业务错误 | false | false | true | / | / | 不重试 | 成功返回 | true | / | true | / | / | 不重试 |
为什么要有Client层
老实说,这一层不是必须的,很多情况下客户端直接使用服务端声明的Service接口足矣。但若遇到在客户端容灾、增强的场景,则ServiceClient的优势就体现出来。
接口v0.2版
[mw_shl_code=applescript,true]/**
* 日志服务
* @author : cangjingkong.cjk@cainiao.com
* @version: 0.2
*/
public interface LogService {
/**
* 记录INFO级别日志
*
* @param format 日志模版(同String.format())
* @param args 日志参数
* @return 记录日志是否成功
* @throws LogException 记录日志发生异常
*/
ResultDO<Void> info(String format, Serializable... args)
throws LogException;
}[/mw_shl_code]
接口的Wrapper
几乎可以肯定的,在公司中你肯定不是第一个声明接口的人。所以当你定出了远程接口设计规范之后,如何面对老接口则成了一个头疼的问题。
先人的智慧是无穷的,现在我们讨论的问题,我们的前辈都已经面临并解决了(运气不好你可能还会遇到新手练手写的接口),只是解决的方法各种各样,没有形成约定。何解?
此时可以考虑使用装饰模式将不规范的接口重新包装成符合设计规范的接口,这样做有两个好处:
所以无论对方声明的接口是否符合约定,我都会建议客户端不要直接使用Service/ServiceClient,而是Wrapper一层。
|