前言

大家好,我是老马。很高兴遇到你。

我们为 java 开发者实现了 java 版本的 nginx

https://github.com/houbb/nginx4j

如果你想知道 servlet 如何处理的,可以参考我的另一个项目:

手写从零实现简易版 tomcat minicat

手写 nginx 系列

如果你对 nginx 原理感兴趣,可以阅读:

从零手写实现 nginx-01-为什么不能有 java 版本的 nginx?

从零手写实现 nginx-02-nginx 的核心能力

从零手写实现 nginx-03-nginx 基于 Netty 实现

从零手写实现 nginx-04-基于 netty http 出入参优化处理

从零手写实现 nginx-05-MIME类型(Multipurpose Internet Mail Extensions,多用途互联网邮件扩展类型)

从零手写实现 nginx-06-文件夹自动索引

从零手写实现 nginx-07-大文件下载

从零手写实现 nginx-08-范围查询

从零手写实现 nginx-09-文件压缩

从零手写实现 nginx-10-sendfile 零拷贝

从零手写实现 nginx-11-file+range 合并

从零手写实现 nginx-12-keep-alive 连接复用

从零手写实现 nginx-13-nginx.conf 配置文件介绍

从零手写实现 nginx-14-nginx.conf 和 hocon 格式有关系吗?

从零手写实现 nginx-15-nginx.conf 如何通过 java 解析处理?

从零手写实现 nginx-16-nginx 支持配置多个 server

从零手写实现 nginx-17-nginx 默认配置优化

从零手写实现 nginx-18-nginx 请求头+响应头操作

从零手写实现 nginx-19-nginx cors

从零手写实现 nginx-20-nginx 占位符 placeholder

从零手写实现 nginx-21-nginx modules 模块信息概览

从零手写实现 nginx-22-nginx modules 分模块加载优化

从零手写实现 nginx-23-nginx cookie 的操作处理

从零手写实现 nginx-24-nginx IF 指令

从零手写实现 nginx-25-nginx map 指令

从零手写实现 nginx-26-nginx rewrite 指令

从零手写实现 nginx-27-nginx return 指令

从零手写实现 nginx-28-nginx error_pages 指令

从零手写实现 nginx-29-nginx try_files 指令

从零手写实现 nginx-30-nginx proxy_pass upstream 指令

从零手写实现 nginx-31-nginx load-balance 负载均衡

从零手写实现 nginx-32-nginx load-balance 算法 java 实现

从零手写实现 nginx-33-nginx http proxy_pass 测试验证

从零手写实现 nginx-34-proxy_pass 配置加载处理

从零手写实现 nginx-35-proxy_pass netty 如何实现?

nginx 的 upstream+proxy_pass 指令有什么用?

Nginx 的 upstreamproxy_pass 指令主要用于反向代理和负载均衡。

我们可以用一个简单的例子来说明它们的作用。

场景

假设你有一个网站,用户访问的是 www.example.com,但你的服务器有三台机器来处理请求,这三台机器的 IP 分别是 192.168.0.1192.168.0.2192.168.0.3

你希望 Nginx 能够根据负载情况,将用户的请求均匀地分发到这三台机器上。

配置文件示例

以下是一个 Nginx 配置文件的简化示例,展示了如何使用 upstreamproxy_pass 指令:

http {
    upstream my_backend {
        server 192.168.0.1;
        server 192.168.0.2;
        server 192.168.0.3;
    }

    server {
        listen 80;
        server_name www.example.com;

        location / {
            proxy_pass http://my_backend;
        }
    }
}

