【分布式】服务调用的七种模式

  1. HardCode模式

    有些研发直接把要访问的后端服务和端口写死在代码中,这种情况可以让该研发去面壁思过三天,不讨论了。

  2. 配置文件模式

    这是最通行和最简单的模式,无论是后端web服务、还是mc、mysql等等,我们都可以配置在一个配置文件中(ini或者conf)等等。但后端服务发生变化了怎么办?此时运维要去做一次发布,增加了运维变更成本。如果故障了怎么办?会直接增加故障影响时长。配置文件是一种静态管理模式,需要对配置进行操作修改,带来的成本都是很大的。

  3. 类LVS模式

    你一定见过很多内部服务都是走四层LVS模式,有些更可能走七层代理(nginx+keepalive),反正在我维护的业务中,我是严格拒绝这类业务请求。为什么会这样?我把它转换成几个问题:

    A、引入一个组件是否就意味着架构变得复杂了?他的可靠性怎么保证?;

    B、四层和七层代理模式所能探测的异常,是否和业务异常一致的?在四层情况下,四层正常不代表七层是正常的;在七层模式下,还有服务内部的异常情况,比如说服务器过载、socket句柄耗尽等等。

    C、代理模式作为一个单点存在,特别对于包量和流量性服务来说,是否意味着是瓶颈;

    D、当一个七层服务异常的时候,除了运维干预就别无他法了,这个时候运维人是可靠的么?运维还在下班路上故障怎么办?V**有异常怎么办?windows系统蓝屏没法打开怎么办?都会影响故障的处理。

    在内部服务调用上,我建议忘了它。

  4. DNS模式

    太经典的模式。DNS是公网访问的常见模式,由于是标准协议,实现起来方便快捷。但依然是问题多多,

    A、DNS服务发现模式粒度太粗,只能到IP级别,端口依然需要自己去管理;

    B、对于后端一个异常服务来说,DNS无法提供自动容错的能力,此时便需要运维的参与,特别在设置了TTL(10S)的情况下;

    C、DNS没法实现服务的状态收集,这部分信息反过来是可以为运维提供指导的;

    D、DNS是一个静态的资源发现,DNS指向的变更都需要依赖工具来完成。

    不过DNS还是有创新应用的地方,只是你我不知道,后面有个方案会介绍到。

    DNS只能解析。。

  5. 总线bus模式

    在以前很多SOA服务架构中,经常提到的是BUS总线架构,特别是在对历史遗留系统的整合中,也常提及这种模式,比如说以前的CORBA架构。看似服务透明且解耦,并且服务动态可扩展,但依然问题多多。

    A、对总线的服务能力是个严峻的考验,海量的服务请求进入到总线,意味着中总线需要海量的处理能力;

    B、总线还需要有QoS的服务能力,对不同的服务需要有不同的服务质量保证;

    C、总线的高可用,如果很集中,则需要总线发布一致性保证,如果按照业务隔离总线,此时则对运维服务管理又是个负担,太多的总线和业务做了绑定和耦合。

    在前不久和一个金融IT系统的人交流,他们早期采用的总线架构,后续逐渐下线掉,也都遇到了我说的几个问题。

    总线很忙。。

  6. 类zk模式

    自从google的chubby介绍一出,在不久之后便有了开源实现—zookeeper。zk基于zab强一致性协议来实现,而chubby是基于paxos协议来实现的。为了实现zk的高可用,其集群必须是奇数个节点(3、5、7),一般是建议5个。奇数个节点有利于leade选举,偶数个节点是无法公平投票的。由于集群强一致性,当一个写请求递交到zk集群中,此时可以保证写入到所有节点中,那么对于读来说,读任何一个节点取得的结果是一致的。类似流程如下:

    在这里插入图片描述

    zk有很多种使用场景:配置管理、名字服务、产生唯一序列号、锁、权限控制等等。但也有问题,强一致性的要求,对性能有着很大的影响,其次zk集群对于很多小业务来说,显得太重了。

    不过ZK有个很不错的地方,最小粒度的反向通知机制。当client attach的node发生变化的时候,zk cluster会反向事件通知到client,从而更新节点的信息。相比long-polling或者心跳机制来说,可以减少很多无效的消息通知。其次这种细粒度的消息控制,可以进一步减少消息发送。

  7. 类etcd模式

    这是基于最终一致性协议RAFT的go语言实现,目前是docker容器服务注册和服务发现的标准实现。raft协议相比zab或者paxos来说,简单多了,并且容易理解,网上也有非常多的基于raft的开源实现。它有个不是缺点的缺点:很多基于raft的实现都是非稳定版,不好直接线上直接使用。docker中的etcd的实现示意图如下:

    在这里插入图片描述
    raft协议是一个非常轻量级灵巧的协议,值得大家深入看一下,无论是运维或者研发,我对它的关注是因为etcd。很不错的动画演示【http://thesecretlivesofdata.com/raft/】,另外raft官网现在又很多不同语言的实现,具体请参考【https://raftconsensus.github.io】。

