Apisix网关实现技术解析

 

引言

Apisix 是一个著名的开源高性能API 网关,提供了负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。

Apisix 的诞生主要是为了是解决 Nginx 的动态配置问题以及网关功能扩展问题,其基于 Nginx 与 LuaJIT 技术带来的高性能、高灵活等特性打造。

本文重点介绍Apisix的技术栈生态,业务处理流程和配置管理原理等重点模块,读者通过阅读本文可以理解Apisix的实现原理。限于篇幅以及lua语言的小众特点,本文重点不在于解读代码,代码片段只辅助直观认识相关实现原理,重点在于解读功能实现,相关代码细节读者可自行下载Apisix仓库源码学习。

背景介绍

Apisix强大的根源在于其运行于 Nginx 和 LuaJIT 技术栈之上。准确的说是Apisix 运行于 Openresty 之上,Openresty 运行于 Nginx 之上。

Nginx 是一个跨平台的开源 Web 服务器,使用 C 语言开发。Nginx 的优势在于善于处理高并发,能在高并发请求的同时保持高效的服务。其领先的事件驱动型设计和全异步的网络 I/O 处理机制,以及极致的内存分配管理等众多优秀设计,将服务器硬件资源利用到极致。使得 NGINX 成为高性能 Web 服务器的代表。

Openresty的目标是让用户的 Web 服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,对后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等专有协议请求都提供高性能支持。Openresty 通过对 Nginx 做出充分的扩展,引出了许多 hook 点,可以使用 lua 语言对这些 hook 进行实现而扩展功能。

OpenResty 之所以可以保持很高的性能,是因为它借用了 Nginx 的事件处理和 Lua 的实时编译机制。LuaJIT 是 Lua 这种编程语言的实时编译(JIT,Just-In-Time Compilation)器的实现。Lua 是一种优雅、易于学习的编程语言,具有自动内存管理、完整的词法作用域、闭包、迭代器、协程、正确的尾部调用以及使用关联数组进行非常实用的数据处理。

Lua 的设计目标是能与 C 或其它常用的编程语言相互集成,这样就可以利用其它语言已经做好的方面;而它提供的特性又恰好是 C 这类语言不太擅长的,比如相对于硬件层的高层抽象,动态的结构,可移植性高等。因此 Lua 不仅是一个可以作为独立程序运行的脚本语言,也是一个可以嵌入其它应用的嵌入式语言。但此时的 Lua 还有传统脚本语言常见的两个问题:效率低和代码暴露。而 LuaJIT 引入的 JIT 技术能够有效地解决了这两个问题。

LuaJIT 尝试将 Lua 的动态解释和 C 的静态编译两者的优缺点相结合,在脚本语言的执行期间,通过不断地分析正在执行的代码片段,编译或重新编译这段代码,以得到执行效率的提升。LuaJIT 在 Lua 语法的基础上,实现了迄今为止脚本语言中最快的 JIT 之一,并提供了 FFI 等功能,解决了 Lua 效率低和代码暴露的问题,让 Lua 真正成为了高灵活性、高性能和超低内存占用的脚本语言和嵌入式语言。

Apisix 在 Openresty 的基础上,实现了 Openresty 许多的 hook函数(使用lua语言),提供了动态配置 Nginx 的能力以满足复杂的网关功能需求。Apisix本质上是接管了Openresty生命周期的管理框架。那么OpenResty是怎么和lua交互的?本质是OpenResty在Nginx的Master和每个Worker进程中加入了LuaJIT VM来解析lua代码,在lua执行过程中创建的协程会共用一个Lua虚拟机。

