在同一个 nginx 服务器块上公开多个 api uri

Expose multiple api uri on the same nginx server block

目标

我的目标是在同一个 nginx 服务器上设置多个后端 api 容器:

我的后端容器基于 php:7.2-fpm(symfony 托管在每个 apache 容器上)并且它们没有任何名为 [=13 的路由=](我不想在我的后端创建一些无用的 parent 路由)。

所以,我的要求很简单,例如,我希望在调用时能够做到这一点:

我的后端 帐户容器 提供这个 uri :

到目前为止我尝试了什么

会议

这是我的 nginx.conf :

...
server {
        server_name ~.*;
        client_max_body_size 50m;

        location / {
            try_files $uri /index.php$is_args$args;
        }
        # work if i want to serve account-service on http://localhost:80/
        # location ~ ^/index\.php(/|$) {
        #     fastcgi_pass account-service:9000;
        #     fastcgi_buffers 16 16k;
        #     fastcgi_buffer_size 32k;
        #     fastcgi_param SCRIPT_FILENAME /usr/src/app/public/index.php;
        #     include fastcgi_params;
        # }

        # my last attempt with alias
        location ~* ^/api/account {
            alias /;
            index index.php;
            
            location ~ index\.php(/|$) {
                fastcgi_pass account-service:9000;
                fastcgi_buffers 16 16k;
                fastcgi_buffer_size 32k;
                fastcgi_param SCRIPT_FILENAME /usr/src/app/public/index.php;
                fastcgi_intercept_errors on;
                include fastcgi_params;
            }
        }
}
...

docker-compose.yml :

  nginx:
    image: nginx:1.15.3-alpine
    restart: on-failure
    volumes:
      - "./build/nginx/default.conf:/etc/nginx/nginx.conf:ro"
      - "./logs/nginx:/var/log/nginx"
    ports:
      - "80:80"
    depends_on:
      - account-service
      - account-db
      - cart-service
      - cart-db
      - order-service
      - order-db
      - product-service
      - product-db

  account-service:
    env_file:
      - config/account.env
    build: apps/account-service
    restart: on-failure
    expose:
      - "9000"
    volumes:
      - "./apps/account-service:/usr/src/app"
    depends_on:
      - account-db

  cart-service:
     ...

P.S: 我知道你可以将 nginx conf 分成多个服务器块,监听不同的 port/hostname,但这不是我想在这里实现的。

调整fastcgi_param REQUEST_URI是什么意思?如果您尝试在包含 fastcgi_params 文件之前将某些自定义值设置为 REQUEST_URI ,则 fastcgi_params 设置的值将覆盖您的任何调整:

fastcgi_pass service:9000;
fastcgi_param REQUEST_URI /some/path;
include fastcgi_params;
# REQUEST_URI passed as the real request URI

但是这个会按预期工作:

fastcgi_pass service:9000;
include fastcgi_params;
fastcgi_param REQUEST_URI /some/path;
# REQUEST_URI passed as "/some/path"

尝试使用 rewrite 更改此设置将不起作用,因为 REQUEST_URI fastcgi 参数在 fastcgi_params 文件中设置为 $request_uri 内部 nginx 变量值,并且该变量不会被 rewrite 指令规则改变,它是一个 $uri 指令规则。

这是最简单的解决方案:

server {
    ...
    location ~ ^/api(/(?:account|cart|order|product)/.*) {
        # strip "/api" part from the URI and search for the new location block
        rewrite ^  last;
    }

    location /account {
        # strip "/account" part from the URI and continue processing within the current location block
        rewrite ^/account(.*)  break;
        # include default fastcgi parameters first
        include fastcgi_params;
        # all our tweakings goes after it
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
        # use the rewrited $uri variable instead of the default $request_uri
        # $uri variable does not include query arguments, so add them manually if they exists
        fastcgi_param REQUEST_URI $uri$is_args$args;
        fastcgi_param SCRIPT_FILENAME /usr/src/app/public/index.php;
        fastcgi_intercept_errors on;
        fastcgi_pass account-service:9000;
    }
    location /cart {
        rewrite ^/cart(.*)  break;
        ...
        fastcgi_pass cart-service:9000;
    }
    location /order {
        rewrite ^/order(.*)  break;
        ...
        fastcgi_pass order-service:9000;
    }
    location /product {
        rewrite ^/product(.*)  break;
        ...
        fastcgi_pass product-service:9000;
    }
}

可以使用高级 nginx 技术极大地优化此解决方案:

server {
    ...
    # This is a very important one!
    # Since we are using variables for backend name, we need a resolver to resolve it at the runtime
    # Docker default internal resolver is 127.0.0.11
    resolver 127.0.0.11;

    location ~ ^/api/(?<api>account|cart|order|product)(?<path>/.*) {
        include fastcgi_params;
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
        # note we are using the $path variable here instead of the $uri one
        fastcgi_param REQUEST_URI $path$is_args$args;
        # assuming this path is the same within all the backend services
        fastcgi_param SCRIPT_FILENAME /usr/src/app/public/index.php;
        fastcgi_intercept_errors on;
        # using $api variable as part of backend container name
        fastcgi_pass $api-service:9000;
    }
}

请注意,我们需要一个新的 resolver 指令,因为我们使用变量来指定后端名称。您可以阅读更多详细信息 here, and the resolver address for docker taken from 答案。


更新@2022.05.18

实际上 一种无需附加 resolver 指令即可使上述配置工作的方法。为此,我们需要将每个服务定义为上游。这样 nginx 将只需要在启动时将使用的服务名称解析为 docker 容器 IP 地址,从而消除所有内部 DNS 流量并使整个配置的性能更高:

upstream account {
    server  account-service:9000;
}
upstream cart {
    server  cart-service:9000;
}
upstream order {
    server  order-service:9000;
}
upstream product {
    server  product-service:9000;
}

完成后,上面给出的配置的最后一行可以更改为

fastcgi_pass $api;

resolver 指令可以安全地从 nginx 配置中删除。


如果您的脚本路径因不同的 API 后端容器而异,您可以使用额外的 map 块从 $api 变量值中获取脚本路径:

map $api $script {
    account    /usr/src/app/public/index.php;
    cart       /some/other/path;
    ...
}

server {
    ...
    location ~ ^/api/(?<api>account|cart|order|product)(?<path>/.*) {
        ...
        fastcgi_param SCRIPT_FILENAME $script;
        ...
    }
}