使用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文件中创建一个简单的规则。
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,而不是直接使用它们的地址。因此,我们将配置修改成:
conf
upstream products_api_server {
server products_api:8001;
}
upstream users_api_server {
server users_api:8002;
}
之后:
conf
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服务器组中。
conf
upstream products_api_server {
server products_api:8001;
}
upstream users_api_server {
server users_api:8002;
server users_api_balance:8002;
}
通过这些设置,请求在服务器之间均匀分布。
但是我们如何配置我们的平衡规则呢?现在让我们定义一下,我们应该将新请求引导到活动连接数量最少的服务器。同样,我们只需要配置一下least_conn字段
conf
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_hashGeneric HashRandomLast Time(nginx+专属)
不过,我们假设Users_API 服务器有一个更好的基础结构,我们希望优先处理对这个服务器的请求。我们可以通过设置 weight 参数。
conf
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 。
conf
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。
conf
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键身份验证,我们使用一个映射块来创建一个允许访问我们服务的客户机名称列表。
conf
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键验证分离到另一种方法中。
conf
# 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)
}
conf
#
# 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.