nginx

nginx 安装

https://rhel.pkgs.org/8/nginx-x86_64/nginx-1.22.0-1.el8.ngx.x86_64.rpm.html

概念

进程模型

unix系统中会以daemon的方式在后台运行,后台进程包含一个master进程和多个worker进程。

nginx默认是以多进程的方式来工作的

独立的进程,不需要加锁,所以省掉了锁带来的开销。采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master进程则很快启动新的worker进程。

  • master进程

    接收来自外界的信号,向各worker进程发送信号,监控worker进程的运行状态

  • worker进程

    处理基本的网络事件

多个worker进程之间是对等的,互相之间是独立。一个请求,只可能在一个worker进程中处理。为保证只有一个进程处理该连接,所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该连接。

锁的竞争,worker_connections 每个worker进程所能建立连接的最大值可用的连接变少,让出accept_mutex的机会变大。

worker进程个数通常等于机器cpu核数,更多的worker数,只会导致进程来竞争cpu资源了,从而带来不必要的上下文切换。

工作方式

异步非阻塞

同时监控多个事件,调用他们是阻塞的,但可以设置超时时间,在超时时间之内,如果有事件准备好了,就返回。

只要有事件准备好了,我们就去处理它,只有当所有事件都没准备好时,才会等待。

这里的并发请求,是指未处理完的请求,线程只有一个,所以同时能处理的请求当然只有一个了,只是在请求间进行不断地切换而已,切换也是因为异步事件未准备好,而主动让出的。这里的切换是没有任何代价,你可以理解为循环处理多个准备好的事件,事实上就是这样的。与多线程相比,这种事件处理方式是有很大的优势的,不需要创建线程,每个请求占用的内存也很少,没有上下文切换,事件处理非常的轻量级。并发数再多也不会导致无谓的资源浪费(上下文切换)。更多的并发数,只是会占用更多的内存而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
while (true) {
    for t in run_tasks:
        t.handler();
    update_time(&now);
    timeout = ETERNITY;
    for t in wait_tasks: /* sorted already */
        if (t.time <= now) {
            t.timeout_handler();
        } else {
            timeout = t.time - now;
            break;
        }
    nevents = poll_function(events, timeout);
    for i in nevents:
        task t;
        if (events[i].type == READ) {
            t.handler = read_handler;
        } else { /* events[i].type == WRITE */
            t.handler = write_handler;
        }
        run_tasks_add(t);
}

请求处理

请求处理流程

常用配置

nginx 变量定义

反向代理

反向代理,客户端对代理是无感知的,客户端不需要任何配置就可以访问,客户端将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,在返回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器IP地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
server {
	listen 80;
	# http https 重定向
	return 301 https://$host$request_uri;
}

server {
    listen       443 ssl;
    server_name  localhost;
    # 上传大小
    client_max_body_size 50M;
    # ssl
    ssl_certificate      /path/master_server.crt;
    ssl_certificate_key  /path/master_server.key;

    ssl_session_cache    shared:SSL:1m;
    # 客户端连接可以复用sslsession cache中缓存的ssl参数的有效时长
    ssl_session_timeout  5m;

    ssl_ciphers  HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers  on;


	# 实际访问 本地 /xx/dd/update/ 目录下的文件
    location /update/ {
        root /xx/dd/;
    }
    # 实际访问 本地 /xx/dd/ 目录下的文件
  	location /analyses/ {
      alias /xx/dd/;
      autoindex on;
      autoindex_exact_size off;
      autoindex_localtime on;
	}

    location /api {
        proxy_pass http://127.0.0.1:9002;
        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_read_timeout 300s;
    }
	# wss 设置 Upgrade Connection  http://nginx.org/en/docs/http/websocket.html
    location /ws {
        proxy_pass http://127.0.0.1:9002/ws;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 300s;
    }

    error_page 502 /502.json;
    location /502.json {
        return 502 '{"code": -1, "msg": "服务器未响应"}';
    }
    
    # 限制某接口可上传大小
  	location = /api/2/xxx {
        proxy_pass http://127.0.0.1:9002/api/2/xxx;
        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_read_timeout 300s;
        client_max_body_size 201k;
    }

  
  
}

关于location和proxy_pass是否结尾加/

