最近遇到一个case:我在2018年接收过一个业务,当时交接的人员没有详细的文档,只能通过当时的app端上的请求判断是哪个接口,而业务监控是通过此接口进行统计QPS的,例如统计的接口是pxxx/saaaaa, 接口统计量在3万6左右(虚设),用当时的主要客户端app测试也是请求的这个接口,接口内对应的主要模块是 pyyyy,其中pyyy模块比较复杂,综合业务比较多。某日,某业务A想要接入业务,产品和技术都很顺利,当评估调用QPS是,我根据上面的统计数量告诉对方3万6万,也把监控图数据和对方沟通了。当业务实际上线50%,对方突然发现模块pyyy内对业务A提供的接口B的调用量高了20%,然后对方找到我们,让我们提供原因。通过对方的接口(只在这个场景下使用)反向查调用,结果很出乎意料,在这个接口pxxx/saaaaa的数量比总数量确实少了20%。然后根据结果分析,竟然有其他的接口直接在服务器redirect这里,调用同一个业务模块,而额外接口的业务因为之前未知也没办法统计,这个模块下请求的其他接口都是在别的接口调用的,所以也没做过反向查找。

当时感觉真的好被动和尴尬。这一直以来都是拿着自己看到的监控的数量去和其他业务方拍桌子,架方案。事后在反思,到底问题出在哪里,我们应该如何避免呢。作为一线攻城人员,每一项数据指标的统计都是底气足的根本。有误差不在认知范围内的情况,往往都会让己方陷入不利的局面。于是,我总结了下业务、接口和统计之间的关系,及我们在实际开发中如何进行监控统计的。

和大多数web服务端一样,我们的服务主要给客户端提供接口和一些内网的服务,内网服务主要供给其他业务方服务器对服务器的调用。本系统内部有一些对于其他底层业务接口的依赖,架构如下图,为了简化说明,服务端mobile Api层简称服务端:

 架构师03-业务、路由与监控统计的设计_接口设计

理想状态:以网络为界,对上层服务端需要记录来自上层的每次调用,对后面的业务接口记录每一次对业务接口的每次调用,即:客户端上A业务是通过接口B调用的,而A接口内部逻辑实现中有有对于业务方的接口调用C,D。

接口B的记录是通过http请求到达服务器后access日志进行统计的,即nginx的访问日志。如:http://api.example/user/b,http://api.example/后面的部分即为B接口的掉用的统计。同时记录下时间,状态(200,5xx,4xx等),请求时间(这里的时间只代表nginx层转发到业务逻辑层,业务逻辑层执行所需要的时间,不代表app客户端统计的时间)。

架构师03-业务、路由与监控统计的设计_客户端_02

而对于业务层接口的调用是记录每一次业务日志调用,包括:执行时间,接口名字(红色1),状态,uid,来自客户端的接口A名字(这很重要)等。

架构师03-业务、路由与监控统计的设计_业务监控的内容_03

这样,上面日志经过采集工具,例如如flume,rsyslog等再经过聚合计算落地等即可成为监控的数据。

架构师03-业务、路由与监控统计的设计_业务监控的内容_04

上图的监控样式图,极为清晰的展示这个业务对应的接口,外部调用服务端接口的情况和这个接口依赖的业务接口调用情况。

 代理接口cardsApi的使用 

查看手机App,你会发现某些app的业务分成几类:1. 定制化的一级页面,2. 定制化的二级页面,3. 通用化的二级页面。这里讲的是通用化的二级页面。我们在应用中建立起了一套通用的业务协议称之为卡片Card。在业务上定义返回相同格式Card1, Card2... CardN,不同的业务通过业务号进行区分,这实际上是个代理模式。

架构师03-业务、路由与监控统计的设计_接口设计_05