名字服务中心要解决的核心问题

1、我能访问的服务实例是…

这个问题分解成两个小问题,第一、服务注册,对外宣告某服务的实例有哪些?第二、服务发现,我能访问哪些服务,通过某种方式找到这些服务实例。

2、我最应该访问谁

同样分成两个小问题,第一、我有权限访问么?第二、我能访问的最优服务实例是?

3、当我访问的服务实例有问题怎么办

这是服务的容错要求,当后端服务实例出现问题的时候,需要有一个机制对问题服务实例进行自动剔除处理,避免人为干预。

名字服务中心的业界实现

1、SmartStack

SmartStack是Airbnb开源的一个自动注册和发现的框架,目前已经在github上开源,其核心服务组件:Nerve和Synapse,Nerve,用于服务注册,Synapse,用于服务发现。其实现非常简单,并且巧妙的利用了haproxy的代理能力,可以作为TCP和HTTP服务的代理,架构图如下:

在这里插入图片描述
A、Nerve

负责对后端的服务进行健康监测,并注册到zk集群。后端拨测的服务由两种,一种是http服务、另外一种是TCP服务。http服务采用标准的方法,每个服务都需要暴漏一个/health的协议出来,供Nerve直接调用,返回200则算正常,其他则算异常。TCP协议的服务健康检查,则可以由研发自定义。拨测通过的服务,则放到zk的节点池中,供后续的服务发现组件使用。

B、Synapse

服务发现组件,从zk集群中获取可用服务结果,产生haproxy进程,业务请求每次经过haproxy,此时haproxy做tcp或者http代理进行请求路由。

缺点:

A、拨测灵敏度如何与业务同步。如果以分钟和秒级的服务要求来看,此时服务的拨测灵敏度则要求非常的高,必然会产生大量的无效请求到后端服务。并且这种粒度还需要随着业务的要求变化而变化,必然说一个存储服务,可能灵敏度要求则非常的高。

B、拨测的结果如何真实反应业务结果。业务health检查正常,并不代表业务正常。业务的正常与否是有业务请求自身决定的。

C、ZK会成为一个瓶颈。随着集群规模越来越大,nerve和synapse都需要zk集群进行交互,此时zk集群比如成为瓶颈。

D、应用application缺少自决策的能力。application的业务访问结果是最好的决策依据,而该方案恰恰忽略这个数据,把决策依据交给了外围组件的一些旁路数据。

2、Eureka

Eureka是Netflix开源的一个名字服务架构,定位在中间层服务的负载均衡、容错服务架构上。因为Netflix的服务是运行在AWS上,我们都知道公有云没有这块的服务能力(公有云提供的都是类LVS的服务)。其架构图如下:

在这里插入图片描述

A、Eureka Server