以路由为例,Nginx 需要在配置文件内进行配置,每次更改都需要 reload 之后才能生效。而为了实现路由动态配置,Apache APISIX 在 NGINX 配置文件内配置了单个 server,这个 server 中只有一个 location。我们把这个 location 作为主入口,所有的请求都会经过这个 location,再由 APISIX Core 动态指定具体上游。因此 Apisix 的路由模块支持在运行时增减、修改和删除路由,实现了动态加载,所有的这些变化,对客户端都零感知。 比如增加某个新域名的反向代理,在 Apisix 中只需创建上游,并添加新的路由即可,整个过程中不需要 Nginx进程重启。再比如插件系统,Apisix 可以通过 ip-restriction 插件实现 IP 黑白名单功能,这些能力的更新也是动态方式,同样不需要重启服务。借助 etcd配置中心化存储,配置策略以增量方式实时推送,最终让所有规则实时、动态的生效,为用户带来良好的体验。

简单来说,Openresty 对 Nginx 做了充分的扩展,引出了许多 hook 点,Apisix使用 lua 语言对这些 hook 进行了实现。比如Apisix 中的转发上游服务的负载均衡逻辑实际上是使用了lua-resty-core/ngx/balancer库。Lua 的灵活性,也使得 Apisix 的路由分发模块,可以轻易地支持通过特定的表达式等方法,对同一前缀的下级路由进行匹配。最终在替代 NGINX 原生路由分发功能的前提下,实现了兼具高性能、高灵活性的动态配置功能。另外,不只是路由,从负载均衡、健康检查,到上游节点配置、服务端证书,以及扩展 APISIX 功能的插件本身,都能在 APISIX 不重启的情况下重新加载。

启动流程

Apisix的启动流程核心是根据Apisix配置生成nginx.conf,接着运行openresty。初始化结束后,Apisix将会接管openresty的各种生命周期管理。具体包括如下阶段:

  • init阶段初始化 nginx 配置,通过读取 Apisix conf/config.yaml ,解析配置生成 nginx config 文件。供 openresty下的nginx使用。
  • init_etcd建立etcd连接,初始化 etcd 中的存储目录。
  • apisix start函数实际执行 “openresty -p /usr/local/apisix -g 'daemon off' ”命令,启动 openresty 。
  • apisix.http_init(),初始化 nginx 相关进程。设置 dns resolver。启动 "privileged agent"。
  • apisix.http_init_worker()是Apisix 的核心初始化逻辑,执行初始化 openresty worker event ,以及discovery.init_worker()。
  • Apisix判断当前 Apisix 文件路径,寻找配置的openresty 路径以确定 luajit 位置,依据环境使用 luajit 启动 Apisix或者直接使用 lua 启动 Apisix。
./Apisix/cli/Apisix.lua
    # use the luajit of openresty
    echo "$LUAJIT_BIN $APISIX_LUA $*"
    exec $LUAJIT_BIN $APISIX_LUA $*
elif [[ "$LUA_VERSION" =~ "Lua 5.1" ]]; then
    # OpenResty version is not 1.19, use Lua 5.1 by default
    echo "lua $APISIX_LUA $*"
exec lua $APISIX_LUA $*

Apisix init 生成的 nginx 配置文件。通过阅读 nginx 文件,可以了解 Apisix 整个流程。包括主要的监听端口以及admin管理服务和业务转发服务配置等,配置示例如下,后续章节会讲解对应参数配置的含义。

nginx.conf 文件关键配置示例:

http {
...
    # http configuration snippet starts
    upstream apisix_backend {
        server 0.0.0.1; # 随便填一个无效的值
        balancer_by_lua_block {
            Apisix.http_balancer_phase()
        }
    ...
    }
    init_by_lua_block {
        require "resty.core"
        Apisix = require("Apisix")
        local dns_resolver = { "127.0.0.53", }
        local args = {
            dns_resolver = dns_resolver,
        }
        Apisix.http_init(args)
    }
    init_worker_by_lua_block {
        Apisix.http_init_worker()
    }
    server {
        listen 127.0.0.1:9090;
        access_log off;
    }
    server {
        listen 9080 default_server reuseport;
        listen 9443 ssl default_server http2 reuseport;
        listen [::]:9080 default_server reuseport;
        listen [::]:9443 ssl default_server http2 reuseport;
        server_name _;
        # Apisix ssl 服务端证书配置
        ssl_certificate      cert/ssl_PLACE_HOLDER.crt;
        ssl_certificate_key  cert/ssl_PLACE_HOLDER.key;
        location /Apisix/admin {
            set $upstream_scheme             'http';
            set $upstream_host               $http_host;
            set $upstream_uri                '';
            proxy_pass      $upstream_scheme://Apisix_backend$upstream_uri;
            mirror          /proxy_mirror;
            header_filter_by_lua_block {  allow 127.0.0.0/24;
                deny all;
        location / {
            access_by_lua_block {
                Apisix.http_access_phase()
            }
                Apisix.http_header_filter_phase()
            }
            body_filter_by_lua_block {
                Apisix.http_body_filter_phase()
            }
            log_by_lua_block {
                Apisix.http_log_phase()
            }
        }

配置管理

Nginx 很大的问题是不具备动态配置、远程 API 接口及集群管理的能力,而 Apisix基于 etcd 和 Lua 实现了对 Nginx 集群配置的动态管理。

让 Nginx 具备动态、集群管理能力需要解决以下问题:

  • 微服务架构的上游服务种类多,数量大,这导致路由规则、上游 Server 的变更频繁。而 Nginx 的路由匹配是基于静态的 Trie 前缀树实现的,一旦server_name、location 变动,不执行 reload 就无法实现配置的动态变更。
  • 多进程架构增大了 Worker 进程间的数据同步难度,必须保证每个 Nginx 节点Worker 进程都持有最新的配置。

Apisix基于 Lua 定时器及 lua-resty-etcd 模块实现了配置的动态管理。 etcd提供了集群中心化的配置数据库。etcd 的 watch 机制允许客户端监控某个 key 的变动,类似 /nginx/http/upstream 这种 key 的 value 值发生变动,watch 客户端会立刻收到通知,如下图所示:

Nginx 内核采用了 epoll + nonblock socket 这种多路复用机制实现事件处理模型,其中每个 worker 进程会循环处理网络 IO 及定时器事件:

//参见 Nginx 的 src/os/unix/ngx_process_cycle.c 文件
ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{
    for ( ;; ) {
        ngx_process_events_and_timers(cycle);
    }}
// 参见 ngx_proc.c 文件
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
timer = ngx_event_find_timer();
    (void) ngx_process_events(cycle, timer, flags);
    ngx_event_process_posted(cycle, &ngx_posted_accept_events);
    ngx_event_expire_timers();
    ngx_event_process_posted(cycle, &ngx_posted_events);
}

ngx_event_expire_timers 函数会调用所有超时事件的 handler 方法。定时器是由红黑树结构实现的,其中 key 是每个事件的绝对过期时间。这样,只要将最小节点与当前时间做比较,就能快速找到过期事件。当调用 ngx.timer.at Lua 定时器时,就是在 Nginx 的红黑树定时器里加入了 ngx_http_lua_timer_handler 回调函数,这个函数不会阻塞 Nginx。

Apisix在每个 Nginx Worker 进程的启动过程中,通过 ngx.timer.at 函数将 _automatic_fetch 插入定时器。_automatic_fetch 函数执行时会通过 sync_data 函数,基于 watch 机制接收 etcd 中的配置变更通知,这样每个 Nginx 节点、Worker 进程都将保持最新的配置。如此设计etcd 中的配置直接写入 Nginx Worker 进程中,处理请求时就能直接使用新配置,无须在进程间同步配置,这要比启动独立agent进程更高效。Apisix 在每个 worker 进程中,通过 ngx.timer.at 和 lua-resty-etcd 库反复请求 etcd,以此保证每个 Worker 进程中都含有最新的配置。

Apisix提供如下修改配置的机制:访问任意一个Nginx 节点,通过其 Worker 进程中的 Lua 代码校验请求成功后,再由put 接口写入 etcd 中。

首先,生成的 nginx.conf 会自动监听 9080 端口(通过config.yaml 中 Apisix.node_listen 配置设置),nginx.conf 包含以下配置:

server {
listen 9080 default_server reuseport;
    location/Apisix/admin {
        content_by_lua_block {
            Apisix.http_admin()
        }
    }}

这样,Nginx 接收到的 /Apisix/admin 请求将被 http_admin 函数处理:

-- /Apisix/init.lua 文件
function _M.http_admin()
local ok = router:dispatch(get_var("uri"), {method = get_method()})
end

当 method 方法与 URI 不同时,dispatch 会执行不同的处理函数,其依据如下:

-- /Apisix/admin/init.lua 文件
local uri_route = {
{
    paths = [[/Apisix/admin/*]],
    methods = {"GET", "PUT", "POST", "DELETE", "PATCH"},
    handler = run,
    },
    {
    paths = [[/Apisix/admin/stream_routes/*]],
    methods = {"GET", "PUT", "POST", "DELETE", "PATCH"},
    handler = run_stream,
    },
    {
    paths = [[/Apisix/admin/plugins/list]],
    methods = {"GET"},
    handler = get_plugins_list,
    },
    {
     paths = reload_event,
         methods = {"PUT"},
      handler = post_reload_plugins,
    },
}

比如,当通过 /Apisix/admin/upstreams/1 和 PUT 方法创建 1 个 Upstream 上游配置:

这里通过resources 字典决定具体修改的资源类型。 put 函数的实现:

-- /Apisix/admin/upstreams.lua文件
function _M.put(id, conf)
    -- 校验请求数据的合法性
    local id, err = check_conf(id, conf, true)
    local key = "/upstreams/" .. id
    core.log.info("key: ", key)
    -- 生成etcd中的配置数据
    local ok, err = utils.inject_conf_with_prev_conf("upstream", key, conf)
    -- 写入etcd
    local res, err = core.etcd.set(key, conf)
end
-- /Apisix/core/etcd.lua
local function set(key, value, ttl)
    local res, err = etcd_cli:set(prefix .. key, value, {prev_kv = true, lease = data.body.ID})
end

最终新配置被写入 etcd 中。可见Nginx 会校验数据再写入 etcd,这样其他 Worker 进程、Nginx 节点都将通过 watch 机制接收到正确的配置。

从 nginx.conf 配置可以看到,访问业务的域名、URI 的请求都会匹配到 http_access_phase 这个 lua 函数:

server {
    server_name _;
    location / {
        access_by_lua_block {
            Apisix.http_access_phase()
        }
        proxy_pass      $upstream_scheme://Apisix_backend$upstream_uri;
    }

而在 http_access_phase 函数中,使用基数前缀树匹配 Method、域名和 URI,这个库就是 lua-resty-radixtree。每当路由规则发生变化,Lua 就会自动重建radixtree,这样路由变化后就可以不 reload 而使其生效。Plugin 启用、参数及顺序调整的规则与此类似。

动态修改 Nginx 配置的关键在于两点:Lua 语言的灵活度高于 nginx.conf 语法,而且 Lua 代码可以通过 loadstring 从外部配置中直接导入调用。为了保障路由匹配的执行效率,Apisix 通过LuaJit FFI机制调用 C 语言实现的红黑树,基于 Host、Method、URI 进行请求匹配,在保障动态性的基础上提升了性能。

请求处理

Apisix代码目录结构如下,功能划分比较直观,功能描述见对应目录注释。

Nginx 框架为 C 模块开发提供了许多钩子函数(hook), OpenResty 将部分hook以 Lua 语言形式暴露了出来,Apisix 通过在 Nginx 中不同阶段设置hook 完成自身的功能。

各函数调研流程如下图所示:

APISIX 使用的hook函数说明:

  • init_by_lua:Master 进程启动时的初始化。
  • init_worker_by_lua:每个 Worker 进程启动时的初始化,包括 privileged agent 进程的初始化。
  • ssl_certificate_by_lua:在处理 TLS 握手。
  • access_by_lua:接收到下游的 HTTP 请求头部后,在此匹配 Host 域名、URI、Method 等路由规则,并选择 Service、Upstream 中的插件及上游 Server。
  • balancer_by_lua:在 content 阶段执行的所有反向代理模块,在选择上游 Server 时都会回调 init_upstream 函数,OpenResty 将其命名为 balancer_by_lua。
  • header_filter_by_lua:将 HTTP 响应头部发送给下游前执行的钩子函数。
  • body_filter_by_lua:将 HTTP 响应包体发送给下游前执行的钩子函数。
  • log_by_lua:记录 access 日志时的钩子。

Apisix 的功能通过不同的插件完成,Apisix通过admin api对插件进行管理,并根据插件的功能,在上述的不同阶段调用插件完成功能。Apisix通过在不同的阶段传入阶段名称进行该阶段的插件调用,内部调用 plugin.run_plugin(phase_name, nil, api_ctx) 来运行插件。插件实现为阶段的函数以在该阶段被调用。例如,在limit-conn插件中实现了function _M.access(conf, ctx)

nginx.conf 设定了请求进入 Apisix 后的处理流程,请求进入 nginx后,如果是 /Apisix/admin 路径请求,则进入 Apisix.http_admin(),完成配置管理后返回。如果是常规业务员请求(/)则进入 Apisix.http_access_phase() 阶段,初始化 api_ctx 上下文。access_by_lua 阶段插件先根据 Route、Service 匹配的插件,创建临时 Table 储存 plugin 和 plugin_conf,存入 ctx 中。根据是路由否为 Apisix 已经注册的路径,对请求进行匹配,内部使用“基数树”(Radix tree) 的Openresty 路由数据结构进行匹配。向上下文注入该请求匹配的 route ,service 等信息用于后续阶段使用。然后向上下文注入该请求相关的插件,比如请求对应的路由存在插件,若请求存在对应的 service 则加入 service 定义的插件,以及全局插件。

Apisix.http_balancer_phase() 阶段是 openresty balancer 的实现,主要功能是执行实际的流量转发配置,http和grpc 等类型的请求均会经过该阶段。如果上下文中存在 pick_server 则在上个阶段中执行 loadbalancer 后选择出的 server, 配置 nginx 直接转发至该 server。如果不存在 pick_server,则再次执行 loadbalancer,调用 openresty set_current_peer(server, ctx) 完成 proxy_pass 配置,执行 proxy_pass。NGINX proxy_pass 组件实际执行了请求转发的功能,Apisix 仅对其作了配置。具体实现方面Apisix 基于 lua-resty-balancer 设置负载均衡策略,基于 lua-resty-healthcheck实现节点健康检查,set_current_peer 设置当前请求上游地址,set_more_tries 设置请求失败重试次数,get_last_failure 获取上一次请求失败结果判断是否需要继续重试,set_timeouts 设置请求超时时间。

然后进入 Apisix.http_header_filter_phase() 阶段,该阶段主要对响应 header 做更改,设置消息头 ,设置上游状态头:X-APISIX-Upstream-Status等。接着进入 Apisix.http_body_filter_phase() 阶段,执行 “body_filter” 阶段的插件。进入 Apisix.http_log_phase() 阶段,执行 “log” 阶段的插件,最后回收 api_ctx 上下文资源。

总结

本文主要讲解了Apisix技术栈的实现原理,包括底层的相关支撑技术,以及启动流程,配置更新实现机制和业务请求处理流程,阅读本文可了解Apisix技术实现的要点。Apisix本身相关技术点较多,读者对某个技术点需要深入了解可进一步查阅相关资料。

参考文献

云原生网关 APISIX 核心流程源码分析与进化方向思考 | 云原生社区(中国) (cloudnative.to)

Apache APISIX 架构分析:如何动态管理 Nginx 集群? | Apache APISIX® -- Cloud-Native API Gateway

Apache APISIX 在 API 和微服务领域的探索 | Apache APISIX® -- Cloud-Native API Gateway

软件架构 | Apache APISIX® -- Cloud-Native API Gateway

配置路由 | Apache APISIX® -- Cloud-Native API Gateway

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值