即客户端上各个页面相似的业务,调用服务端上的卡片列表接口cardsApi,传入不同的业务号businessId,然后cardsApi通过配置中的map列表找到对应的业务进行转发,在业务方返回数据后再进行必要的格式化,业务日志的记录等。根据业务的复杂度,有些是经过cardsApi内部深度加工的,有些没有进行内部深度处理。这里提到“透传“二字,其实我不太喜欢这个词,好像cardsApi什么也没干,只是向后面转发了一样。其实一些基础的工作还是要靠这个cardsApi进行处理的。比如:鉴权,来自外部的请求是需要鉴权的,通过校验,只让合法的请求通过,把外部的sessionid转成内部的登陆用户uid等内容,所以我认为“全内容业务接入”比较合适。

 特定场景的监控统计 

这个方法对于产品业务侧是友好的,在过去的几年中通过统一的内容接入极大的提高了生产力,但对于常规监控来说要求更高:业务区分在服务端项目路由到cardsApi接口层后的更细一层,所以不能使用常规的access接口统计去区分每个业务的调用情况,需要在接口内部做特定场景的统计实现,这样就会存在接口的通用日志和能够区分业务的统计监控日志。缺点会造成记录双份的结果,资源的额外使用。需要运维去额外评估成本以支持业务。如果是资源紧缺的项目无疑是困难的。

 通用的接口转专用的接口 

即然无法在现有的资源下实现cardsApi内部业务区分的接口监控统计,而现实中的业务又需要统计怎么办?转通用接口为专用接口,例如:D业务比较重要,那么我们就不通过cardsApi提供服务,通过DcardsApi进行专项业务专门的内容处理,内部再进行一个转发到cardsApi模块里,此时就可以通过access日志进行统计了,当然这样只能够处理一些重点的业务的权宜之计,如果是通用的统计还应该做单独的业务模块级监控统计。

程序内跳转的举例

架构师03-业务、路由与监控统计的设计_接口设计_06

如果一个业务模块存在多个业务接口的重定向,而不知道所有的调用入口,就会造成对业务模块的统计认知不足。即我开头提到的case,我不能从现有的接口统计中看到我的模块的实际调次数,因为有其他的接口也重定向到了这个模块,以至于如果模块内接入新的接口业务的时候,会造成估计不足的被动局面。怎么破局?这需要严格管理接入的接口,用版本,内容条件等限制接口的调用。不给其他接口随意重定向的机会。如果需要,做新接口接入流程化处理,让相关干系人知道这个事情,以便加上必要的监控和说明。

 restful设计规范的使用 

这是在跟某个业务方联调时,出现的case。之前我们的业务不管是对外提供的接口,还是业务方的接口都没有推广引入restfulAPI的设计理念。restfulAPI是目前比较成熟的一套互联网应用程序的API设计理念,也在互联网应用了很多年了,没有问题,这是无疑的,但在我们的统计框架下就不太合适。 restful会把一些资源id内容等可变部分放入url中,如:http://api.example.com/detail/12?from=abc,即 detail/12会成为接口统计聚合的部分。而12又是可变的内容,所有原有的统计方式如果不进行特例处理,就会出现无法聚合统计的局面。我们之前碰到过一次,导致增加了多个索引,磁盘直接打满的局面。后来只能临时对这个接口进行临时策略,或单独处理或抛弃统计。

如果依赖第三方合作的接口使用了restful设计模式,而业务上又无法规避。那么只能针对接口进行特定分析处理,即做特例。如果公司内部可约定的,还是根据现有的统计设计方案进行选择合适的设计方法为上策。

 rpc框架yar的使用 

早些时候我们的项目对内部其他部门提供了内网服务的接口,因为项目是php语言,所以引入了yar对业务提供服务。以串行为例,我们先看下调用方法:

客户端调用:

// 客户端调用
$client = new Yar_Client("http://api.example.com/api/abc");

