soa接口原则及规范
###本文档谨作为个人学习总结,欢迎阅读、讨论及指正!
一. soa接口前世今生
伴随公司业务的飞速发展,业务的体量及复杂度在逐步增加。 为了提升服务可维护性、可扩展性以及解决服务性能瓶颈,“soa治理”随之而生。 几乎国内各家大型互联网公司都有自研的rpc框架,其核心理念即: 合适的通信协议+合理的业务划分。 经过soa的理念的洗礼,服务实现了可横向插拔,业务相互解耦, 互联网公司的数据体量已经几乎能承受中国13亿人口的随意践踏, 但是对于业务开发却增加了额外负担。业务数据散落在不同的服务中, 如何将业务“拼接”起来并“联调”成功,成为了每次功能迭代的痛。 深究痛的原因,基本集中在“soa接口”,本文旨在结合自己的理解, 将若干资料文献进行整合进行描述,欢迎共同学习讨论。
二. soa接口考量维度
1. 准确的业务阈
a.业务阈区分理念
soa治理是按照业务领域进行拆分,那么定义什么样的接口当然要在自身的业务域内。通常界定业务域的简单方式为:业务数据在不在本服务的db内(当然部分facade模式、adapter模式的服务除外),除此之外我们都有充分的理由去和需求方讨价还价,准确的业务层数据在下游基础服务,为何要增加proxy或者如果下游业务模式变更导致的异常我们如何挽回。
b.业务阈划分事例
购物车几乎汇聚了用户的一切购买信息,通过购物车的收集最终完成了一笔完备的订单数据。“猜你喜欢”是电商增加复购率的核心板块,经过大数据分析结合当前促销信息为用户进行精准化推荐。为了准备一年一度的双11,首页增加cms页面提供部分促销商品展示给用户,于是cms希望直接使用购物车的“猜你喜欢”接口减少自身业务开发。 这样的促销信息很明显不属于购物车业务领域,虽然能够实现功能快速上线,但是长远来看这对购物车领域是毁灭性破坏。这条cms的促销逻辑迭代优化、性能调优不仅强耦合进购物车,同时前台导购流量的不可控也会影响核心的购物链。
c.业务阈划分理念
“承诺自身能力范围内的任务,服务治理需要长远考虑”
2. 恰当的粒度
a.粒度的概念
经过服务拆分后,虽然接口变成了一次rpc调用,但是对于内部业务我们依旧任务是一个普通的方法。既然是方法,我们很容易联想到clean_code的理念:“只做一件事”、“高复用”,这也是接口粒度的初始考量标准。
b.粒度划分事例
订单是电商的核心模型,查询订单信息几乎是各个soa服务首要业务动作。而订单是一个庞大的数据结构,在融合订单基本信息同时,需要冗余部分用户基础信息、商户基础信息、促销基础信息、订单标签信息、甚至包含简单的配送信息等等。如此描述,订单服务提供getorder接口返回所有订单相关数据,一个接口搞定一切是否就是极高的“高复用”体现呢?这里我们暂不讨论性能方面问题,我们只聚焦业务。几乎80%的依赖方几乎只需要查询订单号、用户ID、商户ID及其状态,返回一切的接口绝大部份的数据毫无用处,而且由于业务信息的极度暴露,导致业务方会认为订单是万能的,导致对订单数据的滥用。一个大而全的接口势必会有极高的业务复杂度,业务数据拼接也会导致业务逻辑的强耦合。
c.粒度划分理念
“划分合理的数据结构,保证一个接口做一件简单的事情”
3. 流量及性能考量
a.流量性能的理解
接口性能决定了其流量承载能力,互联网公司在soa接口100ms优化的路上一直前行。在确定了业务数据结构后,我们通常要迅速考虑其实现逻辑,保证接口的性能。一个性能没有保障的接口,几乎是没有任何使用价值的。
b.性能事例
通常性能考量一般会在提供批量接口时特别明显。简单的db查询,通常约束in小于500;具体到业务数据拼接,一般限制批量在20左右以保证接口性能。这些在接口设计之初要严格协商,需求方要妥协。 对于部分极其关键的链路接口,通常要精益求精,对每一个字段都否需要对外提供都要斟酌,少select一个字段的性能在大流量面前会极大的性能提升;同时接口能否短缓存部分热数据也是重要的考量点。 接口内部有下游依赖的业务,需要认真评估下游接口性能,设置好必要的超时降级机制。在考虑完一切可能的性能干扰因素后,给出你的接口定义;不满足性能要求的一律重新梳理业务姿态,降低接口承诺粒度。
c.性能理念
“性能不达标的接口,反映了业务接口设计存在极大的不合理”
4. 良好的用户体验
a.用户体验概述
对于接口使用方,接口可以等价于内部方法。于是,要让用户使用你的接口感受平和,就需要想一个普通方法一样设计。返回合理的业务code、关键的message信息和定义的数据结构是rpc接口的返回结果集,其中接口异常也要合理的抛出,让调用方使用舒适。
b.体验事例
何时抛出异常,何时返回错误码这个问题引起了很长时间的争论。阿里巴巴代码规范给出的建议是:内部应用之间使用错标码来交互。
阿里巴巴总结认为返回result比异常好的理由:
1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
2)如果不加栈信息,只是new自定义异常,加入自己的理解的error message,对于调用 端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输 的性能损耗也是问题。
实际使用中,饿了么soa框架首先合理的支持了接口异常抛出,同时熔断、etrace等依赖异常信息监控,所以异常的抛出是必不可少的。 通常对于频繁的返回结果类型,例如:打款失败、支付失败等,我们采用错误码表示,因为这是属于正常的业务状态;而对于下游依赖服务异常,接口入参校验等要通过异常抛给调用方。我们将接口设想为在只是一个普通的业务方法就可以很合理的感知到什么时候才用错误码、什么时候才用异常来表示。
c.接口体验理念
“rpc接口只是一个普通的业务方法”
5. 清晰的描述
a.接口描述概述
接口是不同应用之间的合约,契约需要详细将条款写在文档中,供使用方参考同时方便后续开发维护。一个好的接口通常应该知其名晓其意,合理的命名可以快速的帮助理解业务。
b.接口描述
通常接口的命名最好能反应业务属性,具有逻辑性的参数名称可以让调用方使用舒适;由于api通常会打成jar进行依赖,所以增加接口api的注视也可以加快业务方理解,帮助业务迅速开发迭代。注释要标明出入参数类型及业务性质,同时接口抛出的异常类型及错误码也可以标注。对应的开发文档要同步更新至wiki或博客,举例说明使用姿势方便调用方快速依赖。
c.接口描述理念
“命名要优雅,合约要详细”
三.接口规范细节
经过以上总结的soa接口衡量纬度,给出接口实施细节如下(其中部分原则参考阿里巴巴开发手册):
1. 给你的接口起个好名字……^_^
为什么把命名放在首位?因为这是我们现在最棘手的问题。 接口名称相当于是服务的门面,通常接口由两部分组成:servicename + methodname;一般而言不同的servicename标示不同的业务域,具体的方法名称则是业务域内的详细操作。 举例如下:
促销服务:总体分为满减促销、满赠促销、折扣促销、促销抵用券、促销分摊计算、
促销客服查询、促销商家维护、促销购物流程,
整个促销体系在电商架构中随处可见,业务渗透在多个模块。
促销应用按照自己的业务领域,划分出了多个service:
discountService\giftService\coupenService\
CalculateService\backendService\promotionService
methodName标示业务域的详细动作:
例如:promotionService是面向整个核心链路的促销查询服务:
/**
* 根据用户ID查询其享受的折扣等级
* @param userId
* @return Discount
*/
Discount getDiscountByUserid(Long userId) throw PromotionServiceException;
通过接口名称及参数,我们和容易了解改接口的业务模式,同时接口名称限定了接口业务域。
关于接口命名通常每个程序猿有自己的一套蹩脚english风格,这里建议在接口命名的时候最好团队成员共同斟酌命名,起个好名称真心是门学问
2. “通用接口”和“定制化接口”的取舍
接口复用性高低很多时候由接口返回的业务数据决定,多数情况下一个接口尽可能的返回丰富的业务数据,势必能让多个依赖方复用一个接口,各取所需字段信息即可。但是大而全的接口返回势必代表着底层sql select很多字段、join关联表信息甚至还查询了第三方接口拼装了数据,这样的业务接口预示着业务复杂度较高,接口性能也会有极大的损耗。 那么这样的万能接口是否又一定是错误到不可饶恕的地步呢? 针对部分较冷的业务链条,对性能要求较低的调用方,一个适度臃肿的接口可以应对好几个依赖方,我们并不希望为每一个调用方做定制化接口,接口设计的通用会使我们的开发量大大降低。 于是,我们怎么合理的把握度呢? 具体事例如下:
以促销折扣接口为例:
整张促销主表数据记录了促销的名称,活动起止时间,促销状态,以及促销折扣信息,同时还荣誉了部分促销描述字段等,作为通用的促销查询接口,我们返回了一个几乎全字段的接口,满足了各种调用方的需求,同时性能也基本表现良好,那么我们认为这是一个非常好的通用接口定义。 然而,当购物主链路查询促销信息时,其基本只关心打了8折还是9折,并且由于是核心链路依赖,接口的性能表现直接会影响着整个购物链路的表现,在大流量的请求下性能似乎需要达到极致为最佳。此时我们会为这样的“核心”业务去开设“定制化”接口以使性能达到极致。同时,热核心接口的缓存策略势必也和冷接口不同,所以这样的定制化我们认为是合理的。减少字段降低select*对db的压力,同时在soa的数据包传输也会大大缩小,这样的热接口做缓存,缩小的数据接口对redis也会起到良性反应。
“通用接口”和“定制化接口”原则如下:
通常对外提供单表核心字段接口做为通用业务接口,最多容忍连接一张表增加extra信息,这样基本能保证接口性能在100ms以下并且接口复用性较高;当有核心链路,流量较大的方需要接入时,考虑单开定制化接口。当接口内含复杂逻辑,甚至第三方接口调用时,考虑接口单开,不将接口开放为通用接口。通常写操作接口,不提供通用接口,定制化设计方便业务监控,防止写入业务数据耦合,事务治理混乱等。
3. 不轻易暴露内部实现
get_master \ get_slave 类似接口对外暴露了业务实现。一方面调用方关心的是业务数据,并不希望自己考量是从主库还是备库获取数据;并且,调用方在使用get_slave接口时会产生很多技术想法,多方面怀疑自己的数据来自备库是否实时性无保障等一些列不必要的思考; 甚至某些接口声明为getDeliveryOrderWithCache,试想你缓不缓存关调用方何关,暴露实现是为了秀自己性能优化嘛? 一般来说,soa接口是不建议暴露实现细节的,接口本意是一层抽象的业务概念。
4. 接口的返回和入参最好是结构体
接口在定义时要考虑后续的可扩展性,确保后续需求迭代可以通过增加字段复用接口,而不是重开新接口。rpc接口的序列化给我们增加字段升级接口提供了可能,但是前提是你需要设计成可增减字段的结构才行。
以是否是饿了么会员接口为例:
整个接口看文字意义是判断一个用户是不是会员,貌似返回boolean值特别切合文意,然而随着业务的扩展,变态产品形态的变化,接口需要返回不是饿了么会员的原因,这就尴尬了,boolean只能表示是否,毫无扩展性可言。
5. 使用友好
Map shopQuery = new HashMap<>();
shopQuery.put("shopId", Long.valueOf(order.getRestaurant_id()));
try {
ApiResponse response = serviceProxy.getDwApi().request("insurance/getInsuranceRiskShopInfo", shopQuery);
Map<String, Object> result = (Map<String, Object>) response.getResult();
if ((Integer) result.get("code") != 0) {
throw new InsuranceServiceException(ExceptionCode.DEPENDENCY_ERROR, "查询商户风控信息失败");
}
这里我们直接贴出了大数据的对外接口,这定义的方式分明不是soa模式啊,整个soa-api一个接口搞定啊有木有,抽象层次也太高了。把soa当作一个“请求”暴露在众目睽睽之下,谈什么有好的交互啊,满满的http请求既视感。
6. 合理的处理业务异常及错误码
个人认为接口交互返回错误码没有什么问题,如果错误码是业务的一种分枝状态,那么是相当合理的;但是如果是参数validate异常、下游第三方依赖服务异常、批量接口查询数据超限异常、业务幂等性校验不通过等,不属于业务范畴的错误分枝,应该果断的异常抛出,提示调用方当前的使用姿态不正确。抛出异常是对调用方使用姿态的警告性行为,其告警等级较高,旨在表达系统对当前业务流的“不满”,抛出异常对自身及使用方都是有利的,有利于使用方代码的编写。
金融的soa接口完全是木有异常抛出的,几乎所有的操作都是依靠返回错误码的方式,
这让适用方也会感到费解,于是我们通过判断错误码后给自己new 出了异常……^_^
AccountChargeResponse response = chargeService.specialAccountCharge(request);
if(response.getTransactionStatus() != ACCT_SUCCESS){
throw new InsuranceServiceException(ExceptionCode.DEPENDENCY_ERROR);
7.java接口命名规约(copy 阿里巴巴的)
1. 【强制】 代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
反例: _name / __name / $Object / name_ / name$ / Object$
2. 【强制】 代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。 说明:正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,即使纯拼音命名方式 也要避免采用。
反例: DaZhePromotion [打折] / getPingfenByName() [评分] / int 某变量 = 3 正例: alibaba / taobao / youku / hangzhou 等国际通用的名称,可视同英文。
3. 【强制】类名使用 UpperCamelCase 风格,必须遵从驼峰形式,但以下情形例外:(领域模型 的相关命名)DO / BO / DTO / VO等。
正例:MarcoPolo / UserDO / XmlService / TcpUdpDeal / TaPromotion 反例:macroPolo / UserDo / XMLService / TCPUDPDeal / TAPromotion
4. 【强制】方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格,必须遵从 驼峰形式。
正例: localValue / getHttpMessage() / inputUserId
领域模型命名规约:
1) 数据对象:xxxDO,xxx即为数据表名。
2) 数据传输对象:xxxDTO,xxx为业务领域相关的名称。
3) 展示对象:xxxVO,xxx一般为网页名称。
4) POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO。