第 3 章 分布式调用(1)
崔皓
讲述:Alloy大小:8.08M时长:35:20
上一章介绍了分布式系统中的应用服务是如何拆分的,那么拆分后的服务之间如何调用和通信呢?这就是这章将要介绍的内容。服务和应用的调用基于场景的不同会分为几种情况,系统外的客户端调用系统内的服务时需要通过反向代理和负载均衡的方式;系统架构内部服务之间的调用需要通过 API 网关;服务之间的互相感知需要用到服务注册与发现;服务之间的通信会使用 RPC 架构,我们会介绍 RPC 的核心原理以及 Netty 的最佳实践。本章是按照请求从外到内、从大到小的顺序来介绍的。我们将上述这些技术总结为以下几点。
负载均衡
API 网关
服务注册与发现
服务间的远程调用
3.1 负载均衡
分布式系统的拆分就是用更低的成本支撑更大的访问量,用更廉价的服务器集群替代性能强劲的单体服务器。因为单个服务器无论如何优化,总会达到性能的瓶颈,随着业务量的扩大自然需要更多服务器作为支持。这也就是我们所说的服务器集群,用来提升系统整体的处理性能。高性能集群的本质是将同一个服务扩展到不同的机器上,每次请求该服务时选其中一台服务器提供响应,也就是说这个请求无论在哪台服务器上执行,都能得到相同的响应。因此高性能集群的设计主要体现在请求分配上,说白了就是将请求按照一定规则分配到不同的服务器上执行,我们把这个分配过程叫作负载均衡,完成负载均衡的组件或者应用叫作负载均衡器。根据使用场景的不同,还可将负载均衡分为 DNS 负载均衡、硬件负载均衡和软件负载均衡。同时需要注意负载均衡并非将请求“平均”分配,在分配时需要考虑策略,例如按照服务器负载进行分配、按照服务器性能进行分配、按照业务进行分配,这些分配规则就是负载均衡算法。下面展开讲解负载均衡的分类和算法。
3.1.1 负载均衡分类
以客户端请求应用服务器为例,如图 3-1 所示,客户端会将请求的 URL 发送给 DNS 服务器,DNS 服务器根据用户所处的网络区域选择最近机房为其提供服务,这个选择过程就是 DNS 负载均衡。每个网络区域都会存在一个或者多个服务器集群,这里会通过硬件负载均衡器(例如 F5)将请求负载均衡到具体的服务器集群,这个过程就是硬件负载均衡。最后,在集群内通过 Nginx 这样的软件负载均衡器将请求分配到对应的应用服务器,就完成了整个负载均衡的过程。在这三类负载均衡中,软件负载均衡是我们接触最多的,其他两类只有在特定的场景下才会存在,下面我们逐一介绍。
![](https://static001.geekbang.org/files/resource/ebook/100014/image00346.jpeg)
图 3-1 负载均衡的分类
DNS 负载均衡
DNS(Domain Name System,域名系统)是互联网的一项服务。它将域名和 IP 地址的相互映射保存在一个分布式数据库中,使人们能够更方便地访问互联网。DNS 服务器用来实现区域级别的负载均衡。例如使华北用户访问华北机房的服务器、使华中用户访问华中机房的服务器,本质是通过 DNS 服务器再解析 URL 以后,返回对应机房的入口 IP 地址。
大型网站或者应用会将自己的服务器部署在不同区域,此时就需要用到 DNS 负载均衡。其成本相对较低,由于将负载均衡工作交给 DNS 服务器完成,因此不需要开发者完成负载均衡的开发和维护工作。同时用户能够就近访问所在区域的服务器,大大提升了请求与响应的速度。不过这种方式也有不足之处,由于负载均衡工作依赖于 DNS 服务器的缓存,如果修改 DNS 配置后,缓存没有立刻刷新,用户就可能继续访问修改前的 IP,这样便达不到负载均衡的目的,并且 DNS 负载均衡算法相对较少,不能根据负载、响应、业务等选择相应的均衡算法。
硬件负载均衡
硬件负载均衡顾名思义就是通过硬件设备来实现负载均衡功能。如图 3-2 所示,硬件负载均衡器和路由器、交换机一样,作为基础的网络设备,架设在服务器集群之上,客户端通过它们与集群中的服务器实现交互。硬件负载均衡器所处的位置决定了其能够比 DNS 负载均衡器支持更多的负载均衡算法,比软件负载均衡器拥有更高的并发量,如果 Nginx 可以支持 5 万~10 万的并发量,硬件负载均衡器就可以支持 100 万以上的并发量。同时作为硬件负载均衡设备,硬件负载均衡器经过了严格测试,从而拥有更高的稳定性,还具备防火墙、防 DoS 攻击等安全功能。不过硬件负载均衡器的缺点也比较明显,就是贵,因此一般业务发展到一定规模时才会用到它。目前比较流行的硬件负载均衡器有 NetScaler、F5、Radware、Array 等产品。
![](https://static001.geekbang.org/files/resource/ebook/100014/image00347.jpeg)
图 3-2 硬件负载均衡器
硬件负载均衡器常在大企业使用,下面我们以 F5 公司的 F5 BIG-IP 产品为例给大家介绍其三大类功能。下面简称 F5 BIG-IP 为 F5,实际上它是一个经过集成的结局方案。
多链路负载均衡
关键业务都需要安排和配置多条 ISP(网络服务供应商)接入链路来保证网络服务的可靠性。如图 3-3 所示,为了保证业务的稳定,选择 ISP A 和 ISP B 同时提供服务。此时如果某个 ISP 停止服务或者服务异常了,就可以用另一个 ISP 替代其服务,提高了网络的可用性。但不同 ISP 的自治域不同,因此需要考虑两种情况:INBOUND 和 OUTBOUND。
INBOUND:来自网络的请求信息。F5 分别绑定两个 ISP 的公网地址,解析来自两个 ISP 的 DNS 解析请求。
F5 可以根据服务器状况和响应情况对 DNS 请求进行发送,也可以通过多条链路分别建立多个 DNS 连接。
OUTBOUND:返回给请求者的应答信息。F5 既可以将流量分配到不同的网络接口,并做源地址的 NAT(网络地址转换),即将 IP 地址转换为源请求地址;也可以采用接口地址自动映射,保证数据包返回时能够被源头正确接收。
![](https://static001.geekbang.org/files/resource/ebook/100014/image00348.jpeg)
图 3-3 多路负载的方式增强了网络接入层的可靠性
防火墙负载均衡
在大量网络请求面前,单一防火墙显得能力有限,而且防火墙本身是要求数据同进同出的,因此为了解决多防火墙负载均衡问题,F5 提出了防火墙负载均衡的防火墙“三明治”方案。
防火墙会监控用户会话的双向数据流,从而保证数据的合法性。如果采用多个防火墙进行负载均衡,可能会造成同一个用户会话的双向数据在多个防火墙上进行处理,在单个防火墙上却看不到完整用户会话的信息,因此误以为数据非法并抛弃数据。
在每个防火墙的两端都架设四层交换机,可以在完成流量分发的同时,维持用户会话的完整性,使属于同一用户的会话数据由一个防火墙来处理。这种场景下,就需要 F5 负载均衡器的协助才能完成转发。
如图 3-4 所示,F5 协助上述方案配置和实现后,会把交换机、防火墙、交换机夹在一起,看起来就好像三明治一样。
![](https://static001.geekbang.org/files/resource/ebook/100014/image00349.jpeg)
图 3-4 防火墙“三明治”
服务器负载均衡
当硬件负载均衡器同时挂接多个应用服务器时,需要为这些服务器做负载均衡,根据规则将请求发送到对应的服务器。服务器负载均衡包括以下功能。
当同样的业务同时被部署在多个服务器上时,服务器负载均衡可以将用户请求根据规则路由到不同的服务器上。
可以在 F5 上对应用服务器进行配置并且实现负载均衡,F5 可以检查服务器的健康状态,如果发现某服务器故障,就将其从负载均衡组中移除。
F5 对于外网有一个真实的 IP 地址,对于内网的每个应用服务器都会生成一个虚拟 IP 地址进行负载均衡和管理工作。因此,F5 能够为大量基于 TCP/IP 的网络应用提供服务器负载均衡服务。
根据服务类型的不同,定义不同的服务器群组。
根据不同服务端口将流量导向对应的服务器,甚至可以对 VIP 用户的请求进行特殊处理,把这类请求导入到高性能的服务器,使 VIP 用户得到最好的服务响应。
根据用户访问内容的不同,将流量导向指定服务器。
软件负载均衡
说完硬件负载均衡,再来谈谈软件负载均衡。软件负载均衡是指在一台或多台服务器的操作系统上安装一个或多个软件来实现负载均衡。其优点是基于特定环境,配置简单,使用灵活,成本低廉,可以满足一般的负载均衡需求。
一般来说将软件负载均衡所在的这一层称作代理层,起承上启下的作用,上连接入层(硬件负载均衡),下接应用服务器。在代理层可以实现反向代理、负载均衡以及动态缓存与过滤的功能。
目前市面上比较流行的软件负载均衡器有 LVS、HAProxy、Ngnix。下面会给大家介绍软件负载均衡器可以实现的功能,使用 Nginx 作为例子给大家讲解。
反向代理和负载均衡
一般来说,应用服务器与互联网之间会加一个反向代理服务器,它先接收用户的请求,然后将请求转发到内网的应用服务器,充当外网与内网之间的缓冲。反向代理服务器除了起到缓冲作用外,还起路由资源的作用。如图 3-5 所示,商品详情和订单操作经过反向代理后,分别被路由到商品服务和订单服务,图中左边和右边的线段分别表示不同的路由途径。
![](https://static001.geekbang.org/files/resource/ebook/100014/image00350.jpeg)
图 3-5 商品详情和订单操作被反向代理到不同的服务
顺着这个思路来想,代理层可以帮助请求找到对应的服务。但如果请求的数量特别巨大,那么一个服务是无法支撑的。这时需要对服务进行水平扩展,如图 3-6 所示,当订单操作的请求量增加以后,通过扩展订单服务的方式可以减轻单个订单服务的压力,假设同时存在两个订单服务,需要通过负载均衡的方式找到响应请求的服务。此时的软件负载均衡器不仅实现了反向代理功能,也实现了负载均衡的功能。前者对客户端与服务端进行了隔离,并且让客户端的请求找到对应的服务;后者针对水平扩展的多个服务进行负载均衡处理,将客户端请求有选择地分配到多个承载相同服务的服务器上。
![](https://static001.geekbang.org/files/resource/ebook/100014/image00351.jpeg)
图 3-6 针对访问量比较大的订单操作,水平扩展订单服务,针对订单服务进行负载均衡
虽然软件负载均衡器能够承受大量来自客户端的请求,但连接数也是有上限的。以 Nginx 为例,单个进程允许的最大连接数为 worker_connections,具体数值依赖于 Linux 系统进程打开的最大文件数,其上限是 6 553 560(理论值)。进程数的配置一般和服务器 CPU 的核心数有关,假设服务器是双核 CPU,那么 worker_processes 会配置为 2。经过上面的假设,一台 Nginx 最大能够支持的并发量是 worker_processes*worker_connections。如下对于 Nginx 的配置,假设 CPU 是双核的,配置有两个进程,每个进程处理 15 000 个连接,那么 2×15 000 = 30 000 就是 Nginx 的并发量。Nginx 的配置代码如下:
需要说明一下,上面计算出的结果基于提到的假设,仅供参考。实际并发量需要根据具体环境进行估算。
通过 Nginx 的负载均衡,处理“万级别”的并发请求应该不在话下。当遇到“百万级”的并发请求时,就需要使用硬件负载均衡配合软件负载均衡来完成了。通常会在“代理层”之上加入“接入层”,先利用类似 F5 的硬件负载均衡器承载大流量,然后在转给 Nginx 这样的软件负载均衡器。如图 3-7 所示,我们来看下硬件负载均衡与软件负载均衡如何配合使用。
![](https://static001.geekbang.org/files/resource/ebook/100014/image00352.jpeg)
图 3-7 硬件负载均衡配合软件负载均衡使用
具体过程如下:
(1) 将客户端请求的 URL 发送给 DNS;
(2) DNS 将 URL 转化成对应的 IP 地址;
(3) 通过 IP 地址找到对应的服务器;
(4) 服务器接收到请求报文,并转交给接入层处理,接入层由于采用了硬件负载均衡器,所以能够扛住大数据量的访问;
(5) 接入层把请求报文再次转交给代理层,代理层的 Nginx 接收到报文根据反向代理的策略,将报文发送给上游服务器(应用服务器)。
动态缓存与过滤
软件负载均衡位于系统的入口,流入分布式系统的请求都会经过这里,换句话说,相对整个系统而言,软件负载均衡是离客户端更近的地方,所以可以将一些不经常变化的数据放到这里作为缓存,降低用户请求访问应用服务器的频率。例如一些用户的基本信息,修改频率就不是很高,并且使用比较频繁,这些信息就可以放到缓存服务器 Redis 中,当用户请求这部分信息时通过软件负载均衡器直接返回给用户,这样就省去了调用应用服务器的环节,从而能够更快地响应用户。如图 3-8 所示,从接入层来的用户请求,通过 Nginx 代理层,按照如下几个步骤对数据进行访问。
(1) 用户请求首先通过 F5 访问 Nginx,如果请求需要获取的数据在 Nginx 本地缓存中有,就直接返回给用户。
(2) 如果没有命中 Nginx 缓存,则可以通过 Nginx 直接调用缓存服务器获取数据并返回。
(3) 如果缓存服务器中也不存在用户需要的数据,则需要回源到上游应用服务器并返回数据。
![](https://static001.geekbang.org/files/resource/ebook/100014/image00353.jpeg)
图 3-8 动态数据缓存调用方案
使用上图第 (3) 步的方案,无非可以减少调用步骤,因为应用服务有可能存在其他的调用、数据转换、网络传输成本,同时还包含一些业务逻辑和访问数据库操作,影响响应时间。从代理层调用的缓存数据有如下特点:
(1) 这类数据变化不是很频繁,例如用户信息;
(2) 业务逻辑简单,例如判断用户是否有资格参加秒杀活动、判断商品是否在秒杀活动范围内;
(3) 此类缓存数据需要专门的进程对其进行刷新,如果无法命中数据还是需要请求应用服务器。
实现这种方案一般需要加入少许的代码脚本。以 Nginx 为例,需要加入 Lua 脚本协助实现,针对 OpenResty Lua 的具体开发,这里不做展开。如图 3-9 所示的例子中描述的是从客户端传输 userId(用户 ID)到系统中查询用户信息,这些 userId 已经事先放到了 Redis 缓存中,表示这部分用户可以参与秒杀活动。在 Nginx 中对比用户请求的 userId 和缓存中存放的 userId 是否一致,如果一致就让其进行后续的访问,否则拒绝请求。这只是一个例子,在实际操作中这个 userId 可以是商品 ID,相应的操作是判断该商品是否为秒杀商品;也可以是用户访问的 IP 地址,通过看该地址是否在黑名单上来判断用户请求是否为恶意请求。
![](https://static001.geekbang.org/files/resource/ebook/100014/image00354.jpeg)
图 3-9 Nginx 动态缓存的例子
图 3-9 中的具体步骤如下。
(1) 用户请求秒杀服务时,会附带 userId 信息。
(2) 系统需要确认用户的身份和权限。Redis 事先缓存了用户的鉴权信息,于是先通过 Nginx 上的 Lua 脚本查询 Redis 缓存,如果能够获得 userId 的相关信息,就直接返回用户拥有的权限,进行后面的操作。
(3) 如果从缓存中获取不到 userId 的相关信息,就去请求上游的用户鉴权服务,之后再进行后面的业务流程。
现在用代码帮助大家理解上面的例子,最重要的是打开思路。上面提到了 Lua 脚本,这是一种轻量小巧的脚本语言,由标准 C 语言编写并以源代码形式开放,嵌入到 Nginx 中为其提供灵活的扩展和定制功能,负责调用 Redis 缓存和上游服务器的业务。接下来看一下 Lua 的具体实现和 Nginx 的配置。
先建立 Lua 脚本,其包括如下功能。
(1) 创建 Redis 连接,以 userId 为键读取 Redis 缓存中的信息,传入 userId 看其是否存在于 Redis 中。在 get_redis 函数中输入参数 userId,返回值 response 若不为空,则说明 userId 在 Redis 中。
(2) 关闭 Reids 连接。在 close_redis 函数中输入参数 redis 即可。
(3) 连接上游的应用服务器。在 get_http 函数中输入参数 userId,获取对应用户的访问权限。
具体的实现代码如下所示:
调用上述代码时,先由 Nginx 获取用户的 URL 请求,截取 userId 参数传给 Lua 脚本。然后 Lua 脚本建立与 Redis 的连接,查询 userId 是否在 Redis 缓存中。如果存在就直接返回结果,并继续后面的操作;如果不存在,则调用 get_http 函数请求上游的用户鉴权服务,最后调用 close_redis 函数关闭 Redis 的连接。
这里对上述代码做简单介绍。
① 引用 Lua 的 redis 模块。
② 引用 Lua 的 cjson 模块,用来实现 Lua 值与 Json 值之间的相互转换。
③ 定义 ngx_var,获取 Nginx 传入的请求参数。
④ 在 get_redis 函数中输入参数 userId,返回值为 response,其不为空说明 userId 对应的用户在 Redis 缓存中。
⑤ 新建 redis 对象。
⑥ 设置超时时间。
⑦ 设置 Redis 的 IP 地址。
⑧ 设置 Redis 的端口号。
⑨ 打开 Redis 连接。
⑩ 输入 userId 获取对应的值,并存放到 response 和 err 变量中。在 response 为空的情况下,可以返回 null,并且关闭 Redis 连接,此处代码并未详细给出。
⑪ 调用完毕 redis 对象后关闭 Redis 连接。
⑫ 返回 response。
⑬ 程序开始执行,首先从请求的参数变量中获取 userId,放到 Lua 本地变量中。
⑭ 调用 get_redis 函数,传入 userId,把返回的结果放到 content 中。
⑮ 如果 Redis 缓存中不存在 userId,就调用 get_http 函数请求上游的用户鉴权服务。
以上 Lua 脚本主要是通过 get_redis 函数传入 userId,再从 Redis 缓存中获取信息。将此脚本保存在 /usr/checkuserid.lua 下面。现在思考一个问题,这个 Lua 脚本是如何与 Nginx 合作的。假设用户通过一个 URL(http://XXX.XXX.XXX.XXX/userid/123)访问应用系统,其中传入的 userid 为 123。Nginx 配置代码如下所示,通过 location 配置的正则表达式 location ~ ^/userid(\d+)$ 实现与 Lua 命令 content_by_lua_file 的绑定,此命令后面的参数是 Lua 脚本存放的地址。这样传入 get_redis 函数的 userId 参数就是 123,于是 Lua 脚本中的 get_redis(userId) 就会执行之后的查找操作了。
动态缓存方式除了缓存用户信息,还能起到过滤功效,可以过滤一些不满足条件的用户。注意,这里提到的用户信息的过滤和缓存只是一个例子。主要想表达的意思是可以将一些变化不频繁的数据提到代理层来缓存,提高响应效率,同时可以根据风控系统返回的信息,过滤疑似机器人的代码或者恶意请求,例如从固定 IP 发送来的、频率过高的请求。特别在秒杀场景中代理层可以识别来自秒杀系统的请求,如果请求中带有秒杀系统的参数,就要把此请求路由至秒杀系统的服务器集群,这样才能将其和正常的业务请求分割开。
3.1.2 负载均衡算法
说完了负载均衡的分类,再来看看负载均衡使用了哪些算法,这里以 Nginx 为例给大家介绍几种负载均衡算法。
round-robin:轮询算法,是负载均衡默认使用的算法。说白了就是挨个查询上游服务器。在下述代码中,在 Nginx 配置的 server 中 location 指向了 sampleservers,其中的 server 定义了两台服务器,分别是 192.168.1.1:8001 和 192.168.1.1:8002,在请求时会轮换请求这两台服务器。轮询算法比较适合日常的系统,因为请求会均匀地分配到每个服务器上面。
weight:权重算法,给应用服务器设置权重值。weight 参数的默认取值为 1,其值越大,代表服务器被访问的几率越大。可以根据服务器的硬件配置设置 weight 值,让资源情况较乐观的服务器承担更多访问量。示例代码如下:
在上述代码中,权重算法的配置与轮询算法不同的是,在 server 地址后面分别加上了 weight 2 和 weight 1 表示权重。这两项的含义是:每有 3 次请求访问,其中的 2 次请求会由服务器 192.168.1.1:8001 来响应,另外 1 次由服务器 192.168.1.2:8002 响应。与轮询算法不同,权重算法会根据服务器的资源情况分配请求。例如在秒杀系统中可以将资源较多的服务器(硬件资源好)挑选出来,设置较高的权重值,响应访问量大的服务,例如订单服务。
IP-hash:这个算法可以根据用户 IP 进行负载均衡,来自同一 IP 的用户请求报文由同一台上游服务器响应,可以让同一客户端的会话(session)保持一致。在配置文件中只需要将 upstream 中加入 ip_hash; 命令即可,如下面代码所示。这样即便设置了权重,从同一 IP 地址发出的用户请求还是会访问同一台服务器,假设第一次访问的是服务器 192.168.1.1:8001,那么此后来自同一 IP 地址的请求都会访问这个服务器:
hash key:这个算法是对 IP-hash 算法的补充。当增加、删除上游服务器时,来自同一 IP 地址的请求可能无法正确地被同一服务器处理。出于对这一问题的考虑,为每个请求都设置 hash key,这样就算服务器发生了变化,只要请求的 key 值没有变,还是可以找到对应的服务器。如图 3-10 所示,假设 hash 值为 3,每个客户端请求都有一个 key,对 key 值与 hash 值进行取模运算,按照所得余数的值将请求分配到对应的服务器上。
least_conn:该算法把请求转发给连接数较少的后端服务器。轮询算法是把请求平均转发给各个后端服务器,使它们的负载大致相同。但是,有些请求占用的时间会很长,导致其所在的后端服务器负载较高。这种情况下,least_conn 算法能够获得更好的负载均衡效果。
![](https://static001.geekbang.org/files/resource/ebook/100014/image00355.jpeg)
图 3-10 hash key 算法图例
3.2 API 网关
3.1 节讲了客户端请求如何调用服务器上的服务与应用,由负载均衡器帮忙找到对应的服务器,然后对请求进行响应。从第 1 章中应用程序架构的演进历程可以发现,随着业务多变性、灵活度的提高,系统需要更加灵活的组合,同时为了应对高并发带来的挑战,微服务架构得到广泛使用。由于在微服务架构中,应用会被拆分成多个服务,因此为了方便客户端调用这些服务,引入了 API 的概念。
3.2.1 API 网关的定义
网关一词最早用于描述网络设备,比如两个相互独立的局域网通过路由器进行通信,中间的路由器就被称为网关。落实到开发层面,网关存在于客户端与微服务系统之间。
从业务层面讲,客户端完成某个业务需要同时调用多个微服务。如图 3-11 所示,客户端发起下单请求后需要调用商品查询、扣减库存以及订单更新服务。如果这些服务需要客户端分别调用才能完成,必然会增加请求的复杂度,同时带来网络调用性能的损耗。因此,针对微服务应用场景推出了 API 网关的调用。在客户端与微服务系统之间加入下单 API 网关,客户端直接给这个 API 网关下达命令,由其完成对其他三个微服务的调用并且返回结果给客户端。
![](https://static001.geekbang.org/files/resource/ebook/100014/image00356.jpeg)
图 3-11 加入下单 API 网关前后的对比
从系统层面讲,任何一个应用系统要想被其他系统调用,都需要暴露 API,这些 API 代表功能点。正如上面用户下单例子中提到的,如果一个下单的功能点需要调用多个服务,那么在这个下单的 API 网关中就需要聚合多个服务的调用。这种聚合方式有点像设计模式中的门面模式(Facade),它为外部调用提供一个统一的访问入口。不仅如此,API 网关还可以协助两个系统进行通信,如图 3-12 所示,就是在系统 A 和系统 B 之间加上一个 API 网关作为中介者,协助 API 的调用。
![](https://static001.geekbang.org/files/resource/ebook/100014/image00357.jpeg)
图 3-12 对接两个系统的 API 网关
从客户端层面讲,为了屏蔽不同类型客户端的调用差异,也可以加入 API 网关。如图 3-13 所示,在实际开发过程中,API 网关可以根据不同的客户端类型(iOS、Android、PC、小程序),提供不同的 API 网关与之对应。
![](https://static001.geekbang.org/files/resource/ebook/100014/image00358.jpeg)
图 3-13 对接客户端和服务端的 API 网关
由于 API 网关处在客户端与微服务系统的交界,因此它还包括路由、负载均衡、限流、缓存、日志、发布等功能。
3.2.2 API 网关的服务定位
上面是从业务、系统、客户端三个方面分析了 API 网关的意义。API 网关还拥有处理请求的能力,从服务定位来看,可以将其分为如下 5 类。
面向 WebAPP 的 API 网关,这部分系统以网站和 H5 应用为主。通过前后端分离的设计方式,将大部分业务功能都放到后端,前面的 Web APP 只展示页面内容。
面向 MobileAPP 的 API 网关,这里的 Mobile 指的是 iOS 和 Android。设计思路和面向 WebAPP 基本相同,区别是此处 API 网关需要做一些移动设备管理工作(MDM),例如设备的注册、激活、使用和淘汰等,即管理移动设备的全生命周期。
鉴于移动设备的特殊性,导致我们在面对来自移动设备的请求时,需要考虑请求、设备以及使用者之间的关系。
面向合作伙伴的 OpenAPI。系统通常会给合作伙伴提供接口,这些接口或者全部开放,或者部分开发,在有条件限制(时间、流量)的情况下允许合作伙伴访问,因此需要更多地考虑如何管理 API 网关的流量和安全,以及协议转换。
企业内部可扩展 API,这种 API 供企业内部的其他部门或者项目使用,也可以作为中台输出的一部分,支持其他系统。这里需要更多地考虑划分功能边界、认证和授权问题。
3.2.3 API 网关的技术原理
正如前面两节提到的,API 网关是内部微服务与外部请求的“门面”,在技术实现的原理方面需要考虑以下问题。
协议转换
同一系统内部多个服务之间的调用,可以统一使用一种协议,例如 HTTP、gRPC。如果每个系统使用的协议不同,那么系统之间的调用或者数据传输,就存在协议转换问题。如何解决这个问题呢?API 网关通过泛化调用的方式实现协议之间的转化。
如图 3-14 所示,API 网关可以将不同的协议转换成“通用协议”,然后再将通用协议转化成本地系统能够识别的协议。通用协议用得比较多的有 JSON,当然 XML 或者自定义 JSON 文件也可以。
![](https://static001.geekbang.org/files/resource/ebook/100014/image00359.jpeg)
图 3-14 不同协议需要转化成通用协议进行传输
链式处理
设计模式中有一种责任链模式,该模式将“处理请求”和“处理步骤”分开。每个处理步骤都只关心自己需要完成的操作,各个处理步骤存在先后顺序。消息从第一个处理步骤流入,从最后一个处理步骤流出,每个处理步骤对流过的消息进行相应处理,整个过程形成一个链条。在 API 网关的技术实现中也用到了类似的模式,即链式处理。
Zuul 是 Netflix 的一个开源 API 网关,下面以它为例进行讲解。在 Zuul 的设计中,消息从流入网关到流出网关需要经历一系列过滤器,这些过滤器之间有先后顺序,并且每个过滤器需要进行的工作各不一样,如图 3-15 所示。
![](https://static001.geekbang.org/files/resource/ebook/100014/image00360.jpeg)
图 3-15 Zuul 网关过滤器链式处理
pre 过滤器:前置过滤器,用来处理通用事务,比如鉴权、限流、熔断降级、缓存。可以通过 custom 过滤器进行扩展。
routing 过滤器:路由过滤器,这种过滤器把用户请求发送给原始应用服务器。它主要负责协议转化和路由工作。
post 过滤器:后置过滤器,从原始应用服务器返回的响应信息会先经过它,再返回给调用者。在返回的响应报文中加入响应头,可以对响应信息进行统计和日志记录。
error 过滤器:错误过滤器,当上面三个过滤器发生异常时,错误信息会进入这里,由它对错误进行处理。
异步请求
所有请求都通过 API 网关来访问应用服务,那么一旦吞吐量上去,API 网关该如何高效处理这些请求?以 Zuul 为例,Zuul1 采用一个线程处理一个请求的方式。线程负责接收请求,然后调用应用服务并返回结果。如果把网络请求看成一次 IO 操作,那么处理请求的线程从接收请求,到服务器返回响应信息,都处于阻塞状态。如果多个线程同时处在这种状态,将会导致系统缓慢。因为每个网关能够开启的线程数量是有限的,特别是在访问高峰期。为了解决这个问题,Zuul2 启动了异步请求机制。每个请求进入网关时,都会被包装成一个事件,CPU 内核会维持一个监听器,不断轮询是否有“请求事件”,一旦发现有,就调用对应的应用服务处理请求,获取到应用服务返回的信息后,按照请求的要求把数据、文件放到指定缓冲区,同时发送一个通知事件给请求端,告诉其数据已经就绪,可以从缓冲区获取数据、文件了。这个过程是异步的,请求线程不用一直等待响应信息返回,在请求完毕后,就可以直接返回了,之后可以做其他事情。当请求数据被 CPU 内核获取,并且发送到指定的数据缓冲区时,请求线程会接到“数据返回”的通知,然后直接使用数据即可,不用自己去完成取数据操作。
API 网关得益于微服务的拆分特性而存在,为了调用相关的多个服务,它会提供一个 API 的聚合供客户端调用。在实际使用中,API 网关和负载均衡器都能起到路由作用,帮助客户端找到对应服务,在这个层面上两者是相通的,在功能上是可以互换的。
公开
同步至部落
取消
完成
0/2000
笔记
复制
AI
- 深入了解
- 翻译
- 解释
- 总结
![](https://static001.geekbang.org/resource/image/d0/e4/d0880927e2a522a038be2d04bf19dfe4.png)
本文深入介绍了分布式系统中的负载均衡技术,包括硬件负载均衡器和软件负载均衡器的功能和应用场景。文章首先解释了负载均衡的概念和分类,包括DNS负载均衡、硬件负载均衡和软件负载均衡。其次详细介绍了硬件负载均衡器的工作原理和多链路负载均衡、防火墙负载均衡的应用。接着,文章介绍了软件负载均衡器的功能,包括反向代理、负载均衡、动态缓存与过滤等。通过图示和实际案例,生动地解释了负载均衡在分布式系统中的重要性和作用。此外,还介绍了Nginx动态缓存的例子,以及负载均衡算法中的轮询算法和权重算法。另外,还介绍了API网关的定义、服务定位和技术原理。整体而言,本文全面介绍了分布式调用中负载均衡的知识点,对于想要深入了解分布式系统负载均衡技术的读者具有很高的参考价值。 文章还介绍了Zuul作为一个开源API网关的设计和实现。Zuul采用了链式处理模式,通过一系列过滤器实现前置过滤、路由、后置过滤和错误处理。此外,文章还探讨了Zuul在处理异步请求方面的优化,通过异步请求机制提高了网关的处理效率。最后,文章指出API网关和负载均衡器在路由作用上是相通的,功能上是可以互换的。这些内容为读者提供了深入了解API网关和负载均衡器的机制和实现细节,对于从事分布式系统开发和架构设计的技术人员具有重要的参考意义。
2024-01-04给文章提建议
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《分布式架构原理与实践》
《分布式架构原理与实践》
立即购买
登录 后留言
精选留言
由作者筛选后的优质留言将会公开显示,欢迎踊跃留言。
收起评论