接受服务的注册、更新、下线、管理等等。服务上线后通过心跳来维持与server的连接,当心跳丢失,此时自动剔除服务。

B、application Client和application service client

根据Eureka的API要求,自动向Eureka Server完成服务的注册、上线、下线等生命周期的自管理能力。application Client同时根据获取到的服务实例,发起远端请求。服务的注册是在appliction的代码中嵌套Eureka的API自动完成。

Eureka只提供服务注册和服务发现的能力,具体的负载均衡能力是由netflix另外一个开源项目Ribbon完成。Eureka server如果发生异常,此时client启动自我保护机制,类似使用本地的cache提供服务能力,不影响线上服务运行。

3、Serf

Serf是一个有意思的项目,目的就是打造一个真正的通用性名字服务平台,从解耦的角度(不含调度能力)来说是完全可以实现的。它从开始的设计目标就提出了去中心化、容错且必须高可用和轻量化。

当前的Serf最新版本是0.6.4,应该还是一个演进中的项目,感觉国内青云的公有云P2P服务器故障检测平台和这个项目的实现原理类似。该项目完全是基于去中心化协议gossip设计完成,gossip协议目前在mangodb、Cassandra集群中都得到实际验证和使用。

Serf会在每个机器上运行一个agent,agent注册member的可以带上name,同时也会带上服务标签的标识,这是服务的属性,形如:

在这里插入图片描述
同一个agent可以带上多标签的标识,标签内可以包含角色、数据中心、IP和端口等等。接下来gossip协议会负责把这个信息在集群内进行传递。机器故障,自然也是gossip协议自动化剔除处理。具体的gossip协议内容,请自行检索阅读。

可以基于Serf的RPC协议标准和格式,封装一个远程访问包,获取服务实例信息。具体的请参考go语言的实现【https://github.com/hashicorp/serf/blob/master/client/rpc_client.go】。它的api文档不是太好,有些要去看其cli的实现。

4、Spotify

Spotify 使用的是DNS来做服务发现,而不是引入一个新的技术方案。之前说过DNS实现有优雅的方案,就是说它。它巧妙的使用了DNS中的SRV记录,你看看DNS中的SRV记录便知他为什么能做到这点,如下:

在这里插入图片描述

接下来客户单只需要使用一些DNS库进行封装即可完成服务发现及其后续的调用操作。虽然使用了一个已有的标准化协议,但问题依然不是,这个DNS记录是一个静态记录,只能简单的根据权重来进行负载均衡。其次DNS始终是个无状态的服务,后端服务的异常没法容错或者负载均衡。

Spotify是完全依赖DNS的一个实现,我们都知道DNS是依赖静态配置文件,因此算是一个静态集群。不过DNS的巧妙应用,也算是其平台的一大特色。

5、NSQ

NSQ是有Bitly用go语言开发的去中心化、分布式实时消息队列。其实NSQ本来不是作为服务发现和服务注册系统来设计的,不过Bitly结合自己的业务需求增加了一个组件(Nsqlookupd)使之满足了服务发现和服务注册的功能。如果把服务注册当作消息发布,把服务发现当做服务订阅,倒是和消息队列有相似之处。NSQ由三部分组成:

Nsqd是一个daemon程序,负责接收消息、分发消息给客户端。

Nsqlookupd是一个Daemon程序,负责管理top视图信息,并且提供最终一致性的服务发现能力

NsqAdmin是一个webUI的管理端

Nsqlookupd是一个非常重要的程序。客户端每次想Nsqlookupd发起请求,通过http接口查询某个主题的Nsqd的生产者,而Nsqd节点负责向其广播消息topic和消息channel的信息。该系统中有两类接口:TCP接口被用来接受Nsqd的广播请求,而http请求负责提供给客户端做服务发现和管理动作。具体的请参见【http://nsq.io/components/nsqlookupd.html】

6、SkyDNS

