apisix 初探
apisix 是一个用使用 lua 语言编写的网关控制器,相比官网介绍的 apisix 是一个网关,apisix 的实际用途更像是一个控制器。因为其本身代码不承载流量。
Apache APISIX is a dynamic, real-time, high-performance API gateway.
apisix 运行于 openresty 之上,openresty 运行于 nginx 之上。
openresty OpenResty® 的目标是让你的 Web 服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。
简单来说,即 openresty 对 nginx 做了充分的扩展,引出了许多 hook 点,你可以使用 lua 语言对这些 hook 进行实现。
apisix 在 openresty 的基础上,实现了 openresty 许多 hook,实现了动态配置 nginx 以应对复杂的网关需求。
举例:apisix 中的转发(负载均衡)逻辑实际上是使用了lua-resty-core/ngx/balancer
源码结构
.
├── apisix # apisix lua源码
│ ├── admin # apisix admin api
│ ├── balancer # upstream 负载均衡
│ ├── cli # command line interface
│ ├── control # control api
│ ├── core # 一些配置,etcd交互,请求解析等
│ ├── discovery # 服务发现
│ ├── http # 路由匹配等
│ ├── plugins # 自带的插件在该目录
│ ├── ssl # ssl sni 相关
│ ├── stream # 流处理相关,包含了流处理的一些插件
│ └── utils
├── benchmark
├── bin # apisix 运行入口(develop)
├── ci
├── conf # 配置文件
├── docs
├── example
├── kubernetes # kubernetes 部署文件
├── logos
├── rockspec # luarocks 依赖描述文件
├── t # test
├── utils
...
└── Makefile # make
启动
apisix 使用 luarocks 进行包管理。因为 lua 是脚本语言,其运行需要使用 lua 解释器解释执行,lua 还提供了 luajit 即时编译。
首次运行 apisix 需要安装 lua luarocks 等,以及 apisix 依赖:
make deps
make deps 会在将 apisix 的所有依赖下载至当前目录的 deps 中。
启动运行:
make run
Makefile 中提供了快速的启动运行方式,其调用了bin/apisix start
,apisix start 内部调用了 openresty 。
在 develop env.lua#L34 时,lua package loader 会被配置 env.lua#L46-L51 寻找 deps 文件夹下的依赖。
使用 make run 非常不便于了解 apisix 是如何真正运行的。但是 apisix-docker/alpine/Dockerfile 告诉了 apisix 是如何启动的,以及和 openresty 是如何配合的。
bin/apisix
apisix文件实质上为一个 bash 文件,内部逻辑为判断当前 apisix 文件路径,寻找 openresty 路径以确定 luajit 位置使用 luajit 启动 apisix;或者直接使用 lua 启动挨 apisix。下级调用至 ./apisix/cli/apisix.lua
#!/bin/bash
...
# 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/cli/apisix.lua
apisix.lua 包含如下命令:
- apisix init , 初始化 nginx 配置,通过读取
conf/config.yaml
生成 nginx config 文件。供 openresty(nginx)使用。 - apisix init_etcd,初始化 etcd 配置,用于与 etcd 同步数据。
- apisix start;实际执行了
openresty -p /usr/local/apisix -g 'daemon off;'
启动 openresty 。
nginx.conf
使用 make init
会执行 apisix init ,生成 nginx 配置文件。
通过阅读 nginx 文件,可以了解 apisix 整个流程。
下面 nginx 文件节选了生成的 nginx.conf 文件中关键内容。
# Configuration File - Nginx Server Configs
# This is a read-only file, do not try to modify it.
# https://github.com/apache/apisix/blob/master/conf/config-default.yaml#L158-L175
# config.yaml 中该节中的配置会被设置至对应的位置
# 下面节是 nginx_config.main_configuration_snippet 中的内容。
# main configuration snippet starts
# main configuration snippet ends
http {
...
# http configuration snippet starts
# http configuration snippet ends
# 参考: https://github.com/openresty/lua-resty-core/blob/master/lib/ngx/balancer.md#http-subsystem
# 这里是 apisix 中对 ngx balancer 的实现。实现了 proxy bypass 的路由选择。对应apisix 中负载均衡 upstream 功能。
# apisix 中的负载均衡实现在 https://github.com/apache/apisix/tree/master/apisix/balancer 中,目前提供了5 种算法。
upstream apisix_backend {
server 0.0.0.1; # 随便填一个无效的值
balancer_by_lua_block {
# https://github.com/apache/apisix/blob/e7d26dc4f0bd690c288867a248a69f0efeaea733/apisix/init.lua#L709
apisix.http_balancer_phase()
}
...
}
# https://openresty-reference.readthedocs.io/en/latest/Directives/#init_by_lua_block
init_by_lua_block {
require "resty.core"
apisix = require("apisix")
local dns_resolver = { "127.0.0.53", }
local args = {
dns_resolver = dns_resolver,
}
# https://github.com/apache/apisix/blob/e7d26dc4f0bd690c288867a248a69f0efeaea733/apisix/init.lua#L61
apisix.http_init(args)
}
init_worker_by_lua_block {
# https://github.com/apache/apisix/blob/e7d26dc4f0bd690c288867a248a69f0efeaea733/apisix/init.lua#L90
apisix.http_init_worker()
}
exit_worker_by_lua_block {
# https://github.com/apache/apisix/blob/e7d26dc4f0bd690c288867a248a69f0efeaea733/apisix/init.lua#L137
apisix.http_exit_worker()
}
server {
listen 127.0.0.1:9090;
access_log off;
location / {
content_by_lua_block {
# https://github.com/apache/apisix/blob/e7d26dc4f0bd690c288867a248a69f0efeaea733/apisix/init.lua#L773
apisix.http_control()
}
}
...
}
server {
listen 127.0.0.1:9091;
access_log off;
location / {
content_by_lua_block {
local prometheus = require("apisix.plugins.prometheus")
prometheus.export_metrics()
}
}
location = /apisix/nginx_status {
allow 127.0.0.0/24;
deny all;
stub_status;
}
}
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;
...
# http server configuration snippet starts
# http server configuration snippet ends
location = /apisix/nginx_status {
allow 127.0.0.0/24;
deny all;
access_log off;
stub_status;
}
location /apisix/admin {
set $upstream_scheme 'http';
set $upstream_host $http_host;
set $upstream_uri '';
allow 127.0.0.0/24;
deny all;
content_by_lua_block {
# https://github.com/apache/apisix/blob/e7d26dc4f0bd690c288867a248a69f0efeaea733/apisix/init.lua#L752
apisix.http_admin()
}
}
ssl_certificate_by_lua_block {
# https://github.com/apache/apisix/blob/e7d26dc4f0bd690c288867a248a69f0efeaea733/apisix/init.lua#L142
apisix.http_ssl_phase()
}
proxy_ssl_name $upstream_host;
proxy_ssl_server_name on;
location / {
...
access_by_lua_block {
# https://github.com/apache/apisix/blob/e7d26dc4f0bd690c288867a248a69f0efeaea733/apisix/init.lua#L341
apisix.http_access_phase()
}
...
# https://github.com/apache/apisix/blob/e7d26dc4f0bd690c288867a248a69f0efeaea733/apisix/init.lua#L709
proxy_pass $upstream_scheme://apisix_backend$upstream_uri;
mirror /proxy_mirror;
header_filter_by_lua_block {
# https://github.com/apache/apisix/blob/e7d26dc4f0bd690c288867a248a69f0efeaea733/apisix/init.lua#L575
apisix.http_header_filter_phase()
}
body_filter_by_lua_block {
# https://github.com/apache/apisix/blob/e7d26dc4f0bd690c288867a248a69f0efeaea733/apisix/init.lua#L612
apisix.http_body_filter_phase()
}
log_by_lua_block {
# https://github.com/apache/apisix/blob/e7d26dc4f0bd690c288867a248a69f0efeaea733/apisix/init.lua#L670
apisix.http_log_phase()
}
}
# 对于 grpc 类型的请求,会被跳转至此处执行
location @grpc_pass {
access_by_lua_block {
# https://github.com/apache/apisix/blob/e7d26dc4f0bd690c288867a248a69f0efeaea733/apisix/init.lua#L553
apisix.grpc_access_phase()
}
grpc_set_header Content-Type application/grpc;
grpc_socket_keepalive on;
grpc_pass $upstream_scheme://apisix_backend;
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()
}
}
...
}
# http end configuration snippet starts
# http end configuration snippet ends
}
处理阶段&插件
从上一节已经了解到 apisix 通过在 nginx 中不同阶段设置 hook 完成的自身功能
apisix 的功能通过不同的插件完成,apisix 对这些插件进行组织(admin api),并根据插件的描述,在上述的不同时机调用插件,完成功能。
在源码中,所有的插件均有一个统一的入口。
function common_phase(phase_name)
在不同的阶段传入阶段名称进行该阶段的插件调用。其内部调用了 plugin.run_plugin(phase_name, nil, api_ctx)
来运行插件。
对于插件,实现名称为阶段的函数以在该阶段被调用。举例:
-
在
basic_auth
插件中,实现了函数 function _M.rewrite(conf, ctx) -
在
limit-conn
中实现了function _M.access(conf, ctx)
目前已知的阶段有:
- preread
- ssl
- access
- balancer
- rewrite
- header_filter
- body_filter
- log
初始化
apisix 启动时调用:
- apisix init,解析配置,生成 nginx.conf 文件
- apisix init_etcd,初始化 etcd 中的存储目录。参见apisix/constants.lua
- 启动 openresty(nginx)
-
apisix.http_init(args),初始化 nginx 相关。
- 设置 dns resolver
- 启动 “privileged agent”
-
apisix.http_init_worker(),apisix 的核心初始化逻辑。
- 初始化 openresty worker event
-
discovery.init_worker()
local discovery = require("apisix.discovery.init").discovery if discovery and discovery.init_worker then discovery.init_worker() end require("apisix.balancer").init_worker() load_balancer = require("apisix.balancer") require("apisix.admin.init").init_worker() require("apisix.timers").init_worker() plugin.init_worker() router.http_init_worker() require("apisix.http.service").init_worker() plugin_config.init_worker() require("apisix.consumer").init_worker() if core.config == require("apisix.core.config_yaml") then core.config.init_worker() end require("apisix.debug").init_worker() apisix_upstream.init_worker() require("apisix.plugins.ext-plugin.init").init_worker() local_conf = core.config.local_conf() if local_conf.apisix and local_conf.apisix.enable_server_tokens == false then ver_header = "APISIX" end
- apisix.http_exit_worker()
- 停止 “privileged agent”
请求调用链分析
通过查看 nginx.conf 我们可以分析出一个请求进入 apisix 后的处理流程。
- 请求进入 nginx
- 如果是 /apisix/admin 路径请求,则进入 apisix.http_admin(),完成后返回
- 如果是常规请求(/)则继续
-
进入 apisix.http_access_phase() 阶段
- 初始化 api_ctx 上下文
- 包含 client tls 验证
- 是否为 apisix 已经注册的路径,对请求进行匹配,内部使用了基于 “基数树”(Radix tree) 的 oenresty 路由组件进行匹配。
- 向上下文注入该请求匹配的 apisix route ,service 等信息用于后续阶段使用。
- 向上下文注入该请求相关的插件,例如:请求对应的路由存在插件,若请求存在对应的 service 则加入 service 定义的插件,以及全局插件。
- 调用 “rewrite” 阶段的插件。
- 调用 “access” 阶段的插件。
- 获取 upstream
- 执行 loadbalancer 选择 server
- 调用 “balancer” 阶段插件
- 判断 upstream ,根据 upstream 类型,grpc dubbo 等进入 @grpc_pass @dubbo_pass 等不同的后续处理流程。 这些配置可在 nginx.conf 中查看。
-
进入 apisix.http_balancer_phase() 阶段
- 此阶段是 openresty balancer 的实现,主要功能是执行实际的流量转发配置,http grpc 等类型的请求均会经过该阶段
- 如果上线文中存在 pick_server 即在上个阶段中执行 loadbalancer 后选择出的 server, 则配置 nginx 直接转发至该 server。
注意:NGINX
proxy_pass
组件实际执行了请求转发的功能,apisix 仅对其作了配置。 - 如果不存在 pick_server,则再次执行 loadbalancer
- 调用 openresty set_current_peer(server, ctx) 完成 proxy_pass 配置
- 执行 proxy_pass
-
进入 apisix.http_header_filter_phase() 阶段,该阶段主要对响应 header 做更改
- 设置头
"Server", APISIX
- 设置上游状态头:
X-APISIX-Upstream-Status
- 执行 “header_filter” 阶段的插件
- 设置头
-
进入 apisix.http_body_filter_phase() 阶段
- 执行 “body_filter” 阶段的插件
-
进入 apisix.http_log_phase() 阶段
- 执行 “log” 阶段的插件
- 回收 api_ctx 上下文
admin api
admin api 是 apisix 的控制面,这里进行了整个 apisix 的配置。
其入口为 apisix/init.lua#L752 ,内部根据apisix/admin/init.lua#L375配置的路由进行分发处理。
apisix/admin/init.lua#L44 在内部进行了二次分发,将请求路由至对应的模块进行处理。
以 enable batch-requests plugin 为例:
http 请求: PUT http://127.0.0.1:9080/apisix/admin/plugin_metadata/batch-requests
对应 apisix/admin/plugin_metadata.lua#L92
function _M.put(plugin_name, conf)
local plugin_name, err = check_conf(plugin_name, conf)
if not plugin_name then
return 400, err
end
local key = "/plugin_metadata/" .. plugin_name
core.log.info("key: ", key)
local res, err = core.etcd.set(key, conf)
if not res then
core.log.error("failed to put plugin metadata[", key, "]: ", err)
return 500, {error_msg = err}
end
return res.status, res.body
end
将在 etcd 中该 plugin_metadata 目录中写入该插件名称(key)和配置(value)
在插件执行运行时使用 plugin.plugin_metadata(plugin_name) 读取出来。
control api
control-api 有两个用途
- 引出内部的状态
- 控制当前 apisix 的行为
监听地址为 127.0.0.1:9090,因其包含敏感数据所以仅允许 local 访问。
其将已注册的插件中实现了 control_api() 方法的插件进行执行注册。
以 server_info 插件为例: apisix/plugins/server-info.lua#L194
function _M.control_api()
return {
{
methods = {"GET"},
uris ={"/v1/server_info"},
handler = get_server_info,
}
}
end
在 control api 上注册了路径 /v1/server_info
并指定使用 get_server_info
函数进行处理。