解读配置

  1. 定义后端服务器组(upstream)
    upstream my_backend {
        server 192.168.0.1;
        server 192.168.0.2;
        server 192.168.0.3;
    }
    

    在这个部分,我们定义了一个名为 my_backend 的服务器组,包含三台服务器。这就是 upstream 指令的作用:定义一组可以负载均衡的后端服务器。

  2. 配置代理请求(proxy_pass)
    server {
        listen 80;
        server_name www.example.com;
    
        location / {
            proxy_pass http://my_backend;
        }
    }
    

    在这个部分,我们配置了一个监听在 80 端口的服务器,并指定当用户访问 www.example.com 时,Nginx 会将请求代理到 my_backend 定义的服务器组中。proxy_pass http://my_backend; 这条指令告诉 Nginx,将匹配到的请求(这里是所有路径 /)转发到 my_backend

简单总结

  • upstream:用来定义一组后端服务器,这些服务器会被 Nginx 用于负载均衡。
  • proxy_pass:用来指定请求的目标地址,可以是单个服务器,也可以是一个 upstream 组。

通过这样的配置,Nginx 就可以将用户的请求分发到不同的后端服务器上,实现负载均衡。

这种配置方式灵活且易于管理,让你可以根据需要轻松扩展或修改服务器组的配置。

nginx 为什么选择 upstream+proxy_pass 这两个名字?

Nginx 选择 upstreamproxy_pass 这两个名字,是为了形象地描述它们在请求处理过程中的角色。

让我们用一个通俗易懂的方式来解释它们的名字选择。

Upstream

“Upstream” 的意思是“上游”。想象你正在组织一个农产品的分销网络。

  • 上游:就是农民和农场,他们是最初生产产品的人。
  • 下游:就是超市或市场,他们是最终销售产品给消费者的地方。

在 Nginx 的世界里,“上游”服务器就是那些实际处理和响应请求的后端服务器。

所以,“upstream”这个名字表示这些服务器在请求处理链条中是“上游”的部分,它们是数据和响应的源头。

Proxy Pass

“Proxy” 就是代理。想象你是一个物流公司,负责把农产品从农场运到市场。

  • 代理(proxy):你作为中介,负责把产品从农场转运到市场。
  • 传递(pass):你把农产品从农场运到市场。

在 Nginx 中,“proxy_pass”指令就是告诉 Nginx 要把请求传递给谁,就像你告诉物流公司要把货物送到哪个市场一样。

“proxy_pass”表示“代理传递”的意思,它明确了 Nginx 需要将客户端的请求传递到哪个上游服务器。

为什么是 proxy_pass http://my_backend; 而不干脆是 proxy_pass my_backend;?

Nginx 的 proxy_pass 需要指定协议和地址,以确保配置的明确性和灵活性。下面是对这个问题的详细解释。

为什么需要指定协议?

  1. 明确协议类型http://my_backend 指明了请求应该使用 HTTP 协议进行代理。如果省略协议,如 proxy_pass my_backend;,Nginx 就不知道应该使用 HTTP 还是 HTTPS,或者其他可能的协议。

  2. 一致性和兼容性:Nginx 的 proxy_pass 支持多种协议,比如 HTTP、HTTPS、FastCGI 等。通过显式指定协议,配置文件的可读性和可维护性更好,避免混淆和潜在的配置错误。

如何判断是 upstream 组还是具体 URL?

Nginx 通过以下规则来判断 proxy_pass 是指向 upstream 组还是具体的 URL:

1) 如果 proxy_pass 后面包含协议(如 http://https://),且不在 upstream 组中Nginx 会将其视为具体的 URL。

2) 否则以 http:// or https:// 开头,则认为是 URL

配置示例

假设有以下配置:

http {
    upstream my_backend {
        server 192.168.0.1;
        server 192.168.0.2;
    }

    server {
        listen 80;
        server_name www.example.com;

        location / {
            proxy_pass http://my_backend;
        }

        location /specific {
            proxy_pass http://192.168.0.3;
        }

        location /another {
            proxy_pass my_backend;  # 这种写法会出错
        }
    }
}

