category
“高速”、“最小延迟”和“终极性能”经常被用来描述Apache APISIX。即使有人问我关于APISIX的问题,我的回答总是包括“高性能云原生API网关”
性能基准测试(与孔,Envoy)证实了这些特性确实是准确的(自己测试)。
测试在标准D8s v3(8个vCPU,32 GiB内存)上运行了10轮,有5000条独特的路由。
但是APISIX是如何做到这一点的呢?
要回答这个问题,我们必须看三件事:etcd、哈希表和基数树。
在本文中,我们将深入了解APISIX,了解它们是什么,以及所有这些是如何协同工作的,以使APISIX在处理大量流量的同时保持峰值性能。
Etcd作为配置中心
APISIX使用etcd来存储和同步配置。
etcd被设计为作为大规模分布式系统配置的关键值存储。APISIX旨在从一开始就具有分布式和高度可扩展性,在传统数据库上使用etcd有助于实现这一点。
API网关的另一个关键的不可或缺的功能是高度可用,避免停机和数据丢失。您可以通过部署etcd的多个实例来确保容错的云原生架构,从而有效地实现这一点。
APISIX可以以最小的延迟从/向etcd读取/写入配置。配置文件的更改会立即得到通知,从而允许APISIX仅监视etcd更新,而不是频繁轮询数据库,这可能会增加性能开销。
此图表总结了etcd与其他数据库的比较情况。
IP地址的哈希表
基于IP地址的allowlists/denylist是API网关的常见用例。
为了实现高性能,APISIX将IP地址列表存储在哈希表中,并将其用于匹配(O(1)),而不是迭代列表(O(N))。
随着列表中IP地址数量的增加,使用哈希表进行存储和匹配对性能的影响变得显而易见。
在后台,APISIX使用lua-resty-ipmatcher库来实现此功能。以下示例显示了库的使用方式:
local ipmatcher = require("resty.ipmatcher")
local ip = ipmatcher.new({
"162.168.46.72",
"17.172.224.47",
"216.58.32.170",
})
ngx.say(ip:match("17.172.224.47")) -- true
ngx.say(ip:match("176.24.76.126")) -- false
该库使用作为哈希表的Lua表。IP地址被散列并作为索引存储在表中,要搜索给定的IP地址,只需对表进行索引并测试它是否为零。
要搜索IP地址,它首先计算哈希(索引)并检查其值。如果它不是空的,我们有一个匹配。这是在恒定时间O(1)内完成的。
路由的根树 (Radix Trees for Routing)
请原谅我骗你上数据结构课!但请听我说完;这就是它变得有趣的地方。
APISIX优化性能的一个关键领域是路由匹配。
APISIX将路由与其URI、HTTP方法、主机和其他信息中的请求进行匹配(请参阅路由器)。这需要有效率。
如果你已经阅读了上一节,一个显而易见的答案是使用哈希算法。但是路由匹配很棘手,因为多个请求可以匹配同一个路由。
例如,如果我们有一个路由/api/*,那么/api/create和/api/decurt都必须与该路由匹配。但这在哈希算法中是不可能的。
正则表达式可以是另一种解决方案。路由可以在regex中进行配置,它可以匹配多个请求,而无需对每个请求进行硬编码。
如果我们以前面的例子为例,我们可以使用regex/api/[A-Za-z0-9]+来匹配/api/create和/api/default。更复杂的正则表达式可以匹配更复杂的路由。
但是regex很慢!我们知道APISIX很快。因此,APISIX使用基数树,这是一种压缩前缀树(trie),非常适合快速查找。
让我们看一个简单的例子。假设我们有以下单词:
- romane
- romanus
- romulus
- rubens
- ruber
- rubicon
- rubicundus
前缀树会这样存储它:
突出显示的遍历显示单词“rubens”
如果一个节点只有一个子节点,基数树通过合并子节点来优化前缀树。我们的示例trie看起来像一个基数树:
突出显示的遍历仍然显示单词“rubens”。但树看起来要小得多!
当您在APISIX中创建路由时,APISIX会将它们存储在这些树中。
APISIX可以完美地工作,因为匹配路由所需的时间仅取决于请求中URI的长度,并且与路由的数量无关(O(K),K是密钥/URI的长度)。
因此,当你第一次出发时匹配10条路线,当你扩展时匹配5000条路线时,APISIX将与之一样快。
这个粗略的例子展示了APISIX如何使用基数树存储和匹配路由:
突出显示的遍历显示路由/user/*,其中*表示前缀。因此,像/user/navendu这样的URI将匹配此路由。下面的示例代码应该会使这些想法更加清晰。
APISIX使用lua-resty-radixtree库,该库围绕C中的基数树实现rax进行封装。与在纯lua中实现该库相比,这提高了性能。
以下示例显示了库的使用方式:
local radix = require("resty.radixtree")
local rx = radix.new({
{
paths = { "/api/*action" },
metadata = { "metadata /api/action" }
},
{
paths = { "/user/:name" },
metadata = { "metadata /user/name" },
methods = { "GET" },
},
{
paths = { "/admin/:name" },
metadata = { "metadata /admin/name" },
methods = { "GET", "POST", "PUT" },
filter_fun = function(vars, opts)
return vars["arg_access"] == "admin"
end
}
})
local opts = {
matched = {}
}
-- matches the first route
ngx.say(rx:match("/api/create", opts)) -- metadata /api/action
ngx.say("action: ", opts.matched.action) -- action: create
ngx.say(rx:match("/api/destroy", opts)) -- metadata /api/action
ngx.say("action: ", opts.matched.action) -- action: destroy
local opts = {
method = "GET",
matched = {}
}
-- matches the second route
ngx.say(rx:match("/user/bobur", opts)) -- metadata /user/name
ngx.say("name: ", opts.matched.name) -- name: bobur
local opts = {
method = "POST",
var = ngx.var,
matched = {}
}
-- matches the third route
-- the value for `arg_access` is obtained from `ngx.var`
ngx.say(rx:match("/admin/nicolas", opts)) -- metadata /admin/name
ngx.say("admin name: ", opts.matched.name) -- admin name: nicolas
高效管理大量路由的能力使APISIX成为许多大型项目选择的API网关。
看看引擎盖下面
在一篇文章中,我只能解释APISIX的内部工作原理。
但最棒的是,这里提到的库和ApacheAPISIX都是完全开源的,这意味着你可以自己查看并修改内容。
如果你能改进APISIX以获得最后一点性能,你就可以将更改贡献回项目,让每个人都从你的工作中受益。
- 登录 发表评论
- 17 次浏览
Tags
最新内容
- 3 days 20 hours ago
- 3 days 20 hours ago
- 3 days 21 hours ago
- 3 days 20 hours ago
- 5 days 2 hours ago
- 1 week 6 days ago
- 1 week 6 days ago
- 1 week 6 days ago
- 2 weeks 1 day ago
- 2 weeks 2 days ago