服务调用的演进历史
这是2019年给组内分享时整理的一篇服务调用演进历史的科普文,微信公众号上首发于”高可用架构“公众号,做了一些调整重新发在我自己的公众号,最下方的”阅读原文“可以跳转到博客原文。
写作本文的时候,我自己最大的感受是:如果能清楚理解演化历史中的一些原则和思路,就会发现现在的变化并不新鲜。它们不是今天才有,也不会止于今天的演化。在技术大发展的今天,更多的关注本质才能让我们不至于在变化中失去方向。
服务的三要素
地址:调用方根据地址访问到网络接口。地址包括以下要素:IP地址、服务端口、服务协议(TCP、UDP,etc)。 协议格式:协议格式指的是该协议都有哪些字段,由接口提供者与协议调用者协商之后确定下来。 协议名称:或者叫协议类型,因为在同一个服务监听端口上面,可能同时提供多种接口服务于调用方,这时候需要协议类型(名称)来区分不同的网络接口。
IP地址提供了在互联网上找到这台机器的凭证。 协议以及服务端口提供了在这台机器上找到提供服务的进程的凭证。
http:指明使用的是哪种应用层协议,同类型的还有“https”、“ftp”等。 www.abc.com:域名地址,最终会由DNS域名解析服务器解析成数字的IP地址。 8080:前面解析成数字化的IP地址之后,就可以访问到具体提供服务的机器上,但是上面提供服务的进程可能有很多,这时候就需要端口号来告诉协议栈到底是访问哪个进程提供的服务了。 hello:该服务进程中,可能提供多个接口供访问,所以需要接口名+协议(即前面的http)告诉进程访问哪个协议的哪个接口。 msg=world:不同的接口,需要的参数不同,最后跟上的查询参数(query param)告诉服务请求该接口服务时传入的参数。
服务实例:服务对应的IP地址加端口的简称。需要访问服务的时候,需要先寻址知道该服务每个运行实例的地址加端口,然后才能建立连接进行访问。 服务注册:某个服务实例宣称自己提供了哪些服务,即某个IP地址+端口都提供了哪些服务接口。 服务发现:调用方通过某种方式找到服务提供方,即知道服务运行的IP地址加端口。
基于IP地址的调用
IP地址是难于记忆并且无意义的。 另外,从上面的服务三要素可以看到,IP地址其实是一个很底层的概念,直接对应了一台机器上的一个网络接口,如果直接使用IP地址进行寻址,更换机器就变的很麻烦。
域名系统
协议的接收与解析
协议包头:这里存储协议的元信息(meta infomation),其中可能会包括协议类型、报体长度、协议格式等。需要说明的是,包头一般为固定大小,或者有明确的边界(如HTTP协议中的\r\n结束符),否则无法知道包头何时结束。 协议包体:具体的协议内容。
使用json或者xml这样的数据格式。好处是可视性强,表达起上面的复杂类型也方便,缺陷是容易被破解,传输过去的数据较大。 自定义二进制协议。每个公司做大了,在这一块难免有几个类似的轮子。笔者见过比较典型的是所谓的TLV格式(Type-Length-Value),自定义二进制格式最大的问题出现在协议联调与协商的时候,由于可视性比较弱,有可能这边少了一个字段那边多了一个字段,给联调流程带来麻烦。
使用proto格式文件来定义协议格式,proto文件是一个典型的DSL(domain-specific language)文件,文件中描述了协议的具体格式,每个字段都是什么类型,哪些是可选字段哪些是必选字段。有了proto文件之后,C\S两端是通过这个文件来进行协议的沟通交流的,而不是具体的技术细节。 PB能通过proto文件生成各种语言对应的序列化反序列化代码,给跨语言调用提供了方便。 PB自己能够对特定类型进行数据压缩,减少数据大小。
服务网关
如何检查这些实例的健康情况,同时在发现出现问题的时候增删服务实例地址?即所谓的服务高可用问题。 把这些服务实例地址都暴露到外网,会不会涉及到安全问题?即使可以解决安全问题,那么也需要每台机器都做安全策略。 由于DNS协议的特点,增删服务实例并不是实时的,有时候会影响到业务。
负载均衡功能:根据某些算法将请求分派到服务实例上。 提供管理功能,可以给运维管理员增减服务实例。 由于它决定了服务请求流量的走向,因此还可以做更多的其他功能:灰度引流、安全防攻击(如访问黑白名单、卸载SSL证书)等。
做为四层负载均衡的LVS
Direct Server(以下简称DS):前端暴露给客户端进行负载均衡的服务器。 Virtual Ip地址(以下简称VIP):DS暴露出去的IP地址,做为客户端请求的地址。 Direct Ip地址(以下简称DIP):DS用于与Real Server交互的IP地址。 Real Server(以下简称RS):后端真正进行工作的服务器,可以横向扩展。 Real IP地址(以下简称RIP):RS的地址。 Client IP地址(以下简称CIP):Client的地址。
使用VIP地址访问DS,此时的地址二元组为<src:CIP,dst:VIP>。 DS根据自己的负载均衡算法,选择一个RS将请求转发过去,在转发过去的时候,修改请求的源IP地址为DIP地址,让RS看上去认为是DS在访问它,此时的地址二元组为<src:DIP,dst:RIP A>。 RS处理并且应答该请求,这个回报的源地址为RS的RIP地址,目的地址为DIP地址,此时的地址二元组为<src:RIP A,dst:DIP>。 DS在收到该应答包之后,将报文应答客户端,此时修改应答报文的源地址为VIP地址,目的地址为CIP地址,此时的地址二元组为<src:VIP,dst:CIP>。
做为七层负载均衡的Nginx
负载均衡:希望在这个反向代理的服务器中,将请求均衡的分发到后面的服务器中。 安全:不想向客户端暴露太多的服务器地址,统一接入到这个反向代理服务器中,在这里做限流、安全控制等。 由于统一接入了客户端的请求,所以在反向代理的接入层可以做更多的控制策略,比如灰度流量发布、权重控制等等。
upstream hello {server A:11001;server B:11001;}location / {root html;index index.html index.htm;proxy_pass http://hello;}服务发现与RPC
域名系统解决了需要记住复杂的数字IP地址的问题。 PB类软件库的出现解决协议定义解析的痛点。 网关类组件解决客户端接入以及服务器横向扩展等一系列问题。
DNS服务发现的粒度太粗,只能到IP地址级别,而服务的端口还需要用户自己维护。 对于服务的健康状况的检查,DNS的检查还不够,需要运维的参与。 DNS对于服务状态的收集很欠缺,而服务状态最终应该是反过来影响服务被调用情况的。 DNS的变更需要人工的参与,不够智能以及自动化。
服务发现系统:用于提供服务的寻址、注册能力,以及对服务状态进行统计汇总,根据服务情况更改服务的调用情况。比如,某个服务实例的响应慢了,此时分配给该实例的流量响应的就会少一些。而由于这个系统能提供服务的寻址能力,所以一些寻址策略就可以在这里做,比如灰度某些特定的流量只能到某些特定的实例上,比如可以配置每个实例的流量权重等。 一套与该服务系统搭配使用的RPC库,其提供以下功能: 服务提供方:使用RPC库注册自己的服务到服务发现系统,另外上报自己的服务情况。 服务调用方:使用RPC库进行服务寻址,实时从服务发现系统那边获取最新的服务调度策略。 提供协议的序列化、反序列化功能,负载均衡的调用策略、熔断限流等安全访问策略,这部分对于服务的提供方以及调用方都适用。
写业务逻辑的,再也不用关注服务地址、协议解析、服务调度、自身服务情况上报等等与业务逻辑本身并没有太多关系的工作,专注于业务逻辑即可。 服务发现系统一般还有与之搭配的管理后台界面,可以通过这里对服务的策略进行修改查看等操作。 对应的还会有服务监控系统,对应的这是一台实时采集服务数据进行计算的系统,有了这套系统服务质量如何一目了然。 服务健康状态的检查完全自动化,在状况不好的时候对服务进行降级处理,人工干预变少,更加智能以及自动化。
如果需要支持多语言,该怎么做?每个语言实现一个对应的RPC库吗? 库的升级很麻烦,比如RPC库本身出了安全漏洞,比如需要升级版本,一般推动业务方去做这个升级是很难的,尤其是系统做大了之后。
总结
域名的出现屏蔽了IP地址。 服务发现系统屏蔽协议及端口号。 PB类序列化库屏蔽了使用者自己对协议的解析。