SkyDNS是基于RAFT协议构建的一个名字服务,同时提供基于HTTP和DNS协议的访问呢机制。服务注册的时候,client端发起HTTP请求把服务以SRV记录的形式注册到skyDNS。注册成功后,服务需要周期性的进行心跳交互,向服务端上报自己的状态。etcd依然是最底层的服务发现,是有skyDNS和etcd交互获取服务注册的结果,然后写进skyDNS以SRV记录存在。对于服务发现,此时可以通过DNS协议向服务器组发起访问,获取相关的DNS SRV记录。

SkyDns支持服务的动态注册,这点和Spotify有所不同,它集成了zk和etcd的动态能力。该服务平台同时柔和了DNS和RAFT技术来构建服务中心,属于奇思妙想类。

7、腾讯L5模式

名字有点怪,不知道为什么叫L5?(后来求证到是5个9的意思)L5在腾讯某部门大面积的使用。具体的L5架构如下:

在这里插入图片描述
L5容错组件包含DNS Server和L5_Agent两个部分。

L5_Agent对后台服务群提供容错访问,并在此基础上对后台服务群进行过载保护,避免后台服务器因访问量大,而引发故障.或雪崩效应。CGI或者Server通过API与L5_Agent通信,完成上报、获取。是个旁路服务组件,业务访问路径无需经过L5 agent。

L5_DNS Server根据配置完成IP路由下发,保证所有L5_Agent获取到最新、最完整的路由信息,在本地有个DNS agent接收,存放共享内存区域,L5agent可以通过共享内存获取。

L5 agent里面还有一个policy agent使用时间片内的访问作为统计单位,时间片内所有访问的平均延时、成功率等信息作为负载和容错作为下时间片内请求参照。

A、业务请求路径

1)app或者cgi向L5 agent发起请求。

2)L5 agent根据之前时间片内的服务访问(延时、失败)情况返回最优的后端服务实例。

3)app或者cgi向该实例发起业务请求。

4)请求完成后,app或者cgi把访问的状态信息上报给L5_agent

5)L5_agent继续根据时间算法进行运算确定下次访问的最优实例。

B、名字服务请求路径

两种更新路径:dns server如果有实例发生变化,此时会触发下发变更操作;dns agent每次重新启动,都会和dns server发起请求,确定配置版本号,确认是否需要更新。

8、我们的实践

后面就开始详细介绍,姑且用代号“SkyName”来代替,但内部不是这个名字哈。

五、SkyName的设计原则

其实对于大多服务设计来说,所遵循的原则是一致和类似的,比如说高可用、可扩展等等。当然在这种核心服务上,可扩展、高可用性几乎是一个通用的要求,那么对于名字服务中心来说,有自己的设计原则,如下:

1、简单

对于很多业务新架构来说,确保服务接入的可平滑性是一个项目成功的关键。在我们名字服务中心这个项目上,也是牢牢的记住了这个准则。这个简单体现在两个方面,首先旧业务接入简单,开发无需修改代码完成(当然对于你们不是哈);其次新业务接入也非常简单,只需要按照格式注册且宣告服务即可,其他就由服务中心统一接管服务的整个生命周期管理。

2、高可用

此时需要两种方式来确保高可用:

A、去中心化。对于一次正常的业务调用来说,名字服务可以本地缓存的机制,降低对中心节点的依赖,而对于选择最优的调度节点决策也不应该依赖中心去完成,可以由client自己决策,进一步降低对中心点的依赖。

B,避免人的参与。人是不可靠的,可以看看一个结果【引用论文Recovery Oriented Computing Motivation, Definition】

在这里插入图片描述

在重大节假日期间,因封网会禁止变更恰恰是故障的最少时期,这是我们的最直接经验。另外我们一定有过rm删错文件、配置文件修改错的可怕运维经验。

3、安全性

