Skip to content

Commit 53c7e73

Browse files
authored
docs: add extention guide blog (#688)
1 parent 7428dcd commit 53c7e73

File tree

2 files changed

+422
-0
lines changed

2 files changed

+422
-0
lines changed
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
---
2+
title: "Apache APISIX Extensions Guide"
3+
author: "Zexuan Luo"
4+
authorURL: "https://github.com/spacewander"
5+
authorImageURL: "https://avatars.githubusercontent.com/u/4161644?v=4"
6+
keywords:
7+
- Apache APISIX
8+
- Plugin
9+
- HTTP
10+
- Apache
11+
description: This article provides an extension guide for Apache APISIX, aiming to provide users with some ideas for extending Apache APISIX.
12+
tags: [technology]
13+
---
14+
15+
> This article provides an extension guide for Apache APISIX, aiming to provide users with some ideas for extending Apache APISIX.
16+
17+
<!--truncate-->
18+
19+
Apache APISIX provides more than 50 plugins, several commonly used load balancing selectors, and support for mainstream service discovery (such as Nacos and DNS). The API gateway is closely related to the internal business of the enterprise. In order to meet the business needs of the enterprise, users usually need to add some code on the basis of Apache APISIX to realize the functions required by the business. How to expand Apache APISIX has become a common pain point for many users: on the premise of ensuring the smooth operation of Apache APISIX, how to add business code to meet actual needs?
20+
21+
This article provides an extension guide for Apache APISIX, aiming to provide users with some ideas for extending Apache APISIX. Since Apache APISIX is in a stage of rapid development and the frequency of version iterations is relatively high, this article will be based on the first LTS version v2.10.0 of Apache APISIX. If your Apache APISIX version is lower than 2.10.0, you may need to make some modifications based on actual conditions. In addition, although this article only explains the HTTP-related logic, the TCP-related parts are generally similar.
22+
23+
## Expansion Direction 1: Rewrite or Access?
24+
25+
Let's start with the life cycle of the request: when a request enters Apache APISIX, it will first be processed by the method `http_access_phase`. Readers who are familiar with the concept of OpenResty phases may be a little confused: OpenResty has a total of 6 phases, which are arranged in order of execution: `rewrite`, `access`, `before_proxy`, `header_filter`, `body_filter` and `log`, why is `access` at the beginning, and where is `rewrite`?
26+
27+
The phases concept of the Apache APISIX plug-in is slightly different from the OpenResty phases concept. In order to improve the performance of Apache APISIX, the rewrite method of the APISIX plugin will run in the access phase of OpenResty. Users can still customize the logic of `rewrite` at the plugin level, but at the code level, `rewrite` is actually executed in `access`.
28+
29+
Although both the logic of `rewrite` and the logic of `access` run in the access phase, the logic of `rewrite` will still be executed before the logic of `access`. In order to avoid the failure of subsequent plugins to execute `rewrite` and fail to execute `access`, which will cause trace omissions, trace logic must be added to `rewrite`.
30+
31+
In addition to the order of execution, there is another difference between `rewrite` and `access`, that is, there is a logic for processing `consumer` between them:
32+
33+
```Lua
34+
plugin.run_plugin("rewrite", plugins, api_ctx)
35+
if api_ctx.consumer then
36+
...
37+
end
38+
plugin.run_plugin("access", plugins, api_ctx)
39+
```
40+
41+
`consumer` represents an identity. You can control permissions for different consumers. For example, use the plugin `consumer-restriction` to implement role-based permission control, which is what everyone calls RBAC. In addition, you can also set corresponding current limiting strategies for different `consumer`.
42+
43+
The authentication plugin in Apache APISIX (with `type = "auth"` in the plugin definition), you need to select the `consumer` in the `rewrite` stage. Here we use the `key-auth` plugin as an example:
44+
45+
```Lua
46+
local _M = {
47+
version = 0.1,
48+
priority = 2500,
49+
type = 'auth',
50+
name = plugin_name,
51+
schema = schema,
52+
consumer_schema = consumer_schema,
53+
}
54+
55+
...
56+
function _M.rewrite(conf, ctx)
57+
...
58+
local consumer_conf = consumer_mod.plugin(plugin_name)
59+
if not consumer_conf then
60+
return 401, {message = "Missing related consumer"}
61+
end
62+
63+
local consumers = lrucache("consumers_key", consumer_conf.conf_version,
64+
create_consume_cache, consumer_conf)
65+
66+
local consumer = consumers[key]
67+
if not consumer then
68+
return 401, {message = "Invalid API key in request"}
69+
end
70+
71+
consumer_mod.attach_consumer(ctx, consumer, consumer_conf)
72+
end
73+
```
74+
75+
The execution logic of the authentication plugins is similar: first obtain a certain set of parameters from the input of the users, then find the corresponding `consumer` according to the parameters, and finally append the `consumer_conf` corresponding to the plugin to `ctx`.
76+
77+
In summary, for plugins that do not need to be executed in the early stage of the request and do not need to find the `consumer`, it is recommended to write the logic in the `access`.
78+
79+
## Extension Direction 2: Configure Service Discovery
80+
81+
After executing the `access`, we are about to deal with the Upstream. Normally, the Upstream node is hard-coded on the Upstream configuration. However, it is also possible to obtain nodes from the service discovery to implement discovery.
82+
83+
Next, we will take Nacos as an example to talk about how to implement it.
84+
85+
An Upstream configuration that dynamically acquires a node managed by Nacos is as follows.
86+
87+
```JSON
88+
{
89+
"service_name": "APISIX-NACOS",
90+
"type": "roundrobin",
91+
"discovery_type": "nacos",
92+
"discovery_args": {
93+
"namespace_id": "test_ns",
94+
"group_name": "test_group"
95+
}
96+
}
97+
```
98+
99+
We can see three of these important variables:
100+
101+
1. `discovery_type`: Types of Service Discovery,`"discovery_type": "nacos"` indicates service discovery using Nacos.
102+
2. `service_name`: Service Name。
103+
3. `discovery_args`: different discovery-specific parameters, specific parameters of Nacos include: `namespace_id` and `group_name`.
104+
105+
The Lua code corresponding to Nacos discovery is located in `discovery/nacos.lua`. Open the file `nacos.lua`, we can see that several required methods are implemented in it.
106+
107+
A discovery needs to implement at least two methods: `nodes` and `init_worker`.
108+
109+
```Lua
110+
function _M.nodes(service_name, discovery_args)
111+
local namespace_id = discovery_args and
112+
discovery_args.namespace_id or default_namespace_id
113+
local group_name = discovery_args
114+
and discovery_args.group_name or default_group_name
115+
116+
...
117+
end
118+
119+
function _M.init_worker()
120+
...
121+
end
122+
```
123+
124+
The function signature of `nodes` has already explicitly shown the query parameters used to get new nodes: `service_name` and `discovery_args`. For each request, Apache APISIX will use this set to query for the latest node. The method returns an array:
125+
126+
```Bash
127+
{
128+
{host = "xxx", port = 12100, weight = 100, priority = 0, metadata = ...},
129+
# priority and metadata are optional
130+
...
131+
}
132+
```
133+
134+
And `init_worker` is responsible for starting a timer in the background to ensure that the local node data is consistent with the data discovered by the service.
135+
136+
## Expansion Direction 3: Configure Load Balancing
137+
138+
After obtaining a set of nodes, we have to decide which node to try first in accordance with the rules of load balancing. If several commonly used load balancing algorithms cannot meet your needs, you can also implement a load balancing by yourself.
139+
140+
Let's take load balancing with the least number of connections as an example. The corresponding Lua code is located in `balancer/least_conn.lua`. Open the file `least_conn.lua`, we can see that it implements several required methods: `new`, `get`, `after_balance` and `before_retry_next_priority`.
141+
142+
- `new` is responsible for doing some initialization work.
143+
144+
- `get` is responsible for executing the logic of the selected node.
145+
146+
- `after_balance` will run in the following two situations:
147+
148+
- Before each retry (when before_retry is true)
149+
- After the last try
150+
151+
- `before_retry_next_priority` runs before preparing to try the next set of nodes with the same priority, while the current set has been tried.
152+
153+
```Lua
154+
function _M.new(up_nodes, upstream)
155+
...
156+
157+
return {
158+
upstream = upstream,
159+
get = function (ctx)
160+
...
161+
end,
162+
after_balance = function (ctx, before_retry)
163+
...
164+
if not before_retry then
165+
if ctx.balancer_tried_servers then
166+
core.tablepool.release("balancer_tried_servers", ctx.balancer_tried_servers)
167+
ctx.balancer_tried_servers = nil
168+
end
169+
170+
return nil
171+
end
172+
173+
if not ctx.balancer_tried_servers then
174+
ctx.balancer_tried_servers = core.tablepool.fetch("balancer_tried_servers", 0, 2)
175+
end
176+
177+
ctx.balancer_tried_servers[server] = true
178+
end,
179+
before_retry_next_priority = function (ctx)
180+
if ctx.balancer_tried_servers then
181+
core.tablepool.release("balancer_tried_servers", ctx.balancer_tried_servers)
182+
ctx.balancer_tried_servers = nil
183+
end
184+
end,
185+
}
186+
end
187+
```
188+
189+
If there is no internal state to maintain, you can directly borrow the fixed template code (in the above code, outside the ellipsis) to fill in the two methods of `after_balance` and `before_retry_next_priority`.
190+
191+
After selecting the node, we can also add additional logic in the form of a plugin. The plugin can implement the `before_proxy` method. This method will be called after the node is selected, and we can record the information of the currently selected node in this method, which will be useful in trace.
192+
193+
## Extension Direction 4: Handling Response
194+
195+
We can process the responses returned from upstream in `header_filter` and `body_filter` through the `response-rewrite` plugin. The former method modifies the response header, the latter modifies the response body. Note that Apache APISIX response processing is streaming, so if the response header is not modified inside `header_filter`, the response header will be sent out first and there will be no way to modify the response body when it reaches `body_filter`.
196+
197+
This means that if you want to modify the body later, but there are body-related response headers like Content-Length in the header, you have to change those headers in the `header_filter` in advance. We provide a helper method: `core.response.clear_header_as_body_modified`, which can be called in `header_filter`.
198+
199+
The `body_filter` is also streaming and will be called multiple times. So if you want to get the full response body, you need to put together the partial response body provided by each `body_filter`. On the Apache APISIX master branch, we provide a method called `core.response.hold_body_chunk` to simplify the operation. Interested readers can take a look at the code.
200+
201+
## Extension Direction 5: Reporting Logs and Monitoring Parameters
202+
203+
After the request is finished, we can also do some cleanup work with the `log` method. This type of work can be divided into two categories:
204+
205+
1. Record metrics, such as the `prometheus` plugin.
206+
2. Record the access log, and then report it regularly, such as the `http-logger` plugin.
207+
208+
If you are interested, you can take a look at how the `log` method of these two plugins is implemented:
209+
210+
- [`prometheus` plugin documentation](https://apisix.apache.org/zh/docs/apisix/plugins/prometheus/)
211+
- [`http-logger` plugin documentation](https://apisix.apache.org/zh/docs/apisix/plugins/http-logger/)

0 commit comments

Comments
 (0)