面试官:Nginx和Apache的区别是什么?如何解决前端跨域问题?Nginx如何限流?Nginx如何应对惊群效应?

面试题概览:

  • Nginx和Apache(在性能上)的区别是什么?
  • 如何用Nginx解决前端跨域问题?
  • 漏桶流算法和令牌桶算法知道吗?如何使用Nginx限流?
  • Nginx怎么判断别的IP不可访问?
  • 什么是惊群效应,惊群效应消耗了什么,Nginx的惊群效应是如何发生的,又是如何应对的惊群效应?
  • Nginx如何实现后端服务的健康检查?

面试官:Nginx和Apache(在性能上)的区别是什么?

Nginx和Apache在性能上的区别主要体现在以下几个方面:

一、并发连接处理能力

  1. Nginx

    • Nginx采用了事件驱动的异步非阻塞架构,这种设计使得Nginx能够高效地处理大量并发连接。
    • Nginx在处理并发连接时,资源消耗较低,能够保持较高的性能和稳定性。
  2. Apache

    • Apache使用基于进程或线程的模型来处理请求,每个请求通常会创建一个独立的进程或线程。
    • 在处理大量并发连接时,Apache可能会因为创建过多的进程或线程而导致资源消耗增加,进而影响性能。

二、内存消耗

  1. Nginx

    • Nginx的架构更为轻量化,代码量较少,因此通常比Apache消耗更少的内存。
    • 这种低内存消耗的特性使得Nginx在处理大量请求时更为高效。
  2. Apache

    • 相较于Nginx,Apache的内存消耗可能会更高,尤其是在处理大量并发连接时。
    • 不过,Apache的内存消耗也取决于其配置和加载的模块数量。

三、静态文件处理

  1. Nginx

    • Nginx在处理静态文件时表现非常出色,能够高效地提供静态内容。
    • Nginx的静态文件处理能力得益于其高效的I/O处理机制和优化的内存管理。
  2. Apache

    • 虽然Apache也能处理静态文件,但相比之下,其性能可能稍逊于Nginx。
    • Apache在处理静态文件时可能需要更多的资源,尤其是在高并发场景下。

四、动态内容处理

  1. Nginx

    • Nginx本身并不擅长处理动态内容,但可以通过配置反向代理和负载均衡等功能,将动态内容处理的请求转发给后端的应用服务器。
    • Nginx也支持一些模块来处理简单的动态内容,但功能相对有限。
  2. Apache

    • Apache在处理动态内容方面更具优势,因为它支持多种编程语言和模块扩展。
    • Apache可以通过加载不同的模块来支持不同的动态内容处理需求,如PHP、Python等。

面试官:如何用Nginx解决前端跨域问题?

前端跨域问题是指浏览器出于安全考虑,限制从一个域的网页直接访问另一个域中的资源,从而导致AJAX请求失败。Nginx可以通过配置HTTP响应头来解决前端跨域问题。

以下是用Nginx解决前端跨域问题的详细步骤:

一、理解CORS和同源策略

CORS(Cross-Origin Resource Sharing,跨源资源共享)和同源策略是Web安全领域中两个重要的概念,它们在处理跨域请求时起着至关重要的作用。以下是对这两个概念的详细解释:

同源策略(Same-Origin Policy)

  1. 定义

同源策略是一种安全机制,用于防止不同来源的文档或脚本相互干扰。它基于域名、协议和端口来定义一个“源”,只有当两个URL具有相同的协议、主机名和端口号时,它们才被认为是同源的。

  1. 限制

    • DOM访问限制 一个页面中的脚本不能读取或操作另一个页面的内容。
    • Cookie限制 一个页面中的脚本不能访问另一个页面的Cookie。
    • AJAX请求限制 一个页面中的脚本不能向另一个域名发送AJAX请求。
  2. 目的

这种策略的目的是防止恶意网站窃取数据,例如,防止一个网站通过JavaScript访问另一个网站的用户数据。

