逐步演进您的 RESTful API
设计一个直观易用的 RESTful API 并非易事。如果是第一次尝试,那可能已经是一项艰巨的任务。API 的生命周期管理往往被忽略。但即便如此,也并非不可能:在本文中,我想提出一种务实的 API 演进方法,即使最初并未对此进行规划。
初始情况
我们来看一个示例应用程序,它在使用时会显示“Hello”。
> curl http://org. apisix/hello
Hello world
> curl http://org. apisix/hello/Joe
Hello Joe
底层技术并不重要;我们将专注于API部分。
使用 API 网关
第一步也是最关键的一步是停止将应用程序直接暴露在互联网上,并在两者之间设置一个 API 网关。如果您不熟悉 API 网关的概念,可以将其理解为功能更强大的反向代理。维基百科给出了如下定义:
网关:一种充当 API 前端的服务器,它接收 API 请求,执行限流和安全策略,将请求传递给后端服务,然后将响应返回给请求者。网关通常包含一个转换引擎,用于动态地协调和修改请求和响应。网关还可以提供诸如收集分析数据和提供缓存等功能。此外,网关还可以提供支持身份验证、授权、安全、审计和合规性的功能。
-- API 管理
本文将使用Apache APISIX ,但您可以随意使用您最熟悉的 APISIX。
如果只暴露网关而不是应用程序,则需要更新DNS记录,使其指向网关而非应用程序,并等待更改在全球范围内生效。这可能需要一些时间。您可以使用dnschecker之类的网站来跟踪更改生效情况。
不过,首先需要将 HTTP 请求从网关路由到您的应用程序。
使用 APISIX,您可以通过向网关发送 HTTP 请求来创建路由。
curl http://apisix:9080/apisix/admin/routes/1 -H 'X-API-KEY: xyz' -X PUT -d ' # 1-2
{
"name": "Direct Route to Old API", # 3
"methods": ["GET"], # 4
"uris": ["/hello", "/hello/", "/hello/*"], # 5
"upstream": { # 6
"type": "roundrobin", # 8
"nodes": {
"oldapi:8081": 1 # 7
}
}
}'
- APISIX 可以分配一个自动生成的 ID,也可以使用提供的 ID。在本例中,我们选择后者,将其传递到 URL 中,
1并使用PUT动词。 - 要更新路由,我们需要传递 API 密钥。
- 给路线命名并非必须,但有助于我们更好地理解它的功能。
- 用于路由的 HTTP 方法数组
- 要路由的 URL 数组
- 上游指的是后端应用程序。在我们的例子中,它是 Hello World API。
- 节点及其权重的哈希表。权重仅在节点数量较多时才有意义,而在这个简单的场景中,节点数量并不多。
- 配置多个节点时要使用的负载均衡算法
此时,您可以查询网关并获得与之前相同的结果:
> curl http://org. apisix/hello
Hello world
> curl http://org. apisix/hello/Joe
Hello Joe
API 版本控制
API 的演进意味着在某个阶段需要存在多个 API 版本共存的情况。API 的版本控制有三种选择:
| 类型 | 例子 |
|---|---|
| 查询参数 |
curl http://org.apisix/hello?version=1 curl http://org.apisix/hello?version=2 |
| 标题 |
curl -H '版本:1' http://org.apisix/hello curl -H '版本:2' http://org.apisix/hello |
| 小路 |
curl http://org.apisix/v1/hello curl http://org.apisix/v2/hello |
关于最佳版本控制方案,已有许多文章进行了探讨。本文将以基于路径的版本控制为例,因为它应用最为广泛。APISIX 也支持其他版本控制方案,您可以根据需要选择使用。
在上一节中,我们创建了一个路由,该路由封装了一个上游路由。APISIX 允许我们创建一个具有专用 ID 的上游路由,以便在多个路由中重复使用它。
curl http://apisix:9080/apisix/admin/upstreams/1 -H 'X-API-KEY: xyz' -X PUT -d ' # 1
{
"name": "Old API", # 2
"type": "roundrobin",
"nodes": {
"oldapi:8081": 1
}
}'
- 使用
upstreams路径 - 新上游的有效载荷
我们还需要重写发送到网关的查询,然后再将其转发到上游。上游知道/hello,但/v1/helloAPISIX 允许通过插件进行此类转换、过滤等操作。让我们创建一个插件配置来重写路径:
curl http://apisix:9080/apisix/admin/plugin_configs/1 -H 'X-API-KEY: xyz' -X PUT -d ' # 1
{
"plugins": {
"proxy-rewrite": { # 2
"regex_uri": ["/v1/(.*)", "/$1"] # 3
}
}
}'
- 使用
plugin-configs路径 - 使用代理重写插件
- 移除版本前缀
现在我们可以创建引用新创建的上游和插件配置的版本化路由:
curl http://apisix:9080/apisix/admin/routes/2 -H 'X-API-KEY: xyz' -X PUT -d ' # 1
{
"name": "Versioned Route to Old API",
"methods": ["GET"],
"uris": ["/v1/hello", "/v1/hello/", "/v1/hello/*"],
"upstream_id": 1,
"plugin_config_id": 1
}'
- 妈,你看,一条新路线!
现阶段,我们配置了两条路由,一条是版本化的,另一条是非版本化的:
> curl http://org. apisix/hello
Hello world
> curl http://org. apisix/v1/hello
Hello world
将用户从非版本化路径迁移到版本化路径。
我们已经对 API 进行了版本控制,但我们的用户可能仍在使用旧版的非版本化 API。我们希望他们迁移,但我们不能直接删除旧路由,因为用户对此并不知情。幸运的是,HTTP 301 状态码可以帮上忙:我们可以告知用户资源已从http://org.apisix/hello迁移到http://org.apisix/v1/hello 。这需要在初始路由上配置重定向插件:
curl http://apisix:9080/apisix/admin/routes/1 -H 'X-API-KEY: xyz' -X PATCH -d '
{
"plugins": {
"redirect": {
"uri": "/v1$uri",
"ret_code": 301
}
}
}'
结果很有意思:
>curl http://apisix. org/hello
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>openresty</center>
</body>
</html>
>curl -L apisix:9080/hello # 1
Hello world
- 该
-L选项遵循重定向规则。
用户要么会因为遵循新端点而透明地使用它,要么他们的集成会中断,他们会注意到 301 状态和要使用的新 API 位置。
了解你的用户
您可能已经注意到,我们之前完全不知道谁在使用我们的API。当我们需要进行更改时,必须想方设法避免影响用户的正常使用。其他一些更改可能就没那么容易应对了。因此,我们应该努力了解用户,以便在必要时与他们取得联系。
坦白说,大多数开发者,包括我自己,如果可以避免,都不喜欢注册并提供联系方式。我想这要归咎于市场团队,他们不了解我们的想法——别给我打电话,我会主动联系你们。然而,在这个特殊情况下,这样做或许会有好处。
“核弹级”方案会完全禁止用户在注册前调用我们的 API。我更倾向于另一种方案:限制未注册用户在一段时间内可以调用的 API 次数。如果达到限制,我们将返回臭名昭著的 429 HTTP 状态码,并发送一条邀请他们注册的消息。
截至撰写本文时,尚无现成的插件能够实现此功能。但我们可以自行编写插件。APISIX 基于 Lua 引擎,所有提供的插件均使用 Lua 编写。此外,您还可以使用 Go、Python、WebAssembly 或任何基于 JVM 的语言编写插件。
为了简化操作,我编写了一个 Lua 插件。由于本文的目的并非讲解 Lua,因此我不会深入探讨。如果您对代码感兴趣,可以在GitHub上找到它。公开发布后,我们还需要完成以下几个步骤:
-
配置 APISIX 以使用该目录:
apisix: extra_lua_path: "/opt/apisix/?.lua" # 1- APISIX 可以使用位于该
/opt/apisix/文件夹中的任何 Lua 脚本
- APISIX 可以使用位于该
-
加载插件:
APISIX 可以热重载自身。我们无需重启它(从而避免停机)即可添加其他插件!
curl http://apisix:9080/apisix/admin/plugins/reload -H 'X-API-KEY: xyz' -X PUT -
修改现有插件配置:
最后,我们需要配置插件本身。由于我们已经创建了一个专门的插件配置文件,因此只需将其更新为新的配置即可:
curl http://apisix:9080/apisix/admin/plugin_configs/1 -H 'X-API-KEY: xyz' -X PATCH -d ' { "plugins": { "proxy-rewrite": { # 1 "regex_uri": ["/v1/(.*)", "/$1"] }, "unauth-limit": { # 2 "count": 1, # 3 "time_window": 60, # 3 "key_type": "var", # 4 "key": "consumer_name", # 4 "rejected_code": 429, "rejected_msg": "Please register at https://apisix. org/register to get your API token and enjoy unlimited calls" } } }'- 很遗憾,我们需要重复使用现有的插件配置。APISIX 团队正在努力修复此问题,以便您能够在不了解现有插件的情况下向配置中添加新插件。
- 我们的插件!
- 如果用户已通过身份验证,该插件会将每 60 秒的调用次数限制为一次。否则,它不会进行任何限制。
- 下一节将对此进行解释。
现在我们可以检查它是否按预期运行:
>curl apisix:9080/v1/hello
Hello world
>curl apisix:9080/v1/hello
{"error_msg":"Please register at https:\/\/apisix. org\/register to get your API token and enjoy unlimited calls"}
确实如此。
创建用户
根据您对未经认证用户访问的限制程度,您应该会开始看到用户访问注册页面。注册涉及诸多方面,例如:
- 可以根据需要进行自动化验证,或者需要任意数量的手动验证步骤。
- 免费或付费
- 可以是像要求提供电子邮件地址这样简单的操作,也可以根据需要收集更多数据而变得复杂。
- ETC。
这取决于你的具体情况。
关于 APISIX,最终它意味着需要创建一个新的消费者。要创建这样的消费者,我们需要配置一个用于身份验证的插件。有一些现成的身份验证插件可供选择:基本身份验证、API 密钥、JWT、OpenID、LDAP、Keycloak 等。
本文中,key-auth 插件就足够了。让我们配置一个通过 API 密钥进行身份验证的消费者对象:
curl http://apisix:9080/apisix/admin/consumers -H 'X-API-KEY: xyz' -X PUT -d '
{
"username": "johndoe", # 1
"plugins": {
"key-auth": { # 2
"key": "mykey" # 3
}
}
}'
- 消费者身份
- 使用的插件
- 有效令牌是
mykey
请注意,默认标头为apikey。可以配置其他标头:请查看 key-auth 插件文档。
现在我们可以测试我们的设置,并验证它是否符合我们的要求:
>curl -H 'apikey: mykey' apisix:9080/v1/hello
Hello world
>curl -H 'apikey: mykey' apisix:9080/v1/hello
Hello world
生产环境中的测试
现阶段,我们已准备好向用户发布改进后的 Hello World API。我假设我们的团队已经进行了全面的测试,但新代码始终存在风险。部署一个漏洞百出的现有应用程序新版本可能会对 API 提供商的形象(以及收入!)产生负面影响。
为了最大限度地降低风险,双方一致同意采取的策略是进行“金丝雀发布”:
金丝雀发布是一种降低在生产环境中引入新软件版本风险的技术,它通过在向整个基础设施推出新版本并使其对所有人可用之前,先将更改缓慢地推出给一小部分用户。
如果出现故障,只会影响一小部分用户,我们可以在不造成太大影响的情况下回滚更改。但是,借助 API 网关,我们可以在金丝雀发布之前增加一个步骤:将生产环境的流量复制到新的 API 端点。虽然网关会丢弃响应,但我们可以在不影响用户的情况下发现其他错误。
APISIX 提供了proxy-mirror插件,可以将生产环境流量复制到其他节点。让我们更新一下插件配置:
curl http://apisix:9080/apisix/admin/plugin_configs/1 -H 'X-API-KEY: xyz' -X PATCH -d '
{
"plugins": {
"proxy-rewrite": {
"regex_uri": ["/v1/(.*)", "/$1"]
},
"unauth-limit": {
"count": 1,
"time_window": 60,
"key_type": "var",
"key": "consumer_name",
"rejected_code": 429,
"rejected_msg": "Please register at https://apisix. org/register to get your API token and enjoy unlimited calls"
},
"proxy-mirror": {
"host": "http://new. api:8082" # 1
}
}
}'
- APISIX 也会向此主机发送流量
我们可以同时监控新旧端点,确保新端点上的错误数量不会超过旧端点。如果出现错误,我们可以修复错误并重新部署,直到达到预期效果。现在,我们可以进行金丝雀发布了。
首先,我们创建一个指向新 API 的上游:
curl http://apisix:9080/apisix/admin/upstreams/2 -H 'X-API-KEY: xyz' -X PUT -d '
{
"name": "New API",
"type": "roundrobin",
"nodes": {
"newapi:8082": 1
}
}'
proxy-mirror然后,我们可以用流量分割插件替换原插件:
curl http://apisix:9080/apisix/admin/plugin_configs/1 -H 'X-API-KEY: xyz' -X PATCH -d '
{
"plugins": {
"proxy-rewrite": {
"regex_uri": ["/v1/(.*)", "/$1"]
},
"unauth-limit": {
"count": 1,
"time_window": 60,
"key_type": "var",
"key": "consumer_name",
"rejected_code": 429,
"rejected_msg": "Please register at https://apisix. org/register to get your API token and enjoy unlimited calls"
},
"traffic-split": {
"rules": [
{
"weighted_upstreams": [ # 1
{
"upstream_id": 2,
"weight": 1
},
{
"weight": 1
}
]
}
]
}
}
}'
- 为了演示目的,将 50% 的流量发送到新的 API。实际应用中,您可能会从更低的比例开始,甚至可能只允许内部用户访问新接口。
curl -L -H 'apikey: mykey' apisix:9080/hello
Hello world
curl -L -H 'apikey: mykey' apisix:9080/hello
Hello world (souped-up version!)
如果一切顺利,我们可以逐步增加发送到新 API 的流量比例,直到达到 100%。现在我们可以取消流量拆分,并将流量从默认端点重定向到 v2 而不是 v1。
已弃用旧版本
大多数用户可能会迁移到新版本以享受其带来的好处,但仍有一部分用户会继续使用 v1 版本。原因可能有很多:时机不对(提示:永远没有合适的时机)、成本太高、缺乏迁移动力等等。但作为 API 提供商,每个已部署的版本都会产生一定的成本。您迟早都需要停用 v1 版本。
REST 并非标准,但IETF制定了相关的规范草案。更多详情请参阅“弃用 HTTP 标头字段”文档。顾名思义,它基于一个特定的 HTTP 响应标头。
借助 API 网关,我们可以配置路由,使其能够传达未来弃用和替换的信息。为此,APISIX 提供了响应重写功能。虽然它可以重写响应的任何部分,但我们将使用它来添加额外的弃用标头:
curl -v http://apisix:9080/apisix/admin/plugin_configs/1 -H 'X-API-KEY: xyz' -X PATCH -d '
{
"plugins": {
"proxy-rewrite": {
"regex_uri": ["/v1/(.*)", "/$1"]
},
"unauth-limit": {
"count": 1,
"time_window": 60,
"key_type": "var",
"key": "consumer_name",
"rejected_code": 429,
"rejected_msg": "Please register at https://apisix. org/register to get your API token and enjoy unlimited calls"
},
"response-rewrite": {
"headers": {
"Deprecation": "true",
"Link": "<$scheme://apisix:$server_port/v2/hello>; rel=\"successor-version\""
}
}
}
}'
curl -v -H 'apikey: mykey' apisix:9080/v1/hello
< HTTP/1. 1 200
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 11
< Connection: keep-alive
< Date: Fri, 18 Feb 2022 16:33:30 GMT
< Server: APISIX/2. 12. 0
< Link: <http://apisix:9080/v2/hello>; rel="successor-version"
< Deprecation: true
<
Hello world
结论
本文介绍了一种管理 API 生命周期的简单分步流程:
- 不要直接暴露你的 API;在前端设置一个 API 网关。
- 使用路径、查询参数或请求头对现有 API 进行版本控制。
- 使用 301 状态码将用户从非版本化端点迁移到版本化端点。
- 温和地引导用户注册
- 首先在生产环境中进行测试,方法是复制流量,然后将一小部分用户迁移到新版本。
- 正式发布新版本
- 通过标准响应头通知用户旧版本已弃用
更进一步:
原文发表于A Java Geek 网站,日期为2022 年2 月 27日。
文章来源:https://dev.to/apisix/evolving-your-restful-apis-a-step-by-step-approach-4plo