访问URL location配置 proxy_pass配置 后端接收的请求 备注
test.com/user/test.html /user/ http://test1/ /test.html  
test.com/user/test.html /user/ http://test1 /user/test.html  
test.com/user/test.html /user http://test1 /user/test.html  
test.com/user/test.html /user http://test1/ //test.html  
test.com/user/test.html /user/ http://test1/haha/ /haha/test.html  
test.com/user/test.html /user/ http://test1/haha /hahatest.html  

正则 不同路径location匹配不同proxy_pass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    location ^~ /gateway-api/ { # 这个开头
      location ~ ^/gateway-api/v1/chat/(.*) { # 这个路径匹配这个proxy_pass
            limit_req zone=one burst=15 nodelay;
            proxy_pass http://chat_api/v1/chat/$1;
            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_read_timeout 3000000s;
            proxy_connect_timeout 3000000s;
        }

        # 其他的匹配宁外一个proxy_pass
        limit_req zone=one burst=15 nodelay;
        proxy_pass http://embedding_api/;
        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_read_timeout 3000000s;
        proxy_connect_timeout 3000000s;
    }

端口转发 4层

1
2
3
4
5
6
7
8
9
10
11
12
13
 #  对domain:dddd的请求转发到本地xxxx端口

upstream redis {
    server 127.0.0.1:xxxx max_fails=3 fail_timeout=60s;
}