CORS(跨源资源共享)

  1. 定义

CORS是一种机制,它使用额外的HTTP头来告诉浏览器允许来自不同源的Web应用访问该资源。通过配置CORS,服务器可以指定哪些源可以访问其资源,以及允许哪些类型的请求方法(如GET、POST等)。

  1. 工作原理

    • 预检请求(Preflight Request) 对于某些类型的请求(如PUT、DELETE等),浏览器会先发送一个OPTIONS请求到服务器,询问服务器是否允许实际的请求。这个过程称为预检请求。
    • 响应头 服务器通过设置特定的HTTP响应头来表明是否允许跨域请求。这些响应头包括Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers等。
    • 实际请求 如果预检请求被批准,浏览器将发送实际的请求。

二、Nginx配置CORS

Nginx可以通过配置HTTP响应头来支持CORS。这些头信息包括Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers和Access-Control-Allow-Credentials等。

  1. 找到或创建Nginx的配置文件:通常位于 /etc/nginx/nginx.conf/etc/nginx/conf.d/ 目录中。
  2. 在需要跨域的server块或location块中添加CORS相关的头部配置 以下是一个配置示例:
server {    
   listen 80;    
   server_name api.example.com;    
   location / {        
      # 设置允许跨域的域名,可以使用通配符'*'允许所有域访问        
      add_header 'Access-Control-Allow-Origin' 'http://study.disign.me';                
      
      # 设置允许的HTTP方法        
      add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT';                
      
      # 设置允许的请求头        
      add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Accept, Origin, X-Requested-With';                
      
      # 如果需要支持cookie,可以设置以下header        
      add_header 'Access-Control-Allow-Credentials' 'true';                
      
      # 如果是预检请求(OPTIONS请求),则直接返回204状态码        
      if ($request_method = 'OPTIONS') {           
         return 204;        
      }                
      
      # 其他正常请求的处理逻辑        
      proxy_pass http://backend_server;    
   }
}

