使用nginx作为API网关
如果我们需要部署反向代理,我们可能已经听说过 nginx
。如果我们还没听说过,让我们在这篇文章谈一谈它,以及我们如何使用它作为API
网关。
什么是nginx?
nginx
是一个HTTP
服务器和反向代理,一个邮件代理服务器,及一个通用的TCP/UDP
代理服务器。
目前,nginx
有一个开源和商业版本(nginx+
)。在本文章中,我们将尝试使用免费版本的一些功能,但也可以在付费版本中使用。
什么是API网关
API
网关是允许开发人员创建、发布、维护、监视和安全API
的流量管理器。
使用API
网关有几个优点,例如:
- 通过单一接口使
API
更加安全。 - 使我们能够更容易地执行访问控制策略、速率限制、路由等。
- 使得能够收集最全面的指标
我们要什么
假设我们有两种API
:a
用于产品,我们称之为 Product API
,另一种是用于消费者,我们称之为 Users API
。最终,我们将发布API
。我们的架构图是开始的:
我们要创建以下路由:
/api/products
:指向Product API
服务/api/users
: 指向Users API
服务
在我们的设置中,并不想公开我们的服务。因此,我们的唯一网关应该是我们在上面的图表中称为API
网关的nginx
。
为了模拟我们的API
,我们构建了两个简单的服务( Product API
和 Users API
)。
为此,让我们在gateway.conf
文件中创建一个简单的规则。
server {
listen 80 default_server;
listen [::]:80 default_server;
#
# Products API
#
location /api/products {
proxy_pass http://products_api:8001;
}
#
# Users API
#
location /api/users {
proxy_pass http://users_api:8002;
}
}
在上面的配置中,请注意我为API
容器创建了gateway
别名(可查看配置docker-compose.yml
)。
当我们请求不同路径时就会调用不同的服务:
GET http://localhost/api/products
{
"name": "Product 1",
"description": "Detail about product 1"
}
GET http://localhost/api/users
{
"name": "User 1",
"email": "email@email.com"
}
这样,我们就有了 nginx
作为反向代理的作用。为了改进我们的配置,我们将定义upstream
用于我们的 API
,而不是直接使用它们的地址。因此,我们将配置修改成:
upstream products_api_server {
server products_api:8001;
}
upstream users_api_server {
server users_api:8002;
}
之后:
server {
listen 80 default_server;
listen [::]:80 default_server;
#
# Products API
#
location /api/products {
proxy_pass http://products_api_server;
}
#
# Users API
#
location /api/users {
proxy_pass http://users_api_server;
}
}
upstream
用于定义一组可以通过proxy_pass
,fastcgi_pass
,uwsgi_pass
, scgi_pass
,memcached_pass
,grpc_pass directives
声明的服务器。
负载均衡
在下一个场景中,假设我们的Users API
被重载,我们想要添加一个新的实例(users_api_balance
)接收用户的请求。
我们将配置API
网关来实现平衡。为此,我们将把新服务器添加到 Users API
服务器组中。
upstream products_api_server {
server products_api:8001;
}
upstream users_api_server {
server users_api:8002;
server users_api_balance:8002;
}
通过这些设置,请求在服务器之间均匀分布。
但是我们如何配置我们的平衡规则呢?现在让我们定义一下,我们应该将新请求引导到活动连接数量最少的服务器。同样,我们只需要配置一下least_conn
字段
upstream products_api_server {
server products_api:8001;
}
upstream users_api_server {
least_conn;
server users_api:8002;
server users_api_balance:8002;
}
我们将暂时使用这个设置,但是nginx
还有一些其他的平衡方法:
ip_hash
Generic Hash
Random
Last Time
(nginx+
专属)
不过,我们假设Users_API
服务器有一个更好的基础结构,我们希望优先处理对这个服务器的请求。我们可以通过设置 weight
参数。
upstream products_api_server {
server products_api:8001;
}
upstream users_api_server {
least_conn;
server users_api:8002 weight=5;
server users_api_balance:8002;
}
就这样,users_api
服务器的优先级是其他服务器的五倍。
缓存
在这个场景中,让我们假设Users API
服务有波动性,并且我们希望优化响应时间。为此,我们将在API
网关中添加一个基本缓存,
实现基本缓存只需要两个指令:proxy_cache_path
和 proxy_cache
。
upstream products_api_server {
server products_api:8001;
}
upstream users_api_server {
ip_hash;
server users_api:8002;
server users_api_balance:8002;
}
proxy_cache_path /tmp/products levels=1:2 keys_zone=products_cache:10m max_size=10g inactive=60m use_temp_path=off;
server {
listen 80 default_server;
listen [::]:80 default_server;
#
# Products API
#
location /api/products {
proxy_cache products_cache;
proxy_pass http://products_api_server;
}
#
# Users API
#
location /api/users {
proxy_pass http://users_api_server;
}
}
proxy_cache_path
指令定义了以下设置:
- 缓存的本地磁盘目录被称为
/tmp/products
. levels
设置了两个目录层次结构/tmp/products
。如果levels
参数为空,nginx
将所有文件放在同一个目录中。keys_zone
设置共享内存区,用于存储缓存键和元数据,如使用计时器。max_size
设置缓存大小的上限(在本例中为10m
)。它是可选的,不指定一个值,允许缓存增长以使用所有可用磁盘空间。inactive
指定一个项目可以在缓存中停留多长时间而不被访问(默认10分钟)。在本例中,缓存管理器过程自动从缓存中删除60分钟未请求的文件,无论该文件是否过期。nginx
首先将指定用于缓存的文件写入一个临时存储区,然后编写use_temp_path=off
指令指示nginx
将它们写入将缓存的同一目录。
nginx
生成的键的默认形式类似于下列 nginx
变量的MD5
哈希:$scheme$proxy_host$request_uri
,当然实际使用的算法稍微复杂一些。
速率限制
在定义了平衡和缓存规则之后,我们现在可以通过为Users API
设置一个速率限制来保护我们的内部服务不受大量请求(DDOS
攻击)的影响。
限速配置有两个主要指令:limit_req_zone
和 limit_req
。
upstream products_api_server {
server products_api:8001;
}
upstream users_api_server {
ip_hash;
server users_api:8002;
server users_api_balance:8002;
}
proxy_cache_path /tmp/products levels=1:2 keys_zone=products_cache:10m max_size=10g inactive=60m use_temp_path=off;
limit_req_zone $binary_remote_addr zone=products_rate:10m rate=1r/s;
limit_req_zone $binary_remote_addr zone=user_rate:10m rate=10r/s;
server {
listen 80 default_server;
listen [::]:80 default_server;
#
# Products API
#
location /api/products {
proxy_cache products_cache;
limit_req zone=products_rate;
limit_req_status 429;
proxy_pass http://products_api_server;
}
#
# Users API
#
location /api/users {
limit_req zone=user_rate;
limit_req_status 429;
proxy_pass http://users_api_server;
}
}
limit_req_zone
有以下三个参数:
key
定义应用限制所依据的请求特性。在这个例子中,它是nginx
变量$binary_remote_addr
,它拥有客户端IP
地址的二进制表示形式。这意味着我们将每个唯一的IP地址限制在第三个参数定义的请求率上。zone
定义用于存储每个IP
地址状态的共享内存区域,以及它访问一个请求限制的URL
的频率。将信息保存在共享内存中意味着它可以在nginx
工作流程之间共享。rate
设置最大请求率。在这个示例中,速率不能超过每秒10个Users API
请求和每秒1个Product API
请求。
默认情况下,当请求限制达到时,nginx
将返回503(服务暂时不可用)。为了改进,我们使用limit_req_status
自定义响应状态代码。在这种情况下,我们使用429
(太多请求)。
API密钥认证
在没有某种形式的认证来保护API
的情况下发布API
是不寻常的。nginx
提供了几种保护API
和认证API
客户端的方法。在我们的解决方案中,我们将使用一个简单的解决方案来验证对我们服务的访问。我们会利用 API
键认证 .
使用API
键身份验证,我们使用一个映射块来创建一个允许访问我们服务的客户机名称列表。
map $http_apikey $api_client_name {
default "";
"KrtKNkLNGcwKQ56la4jcHwxF" "client_one";
"sqj3Ye0vFW/CM/o7LTSMEMM+" "client_two";
"diXnbzglAWMMIvyEEV3rq7Kt" "client_ten";
}
map
参数后边需要两个参数。第一个定义了在哪里找到API
键,在本例中是apikey
客户端http
请求存在$http_apikey
变量。第二个参数创建一个新的变量($api_client_name
)并将其设置为第一个参数与键匹配的行上的第二个参数的值。
现在在我们的服务上启用API
键身份验证。为了避免代码重复,我将把API
键验证分离到另一种方法中。
# API key validation
location = /_validate_apikey {
internal;
if ($http_apikey = "") {
return 401; # Unauthorized
}
if ($api_client_name = "") {
return 403; # Forbidden
}
return 204; # OK (no content)
}
#
# Products API
#
location /api/products {
auth_request /_validate_apikey;
proxy_cache products_cache;
limit_req zone=products_rate;
limit_req_status 429;
proxy_pass http://products_api_server;
}
#
# Users API
#
location /api/users {
auth_request /_validate_apikey;
limit_req zone=user_rate;
limit_req_status 429;
proxy_pass http://users_api_server;
}
在此配置到位后,Users API
和Product API
现在就实现了API
键身份验证。
其他可用于认证的更健壮的解决方案有: OAuth Proxy Module 或 Phantom Token Module.