解释

  • proxy_pass http://my_backend;:明确地告诉 Nginx 使用 HTTP 协议,将请求代理到名为 my_backendupstream 组。
  • proxy_pass http://192.168.0.3;:明确地告诉 Nginx 使用 HTTP 协议,将请求代理到具体的 URL 192.168.0.3
  • proxy_pass my_backend;:这种写法是错误的,因为省略了协议,Nginx 不知道如何处理这个指令。

灵活性和明确性

这种设计保证了配置的灵活性和明确性:

  • 通过显式指定协议,避免了误解和错误配置。
  • 保证了配置文件的清晰度,便于管理和维护。

如何判断是否为 upstream 组?

问题

如果有一个 proxy_pass xxx,又改如何判断到底是到 upstream 组,还是具体的 url 呢?

实现

具体的 java 实现:

import java.util.Set;

public class ProxyResolver {

    public static void main(String[] args) {
        Set<String> upstream = Set.of("upstream1", "upstream2", "upstream3");
        String proxy = "http://upstream1"; // 示例输入

        String result = resolveProxy(proxy, upstream);
        System.out.println("Resolved to: " + result);
    }

    public static String resolveProxy(String proxy, Set<String> upstream) {
        // 先检查是否是具体的 URL
        if (proxy.startsWith("http://") || proxy.startsWith("https://")) {
            // 如果是具体的 URL,判断是否在 upstream 中
            String potentialUpstream = proxy.substring(proxy.indexOf("//") + 2);
            if (upstream.contains(potentialUpstream)) {
                return "Upstream: " + potentialUpstream;
            } else {
                return "URL: " + proxy;
            }
        } else if (upstream.contains(proxy)) {
            // 如果不是具体的 URL,直接判断是否在 upstream 中
            return "Upstream: " + proxy;
        } else {
            // 默认情况,无法判断的情况
            return "Unknown: " + proxy;
        }
    }
}

小结

Nginx 的 upstreamproxy_pass 指令用于实现反向代理和负载均衡,简单总结如下:

  1. Upstream:定义了一组后端服务器,每个服务器可以处理请求。这些服务器可以是物理服务器的地址或者其他可访问的网络位置。Upstream 为 Nginx 提供了一个管理和分发请求的目标列表。

  2. Proxy Pass:指示 Nginx 将客户端的请求代理到指定的后端服务器组(upstream)或者具体的服务器地址。它告诉 Nginx 如何转发请求,并且可以指定不同的路径和条件来匹配不同的后端处理逻辑。

通过这两个指令,Nginx 可以实现高效的负载均衡和反向代理,从而提升网站或应用程序的性能、可靠性和扩展性。


chat

详细介绍一下 nginx 的 upstream 指令

upstream 指令是 Nginx 配置中的一个重要部分,用于定义一组后端服务器,这些服务器将用于负载均衡。

下面是 upstream 指令的详细介绍,包括它的常见配置选项和用法。

1. 基本语法

upstream backend {
    server backend1.example.com;
    server backend2.example.com;
    server backend3.example.com;
}

在这个示例中,backend 是一个自定义的名字,可以是任何有效的标识符。

定义在 upstream 块中的服务器列表将用于负载均衡。

2. server 指令

server 指令用于指定一个后端服务器。它的基本语法是:

server <address> [parameters];

常见参数:

  • weight=number:指定服务器的权重,默认值为1。权重越高,服务器接收的请求越多。
      server backend1.example.com weight=3;
    
  • max_fails=number:允许请求失败的最大次数,默认值为1。
      server backend2.example.com max_fails=3;
    
  • fail_timeout=time:在指定时间内发生 max_fails 次失败后,服务器将被视为不可用,默认值为10秒。
      server backend2.example.com fail_timeout=30s;
    
  • backup:标记此服务器为备用服务器,仅在主服务器不可用时使用。
      server backend3.example.com backup;
    
  • down:标记此服务器为不可用。
      server backend4.example.com down;
    

3. 负载均衡算法