在上述配置中:

  • Access-Control-Allow-Origin 指定允许跨域请求的来源。可以设置为具体的域名(如 http://study.disign.me),或使用通配符 * 允许所有来源。但需要注意,使用通配符时,不允许设置 Access-Control-Allow-Credentialstrue
  • Access-Control-Allow-Methods 指定允许的HTTP请求方法,如GET、POST、OPTIONS、PUT、DELETE等。
  • Access-Control-Allow-Headers 指定允许客户端发送的自定义HTTP头部,如Authorization、Content-Type等。
  • Access-Control-Allow-Credentials 如果客户端请求包括凭据(如Cookies),则该选项必须设置为 true。此时 Access-Control-Allow-Origin 不能为 *,必须为具体的域名。

三、处理预检请求

预检请求是CORS规范中定义的一种机制,用于在实际请求之前探测服务器是否允许某个跨域请求。浏览器在发送某些复杂请求之前,会发送一个OPTIONS请求进行预检,询问服务器是否允许该请求。Nginx可以通过简单的配置处理这种预检请求,例如:

location / {    
   if ($request_method = 'OPTIONS') {       
      add_header 'Access-Control-Allow-Origin' '*';        
      add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';        
      add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';        
      add_header 'Access-Control-Allow-Credentials' 'true';        
      add_header 'Access-Control-Max-Age' 3600;        
      return 204;   
   }    # 其余配置...
}

四、配置通配符和动态设置

  1. 配置通配符:在某些场景中,如果需要允许所有域访问(即不限制跨域请求的来源),可以将 Access-Control-Allow-Origin 设置为 *。但如前所述,此时不能同时启用 Access-Control-Allow-Credentials
  2. 动态设置:如果需要根据请求动态设置 Access-Control-Allow-Origin,可以使用 $http_origin 变量来匹配请求来源。例如:
location / {    
   if ($http_origin ~* 'https?://(www\.)?(example1\.com|example2\.com)') {        
      add_header 'Access-Control-Allow-Origin' "$http_origin";        
      add_header 'Access-Control-Allow-Credentials' 'true';        
      add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';        
      add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Accept';   
   }    
   if ($request_method = 'OPTIONS') {       
      return 204;    
   }    
   proxy_pass http://backend_server;
}

这种配置可以在满足特定条件时,动态地允许多个域名进行跨域访问。

面试题:漏桶流算法和令牌桶算法知道吗?如何使用Nginx限流?

在限流中,有两个关键概念需要了解:阈值(一个单位时间内允许的请求量)和拒绝策略(超过阈值的请求的拒绝策略,常见的拒绝策略有直接拒绝、排队等待等)。

漏桶流算法和令牌桶算法是两种常见的限流算法,以下是这两种算法的具体介绍:

一、漏桶流算法(Leaky Bucket Algorithm)

  1. 工作原理

漏桶算法可以看作是一个带有常量服务时间的单服务器队列。如果漏桶(包缓存)溢出,那么数据包会被丢弃。 漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。漏桶强制一个常量的输出速率,而不管输入数据流的突发性。 当数据包到达漏桶时,它们被放置在桶的底部。然后,数据包以固定的速率(漏出速率)从桶中漏出,注入网络。如果到达速率超过漏出速率,并且桶已满,则新的数据包将被丢弃。

  1. 优点

    • 保证严格的延迟界限,避免一切由缓冲区溢出引起的丢失。
    • 网络可以容易地验证通信量是否符合规范。
  2. 应用场景

    • 漏桶算法适用于需要严格控制输出速率的场景,如网络流量整形。

二、令牌桶算法(Token Bucket Algorithm)

  1. 工作原理

    • 令牌桶算法使用一个虚拟的桶来存放令牌,每个令牌代表一个数据包的发送权限。
    • 系统以固定的速率向桶中添加令牌,每当一个数据包发送时,就从桶中移除一个令牌。
    • 如果桶中没有令牌,数据包则需要等待,直到有令牌可用。如果桶中的令牌数量超过其容量,则新生成的令牌会被丢弃。
  2. 优点

    • 允许一定程度的突发传输,同时限制长时间内的传输速率。
    • 更灵活地处理流量,能够更好地利用网络资源。
  3. 应用场景

    • 令牌桶算法广泛应用于网络流量管理、API请求限流等场景。

三、漏桶流算法与令牌桶算法的比较

漏桶流算法 令牌桶算法
工作原理 突发流量被整形为稳定流量,输出速率恒定 令牌生成速率恒定,数据包发送需消耗令牌
流量特性 强制限制数据传输速率,对突发流量缺乏效率 允许突发传输,同时限制平均传输速率
资源利用 在无拥塞时不能有效利用网络资源 能够更有效地利用网络资源
适用场景 网络流量整形 网络流量管理、API请求限流

在实际应用中,选择哪种限流算法取决于具体的需求和场景。例如,如果系统需要严格控制输出速率并避免突发流量,则漏桶算法可能更合适。而如果系统需要允许一定程度的突发传输,并希望更灵活地处理流量,则令牌桶算法可能更合适。

在Nginx中配置限流,主要通过 limit_reqlimit_conn 两个模块来实现。以下是具体的配置方法:

一、使用limit_req模块限制请求速率

limit_req 模块基于令牌桶算法来限制每秒的请求次数。配置步骤如下:

  1. 定义限流区域

http 块中,使用 limit_req_zone 指令来定义一个限流区域。这个区域会存储在共享内存中,并用于跟踪请求的速率。

   http {    
      limit_req_zone $binary_remote_addr zone=mylimit:10m rate=1r/s;    
      ...
   }

这里, $binary_remote_addr 是客户端IP地址的二进制表示, zone=mylimit:10m 定义了一个名为 mylimit 的共享内存区域,大小为10MB, rate=1r/s 设置了每秒的请求速率限制为1个请求。

  1. 应用限速规则

serverlocation 块中,使用 limit_req 指令来应用限速规则。

   server {    
      ...    
      location / {        
         limit_req zone=mylimit burst=5 nodelay;        
         ...    
      }    
      ...
   }

这里, zone=mylimit 指定了使用前面定义的 mylimit 区域, burst=5 允许在超过速率限制后,突发处理最多5个请求, nodelay 表示这些突发请求将立即处理,而不会等待新的令牌生成。

二、使用limit_conn模块限制并发连接数

limit_conn 模块用于限制同时连接数。配置步骤如下:

  1. 定义连接限制区域

http 块中,使用 limit_conn_zone 指令来定义一个连接限制区域。

   http {    
      limit_conn_zone $binary_remote_addr zone=addr:10m;    
      ...
   }

这里, $binary_remote_addr 是客户端IP地址的二进制表示, zone=addr:10m 定义了一个名为 addr 的共享内存区域,大小为10MB。

  1. 应用连接限制规则

serverlocation 块中,使用 limit_conn 指令来应用连接限制规则。

   server {    
      ...    
      location / {        
         limit_conn addr 10;        
         ...    
      }    
      ...
   }

这里, addr 引用了之前定义的 limit_conn_zone 的名称, 10 表示最大并发连接数限制为10。

三、其他限流配置

除了上述基本的限流配置外,Nginx还支持其他限流策略,如:

  • 设置黑白名单 通过 ngx_http_geo_modulengx_http_map_module 模块,可以设置黑白名单来控制哪些客户端IP地址可以访问或禁止访问特定的资源或整个服务器。
  • 限制数据传输速度 使用 limit_ratelimit_rate_after 指令,可以限制对特定位置(location)的响应速度。

面试官:Nginx怎么判断别的IP不可访问?

Nginx判断别的IP不可访问的功能,通常是通过其内置的ngx_http_access_module模块来实现的。该模块允许基于客户端的IP地址来允许或拒绝对网站资源的访问。

1. 基本配置指令

  • allow 允许指定的IP地址或IP地址段访问服务器。
  • deny 拒绝指定的IP地址或IP地址段访问服务器。

2. 配置位置

这些指令可以配置在http、server、location、limit_except等上下文中,从而对整个服务器、特定虚拟主机或特定资源进行访问控制。

3. 配置示例

拒绝单个IP地址

假设要拒绝IP地址为192.168.1.100的所有访问请求,可以在Nginx配置文件中使用以下配置:

server {    
   listen 80;    
   server_name example.com;    
   location / {        
      deny 192.168.1.100;        
      allow all;    
   }
}

在这个配置中,所有来自192.168.1.100的请求将被拒绝,而其他IP地址的请求将被允许。

拒绝多个IP地址或IP地址段

如果需要同时拒绝多个IP地址或一个IP地址段,可以使用多个deny指令或指定一个CIDR范围。例如:

server {    
   listen 80;    
   server_name example.com;   
   location / {        
      deny 192.168.1.100;        
      deny 192.168.1.101;        
      deny 192.168.1.0/24; # 拒绝整个子网段        
      allow all;    
   }
}

上述配置会拒绝192.168.1.100、192.168.1.101以及192.168.1.0/24网段中的所有IP地址的访问。

允许和拒绝的优先级

在Nginx中,allow和deny指令具有优先级。Nginx从上到下依次读取配置,当有多个allow和deny指令时,首先匹配到的规则会生效。通常情况下,deny指令在allow指令之前时,拒绝的优先级更高。例如:

server {    
   listen 80;    
   server_name example.com;    
   location / {        
      allow 192.168.1.0/24;        
      deny all;    
   }
}

在这个配置中,尽管有一个允许192.168.1.0/24网段访问的规则,但由于deny all指令在其后,所以该规则会被忽略,最终结果是拒绝所有IP地址的访问(这里的配置逻辑实际上是有问题的,因为allow指令在deny之前会先匹配,但这里的示例是为了说明优先级的概念。正确的配置应该是先deny all,然后再根据需要allow特定的IP,或者将deny指令放在allow指令之前以确保拒绝的规则优先生效。不过,由于Nginx会按顺序匹配第一个满足条件的规则,因此通常我们会将更具体的规则(如allow)放在前面,然后用一个deny all作为兜底规则来拒绝所有未明确允许的访问)。然而,为了避免混淆,通常建议按照逻辑顺序和实际需求来配置allow和deny指令,以确保配置的正确性和有效性。

4. 基于HTTP头部的IP信息

在某些情况下,客户端的真实IP地址可能被隐藏在HTTP头部中,如通过反向代理服务器时。在这种情况下,需要基于HTTP头部中的IP信息来进行访问控制。常见的HTTP头部字段包括X-Forwarded-For和X-Real-IP。可以通过 $http_x_forwarded_for 变量来获取该IP信息,并结合if语句进行访问控制。例如:

server {    
   listen 80;    
   server_name example.com;    
   location / {        
      if ($http_x_forwarded_for = "192.168.1.100") {            
         return 403;       
      }        
      proxy_pass http://backend;    
   }
}

在这个配置中,如果请求的X-Forwarded-For头部中包含192.168.1.100,则Nginx会返回403禁止访问的状态码。

5. 访问日志

为了进一步分析和控制IP地址的访问情况,可以使用Nginx的访问日志功能。通过日志记录,可以跟踪哪些IP地址频繁访问服务器,从而有助于做出相应的访问控制策略。在Nginx配置中,可以自定义日志格式并将客户端IP地址记录到日志中。

6. 其他策略

除了使用deny指令直接拒绝访问外,还可以通过以下方式进一步增强IP访问的限制:

  • 使用Nginx的limit_conn和limit_req模块,根据IP地址限制请求的频率或并发连接数。
  • 使用Nginx社区提供的第三方模块(如ngx_http_geoip_module)来基于地理位置限制IP访问。
  • 结合操作系统层面的防火墙(如iptables、firewalld)来限制IP地址的访问。

面试官:什么是惊群效应,惊群效应消耗了什么,Nginx的惊群效应是如何发生的,又是如何应对的惊群效应?**

惊群效应是一个在操作系统多线程或多进程环境中常见的现象。当多个线程或进程同时阻塞等待同一个事件(例如新连接的到来)时,如果该事件触发,系统可能会唤醒所有等待的线程或进程。然而,实际上只有一个线程或进程能够成功获取资源或处理事件,而其他被唤醒的线程或进程则会重新进入休眠状态。这种现象导致了资源的浪费,包括无效的上下文切换和调度,以及可能导致的缓存失效等问题。

对于 Nginx 的惊群问题,我们首先需要理解的是,在 Nginx 启动过程中,master 进程会监听配置文件中指定的各个端口,然后 master 进程就会调用 fork() 方法创建各个子进程,根据进程的工作原理,子进程是会继承父进程的全部内存数据以及监听的端口的,所以 worker 进程在启动之后也是会监听各个端口的。

当客户端有新建连接的请求到来时,就会触发各个 worker 进程的连接建立事件,但是只有一个 worker 进程能够正常处理该事件,而其他的 worker 进程会发现事件已经失效,从而重新循环进入等待状态。这种由于一个连接事件的到来而 “惊” 起了所有 worker 进程的现象就是Nginx的惊群问题。

Nginx的解决方案是:每个 worker 进程被创建的时候,都会调用 ngx_worker_process_init() 方法初始化当前 worker 进程,每个 worker 进程都会调用 epoll_create() 方法为自己创建一个独有的 epoll 句柄。

对于每一个需要监听的端口,都有一个文件描述符与之对应,而 worker 进程只有将该文件描述符通过 epoll_ctl() 方法添加到当前进程的 epoll 句柄中,并且监听 accept 事件,此时才会被客户端的连接建立事件触发。

基于这个原理,nginx 就使用了一个共享锁来控制当前进程是否有权限将需要监听的端口添加到当前进程的 epoll 句柄中。通过这种方式,就保证了每次事件发生时,只有一个 worker 进程会被触发。如下图所示为 worker 进程工作循环的一个示意图:

这里关于图中的流程,需要说明的一点是,每个 worker 进程在进入循环之后就会尝试获取共享锁,如果没有获取到,就会将所监听的端口的文件描述符从当前进程的 epoll 句柄中移除(即使并不存在也会移除),这么做的主要目的是防止丢失客户端连接事件,即使这可能造成少量的惊群问题,但是并不严重。

面试题:Nginx如何实现后端服务的健康检查?

Nginx本身不直接提供内置的后端服务健康检查功能,但可以通过一些模块和配置实现类似的效果。以下是一些常见的方法:

1. 使用 ngx_http_upstream_check_module 模块

ngx_http_upstream_check_module 是一个第三方模块,用于对后端服务进行健康检查。它需要在编译 Nginx 时包含这个模块。

安装和配置示例:

  1. 编译 Nginx 并包含 ngx_http_upstream_check_module

下载 Nginx 源代码并编译:

   wget http://nginx.org/download/nginx-<version>.tar.gz
   tar -zxvf nginx-<version>.tar.gz
   cd nginx-<version>/
   # 添加 --add-module 参数来包含第三方模块
   ./configure --add-module=/path/to/ngx_http_upstream_check_module --prefix=/usr/local/nginx
   make
   sudo make install
  1. 配置 Nginx

在 Nginx 配置文件中(通常是 /usr/local/nginx/conf/nginx.conf),添加健康检查配置:

   upstream backend {    
      server backend1.example.com;    
      server backend2.example.com;
   }
   server {    
      listen 80;    
      location / {        
         proxy_pass http://backend;    
      }    
      # 健康检查配置    
      location /check {        
         check interval=3000 rise=2 fall=3 timeout=2000 type=http;        
         check_http_send "HEAD / HTTP/1.0\r\n\r\n";        
         check_http_expect_status 200-299;    
      }
   }

Nginx会根据 /check location块中的配置,定期(每3000毫秒)向 backend1.example.combackend2.example.com 发送HTTP HEAD请求进行健康检查。

如果 backend1.example.com 连续失败3次( fall=3)健康检查(每次检查超时时间为2000毫秒,且期望的HTTP状态码为200-299),则Nginx会将 backend1.example.com 标记为不健康。

相反,如果 backend1.example.com 在标记为不健康后连续成功2次( rise=2)健康检查,则它会被重新标记为健康。

如果 backend1.example.com 被标记为不健康,Nginx将只会将请求代理到 backend2.example.com(假设它是健康的)。

如果 backend2.example.com 也变为不健康,那么Nginx将无法代理任何新的请求到该上游组,除非有服务器恢复健康或您添加了更多的服务器到上游组中。

2. 使用 proxy_next_upstreamproxy_connect_timeout 配置

虽然这种方法不是真正的健康检查,但可以通过配置 Nginx 的代理行为来间接实现后端服务的故障转移。

配置示例:

upstream backend {    
   server backend1.example.com;    
   server backend2.example.com;
}
server {    
   listen 80;    
   location / {        
      proxy_pass http://backend;       
      proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;        
      proxy_connect_timeout 5s;        
      proxy_read_timeout 30s;        
      proxy_send_timeout 30s;    
   }
}

在这个配置中,如果后端服务返回错误码(如 500, 502, 503, 504),或者连接超时,Nginx 会尝试将请求转发到下一个后端服务器。

原文阅读