传统的服务之间调用授权都是通过被调用服务端来控制的,此时则暴漏了两个问题,调用acl权限规则是静态化,配置文件中,因此它的安全性没法保证,容易被人获取信息,比如加密秘钥;其次静态的规则,也不利于安全的动态变更,就如操作系统需定时更换root密码一样。因此我们对服务的调用准入做了严格的控制,服务之间的访问必须通过授权才能访问,授权之后的服务,会获取到被掉用方颁发的一个Access Token,利用这个Token才能进一步访问,并且这个AccessToken可以任意时间更换。

4、容灾容错

skyName必须能满足业务的智能调度的需求,在服务出现异常的情况下,调用端能够自动容错或启用过载保护,这符合“前端保护后端”的设计思想。

六、SkyName的功能目标

SkyName在设计之初就设定了它的明确目标,具体分为四大功能,这四大功能覆盖了名字服务的全生命周期管理、调用管理、权限管理、服务反馈等等。

1、名字中心

统一接管服务注册和服务发现,并且是越自动化越好。在第一期我们选择的还是人工注册的方式,在第二期我们要实现自动注册,彻底实现对名字服务的生命周期自动化管理。在我们当前业务中,业务的调用都是http协议,通过正常的DNS接口获取服务实例,但名字服务上线之后,我们会把DNS+URI映射服务实例集群上,IP地址和端口对应的服务名后由名字中心提供。

2、调度中心

服务之间的调度必须提供容错和过载保护的能力,并且这个能力需要独立于名字服务中心存在,把这个决策能力下放到客户端自决策。动态请求如果每次都经过服务中心,必然服务中心的压力非常大。因实现静态名字服务产生的价值和收益甚小,必须加入负载均衡的能力,并且要确保对研发的程序透明。

3、鉴权中心

我们都知道被调用服务需要加入ACL的权限控制(系统级别、业务级别),确保接口和数据安全。我们这个安全分两个级别,第一个是机器级别,类似iptables的机制,不过这个IP过滤的能力是在程序级控制的;其次是服务级别,对接口的数据进行对称加密控制,主被调服务两侧约定加密的方法(MD5、CRC)和秘钥(类似干扰码),服务端接收到请求之后,则服务被服务正常接收和处理。

4、服务反馈

所有的服务访问和调用结构都需要上报到服务中心,供运维监控和运维分析使用。由于接入了统一调度系统,服务在调用流上染色成为了可能,为故障定位提供了快捷方便的能力。在宏观层面上,服务之间的调用情况,需要上报,便于整体把握服务的运行状况。比如说A机器访问后端的服务实例延时和错误率如何?最后根据这份调用视图,可以生成详细的业务拓扑视图,拓扑视图的生成可以进一步打通自动化、数据化的运维体系。

七、SkyName的架构

整体架构示意图如下:
在这里插入图片描述

1、架构设计要点

A、application端无agent设计

在application侧,我们没有放置任何agent,完全是通过client api来完成这些服务的注册、更新和管理。利用一个hash容器存储最近分钟之内的请求情况,用于后续的请求分配依据。

B、服务中心服务端无状态设计,无奇数节点要求,可平行扩展

考虑到名字服务中心的服务量,我们采用了轻量级的方案,采用了类似业务技术架构—前端WEB+缓存+DB。一致性的要求全部由数据库底层保证。对于DB的高可用,我们使用了另外一套高可用机制,可以做到平滑迁移、升级和容灾切换。

C、名字服务集群、统计分析集群分离设计

名字服务集群访问、读写模型都不同于调用统计分析集群,在业务上做了集群的分离,确保压力分散,保证核心服务。

D、业务访问路径旁路化设计

业务访问不经过名字服务中心,获取服务实例之后,直接向后端服务实例发起请求。由于我们的请求是http协议,使用了一个http柔性调度框架,访问的结果也是由框架自动收集。业务层面的调用遵循简单的API要求,和http协议调用一样简单,做到彻底的无侵入。

2、调度算法

调度算法是客户端自决策的算法,具体实现细节如下:

