当前位置: 首页 > 科技观察

解释“服务召唤”的良心之作!

时间:2023-03-21 21:14:33 科技观察

本文简要总结了我所学的解决“服务调用”相关技术的演进史。纯属科普文章。图片来自Pexels本文着重介绍了演化过程中每一步的Why和What,尽量不深入技术细节(How)。服务三要素一般来说,一个网络服务包括以下三个要素:地址:调用者根据地址访问网络接口。地址包括以下元素:IP地址、服务端口、服务协议(TCP、UDP等)。协议格式:指协议的字段,由接口提供者和协议调用者协商确定。协议名称:或协议类型,因为在同一个服务监听端口上,可能同时提供多个接口为调用者服务。这时候就需要协议类型(名称)来区分不同的网络接口。需要注意的是,在服务地址中:IP地址提供了在互联网上找到这台机器的凭据。协议和服务端口提供凭据以查找在此机器上提供服务的进程。这些都是TCP/IP协议栈的知识点,这里不再赘述。这里还需要解释一些与服务相关的术语:服务实例:服务对应的IP地址和端口的缩写。当您需要访问一个服务时,您需要寻址并知道该服务的每个运行实例的地址和端口,然后才能建立连接进行访问。服务注册:一个服务实例声明它提供哪些服务,即某个IP地址+端口提供哪些服务接口。服务发现:调用者通过某种方式找到服务提供者,即知道服务运行的IP地址和端口。基于IP地址调用原始网络服务是通过原始IP地址暴露给调用者的。这种方式存在以下问题:IP地址难记且无意义。另外,从上面服务的三要素我们可以看出,IP地址其实是一个很底层的概念,直接对应一台机器上的一个网络接口。如果直接使用IP地址寻址,更换机器会变得很麻烦。.“尽量不要用太底层的概念来提供服务”是这个演进过程中的一个重要原则。比如,今天很少能看到直接用汇编语言写代码的场景。相反,有越来越多的抽象。本文展示了服务调用领域在此过程中的演进过程。目前除非处于测试阶段,否则无法直接以IP地址形式提供服务。域名系统前面的IP地址是主机作为路由器地址的数字标识,不易记忆。这时候域名系统就产生了。与简单地提供IP地址相比,域名系统更容易记住,因为它使用有意义的域名来标识服务。另外还可以更改域名对应的IP地址,方便换机。有了域名后,当调用者需要访问某个网络服务时,首先去域名地址服务,根据DNS协议将域名解析到对应的IP地址,然后根据返回的访问服务IP地址。从这里可以看出,因为域名地址服务查询映射IP地址的过程多了一步,所以多了一步解析。为了减少这一步的影响,调用者会缓存解析后的结果,一段时间内不要使用。expired,省去了这一步查询的开销。协议接收解析上面已经使用了域名系统来解决服务IP地址难记的问题。让我们看看协议格式解析的演变。一般来说,一个网络协议包括两部分:协议头:这里存放的是协议的元信息,可能包括协议类型、主体长度、协议格式等。需要注意的是,头一般是固定的size,或者有明确的边界(比如HTTP协议中的\r\n终止符),否则无法知道header何时结束。协议体:协议的具体内容。无论是HTTP协议还是自定义的二进制网络协议,一般都由这两部分组成。由于客户端的协议数据在很多情况下不能一口气接收到,所以在接收协议数据时,一般使用状态机接收协议数据:接收到网络数据后,协议分析停滞了很久时间。一个协议有多个字段,这些不同的字段有不同的类型。简单的原始类型(比如整型和字符串)还好,但是复杂的类型比如字典和数组就比较麻烦了。.当时常用的方法有以下几种:使用json或xml等数据格式。优点是可见性强,方便表达上述复杂类型。缺点是容易被破解,传输的数据比较大。自定义二进制协议。随着每个公司做大,在这个领域难免会有几个类似的轮子。我见过的典型的就是所谓的TLV格式(Type-Length-Value)。自定义二进制格式最大的问题出现在协议的联调和协商过程中。由于可见性较弱,可能这里少了一个字段,另一边多了一个字段,会给联调过程带来麻烦。直到谷歌的ProtocolBuffer(以下简称PB)出现,上述问题才得到很大改善。PB出现后,又出现了很多类似的技术,比如Thrift、MsgPack等,这里不再赘述,将这类技术统称为PB。与前两种方法相比,PB具有以下优点:使用proto格式文件来定义协议格式。proto文件是典型的DSL(domain-specificlanguage)文件,描述了协议的具体格式。每个字段的类型是什么,哪些是可选的,哪些是必填的。有了proto文件,C\S的两端就是通过这个文件来沟通协议,而不是具体的技术细节。PB可以通过proto文件生成对应各种语言的序列化和反序列化代码,为跨语言调用提供了便利。PB本身可以对特定类型进行数据压缩,以减少数据大小。经过之前服务网关的演变,写一个简单的单机服务器并不困难。但是随着访问量的增加,一台机器已经不足以支撑所有的请求,需要横向扩展,增加更多的业务服务器。但是,之前通过域名访问服务的架构遇到了一个问题:如果有多个服务实例可以提供同一个服务,那么在DNS域名解析中就需要将域名绑定到多个地址。这样的方案存在以下问题:如何检查这些实例的健康度,发现问题的同时增加或删除服务实例地址?这就是所谓的服务高可用问题。将这些服务实例的地址暴露给外网会不会涉及到安全问题?即使安全问题可以解决,那么每台机器都需要执行安全策略。由于DNS协议的特性,增加和删除服务实例不是实时的,有时会影响业务。为了解决这些问题,引入了反向代理网关组件。它提供以下功能:负载均衡功能:根据一定的算法将请求分配给服务实例。提供管理功能:可以为运维管理员添加或删除服务实例。因为它决定了服务请求流量的方向,所以还可以做更多的其他功能:灰度引流、安全防攻击(比如访问黑白名单、卸载SSL证书)等。有四层和七层负载均衡软件,其中四层负载均衡这里介绍LVS,七层负载均衡介绍Nginx。上图是一个简单的TCP/IP协议栈层级图,其中LVS工作在第四层,即当一个请求来到LVS时,它会根据四层来决定这个请求最终会去到哪个服务实例协议。Nginx工作在第七层,主要用于HTTP协议,即根据HTTP协议本身来决定请求的方向。需要注意的是,Nginx也可以工作在第四层,但是用到的地方不多。可以参考Nginx的Stream模块。作为四层负载均衡的LVS,LVS有几种工作模式,我不是很清楚。以下说法仅针对FullNAT模式,以下说法可能有误。LVS有以下组成部分:DirectServer(以下简称DS):暴露给客户端进行负载均衡的前端服务器。虚拟IP地址(以下简称VIP):DS暴露的IP地址,作为客户端请求的地址。直连IP地址(以下简称DIP):DS用来与RealServer交互的IP地址。RealServer(以下简称RS):实际工作在后端的服务器,可以水平扩展。真实IP地址(以下简称RIP):RS的地址。ClientIPaddress(以下简称CIP):客户的地址。客户端发起请求时,流程如下:使用VIP地址访问DS,此时地址二元组为。DS根据自己的负载均衡算法,选择一个RS转发请求。转发的时候把请求的源IP地址改成DIP地址,这样RS就好像是DS在访问它一样。此时地址二元组为RS处理并回复请求,这个报告的源地址为RS的RIP地址,目的地址为DIP地址,此时的地址二元组为。DS收到响应包后,将消息回复给客户端。此时修改响应报文的源地址为VIP地址,目的地址为CIP地址。在开始讨论Nginx之前,我们需要先简单说一下正向代理和反向代理。所谓正向代理(proxy),我的理解就是代理在客户端。比如浏览器中可以配置访问某些网站的代理就是正向代理,但是一般来说不是正向代理而是代理,也就是默认的代理都是正向的。反向代理(reverseproxy)是服务器前面的代理。比如之前的LVS中的DS服务器就是一个反向代理。之所以需要反向代理,一般的原因有以下几点:负载均衡:希望在这个反向代理服务器中,将请求均衡的分发到后面的服务器上。安全性:不想暴露太多的服务器地址给客户端,所以统一连接到这个反向代理服务器,这里做限流,安全控制等。由于客户端的请求是统一访问的,因此可以在反向代理的访问层实现更多的控制策略,比如灰度流量发布、权重控制等。我觉得反向代理和所谓的Gateway、gateway等没有太大的区别,只是名字不同,做的事情也差不多。Nginx应该是最常用的HTTP七层负载均衡软件了。在Nginx中,可以在配置的Server块中定义一个域名,然后将该域名的请求绑定到对应的Upstream,从而达到将请求转发到这些Upstream的作用。如:upstreamhello{serverA:11001;serverB:11001;}location/{roothtml;indexindex.htmlindex.htm;proxy_passhttp://hello;}这是最简单的Nginx反向代理配置,在实际线路后面一个接入层里面可以是多个域名。如果配置变化较大,每次修改域名和对应的上游配置都需要人工干预,效率会很慢。这时候,就会提到一个叫DevOps的名词。我的理解是开发各种自动化运维工具的工程师。通过上面的分析,此时提供七层HTTP访问接口的服务架构大致是这样的:服务发现和RPC解决了单机服务器对外提供服务的大部分问题。复杂数字IP地址的问题。PB软件库的出现解决了协议定义分析的痛点。网关组件解决了客户端访问、服务端水平扩展等一系列问题。但是,一个服务通常并不一定只靠自己提供服务。服务过程还可能涉及查询其他服务的过程,例如MySQL、Redis等数据服务。这种在服务内部进行调用和查询的服务称为内部服务,通常不会直接暴露在外网。面向公网的服务一般以域名的形式提供给外部调用者。但是,域名形式不足以实现服务之间的相互调用。原因是DNS服务发现的粒度太粗,只有IPAddress级别,服务的端口需要用户维护。对于服务的健康状态检查,DNS检查是不够的,需要运维的参与。DNS缺乏对服务状态的收集,服务状态最终应该反过来影响服务的调用。DNS更改需要人工参与,不够智能和自动化。综上所述,内网之间的服务调用通常会自行实现一套“服务发现”系统,包括以下组成部分:服务发现系统:用于为服务提供寻址和注册能力,并对服务状态进行统计汇总,根据服务情况改变服务的调用方式。例如,如果某个服务实例的响应速度较慢,分配给该实例的流量就会响应较少。并且因为这个系统可以提供服务寻址能力,所以这里可以实现一些寻址策略。例如,一些特定的流量只能去灰度中的某些特定实例。例如,可以配置每个实例的流量权重。等待。一组用于服务系统的RPC库。RPC库提供了以下功能:服务提供者:使用RPC库将自己的服务注册到服务发现系统,并报告自己的服务状态。服务调用者:使用RPC库进行服务寻址,实时从服务发现系统获取最新的服务调度策略。提供协议的序列化和反序列化功能,负载均衡的调用策略,熔断限流等安全访问策略,服务提供者和调用者都适用。有了这个服务发现系统以及与之配套使用的RPC库,我们再来看看现在的服务调用是什么样子的:写业务逻辑,再也不用关注服务地址、协议解析、服务调度、自服务服务状态上报等。对于与业务逻辑本身关系不大的工作,只关注业务逻辑即可。服务发现系统一般都有与其匹配的管理后台接口,通过该接口可以修改和查看服务策略。相应的,也会有服务监控系统。相应的,这是一个实时收集业务数据进行计算的系统。有了这个系统,服务质量如何,一目了然。服务健康状态的检查是全自动化的,状态不好就降级服务,减少人工干预,更加智能化和自动化。现在服务架构演变成这样:ServiceMesh架构发展到上面这个层次,其实可以解决大部分问题。近两年,出现了一个非常流行的概念:ServiceMesh,中文翻译为“服务网格”。让我们看看它能解决什么问题。在之前的服务发现系统中,需要一个配套的RPC库,但是这样会存在以下问题:需要支持多种语言怎么办?是否每种语言都实现了相应的RPC库?库的升级非常困难。麻烦,比如RPC库本身存在安全漏洞,比如需要升级版本,一般很难推动业务方去做这个升级,尤其是在系统变大之后。可见,由于RPC库是嵌入在进程中的组件,上面的问题很麻烦,于是想出了一个解决办法:将原来的进程拆分成两个进程。如下图所示:在服务被网格化之前,服务调用者实例通过自己内部的RPC库与服务提供者实例进行通信。服务mesh化后,会在服务调用者同一台机器上部署一个LocalProxy,即ServiceMesh的Proxy。这时候服务调用的流量会先到这个Proxy上去,然后由它来完成原来RPC库响应的工作。至于如何劫持这个流量,答案是使用Iptables将特定端口的流量转发给Proxy。有了这一层分离,业务服务从负责RPC库角色的Proxy中分离出来,以上两个痛点就变成了每台物理机上MeshProxy的升级和维护。多语言不再是问题,因为RPC通信是通过网络调用完成的,而不是在过程中使用RPC库。然而,这种解决方案并非没有任何问题。最大的问题是,加上这层调用之后,必然会影响到原来的响应时间。截至目前(2019年6月),ServiceMesh仍然是一个比实际产品更大的概念。从上面的演化历史可以看出,所谓“中间层理论”,即“计算机科学中的任何问题都可以通过另一层间接来解决(计算机科学领域的任何问题都可以通过添加一个间接的中间层)”在这个过程中被广泛使用。比如为了解决IP地址难记的问题,引入了域名系统,比如为了解决负载均衡问题引入了网关,等等。但是每引入一个中间层,必然会带来其他的影响,比如ServiceMesh对Proxy的调用多了一次,如何平衡又是另外一个问题。另外,回到最初的服务三要素,我们可以看到,整个演进史也逐渐屏蔽了下层组件的过程,例如:域名屏蔽IP地址的出现。服务发现系统屏蔽了协议和端口号。PB类序列化库屏蔽了用户对协议的解析。可以看出,演进过程让业务开发人员更专注于业务逻辑。这种演变过程不仅发生在今天,也不仅仅发生在今天。类似的演变将在未来再次发生。作者:codedump简介:从事互联网服务器后台开发多年。访问作者的博客:https://www.codedump.info/阅读更多文章。