server {
    listen domain:dddd;

    proxy_connect_timeout 60s;
    proxy_timeout 300s;
    proxy_pass redis;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;

load_module /usr/lib/nginx/modules/ngx_stream_module.so;

events {
    worker_connections  1024;
}


http {
    include     /etc/nginx/sites-enabled/*;
    include       mime.types;
    default_type  application/octet-stream;

    #access_log  logs/access.log  main;
    sendfile        on;
    keepalive_timeout  65;


}

stream {
    include /etc/nginx/stream-enabled/*;
}

rpc转发

grpc ssl 转发

1
2
3
4
5
6
7
8
9
10
11
server {
                listen 9100 ssl http2;

                ssl_certificate     /path/rpc_server.crt;
                ssl_certificate_key /path/rpc_server.key;

                location / {
                        grpc_pass grpcs://domain:9100;
                }
        }

正向代理

正向代理服务器位于客户端和服务器之间,为了从服务器获取数据,客户端要向代理服务器发送一个请求,并指定目标服务器,代理服务器将目标服务器返回的数据转交给客户端。这里客户端需要要进行一些正向代理的设置的。

http

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
    resolver 8.8.8.8;
    access_log off;
    listen 6080;
    location / {
        proxy_pass $scheme://$http_host$request_uri;
        proxy_set_header HOST $http_host;

        # 配置缓存大小,关闭磁盘缓存读写减少I/O,以及代理连接超时时间
        proxy_buffers 256 4k;
        proxy_max_temp_file_size 0;
        proxy_connect_timeout 30;

        # 配置代理服务器 Http 状态缓存时间
        proxy_cache_valid 200 302 10m;
        proxy_cache_valid 301 1h;
        proxy_cache_valid any 1m;
        proxy_next_upstream error timeout invalid_header http_502;
    }
}
1
curl -I --proxy 192.168.100.111:6080 http://www.baidu.com

https

使用ngx_http_proxy_connect_modulenginx默认不支持https的正向代理,需要自己重新编译

1
2
3
4
5
6
7
8
9
10
11
12
➜  nginx tree workdir -L 2
workdir
├── nginx-1.20.1.tar.gz
└── nginx_proxy
    ├── LICENSE
    ├── README.md
    ├── config
    ├── ngx_http_proxy_connect_module.c
    ├── patch
    └── t

3 directories, 5 files

–with-debug可开可不开 不支持自签名证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FROM centos:7

RUN yum install -y patch gcc glibc-devel make openssl-devel pcre-devel zlib-devel gd-devel geoip-devel perl-devel

RUN groupadd -g 101 nginx \
          && adduser  -u 101 -d /var/cache/nginx -s /sbin/nologin  -g nginx nginx

COPY ./workdir /workdir
WORKDIR /workdir

RUN tar -zxvf nginx-1.20.1.tar.gz && cd nginx-1.20.1 \
    && patch -p1 < /workdir/nginx_proxy/patch/proxy_connect_rewrite_1018.patch \
    && ./configure --with-debug --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-g -O2 -fdebug-prefix-map=/data/builder/debuild/nginx-1.17.1/debian/debuild-base/nginx-1.17.1=. -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie' --add-module=/workdir/nginx_proxy \
    && make && make install \
    && cd /workdir && rm -rf /workdir/*


CMD ["nginx", "-g", "daemon off;"]

共用端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# nginx 80端口配置 (监听a二级域名)
server {
    listen  80;
    server_name     a.com;
    location / {
        proxy_pass      http://localhost:8080; # 转发
    }
}
# nginx 80端口配置 (监听b二级域名)
server {
    listen  80;
    server_name     b.com;
    location / {
        proxy_pass      http://localhost:8081; # 转发
    }
}

nginx如果检测到a.com的请求,将原样转发请求到本机的8080端口,如果检测到的是b.com请求,也会将请求转发到8081端口

匹配阶段

  • 根据请求中的目的地址和端口进行匹配。如果相同的目的地址和端口同时还会对应多个servers,再根据server_name属性进行进一步匹配。需要强调的是,只有当listen指令无法找到最佳匹配时才会考虑评估server_name指令。
  • 在匹配到server后,Nginx根据请求URL中的path信息再匹配server中定义的某一个location。

负载均衡

  • 轮询 每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    upstream api {
        server 127.0.0.1:9002 ;
        server 127.0.0.1:19002 ;
    }
      
    location /api/2/ {
      proxy_pass http://api;
      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_read_timeout 300s;
    }
      
      
    
  • weight

    权重越高,在被访问的概率越大

    1
    2
    3
    4
    5
    6
    upstream api {
        server 127.0.0.1:9002 backup; # 其它所有的非backup机器down或者忙的时候,请求backup机器)
        server 127.0.0.1:19002 weight=10 max_fails=3 fail_timeout=30s max_conns=1;
        server 127.0.0.1:19003 weight=10 max_fails=3 fail_timeout=30s max_conns=1;
        server 127.0.0.1:19004 weight=10 max_fails=3 fail_timeout=30s max_conns=1;
    }
    
    • max_fails

      默认为1,允许请求失败的次数。当超过最大次数时,返回proxy_next_upstream 模块定义的错误

    • fail_timeout

      默认10秒,max_fails次失败后,在fail_timeout期间内,nginx会认为这台Server暂时不可用,不会将请求分配给它

    • max_conns

      默认0不限制,限制分配给某台Server处理的最大连接数量,超过这个数量,将不会分配新的连接给它

  • ip_hash

    1
    2
    3
    4
    5
    upstream item { # item名字可以自定义
      ip_hash;
      server 192.168.101.60:81;
      server 192.168.101.77:80;
    }
    

    根据客户端IP来分配服务器,解决session共享问题

缓存

1
2
3
4
5
6
location ~* \.(css|js|png|jpg|jpeg|gif|gz|svg|mp4|mp3|ogg|ogv|webm|htc|xml|woff|ttf)$ {
        root /data/;
        access_log off;
        add_header Cache-Control "public,max-age=7*24*3600";
    }

access log fmt

log 打印响应时间,还有upstream addr

nginx: [emerg] “log_format” directive is not allowed here log_format 要放在server外面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
upstream api {
    server 127.0.0.1:9002 backup;
    server 127.0.0.1:19002 weight=10 max_fails=3 fail_timeout=30s max_conns=10;
    server 127.0.0.1:19003 weight=10 max_fails=3 fail_timeout=30s max_conns=10;
    server 127.0.0.1:19004 weight=10 max_fails=3 fail_timeout=30s max_conns=10;
}

log_format custom_log_fmt '$remote_addr - $remote_user [$time_local]'
                ' ups_resp_time: $upstream_response_time '
                ' req_time: $request_time '
                ' "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$upstream_addr"';

server {
    listen       443 ssl;
    。。。。
    access_log /dev/stdout custom_log_fmt;
}

  • request_time

    request processing time in seconds with a milliseconds resolution; time elapsed between the first bytes were read from the client and the log write after the last bytes were sent to the client

    请求处理时长,单位为秒,精度为毫秒,从读入客户端的第一个字节开始,直到把最后一个字符发送张客户端进行日志写入为止

    即$request_time包括接收客户端请求数据的时间、后端程序响应的时间、发送响应数据给客户端的时间(不包含写日志的时间)

  • upstream_response_time

    keeps time spent on receiving the response from the upstream server; the time is kept in seconds with millisecond resolution. Times of several responses are separated by commas and colons like addresses in the $upstream_addr variable.

    从Nginx向后端建立连接开始到接受完数据然后关闭连接为止的时间

  • upstream_addr

    记录负载均衡分发到那个addr

  • /dev/stdout

    docker 容器标准输出

限流控制并发

nginx限流与限速

死磕nginx系列–nginx 限流配置

限制IP的连接和并发分别有两个模块

  • limit_req_zone 用来限制单位时间内的请求数,即速率限制,采用的漏桶算法 “leaky bucket”。
  • limit_req_conn 用来限制同一时间连接数,即并发限制。

请求数

ngx_http_limit_req_module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
http{
    # 固定请求速率为1个请求/每秒
    # 表示通过remote_addr这个标识来做限制,“binary_”的目的是缩写内存占用量,是限制同一客户端ip地址
    # zone=one:10m表示生成一个大小为10M,名字为one的内存区域,用来存储访问的频次信息。
    # rate=1r/s表示允许相同标识的客户端的访问频次,这里限制的是每秒1次,还可以有比如30r/m的
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
    # 当服务器由于limit被限速或缓存时,配置写入日志。延迟的记录比拒绝的记录低一个级别。例子:limit_req_log_level notice延迟的的基本是info。
    limit_req_log_level error;
    # 设置拒绝请求的返回值。值只能设置 400 到 599 之间。
    limit_req_status 503;

    server {
        location /search/ {
            # zone=one 设置使用哪个配置区域来做限制,与上面limit_req_zone 里的name对应。
            # burst爆发的意思,这个配置的意思是设置一个大小为5的缓冲区当有大量请求(爆发)过来时,超过了访问频次限制的请求可以先放到这个缓冲区内。
            # nodelay,如果设置,超过访问频次而且缓冲区也满了的时候就会直接返回503,如果没有设置,则所有请求会等待排队。
            limit_req zone=one burst=5 nodelay;
        }
    }  
}

执行流程:

(1)请求进入后判断最后一次请求时间相对于当前时间是否需要进行限流,若不需要,执行正常流程;否则,进入步骤2; (2)如果未配置漏桶容量(默认0),按照固定速率处理请求,若此时请求被限流,则直接返回503;否则,根据是否配置了延迟默认分别进入步骤3或步骤4; (3)配置了漏桶容量以及延迟模式(未配置nodelay),若漏桶满了,则新的请求将被限;如果漏桶没有满,则新的请求会以固定平均速率被处理(请求被延迟处理,基于sleep实现); (4)配置了漏桶容量以及nodelay,新请求进入漏桶会即可处理(不进行休眠,以便处理固定数量的突发流量);若漏桶满了,则请求被限流,直接返回响应。

连接数

ngx_http_limit_conn_module

限制单个IP的请求数。并非所有的连接都被计数。只有在服务器处理了请求并且已经读取了整个请求头时,连接才被计数。

基于key($binary_remote_addr或者server_name),对网络总连接数进行限流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
http{
    # 针对客户端地址,进行连接数限制
    limit_conn_zone $binary_remote_addr zone=perip:10m;
    # 针对域名,进行连接数限制
    limit_conn_zone $server_name zone=perserver:10m;
    limit_conn_log_level error;
    limit_conn_status 503;

    server {
        # 每个IP仅允许发起10个连接
        limit_conn perip 10;
        # 每个域名仅允许发起100个连接
        limit_conn perserver 100;
    }
}

桶算法

漏桶算法与令牌桶算法的区别在于:

  • 漏桶算法能够强行限制数据的传输速率。
  • 令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输。

在某些情况下,漏桶算法不能够有效地使用网络资源。因为漏桶的漏出速率是固定的,所以即使网络中没有发生拥塞,漏桶算法也不能使某一个单独的数据流达到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。通常,漏桶算法与令牌桶算法结合起来为网络流量提供更高效的控制。

令牌桶不一定比漏洞好,她们使用场景不一样。令牌桶可以用来保护自己,主要用来对调用者频率进行限流,为的是让自己不被打垮。所以如果自己本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制),那么实际处理速率可以超过配置的限制。

而漏桶算法,这是用来保护他人,也就是保护他所调用的系统。主要场景是,当调用的第三方系统本身没有保护机制,或者有流量限制的时候,我们的调用速度不能超过他的限制,由于我们不能更改第三方系统,所以只有在主调方控制。这个时候,即使流量突发,也必须舍弃。因为消费能力是第三方决定的。