A、服务降级

app client根据错误数和错误率来进行降权。在实现中是根据配置文件指定的一个阈值,两个配置项是“或”关系。接口异常分两种情况:一种是接口访问延时;另外一种是接口访问连接不上。所以可以看到,降权的对象是接口,而非机器级别。

失败和失败率的检测一定是周期内的,一般是分钟级左右。

B、重连机制

在接口被置为不可用之后,需要有一个方式确定接口已经恢复正常。常见的策略就是周期性检测(时间策略)和用户请求抽样检测策略。前者可以防止访问量很小的情况,接口能够正常恢复。后者是针对大事务请求,抽样用户请求去检测接口,如果检测接口状态正常连续达到数量,则恢复接口对外提供服务。

C、算法缺点

接口级服务降级粒度过大,需要做到服务实例+接口级别;动态调度没有考虑服务实例间的延时因素,从而确定请求基于服务实例间的分发策略,当然服务中心提供了静态权重(类似wrr),但还是不够。

八、SkyName的运维收益

1、自动化

后面至少服务的上下线可以自动进行,不需要人工干预。但我一个服务接入到线上服务时候,此时就是把一个程序包部署上去,然后直接调用名字服务中心的接口置为启用就可以了。如果经历过很好的测试,其实和名字服务中心之间的交互都可以不要。

同时服务之间的秘钥颁发也不依赖配置文件了,工作流程大大简化,所以在之前我强调了服务中心对运维自动化的简化能力。

2、架构透明化

所有的服务关系的调用都统一上报到名字服务中心,名字服务中心,可以做拓扑视图的构建。另外基于http协议,我们可以无侵入的增加http header头,做服务染色,进一步绘制服务之间的调用关系,最终完成端到端的服务调用关系绘制。

我们都知道,拓扑视图对于小的架构来说,一般都是手工维护,而对于频繁变更或者大的服务架构来说,根本就无法维护。而运维的故障分析、变更都都需要参照这个拓扑视图。

3、服务负载均衡

服务调度通过服务中心提供的api进行,服务负载均衡能力都在底层接管完成。也是根据接口访问的延时和connection的连接情况来做实时的请求分配,这个和L5的算法类似。提供几个权衡指标,超时和连接中断,然后确认失败率是否超过阈值,过阈值则直接做异常节点处理。

4、运维监控和分析

我们不是为了酷而去做这个事情,是真正切切的觉得有运维收益。试想一下我们把底层所有的服务调用数据都采集上来了,一则我们可以做服务监控,二则我们可以做服务状态DashBoard,同时还可以提供给自动化变更服务使用。具体视图如下:

在这里插入图片描述
在视图中,我们知道每个调用的接口延时和失败情况,做到真正业务流可视化。

九、SkyName的未来

1、有状态服务调度

当前我们还不做有状态服务的调度控制。对于业务架构来说,运维是先希望服务去状态化,然后再接入服务中心。比如说类似redis和mc服务,我们都希望中间接入proxy代理,把代理作为服务节点接入到服务中心。但依然还存在一些HASH的分区要求,需要在未来的版本规划中解决。

2、服务中心SDK化

可以提供更多的sdk给其他业务使用,比如说PHP业务,erlnag和其他非标准JAVA服务框架的服务等等,确保这个名字服务可以成为跨平台的标准接入组件。

3、跨IDC的服务调度

当前我们的服务中心是独立IDC的部署,特别对于双中心的业务来说服务之间的调度通过调度中心物理部署隔离的方式来实现调度的隔离。其实可以根据一些IDC、机架等就近访问的规则来设置调度策略,规划在未来的版本之中。这个方案也类似之前在SkyDNS提到的tag概念。

坚持一个判断:在一个分布式系统中,没有名字服务中心调度实现的架构都不是一个好架构。

发布了439 篇原创文章 · 获赞 14 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/LU_ZHAO/article/details/105531029
今日推荐