upstream 块中可以指定负载均衡算法:

  • 轮询(Round Robin):默认算法,按顺序将请求分发到每台服务器。

  • 最少连接数(Least Connections):将请求分发到当前活动连接数最少的服务器。
      upstream backend {
          least_conn;
          server backend1.example.com;
          server backend2.example.com;
      }
    
  • IP哈希(IP Hash):根据客户端IP地址进行哈希,将来自同一IP地址的请求分发到同一台服务器。
      upstream backend {
          ip_hash;
          server backend1.example.com;
          server backend2.example.com;
      }
    

4. 健康检查

Nginx 还支持对后端服务器进行主动的健康检查,以确保请求不会被分发到不可用的服务器上。

upstream backend {
    server backend1.example.com;
    server backend2.example.com;

    # 健康检查配置
    health_check interval=10s fails=3 passes=2;
}

5. 会话保持

会话保持(Sticky Sessions)是将同一客户端的请求始终分发到同一台服务器的方法。

upstream backend {
    server backend1.example.com;
    server backend2.example.com;
    sticky cookie srv_id expires=1h domain=.example.com path=/;
}

基于 IP 的会话保持

upstream backend {
    ip_hash;
    server backend1.example.com;
    server backend2.example.com;
}

6. 完整示例

以下是一个完整的 Nginx 配置示例,包含 upstream 指令及其常见用法:

http {
    upstream backend {
        least_conn;
        server backend1.example.com weight=3 max_fails=3 fail_timeout=30s;
        server backend2.example.com;
        server backend3.example.com backup;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

在这个示例中:

  • backend 上游服务器组使用了最少连接数(Least Connections)算法。
  • backend1 服务器具有权重3,最大失败次数3,失败超时时间30秒。
  • backend3 服务器被设置为备用服务器。
  • 服务器块将所有请求代理到 backend 上游服务器组,并设置了一些头信息以传递客户端的原始信息。

通过合理配置 upstream 指令,可以实现高效的负载均衡,提升应用的性能和可靠性。

nginx upstream 为什么叫这么个名字?感觉很奇怪

Nginx 中的 upstream 名称源于网络架构和数据流的概念。

在网络和数据传输的术语中,数据流通常被分为“上游”(upstream)和“下游”(downstream)。

上游和下游的概念

  • 上游(Upstream):指的是数据流入来源,通常是服务器端。在 Nginx 的上下文中,上游服务器是指那些接收客户端请求并提供响应的服务器。这些服务器可能是应用服务器、数据库服务器或其他类型的服务提供者。
  • 下游(Downstream):指的是数据流出方向,通常是客户端。在 Nginx 的上下文中,客户端(如浏览器或其他应用程序)是接收来自上游服务器的响应的下游。

为什么叫 upstream

  1. 数据流方向:Nginx 作为反向代理服务器,接收客户端的请求并将其转发给一个或多个上游服务器进行处理。这些服务器被视为“上游”源,因为它们是数据处理的来源。
  2. 网络架构术语:在网络架构中,上游和下游是常用术语,用于描述数据流动的方向。使用 upstream 符合这一标准术语,使得技术人员更容易理解其功能和用途。
  3. 负载均衡角色upstream 模块主要用于定义和管理一组后端服务器(即上游服务器),并执行负载均衡。这一功能明确指出这些服务器是处理请求的上游源。

名称的意义和直观理解

  • 直观:对于熟悉网络架构和数据流术语的人来说,upstream 名称直观地表示了这些服务器的位置和角色。
  • 明确:名称明确地指出这些服务器是接收和处理来自客户端请求的源头。
  • 统一标准:遵循网络术语标准,使得文档和配置更加一致和规范。

总结

Nginx 使用 upstream 名称来描述后端服务器组,是基于网络架构中“上游”和“下游”数据流的概念。

虽然名字可能在一开始听起来有点奇怪,但理解其背景后,就会发现它是非常合适且符合标准的。

这个术语帮助明确了数据流的方向和服务器的角色,使得配置和管理变得更为清晰。

详细介绍一下 nginx proxy_pass 指令

proxy_pass 指令是 Nginx 中用于反向代理的重要指令,它将客户端的请求转发到上游服务器。

以下是 proxy_pass 指令的详细介绍,包括其基本用法、常见参数和配置示例。

1. 基本语法

location / {
    proxy_pass URL;
}
  • URL:可以是一个完整的URL(包括协议、主机和路径)或一个上游服务器组(定义在 upstream 块中)。

2. URL 形式

2.1 简单的代理

location / {
    proxy_pass http://backend1.example.com;
}

在这个示例中,所有匹配 / 的请求将被转发到 http://backend1.example.com

2.2 使用上游服务器组

http {
    upstream backend {
        server backend1.example.com;
        server backend2.example.com;
    }

    server {
        location / {
            proxy_pass http://backend;
        }
    }
}

在这个示例中,backend 是一个上游服务器组,Nginx 会根据负载均衡算法将请求分发到 backend1backend2

3. 路径处理

proxy_pass 指令中的路径处理比较复杂,需要根据具体的需求来配置。

3.1 转发根路径

location / {
    proxy_pass http://backend1.example.com/;
}

在这个示例中,请求的路径将直接替换为上游服务器的路径。例如,请求 /foo/bar 将被转发为 http://backend1.example.com/foo/bar

3.2 转发特定路径

location /app/ {
    proxy_pass http://backend1.example.com/;
}

在这个示例中,匹配 /app/ 的请求路径会替换为根路径。例如,请求 /app/foo/bar 将被转发为 http://backend1.example.com/foo/bar

4. 设置头信息

通过 proxy_set_header 指令,可以设置转发请求时的头信息,以传递客户端的原始信息。

location / {
    proxy_pass http://backend1.example.com;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

常见头信息设置:

  • Host:设置请求的主机头为原始请求的主机。
  • X-Real-IP:设置客户端的真实IP地址。
  • X-Forwarded-For:设置客户端的原始IP地址,包含在 X-Forwarded-For 头中。
  • X-Forwarded-Proto:设置原始请求的协议(HTTP 或 HTTPS)。

5. 其他常见参数

5.1 代理超时

  • proxy_connect_timeout:设置与上游服务器建立连接的超时时间。
  • proxy_send_timeout:设置向上游服务器发送请求的超时时间。
  • proxy_read_timeout:设置从上游服务器读取响应的超时时间。
location / {
    proxy_pass http://backend1.example.com;
    proxy_connect_timeout 60s;
    proxy_send_timeout 60s;
    proxy_read_timeout 60s;
}

5.2 缓存代理响应

Nginx 可以缓存从上游服务器接收到的响应,以提高性能和减少上游服务器的负载。

http {
    proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

    server {
        location / {
            proxy_pass http://backend1.example.com;
            proxy_cache my_cache;
            proxy_cache_valid 200 302 10m;
            proxy_cache_valid 404 1m;
        }
    }
}

5.3 修改请求 URI

通过 proxy_set_headerproxy_pass 配合,可以修改请求的 URI。

location /app/ {
    rewrite ^/app/(.*)$ /$1 break;
    proxy_pass http://backend1.example.com;
}

6. 完整示例

以下是一个包含所有主要配置的完整 proxy_pass 配置示例:

http {
    upstream backend {
        server backend1.example.com;
        server backend2.example.com;
    }

    proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

    server {
        listen 80;

        location / {
            proxy_pass http://backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 60s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;

            proxy_cache my_cache;
            proxy_cache_valid 200 302 10m;
            proxy_cache_valid 404 1m;
        }
    }
}

在这个示例中:

  • 定义了一个名为 backend 的上游服务器组。
  • 配置了代理缓存路径和相关参数。
  • 在服务器块中,配置了 proxy_pass 和一些常见的头信息、超时参数和缓存设置。

通过合理配置 proxy_pass 指令,可以实现高效的反向代理,提高应用的性能和可靠性。

nginx proxy_pass 和 upstream 的作用就是反向代理+负载均衡,为什么不设计为一个指令?

proxy_passupstream 设计为两个独立的指令而不是一个指令,主要是出于灵活性和配置清晰度的考虑。具体来说,有以下几点原因:

  1. 职责分离upstream 指令定义了一组后端服务器,这些服务器可以用于负载均衡。而 proxy_pass 指令则用来指定代理请求的目标地址。将这两者分开,可以清晰地表达各自的职责。

  2. 灵活配置:通过将 upstreamproxy_pass 分开,可以在不同的地方使用同一个 upstream 配置。例如,你可以在多个位置块中使用相同的后端服务器组,而不需要重复定义。

  3. 配置复用upstream 配置块可以被多个 serverlocation 块引用,这样可以减少重复配置,提高配置文件的可维护性。

  4. 扩展性upstream 块中可以配置多种负载均衡策略(如轮询、最少连接、IP哈希等),这些策略可以独立于 proxy_pass 的目标地址配置。此外,upstream 块还支持一些高级功能,如健康检查和备用服务器配置,这些功能如果与 proxy_pass 合并在一起,配置将变得复杂且难以管理。

通过将 proxy_passupstream 分开,Nginx 提供了一种灵活且强大的配置方式,使用户能够更清晰地定义代理和负载均衡行为,并能更方便地进行扩展和维护。

如果让你设计?你会怎么设计?

如果我来设计

重新设计时,我会考虑以下几点:

  1. 统一的配置方式:简化配置,减少用户对 proxy_passupstream 区别的理解负担。
  2. 自动化管理:提供自动发现和管理上游服务器的功能,简化配置和维护。
  3. 增强的灵活性:允许更灵活的配置选项,支持更多的负载均衡算法和策略。

设计思路

  1. 统一的代理和负载均衡配置

    • 合并 proxy_passupstream 的概念,使用统一的配置块定义代理和负载均衡。
    • 支持直接定义单个目标地址或多个上游服务器。
    server {
        listen 80;
    
        location / {
            proxy_backend {
                server backend1.example.com weight=3;
                server backend2.example.com;
                load_balance least_conn;
                health_check interval=10s fails=3 passes=2;
            }
        }
    
        location /simple {
            proxy_backend http://example.com;
        }
    }
    
  2. 自动发现和管理

    • 提供自动发现上游服务器的功能,通过服务注册中心(如 Consul 或 etcd)自动更新上游服务器列表。
    • 支持动态管理和配置更新,无需手动修改配置文件和重启服务。
    server {
        listen 80;
    
        location / {
            proxy_backend {
                auto_discover consul;
                service_name my_service;
                load_balance least_conn;
                health_check interval=10s fails=3 passes=2;
            }
        }
    }
    
  3. 增强的灵活性和扩展性

    • 支持更多负载均衡算法和策略,例如一致性哈希、基于权重的轮询等。
    • 提供插件机制,允许用户自定义负载均衡算法和健康检查策略。
    server {
        listen 80;
    
        location / {
            proxy_backend {
                server backend1.example.com weight=3;
                server backend2.example.com;
                load_balance consistent_hash;
                custom_health_check /path/to/custom_check;
            }
        }
    }
    

总结

Nginx 的 proxy_passupstream 设计在灵活性、性能和可维护性之间取得了良好的平衡,但也存在一些学习曲线和配置复杂度的问题。

重新设计时,我会重点考虑统一配置方式、自动化管理和增强的灵活性,以简化用户配置和提升系统可维护性。

通过这种方式,可以进一步提高反向代理和负载均衡的易用性和扩展性。

参考资料