mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 14:13:46 +08:00
mod
This commit is contained in:
100
极客时间专栏/OpenResty从入门到实战/API网关篇/47 | 微服务API网关搭建三步曲(一).md
Normal file
100
极客时间专栏/OpenResty从入门到实战/API网关篇/47 | 微服务API网关搭建三步曲(一).md
Normal file
@@ -0,0 +1,100 @@
|
||||
<audio id="audio" title="47 | 微服务API网关搭建三步曲(一)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fd/1f/fd1b812f439177bf40911e9fd0c0601f.mp3"></audio>
|
||||
|
||||
你好,我是温铭。
|
||||
|
||||
今天这节课,OpenResty 专栏就要进入实战的章节了。我会用三节课的内容,来为你介绍如何实现一个微服务 API 网关。在这个过程中,我们不仅会涉及到前面学过的 OpenResty 知识,我也会从行业、产品、技术选型等多个维度,为你展示下,如何从头做一个新的产品和开源项目。
|
||||
|
||||
## 微服务 API 网关有什么用?
|
||||
|
||||
让我们先来看下微服务 API 网关的作用。下面这张图,是一个简要的说明:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/de/ee/dea3e12608301d1a6b7fffce8bafafee.png" alt="">
|
||||
|
||||
众所周知,API 网关并非一个新兴的概念,在十几年前就已经存在了,它的作用主要是作为流量的入口,统一处理和业务相关的请求,让请求更加安全、快速和准确地得到处理。它有以下几个传统功能:
|
||||
|
||||
- 反向代理和负载均衡,这和 Nginx 的定位和功能是一致的;
|
||||
- 动态上游、动态 SSL 证书和动态限流限速等运行时的动态功能,这是开源版本 Nginx 并不具备的功能;
|
||||
- 上游的主动和被动健康检查,以及服务熔断功能;
|
||||
- 在 API 网关的基础上进行扩展,成为全生命周期的 API 管理平台。
|
||||
|
||||
在最近几年,业务相关的流量,不再仅仅由PC客户端和浏览器发起,更多的来自手机、IoT 设备等,未来随着 5G 的普及,这些流量会越来越多。同时,随着微服务架构的结构变迁,服务之间的流量也开始爆发性地增长。在这种新的业务场景下,自然也催生了API 网关更多、更高级的功能:
|
||||
|
||||
1. 云原生友好,架构要变得轻巧,便于容器化;
|
||||
1. 对接 Prometheus、Zipkin、SkyWalking 等统计、监控组件;
|
||||
1. 支持 gRPC 代理,以及 HTTP 到 gRPC 之间的协议转换,把用户的 HTTP 请求转为内部服务的 gRPC 请求;
|
||||
1. 承担 OpenID Relying Party 的角色,对接 Auth0、Okta 等身份认证提供商的服务,把流量安全作为头等大事来对待;
|
||||
1. 通过运行时动态执行用户函数的方式来实现 Serverless,让网关的边缘节点更加灵活;
|
||||
1. 不锁定用户,支持混合云的部署架构;
|
||||
1. 最后,网关节点要状态无关,可以随意地扩容和缩容。
|
||||
|
||||
当一个微服务 API 网关具备了上述十几项功能时,就可以让用户的服务只关心业务本身;而和业务实现无关的功能,比如服务发现、服务熔断、身份认证、限流限速、统计、性能分析等,就可以在独立的网关层面来解决。
|
||||
|
||||
从这个角度来看,API 网关既可以替代 Nginx 的所有功能,处理南北向的流量;也可以完成 Istio 控制面和 Envoy 数据面的角色,处理东西向的流量。
|
||||
|
||||
## 为什么要新造轮子?
|
||||
|
||||
正因为微服务 API 网关的地位如此重要,所以它一直处于兵家必争之地,传统的 IT 巨头在这个领域很早就都有布局。根据 2018 年 Gartner 发布的 API 全生命周期报告,谷歌、CA、IBM、红帽、Salesforce 都是处于领导地位的厂商,开发者更熟悉的 Kong 则处于远见者的区间内。
|
||||
|
||||
那么,问题就来了,为什么我们还要新造一个轮子呢?
|
||||
|
||||
简单来说,这是因为当前的微服务 API 网关都不足以满足我们的需求。我们首先来看闭源的商业产品,它们的功能都很完善,覆盖了 API 的设计、多语言 SDK、文档、测试和发布等全生命周期管理,并且提供 SaaS 服务,有些还与公有云做了集成,使用起来非常方便。但同时,它们也带来了两个痛点。
|
||||
|
||||
第一个痛点,平台锁定问题。API 网关是业务流量的入口,它不像图片、视频等 CDN 加速的这种非业务流量可以随意迁移,API 网关上会绑定不少业务相关的逻辑。你一旦使用了闭源的方案,就很难平滑和低成本地迁移到其他平台。
|
||||
|
||||
第二个痛点,无法二次开发的问题。一般的大中型企业都会有自己独特的需求,需要定制开发,但这时候你只能依靠厂商,而不能自己动手去做二次开发。
|
||||
|
||||
这也是为什么开源的 API 网关方案开始流行的一个原因。不过,现有的开源产品也不是万能的,自身也有很多不足。
|
||||
|
||||
第一,依赖 PostgreSQL、MySQL 等关系型数据库。这样,在配置发生变化的时候,网关节点只能轮询数据库。这不仅造成配置生效慢,也给代码增加了复杂度,让人难以理解;同时,数据库也会成为系统的单点和性能瓶颈,无法保证整体的高可用。如果你把 API 网关用于 Kubernetes 环境下,关系型数据库会显得更加笨重,不利于快速伸缩。
|
||||
|
||||
第二,插件不能热加载。当你新增一个插件或者修改现有插件的代码后,必须要重载服务才能生效,这和修改 Nginx 配置后需要重载是一样的,显然会影响用户的请求。
|
||||
|
||||
第三,代码结构复杂, 难以掌握。有些开源项目做了多层面向对象的封装,一些简单的逻辑也变得雾里看花。但其实,对于 API 网关这种场景,直来直去的表达会更加清晰和高效,也更有利于二次开发。
|
||||
|
||||
所以,我们需要一个更轻巧、对云原生和开发友好的 API 网关。当然,我们也不能闭门造车,需要先深入了解已有 API 网关各自的特点,这时候,云原生软件基金会(CNCF)的全景图就是一个很好的参考:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/19/f7/19328e6e516ed8ed6f723dd32fef58f7.png" alt="">
|
||||
|
||||
这张图筛选出了业界常见的 API 网关,以开源的方案为主,可以为我们下面的技术选型提供不少有价值的内容。
|
||||
|
||||
## API 网关的核心组件和概念
|
||||
|
||||
当然,在具体实现之前,我们还需要了解 API 网关有哪些核心组件。根据我们前面提到的 API 网关具备的功能点,它至少需要下面几个组件才能开始运行。
|
||||
|
||||
首先是路由。它通过定义一些规则来匹配客户端的请求,然后根据匹配结果,加载、执行相应的插件,并把请求转发给到指定的上游。这些路由匹配规则可以由 host、uri、请求头等组成,我们熟悉的 Nginx 中的 location,就是路由的一种实现。
|
||||
|
||||
其次是插件。这是 API 网关的灵魂所在,身份认证、限流限速、IP 黑白名单、Prometheus、Zipkin 等这些功能,都是通过插件的方式来实现的。既然是插件,那就需要做到即插即用;并且,插件之间不能互相影响,就像我们搭建乐高积木一样,需要用统一规则的、约定好的开发接口,来和底层进行交互。
|
||||
|
||||
接着是schema。既然是处理 API 的网关,那么少不了要对 API 的格式做校验,比如数据类型、允许的字段内容、必须上传的字段等,这时候就需要有一层 schema 来做统一、独立的定义和检查。
|
||||
|
||||
最后是存储。它用于存放用户的各种配置,并在有变更时负责推送到所有的网关节点。这是底层非常关键的基础组件,它的选型决定了上层的插件如何编写、系统能否保持高可用和可扩展性等,所以需要我们审慎地决定。
|
||||
|
||||
另外,在这些核心组件之上,我们还需要抽象出几个 API 网关的常用概念,它们在不同的 API 网关之间都是通用的。
|
||||
|
||||
先来说说Route。路由会包含三部分内容,即匹配的条件、绑定的插件和上游,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6a/0d/6a86c854ec54b07347ff517114482c0d.png" alt="">
|
||||
|
||||
我们可以直接在 Route 中完成所有的配置,这样最简单。但在 API 和上游很多的情况下,这样做就会有很多重复的配置。这时候,我们就需要 Service 和 Upstream 这两个概念来做一层抽象。
|
||||
|
||||
我们接着来看Service。它是某类 API 的抽象,也可以理解为一组 Route 的抽象,它通常与上游服务是一一对应的,而Route 与 Service 之间通常是 N:1 的关系。我也用了一张图来表示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/09/db/0954bddf3828fa26f26a1ba2003c7edb.png" alt="">
|
||||
|
||||
通过 Service 的这层抽象,我们就可以把重复的插件和上游剥离出来。这样,在插件和上游发生变更的时候,我们只需要修改 Service 就可以了,而不用去修改多个 Route 上绑定的数据。
|
||||
|
||||
最后说说Upstream。还是继续上面的示例,如果两个 Route 中的上游是一样的,但是绑定的插件各自不同,那么我们就可以把上游单独抽象出来,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/eb/8e/ebedcfafc5aeafb970097e480f663d8e.png" alt="">
|
||||
|
||||
这样,在上游节点发生变更时,Route 是完全无感知的,它们都在 Upstream 内部进行了处理。
|
||||
|
||||
其实,从这三个主要概念的衍生过程中,我们也可以看到,这几个抽象都基于用户的实际场景,而不是生造出来的。自然,它们适用于所有的 API 网关,和具体的技术方案无关。
|
||||
|
||||
## 写在最后
|
||||
|
||||
今天这节课,我们介绍了微服务 API 网关的作用、功能、核心组件和抽象概念,它们都是 API 网关的基础。
|
||||
|
||||
这里留给你一个思考题:关于传统的南北向流量,和微服务之间的东西向流量,你觉得 API 网关是否都可以处理呢?如果你已经在使用 API 网关了,你也可以写下当初技术选型时的思考。欢迎在留言区和我交流探讨,也欢迎你把这篇文章分享给你的同事、朋友,一起学习和进步。
|
||||
|
||||
|
||||
138
极客时间专栏/OpenResty从入门到实战/API网关篇/48 | 微服务API网关搭建三步曲(二).md
Normal file
138
极客时间专栏/OpenResty从入门到实战/API网关篇/48 | 微服务API网关搭建三步曲(二).md
Normal file
@@ -0,0 +1,138 @@
|
||||
<audio id="audio" title="48 | 微服务API网关搭建三步曲(二)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2d/1f/2dec6a2604e3b17d2fc506d67e4f671f.mp3"></audio>
|
||||
|
||||
你好,我是温铭。
|
||||
|
||||
在明白了微服务 API 网关的核心组件和抽象概念后,我们就要开始技术选型,并动手去实现它了。今天,我们就分别来看下,路由、插件、schema 和存储这四个核心组件的技术选型问题。
|
||||
|
||||
## 存储
|
||||
|
||||
上节课我提到过,存储是底层非常关键的基础组件,它会影响到配置如何同步、集群如何伸缩、高可用如何保证等核心的问题,所以,我们把它放在最开始的位置来选型。
|
||||
|
||||
我们先来看看,已有的 API 网关是把数据存储在哪里的。Kong 是把数据储存在PostgreSQL 或者 Cassandra 中,而同样基于 OpenResty 的 Orange,则是存储在 MySQL 中。不过,这种选择还是有很多缺陷的。
|
||||
|
||||
第一,储存需要单独做高可用方案。PostgreSQL、MySQL 数据库虽然有自己的高可用方案,但你还需要 DBA 和机器资源,在发生故障时也很难做到快速切换。
|
||||
|
||||
第二,只能轮询数据库来获取配置变更,无法做到推送。这不仅会增加数据库资源的消耗,同时变更的实时性也会大打折扣。
|
||||
|
||||
第三,需要自己维护历史版本,并考虑回退和升级。如果用户发布了一个变更,后续可能会有回滚操作,这时候你就需要在代码层面,自己做两个版本之间的 diff,以便配置的回滚。同时,在系统自身升级的时候,还可能会修改数据库的表结构,所以代码层面就需要考虑到新旧版本的兼容和数据升级。
|
||||
|
||||
第四,提高了代码的复杂度。在实现网关的功能之外,你还需要为了前面 3 个缺陷,在代码层面去打上补丁,这显然会让代码的可读性降低不少。
|
||||
|
||||
第五,增加了部署和运维的难度。部署和维护一个关系型数据库并不是一件简单的事情,如果是一个数据库集群那就更加复杂了,并且我们也无法做到快速扩容和缩容。
|
||||
|
||||
针对这样的情况,我们应该如何选择呢?
|
||||
|
||||
我们不妨回到 API 网关的原始需求上来,这里存储的都是简单的配置信息,uri、插件参数、上游地址等,并没有涉及到复杂的联表操作,也不需要严格的事务保证。显然,这种情况下使用关系型数据库,可不就是“杀鸡焉用宰牛刀”吗?
|
||||
|
||||
事实上,本着最小化够用并且更贴近 K8s 的原则,etcd 就是一个恰到好处的选型了:
|
||||
|
||||
- API 网关的配置数据每秒钟的变化次数不会很多,etcd 在性能上是足够的;
|
||||
- 集群和动态伸缩方面,更是 etcd 天生的优势;
|
||||
- etcd还具备 watch 的接口,不用轮询去获取变更。
|
||||
|
||||
其实还有一点,可以让我们更加放心地选择 etcd——它已经是 K8s 体系中保存配置的默认选型了,显然已经经过了很多比 API 网关更加复杂的场景的验证。
|
||||
|
||||
## 路由
|
||||
|
||||
路由也是非常重要的技术选型,所有的请求都由路由筛选出需要加载的插件列表,逐个运行后,再转发给指定的上游。不过,考虑到路由规则可能会比较多,所以路由这里的技术选型,我们需要着重从算法的时间复杂度上去考量。
|
||||
|
||||
我们先来看下,在 OpenResty 下有哪些现成的路由可以拿来使用。老规矩,让我们在 `awesome-resty` 的项目中逐个查找一遍,这其中就有专门的 `Routing Libraries`:
|
||||
|
||||
```
|
||||
• lua-resty-route — A URL routing library for OpenResty supporting multiple route matchers, middleware, and HTTP and WebSockets handlers to mention a few of its features
|
||||
• router.lua — A barebones router for Lua, it matches URLs and executes Lua functions
|
||||
• lua-resty-r3 — libr3 OpenResty implementation, libr3 is a high-performance path dispatching library. It compiles your route paths into a prefix tree (trie). By using the constructed prefix trie in the start-up time, you may dispatch your routes with efficiency
|
||||
• lua-resty-libr3 — High-performance path dispatching library base on libr3 for OpenResty
|
||||
|
||||
```
|
||||
|
||||
你可以看到,这里面包含了四个路由库的实现。前面两个路由都是纯 Lua 实现,相对比较简单,所以有不少功能的欠缺,还不能达到生成的要求。
|
||||
|
||||
后面两个库,其实都是基于 libr3 这个 C 库,并使用 FFI 的方式做了一层封装,而 libr3 自身使用的是前缀树。这种算法和存储了多少条规则的数目 N 无关,只和匹配数据的长度 K 有关,所以时间复杂度为 O(K)。
|
||||
|
||||
但是, libr3 也是有缺点的,它的匹配规则和我们熟悉的 Nginx location 的规则不同,而且不支持回调。这样,我们就没有办法根据请求头、cookie、Nginx 变量来设置路由的条件,对于 API 网关的场景来说显然不够灵活。
|
||||
|
||||
不过,虽说我们尝试从 `awesome-resty` 中找到可用路由库的努力没有成功,但 libr3 的实现,还是给我们指引了一个新的方向:用 C 来实现前缀树以及 FFI 封装,这样应该可以接近时间复杂度和代码性能上的最优方案。
|
||||
|
||||
正好, Redis 的作者开源了一个基数树,也就是压缩前缀树的 [C 实现](https://github.com/antirez/rax)。顺藤摸瓜,我们还可以找到 rax 在 OpenResty 中可用的 [FFI 封装库](https://github.com/iresty/lua-resty-radixtree),它的示例代码如下:
|
||||
|
||||
```
|
||||
local radix = require("resty.radixtree")
|
||||
local rx = radix.new({
|
||||
{
|
||||
path = "/aa",
|
||||
host = "foo.com",
|
||||
method = {"GET", "POST"},
|
||||
remote_addr = "127.0.0.1",
|
||||
},
|
||||
{
|
||||
path = "/bb*",
|
||||
host = {"*.bar.com", "gloo.com"},
|
||||
method = {"GET", "POST", "PUT"},
|
||||
remote_addr = "fe80:fe80::/64",
|
||||
vars = {"arg_name", "jack"},
|
||||
}
|
||||
})
|
||||
|
||||
ngx.say(rx:match("/aa", {host = "foo.com",
|
||||
method = "GET",
|
||||
remote_addr = "127.0.0.1"
|
||||
}))
|
||||
|
||||
```
|
||||
|
||||
从中你也可以看出, `lua-resty-radixtree` 支持根据 uri、host、http method、http header、Nginx 变量、IP 地址等多个维度,作为路由查找的条件;同时,基数树的时间复杂度为 O(K),性能远比现有 API 网关常用的“遍历+hash 缓存”的方式,来得更为高效。
|
||||
|
||||
## schema
|
||||
|
||||
schema 的选择其实要容易得多,我们在前面介绍过的 `lua-rapidjson` ,就是非常好的一个选择。这部分你完全没有必要自己去写一个,json schema 已经足够强大了。下面就是一个简单的示例:
|
||||
|
||||
```
|
||||
local schema = {
|
||||
type = "object",
|
||||
properties = {
|
||||
count = {type = "integer", minimum = 0},
|
||||
time_window = {type = "integer", minimum = 0},
|
||||
key = {type = "string", enum = {"remote_addr", "server_addr"}},
|
||||
rejected_code = {type = "integer", minimum = 200, maximum = 600},
|
||||
},
|
||||
additionalProperties = false,
|
||||
required = {"count", "time_window", "key", "rejected_code"},
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 插件
|
||||
|
||||
有了上面存储、路由和 schema 的基础,上层的插件应该如何实现,其实就清晰多了。插件并没有现成的开源库可以使用,需要我们自己来实现。插件在设计的时候,主要有三个方面需要我们考虑清楚。
|
||||
|
||||
首先是如何挂载。我们希望插件可以挂载到 `rewrite`、`access`、`header filer`、`body filter` 和 `log`阶段,甚至在 `balancer` 阶段也可以设置自己的负载均衡算法。所以,我们应该在 Nginx 的配置文件中暴露这些阶段,并在对插件的实现中预留好接口。
|
||||
|
||||
其次是如何获取配置的变更。由于没有关系型数据库的束缚,插件参数的变更可以通过 etcd 的 watch 来实现,这会让整体框架的代码逻辑变得更加明了易懂。
|
||||
|
||||
最后是插件的优先级。具体来说,比如,身份认证和限流限速的插件,应该先执行哪一个呢?绑定在 route 和绑定在 service 上的插件发生冲突时,又应该以哪一个为准呢?这些都是我们需要考虑到位的。
|
||||
|
||||
在梳理清楚插件的这三个问题后,我们就可以得到插件内部的一个流程图了:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d1/13/d18243966a4973ff8409dd45bf83dc13.png" alt="">
|
||||
|
||||
## 架构
|
||||
|
||||
自然,当微服务 API 网关的这些关键组件都确定了之后,用户请求的处理流程,也就随之尘埃落定了。这里我画了一张图来表示这个流程:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7f/89/7f2b50689a86d382a4c9340b4edb9489.png" alt="">
|
||||
|
||||
从这个图中我们可以看出,当一个用户请求进入 API 网关时,
|
||||
|
||||
- 首先,会根据请求的方法、uri、host、请求头等条件,去路由规则中进行匹配。如果命中了某条路由规则,就会从 etcd 中获取对应的插件列表。
|
||||
- 然后,和本地开启的插件列表进行交集,得到最终可以运行的插件列表。
|
||||
- 再接着,根据插件的优先级,逐个运行插件。
|
||||
- 最后,根据上游的健康检查和负载均衡算法,把这个请求发送给上游。
|
||||
|
||||
当架构设计完成后,我们就胸有成竹,可以去编写具体的代码了。这其实就像盖房子一样,只有在你拥有设计的蓝图和坚实的地基之后,才能去做砖瓦堆砌的具体工作。
|
||||
|
||||
## 写在最后
|
||||
|
||||
其实,通过这两节课的学习,我们已经做好了产品定位和技术选型这两件最重要的事情,它们都比具体的编码实现更为关键,也希望你可以更用心地去考虑和选择。
|
||||
|
||||
那么,在你的实际工作中,你是否使用过 API 网关呢?你们公司又是如何做 API 网关的选型的呢?欢迎留言和我分享你的经历和收获,也欢迎你把这篇文章分享出去,和更多的人一起交流、进步。
|
||||
211
极客时间专栏/OpenResty从入门到实战/API网关篇/49 | 微服务API网关搭建三步曲(三).md
Normal file
211
极客时间专栏/OpenResty从入门到实战/API网关篇/49 | 微服务API网关搭建三步曲(三).md
Normal file
@@ -0,0 +1,211 @@
|
||||
<audio id="audio" title="49 | 微服务API网关搭建三步曲(三)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d4/c7/d4b1aa87b212a3023fc52be2116562c7.mp3"></audio>
|
||||
|
||||
你好,我是温铭。
|
||||
|
||||
今天这节课,微服务 API 网关搭建就到了最后的环节了。让我们用一个最小的示例来把之前选型的组件,按照设计的蓝图,拼装运行起来吧!
|
||||
|
||||
## Nginx 配置和初始化
|
||||
|
||||
我们知道,API 网关是用来处理流量入口的,所以我们首先需要在 Nginx.conf 中做简单的配置,让所有的流量都通过网关的 Lua 代码来处理。
|
||||
|
||||
```
|
||||
server {
|
||||
listen 9080;
|
||||
|
||||
init_worker_by_lua_block {
|
||||
apisix.http_init_worker()
|
||||
}
|
||||
|
||||
location / {
|
||||
access_by_lua_block {
|
||||
apisix.http_access_phase()
|
||||
}
|
||||
header_filter_by_lua_block {
|
||||
apisix.http_header_filter_phase()
|
||||
}
|
||||
body_filter_by_lua_block {
|
||||
apisix.http_body_filter_phase()
|
||||
}
|
||||
log_by_lua_block {
|
||||
apisix.http_log_phase()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里我们使用开源 API 网关 [APISIX](https://github.com/apache/apisix) 为例,所以上面的代码示例中带有 `apisix` 的关键字。在这个示例中,我们监听了 9080 端口,并通过 `location /` 的方式,把这个端口的所有请求都拦截下来,并依次通过 `access`、`rewrite`、`header filter`、`body filter` 和 `log` 这几个阶段进行处理,在每个阶段中都会去调用对应的插件函数。其中, `rewrite` 阶段便是在 `apisix.http_access_phase` 函数中合并处理的。
|
||||
|
||||
而对于系统初始化的工作,我们放在了 `init_worker` 阶段来处理,这其中包含了读取各项配置参数、预制 etcd 中的目录、从 etcd 中获取插件列表、对于插件按照优先级进行排序等。我这里列出了关键部分的代码并进行讲解,当然,你可以在 GitHub 上看到更完整的[初始化函数](https://github.com/apache/apisix/blob/master/lua/apisix.lua#L47)。
|
||||
|
||||
```
|
||||
function _M.http_init_worker()
|
||||
-- 分别初始化路由、服务和插件这三个最重要的部分
|
||||
router.init_worker()
|
||||
require("apisix.http.service").init_worker()
|
||||
require("apisix.plugin").init_worker()
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
通过阅读这段代码,你可以发现,`router` 和 `plugin` 这两部分的初始化相对复杂一些,主要涉及到读取配置参数,并根据参数的不同做一些选择。因为这里会涉及到从 etcd 中读取数据,所以我们使用的是 `ngx.timer` 的方式,来绕过“不能在 `init_worker` 阶段使用 cosocket”的这个限制。如果你对这部分很感兴趣并且学有余力,建议一定要去读读源码,加深理解。
|
||||
|
||||
## 匹配路由
|
||||
|
||||
在最开始的 `access` 阶段里面,我们首先需要做的就是匹配路由,根据请求中携带 uri、host、args、cookie 等,来和已经设置好的路由规则进行匹配:
|
||||
|
||||
```
|
||||
router.router_http.match(api_ctx)
|
||||
|
||||
```
|
||||
|
||||
对外暴露的,其实只有上面一行代码,这里的`api_ctx` 中存放的就是 uri、host、args、cookie 这些请求的信息。而具体的 `match` 函数的[实现](https://github.com/apache/apisix/blob/master/apisix/http/router/radixtree_uri.lua),就用到了我们前面提到过的 `lua-resty-radixtree`。如果没有命中,就说明这个请求并没有设置与之对应的上游,就会直接返回 404。
|
||||
|
||||
```
|
||||
local router = require("resty.radixtree")
|
||||
|
||||
local match_opts = {}
|
||||
|
||||
function _M.match(api_ctx)
|
||||
-- 从 ctx 中获取请求的参数,作为路由的判断条件
|
||||
match_opts.method = api_ctx.var.method
|
||||
match_opts.host = api_ctx.var.host
|
||||
match_opts.remote_addr = api_ctx.var.remote_addr
|
||||
match_opts.vars = api_ctx.var
|
||||
-- 调用路由的判断函数
|
||||
local ok = uri_router:dispatch(api_ctx.var.uri, match_opts, api_ctx)
|
||||
-- 没有命中路由就直接返回 404
|
||||
if not ok then
|
||||
core.log.info("not find any matched route")
|
||||
return core.response.exit(404)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
## 加载插件
|
||||
|
||||
当然,如果路由可以命中,就会走到过滤插件和加载插件的步骤,这也是 API 网关的核心所在。我们先来看下面这段代码:
|
||||
|
||||
```
|
||||
local plugins = core.tablepool.fetch("plugins", 32, 0)
|
||||
-- etcd 中的插件列表和本地配置文件中的插件列表进行交集运算
|
||||
api_ctx.plugins = plugin.filter(route, plugins)
|
||||
|
||||
-- 依次运行插件在 rewrite 和 access 阶段挂载的函数
|
||||
run_plugin("rewrite", plugins, api_ctx)
|
||||
run_plugin("access", plugins, api_ctx)
|
||||
|
||||
```
|
||||
|
||||
在这段代码中,我们首先通过 table pool 的方式,申请了一个长度为 32 的 table,这是我们之前介绍过的性能优化技巧。然后便是插件的过滤函数。你可能疑惑,为什么需要这一步呢?在插件的 `init worker` 阶段,我们不是已经从 etcd 中获取插件列表并完成排序了吗?
|
||||
|
||||
事实上,这里的过滤是和本地配置文件来做对比的,主要有下面两个原因。
|
||||
|
||||
- 第一,新开发的插件需要灰度来发布,这时候新插件在 etcd 的列表中存在,但只在部分网关节点中处于开启状态。所以,我们需要额外做一次交集的运算。
|
||||
- 第二,为了支持 debug 模式。终端的请求经过了哪些插件的处理?这些插件的加载顺序是什么?这些信息在调试的时候会很有用,所以在过滤函数中也会判断其是否处于 debug 模式,并在响应头中记录下这些信息。
|
||||
|
||||
因此,在 access 阶段的最后,我们会把这些过滤好的插件,按照优先级逐个运行,如下面这段代码所示:
|
||||
|
||||
```
|
||||
local function run_plugin(phase, plugins, api_ctx)
|
||||
for i = 1, #plugins, 2 do
|
||||
local phase_fun = plugins[i][phase]
|
||||
if phase_fun then
|
||||
-- 最核心的调用代码
|
||||
phase_fun(plugins[i + 1], api_ctx)
|
||||
end
|
||||
end
|
||||
|
||||
return api_ctx
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
你可以看到,在遍历插件的时候,我们是以 `2` 为间隔进行的,这是因为每个插件都会有两个部分组成:插件对象和插件的配置参数。现在,我们来看上面示例代码中最核心的那一行代码:
|
||||
|
||||
```
|
||||
phase_fun(plugins[i + 1], api_ctx)
|
||||
|
||||
```
|
||||
|
||||
单独看这行代码会有些抽象,我们用一个具体的 `limit_count` 插件来替换一下,就会清楚很多:
|
||||
|
||||
```
|
||||
limit_count_plugin_rewrite_function(conf_of_plugin, api_ctx)
|
||||
|
||||
```
|
||||
|
||||
到这里,API 网关的整体流程,我们就实现得差不多了。这些代码都在同一个代码[文件](https://github.com/apache/apisix/blob/master/apisix/init.lua)中,它里面有 400 多行代码,但核心的代码就是我们上面所介绍的这短短几十行。
|
||||
|
||||
## 编写插件
|
||||
|
||||
现在,距离一个完整的 demo 还差一件事情,那就是编写一个插件,让它可以跑起来。我们以 `limit-count` 这个限制请求数的插件为例,它的[完整实现](https://github.com/apache/apisix/blob/master/apisix/plugins/limit-count.lua)只有 60 多行代码,你可以点击链接查看。下面,我来详细讲解下其中的关键代码。
|
||||
|
||||
首先,我们要引入 `lua-resty-limit-traffic` ,作为限制请求数的基础库:
|
||||
|
||||
```
|
||||
local limit_count_new = require("resty.limit.count").new
|
||||
|
||||
```
|
||||
|
||||
然后,使用 rapidjson 中的 json schema ,来定义这个插件的参数有哪些:
|
||||
|
||||
```
|
||||
local schema = {
|
||||
type = "object",
|
||||
properties = {
|
||||
count = {type = "integer", minimum = 0},
|
||||
time_window = {type = "integer", minimum = 0},
|
||||
key = {type = "string",
|
||||
enum = {"remote_addr", "server_addr"},
|
||||
},
|
||||
rejected_code = {type = "integer", minimum = 200, maximum = 600},
|
||||
},
|
||||
additionalProperties = false,
|
||||
required = {"count", "time_window", "key", "rejected_code"},
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
插件的这些参数,和大部分 `resty.limit.count` 的参数是对应的,其中包含了限制的 key、时间窗口的大小、限制的请求数。另外,插件中增加了一个参数: `rejected_code`,在请求被限速的时候返回指定的状态码。
|
||||
|
||||
最后一步,我们把插件的处理函数挂载到 `rewrite` 阶段:
|
||||
|
||||
```
|
||||
function _M.rewrite(conf, ctx)
|
||||
-- 从缓存中获取 limit count 的对象,如果没有就使用 `create_limit_obj` 函数新建并缓存
|
||||
local lim, err = core.lrucache.plugin_ctx(plugin_name, ctx, create_limit_obj, conf)
|
||||
|
||||
-- 从 ctx.var 中获取 key 的值,并和配置类型和配置版本号一起组成新的 key
|
||||
local key = (ctx.var[conf.key] or "") .. ctx.conf_type .. ctx.conf_version
|
||||
|
||||
-- 进入限制的判断函数
|
||||
local delay, remaining = lim:incoming(key, true)
|
||||
if not delay then
|
||||
local err = remaining
|
||||
-- 如果超过阈值,就返回指定的状态码
|
||||
if err == "rejected" then
|
||||
return conf.rejected_code
|
||||
end
|
||||
|
||||
core.log.error("failed to limit req: ", err)
|
||||
return 500
|
||||
end
|
||||
|
||||
-- 如果没有超过阈值,就放行,并设置对应响应头
|
||||
core.response.set_header("X-RateLimit-Limit", conf.count,
|
||||
"X-RateLimit-Remaining", remaining)
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
上面的代码中,进行限制判断的逻辑只有一行,其他的都是来做准备工作和设置响应头的。如果没有超过阈值,就会继续按照优先级运行下一个插件。
|
||||
|
||||
## 写在最后
|
||||
|
||||
今天这节课,通过整体框架和插件的编写,我们就完成了一个 API 网关的 Demo。更进一步,利用本专栏学到的 OpenResty 知识,你可以在上面继续添砖加瓦,搭建更丰富的功能。
|
||||
|
||||
最后,给你留一个思考题。我们知道,API 网关不仅可以处理七层的流量,也可以处理四层的流量,基于此,你能想到它的一些使用场景吗?欢迎留言说说你的看法,也欢迎你把这篇文章分享出去,和更多的人一起学习、交流。
|
||||
104
极客时间专栏/OpenResty从入门到实战/API网关篇/50 | 答疑(五):如何在工作中引入 OpenResty?.md
Normal file
104
极客时间专栏/OpenResty从入门到实战/API网关篇/50 | 答疑(五):如何在工作中引入 OpenResty?.md
Normal file
@@ -0,0 +1,104 @@
|
||||
<audio id="audio" title="50 | 答疑(五):如何在工作中引入 OpenResty?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/03/94/03cb04deab8b531696b1a6e28408c694.mp3"></audio>
|
||||
|
||||
你好,我是温铭。
|
||||
|
||||
几个月的时间转瞬即逝,到现在,OpenResty专栏的最后一个版块微服务 API 网关篇,我们就已经学完了。恭喜你没有掉队,始终在积极学习和实践操作,并且热情地留下了你的思考。
|
||||
|
||||
很多留言提出的问题很有价值,大部分我都已经在App里回复过,一些手机上不方便回复的或者比较典型、有趣的问题,我专门摘了出来,作为今天的答疑内容,集中回复。另一方面,也是为了保证所有人都不漏掉任何一个重点。
|
||||
|
||||
下面我们来看今天的这 5 个问题。
|
||||
|
||||
## 问题一:OpenResty 在工作中的使用
|
||||
|
||||
Q:快结课了,我也基本上跟下来了,但自己的实践还是偏少(工作中目前未用)。不过,这确实是很强大的一门课。感谢温老师的持续分享,后期工作中我也会择机引入。
|
||||
|
||||
A:感谢这位同学的认可,关于这条留言,我想聊一聊,如何在工作中引入 OpenResty,这确实是一个值得一谈的话题。
|
||||
|
||||
OpenResty 基于 Nginx,并在它的基础之上加了 lua-nginx-module 的 C 模块和众多 lua-resty 库,所以 OpenResty 是可以无痛替换 Nginx 的,这是成本最低的开始使用 OpenResty 的方法。当然,这个替换过程也是有风险的,你需要注意下面这三点。
|
||||
|
||||
第一,确认线上 Nginx 的版本。OpenResty 的主版本号与 Nginx 保持一致,比如 OpenResty 1.15.8.1 使用的就是 Nginx 1.15.8 的内核。如果目前线上 Nginx 的版本号比 OpenResty 的最新版高,那么你最好谨慎换用 OpenResty,毕竟,OpenResty 升级的速度还是比较慢的,离 Nginx 的主线版本要落后半年到一年的时间。如果线上 Nginx 的版本和 OpenResty 的一致或者比 OpenResty 的低,那就具备了升级的前提条件。
|
||||
|
||||
第二,测试。测试是最主要的一个环节,使用 OpenResty 替换 Nginx 的风险很低,但肯定也存在一些风险。比如,是否有自定义的 C 模块需要编译,OpenResty 依赖的 openssl 版本,以及 OpenResty 给 Nginx 打的 patch 是否对业务会造成影响等。你需要复制一些业务的流量过来做验证。
|
||||
|
||||
第三,流量切换。基本的验证通过后,你还需要线上真实流量的灰度来验证,这时候为了能够快速的回滚,我们可以新开几台服务器来部署 OpenResty,而不是直接替换原有的 Nginx 服务。如果没有问题,我们可以选择二进制文件热升级的方式,或者是从 LB 中逐步摘掉和替换 Nginx 的方式来升级。
|
||||
|
||||
OpenResty 除了可以替代 Nginx 外,另外两个比较容易的切入点是 WAF 和 API 网关,它们都是对性能和动态有比较高要求的场景,也有对应的开源项目可以开箱即用,我在专栏中也有部分涉及到。
|
||||
|
||||
再继续把 OpenResty 深入到业务层面的话,就需要考虑比较多技术之外的因素了,比如是否容易招聘到 OpenResty 相关的工程师?是否能够和公司原有的技术系统进行融合等等。
|
||||
|
||||
总的来说,从替代 Nginx 的角度来切入,然后慢慢扩散来使用OpenResty ,是一个不错的注意。
|
||||
|
||||
## 问题二:OpenResty 的数据库封装
|
||||
|
||||
Q:根据你的指点,要尽量少用 `..`字符串拼接,特别是在代码热区。但是我在处理数据库访问时,需要动态构建SQL语句(在语句中插入变量),这应该是非常常见的使用场景。可是对于这个需求,我目前感觉,只有字符串拼接是最简单的办法,其他真的想不到既简单又高性能的办法。
|
||||
|
||||
A:你可以先用我们前面课程介绍过的 SystemTap 或者其他工具分析下,看 SQL 语句的拼接是否是系统的瓶颈。如果不是,自然就没有优化的必要性,毕竟,过早的优化是万恶之源。
|
||||
|
||||
如果瓶颈确实是 SQL 语句的拼接,那么我们可以利用数据库的 `prepare` 语句来做优化,也可以用数组的方式来做拼接。但 `lua-resrty-mysql` 对 `prepare` 的支持一直处于 TODO 状态,所以只剩下数组拼接的方式了。这也是一些 lua-resty 库的通病,实现了大部分的功能,处于能用的状态,但更新得并不够及时。除了数据库的 `prepare` 语句外,`lua-resty-redis` 对 `cluster` 也一直没有支持。
|
||||
|
||||
字符串拼接,包括 lua-resty 库的这类问题,OpenResty 是希望用 DSL 来彻底解决的——使用编译器的技术自动生成数组来拼接字符串,把这些细节隐藏起来,上层的用户不用感知;使用小语言 wirelang 来自动生成各种 lua-resty 网络通信库,不再需要手写。
|
||||
|
||||
这听上去很美好吧?但有一个问题必须正视,那就是自动生成的代码对人类是不友好的。如果你要学习或者修改生成的代码,就必须再学习编译器技术以及一门可能不会开源的 DSL,这会让参与社区的门槛越来越高。
|
||||
|
||||
## 问题三:OpenResty 的 Web 框架
|
||||
|
||||
Q:我现在想用 OpenResty 做一个Web项目,但做起来很痛苦,主要是没找到成熟的框架,需要自己造很多轮子,就比如说上面的数据库操作问题(没找到可以动态构建SQL语句、连贯操作的类库)。所以想问下老师,在Web框架上有什么好的可以推荐吗?
|
||||
|
||||
A:在 `awesome-resty` 这个仓库中,我们可以看到有专门的 [W](https://github.com/bungle/awesome-resty#web-frameworks)[eb 框架分类](https://github.com/bungle/awesome-resty#web-frameworks),有 20 个 开源项目,不过大部分项目都处于停滞的状态。其中,Lapis、lor 和香草这三个项目你可以尝试下,看看哪一个更适合。
|
||||
|
||||
确实,由于没有强大的 Web 框架作为支撑,OpenResty 在处理大项目的时候就会力不从心,这也是很少有人用 OpenResty 做业务系统的原因之一。
|
||||
|
||||
## 问题四:修改了响应体,怎么修改响应头中的content-length?
|
||||
|
||||
Q:如果需要修改respones body的内容,就只能在body filter里做修改,但这样会引起body长度与 content-length 长度不一致,应该如何处理呢?
|
||||
|
||||
A:在这种情况下,我们需要在 body filter 之前的 header filter 阶段中,把 content length 这个响应头置为 nil,不再返回,改为流式输出。
|
||||
|
||||
下面是一段示例代码:
|
||||
|
||||
```
|
||||
server {
|
||||
listen 8080;
|
||||
|
||||
location /test {
|
||||
proxy_pass http://www.baidu.com;
|
||||
header_filter_by_lua_block {
|
||||
ngx.header.content_length = nil
|
||||
}
|
||||
body_filter_by_lua_block {
|
||||
ngx.arg[1] = ngx.arg[1] .. "abc"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
通过这段代码你可以看到,在 body filter 阶段中,`ngx.arg[1]` 代表的就是响应体。如果我们在它后面增加了字符串 `abc`,响应头 content length 就不准确了,所以,我们在 header filter 阶段直接把它禁用掉就可以了。
|
||||
|
||||
另外,从这个示例中,我们还可以看到 OpenResty 的各个阶段之间是如何来配合工作的,这一点也希望你注意并思考。
|
||||
|
||||
## 问题五:Lua 代码的查找路径
|
||||
|
||||
Q:`lua_package_path` 似乎配置的是Lua依赖的搜索路径。对于`content_by_lua_file`,我试验发现,它只在prefix下根据指令提供的文件相对路径去搜索,而不会到 `lua_package_path` 下搜索。不知道我的理解对不对?
|
||||
|
||||
A:这位同学自己动手试验和思考的精神非常值得肯定,并且这个理解也是对的。`lua_package_path` 这个指令是用来加载 Lua 模块而使用的,比如我们在调用 `require 'cjson'` 时,就会到`lua_package_path` 中的指定目录中,去查找 cjson 这个模块。而 `content_by_lua_file` 则不同,它后面跟随的是磁盘中的一个文件路径:
|
||||
|
||||
```
|
||||
location /test {
|
||||
content_by_lua_file /path/test.lua;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
而且,如果这里不是绝对路径而是相对路径:
|
||||
|
||||
```
|
||||
content_by_lua_file path/test.lua;
|
||||
|
||||
```
|
||||
|
||||
那么就会使用 OpenResty 启动时指定的 `-p` 目录,来做一个拼接,从而得到绝对路径。
|
||||
|
||||
今天主要解答这几个问题。最后,欢迎你继续在留言区写下你的疑问,我会持续不断地解答。希望可以通过交流和答疑,帮你把所学转化为所得。也欢迎你把这篇文章转发出去,我们一起交流、一起进步。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user