$result = $client->$api("parameter);

服务端实现,需要在controller /api/abc里面实现(/api/abc对应的是常规http请求路由后的controller类):

$class = Yar_True_Model_Class;
$server = new Yar_Server(new $class);
$server->handle();

因为是在之前的客户端提供的服务中增加少量的接口,所以共用的一个项目。

架构师03-业务、路由与监控统计的设计_客户端_07

 yar项目监控统计 

上述调用方式能用常规的access接口调用方式统计么?答案是不能,业务差别在服务端的url上反应不出来,而是粒度更细的模块级别。首先看调用方client函数,它调用的是同一个接口么?是,业务1和业务2通过不同的方法进行区分。如果想要做统计,在调用远程函数的地方加上调用统计,这个统计不是开头的常规调用统计。再来看下服务端,服务端在路由层知道调用哪个接口么,知道。但是/api/abc只是个rpc服务的公共是rpc服务的一个公共接口,业务粒度在Yar_True_Model_Class里面。在controller层知道哪个业务函数模块被调用了么?不知道,原因是路由被封装在了Yar_Server里面。我们在往下看,在Yar_True_Model_Class里面函数里面知道自己被调用了么?这里知道。只有在Yar_True_Model_Class里面加统计才能知道被调用了多少次,而这个统计需要额外的技术和成本支持。

 设想的改进方案 

即然上面的业务落实到应用中是接口下面的模块统计而且需要额外的统计成本,那么是否可以优化呢。剑走偏锋,应该可以,虽然落地方式有些不符合yar的最初设想,这里列举出我认为可以尝试的方,例如:将模块转成接口url,因为我们有接口的统计资源,所以可以利用接口的统计进行模块的统计。例如上面的内容,服务端可以提供两个不同的url,分别给业务1和业务2进行调用:http://api.example.com/api/abc/api1 和http://api.example.com/api/abc/api2,这样就可以进行统计,不过服务端可能有接口的冗余。

 集中式和分散式取函数的方式 

说到yar的使用,不得不说我们在某个系统中使用过程中遇到的一种情况。即集中式和分散式的取参数方式,某一版本我们是在controller层取得参数的,来自yar的请求和来自客户端的请求分别进入不同的controller,在controller层,根据不同的方法进行初始化参数。普通的请求通过_GET,_POST等http常用的方式解析,而yar的请求只能根据业务约定获得参数。比如:约定方法里有个params参数,所有的业务参数都放在这里,如果通过_POST获得参数会产生什么结果,答案是可能解析出来内容,但不是自己期望的内容。是不是很诡异。原因就在于,yar的协议是在http协议之上的内容协议。上层的应用当然应该用上层的协议去解,如果使用下层的协议去解包,一定会产生意外的情况。

V1取参数的版本

架构师03-业务、路由与监控统计的设计_接口调用_08

后来的一个版本不是用上面的方式初始化参数,而是各处采用一个全局函数的方式去取得函数。其中:Request::params()  里面是根据_GET,_POST等获得参数的,我们在业务实际运行过程中真的碰到了意想不到的结果,以至于不得不在rpc controller中特例处理下基本参数,以满足结果的正确性。所以,我们还是应该根据项目实际情况去选择合适的架构模式。

V2取参数的版本

架构师03-业务、路由与监控统计的设计_客户端_09

 变通的对外服务接口设计 

最后再聊一个case:某天,A君想开发了一个接口B,目的是给C业务方用,运行了很长时间后,D业务想接入,那么A直接把B接口给D么。如果你是普通的开发者,直接给就行,但我想告诉你,你最好考虑一些其他因素。比如,D的业务规模多大,统计上是否可以实现B接口对于C和D的区分,如果D对于这个接口调用量很大,是否要对D限流?如果你有接口内按照业务的统计,提供相同的接口是没有问题的。如果你只有接口层的access统计,那么你的落地方式不如直接提供个D专门使用的接口,内部做个重定向,成本是有,但是很低,至少能达到目的。

架构师03-业务、路由与监控统计的设计_接口调用_10

当然,如果你的业务本身是个基础业务部门,上层用户非常多,而对于内部做了同一个接口内不同业务方做了比较完善的监控统计,有足够的资源保障,那么你可以随心所欲的朝着公共平台接口方向提供服务,当然,业务方接口token设计,限流熔断等降级策略的实行也是必须考虑的。

  总之,我们在开发过程中,不仅仅要实现功能。更要对实际场景进行全方位的考虑。相比于功能开发,更要关心,资源,时间,运维,风险等一系列的内容。正如我之前在内部分享的,开发者要考虑业务复杂度,数据规模,时间成本,质量内容,运维等等。一切开发模式的落地的方案都是各方面折中的结果