nginx经验汇总

2021-06-04 fishedee 后端

1 概述

nginx经验汇总

2 安装与部署

2.1 安装

brew install nginx

mac 安装方式,配置目录在/usr/local/etc/nginx

sudo apt-get install nginx

ubuntu 安装方式,配置目录在/etc/nginx

2.2 启动与关闭

# 启动
sudo nginx

# 刷新配置
sudo nginx -s reload

# 停止
sudo nginx -s stop

mac的方式

# 启动
sudo service nginx start

# 刷新配置
sudo service nginx reload

# 停止
sudo service nginx stop

ubuntu的方式

2.3 静态文件

server{
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    root /var/www/OfficialSite;
    index index.html;

    location / {
        index index.html;
        try_files $uri $uri/ =404;
        add_header Cache-Control "private, no-cache";
    }
}

静态文件可以说是入门用法了,直接用try_files就可以了。另外,默认服务器,可以用default_server,和任意匹配server_name的_指令。

你可以根据需要自行调整Cache-Control头信息的值。这里是一些常用的值:

"public, max-age=3600":允许公共缓存,缓存时间为1小时。
"private, no-cache":禁止公共缓存,但允许私有缓存,需要每次向服务器验证缓存是否过期。
"no-store":禁用缓存,每次都要从服务器请求资源。
"no-cache":允许缓存,但需要从服务器验证缓存是否过期。

add_header Cache-Control "public, max-age=3600";  # 设置缓存时间为1小时
add_header Cache-Control "private, no-cache"; #禁止公共缓存,但允许私有缓存,需要每次向服务器验证缓存是否过期。

注意Cache-Control的写法,来自ChatGPT

2.4 docker安装

curl -sSL https://raw.githubusercontent.com/bitnami/containers/main/bitnami/nginx/docker-compose.yml > docker-compose.yml

下载docker-compose.yml,看这里

# Copyright VMware, Inc.
# SPDX-License-Identifier: APACHE-2.0

version: '2'

services:
  nginx:
    image: docker.io/bitnami/nginx:1.25
    restart: always
    volumes:
      - C:\Users\fishe\Util\nginx\conf\mylocalhost.conf:/opt/bitnami/nginx/conf/server_blocks/mylocalhost.conf
    ports:
      - '80:80'

将配置文件进行映射,并配置好端口

upstream erp {
    server 192.168.1.8:8000;
    keepalive 8;
}

server {
    listen 80 default;
    listen [::]:80;

    server_name mylocalhost.com;

    location / {
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Connection "";
        proxy_pass http://erp;
    }
}

注意,ip地址不能写127.0.0.1,因为nginx运行在docker环境中

docker compose up -d

3 日志输出

log_format timed_combined '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" ' '$request_time $upstream_response_time';
access_log /var/log/nginx/access.log timed_combined;
error_log /var/log/nginx/error.log;

调优的第一步是获取性能输出文件,找出瓶颈在哪里。添加以上配置到nginx.conf文件,日志中就会输出请求的总处理时间(request_time),以及后端的cgi的处理时间(upstream_response_time)

4 压缩文件

gzip on;
gzip_disable "msie6";

gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

添加以上配置到nginx.conf,以打开gzip压缩,对于静态文件,和大json文件而言,这个方法很有效

5 连接数量

ulimit -n

在命令行中,输入这一句获取单个进程的最大打开文件数量。由于在linux中,这个数值为1024,这意味着nginx最多只能同时处理1024个请求。

* soft noproc 10240
* hard noproc 10240
* soft nofile 10240
* hard nofile 10240

在/etc/security/limits.conf文件添加以上命令即可让所有用户的的单个进程数量提高到10240。注意,该数值应该与机器的配置相关。

6 长连接

即使将最大连接数量提高了,在大并发的环境下,nginx也可能因为连接文件有限而被迫等待。这可能是因为nginx与后端cgi的连接是短连接的方式,每处理完一个请求就断开,导致大量的连接不断使用新建和丢弃,大大浪费了连接资源。

netstat -an | awk '/^tcp/ {++y[$NF]} END {for(w in y) print w, y[w]}'
wrk -t8 -c100 -d30s "http://www.baidu.com"
netstat -an | awk '/^tcp/ {++y[$NF]} END {for(w in y) print w, y[w]}'

为了看一下是不是由于这个问题导致的,我们先执行netstat查看TIME_WAIT状态的socket资源有多少,然后用wrk长连接来压测nginx,最后再次用netstat来查看TIME_WAIT状态的socket资源有多少。如果压测以后,TIME_WAIT状态大幅飙升,这就意味着有很多socket资源被短连接浪费了。

upstream yinghao {
        server localhost:9595;
        keepalive 100;
}

server {
        listen 80;
        listen [::]:80;
        server_name yinghao.fishedee.com;

        location / {
                proxy_http_version 1.1;
                proxy_set_header Connection "";
                proxy_pass http://yinghao;
        }
}

解决办法是在nginx的sites-enabled下建立upstream的连接池,然后在proxy_pass来指定连接池来获取socket资源,注意,为了让长连接生效,需要配置http_version为1.1,以及配置正确的Connection的Header。

db.SetMaxIdleConns(config.MaxIdleConnection)

当然,长连接不仅仅是在nginx与cgi之间,也有可能是cgi与数据库之间。所以cgi与数据库的连接也最好使用长连接的方式,在golang写的cgi中,配置好MaxIdleConnection为一个非0值就可以了,例如是100。

import (
    "github.com/garyburd/redigo/redis"
)

func NewRedisPool(redisURL string) *redis.Pool {
    return &redis.Pool{
        MaxIdle:     redisMaxIdle,
        Dial: func() (redis.Conn, error) {
            c, err := redis.DialURL(redisURL)
            if err != nil {
                return nil, fmt.Errorf("redis connection error: %s", err)
            }
            //验证redis密码
            if _, authErr := c.Do("AUTH", RedisPassword); authErr != nil {
                return nil, fmt.Errorf("redis auth password error: %s", authErr)
            }
            return c, err
        },
        TestOnBorrow: func(c redis.Conn, t time.Time) error {
            _, err := c.Do("PING")
            if err != nil {
                return fmt.Errorf("ping redis error: %s", err)
            }
            return nil
        },
    }
}

同样的,在golang连接redis的时候,也要使用redis连接池的方式,配置好MaxIdle和IdleTimeout即可。

type Transport struct {
    // DisableKeepAlives, if true, disables HTTP keep-alives and
    // will only use the connection to the server for a single
    // HTTP request.
    //
    // This is unrelated to the similarly named TCP keep-alives.
    DisableKeepAlives bool

    // MaxIdleConns controls the maximum number of idle (keep-alive)
    // connections across all hosts. Zero means no limit.
    MaxIdleConns int // Go 1.7

    // MaxIdleConnsPerHost, if non-zero, controls the maximum idle
    // (keep-alive) connections to keep per-host. If zero,
    // DefaultMaxIdleConnsPerHost is used.
    MaxIdleConnsPerHost int

    // MaxConnsPerHost optionally limits the total number of
    // connections per host, including connections in the dialing,
    // active, and idle states. On limit violation, dials will block.
    //
    // Zero means no limit.
    //
    // For HTTP/2, this currently only controls the number of new
    // connections being created at a time, instead of the total
    // number. In practice, hosts using HTTP/2 only have about one
    // idle connection, though.
    MaxConnsPerHost int // Go 1.11

    // IdleConnTimeout is the maximum amount of time an idle
    // (keep-alive) connection will remain idle before closing
    // itself.
    // Zero means no limit.
    IdleConnTimeout time.Duration // Go 1.7
}

同样地,如果golang的cgi与其他的服务器连接,也需要配置好MaxIdleConns,MaxIdleConnsPerHost,MaxConnsPerHost等参数,以最大限度地复用原有的链接,既不会不断频繁地创建连接,也不会因为并发数太多而导致等待。

7 HTTPS

7.1 公认证书

upstream backendserver {
    server localhost:9598;
    keepalive 8;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    ssl_certificate   /home/folder/abc.com.cer;
    ssl_certificate_key  /home/folder/abc.com.key;
    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;

    server_name abc.com;

    location / {
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Connection "";
        proxy_pass http://backendserver;
    }
}

server {
    listen 80;
    listen [::]:80;

    server_name abc.com;

    #rewrite ^(.*) https://abc.com$1 permanent;
    return 301 https://$host$request_uri;
}

对于公认的证书,可以使用以上的方法来配置nginx。注意将ssl on的配置去除,新版本的nginx不需要这个配置。

注意,证书更改后需要停止服务器重启才能生效,不能只是reload配置。

7.2 自签证书

可以看这里。自签证书主要是为了本地的测试开发用,方便,而且可以用任意的域名。

# 生成RSA密匙对
openssl genrsa -des3 -out domain.key 1024

# 提取出私有钥匙中没密码包含的版本
openssl rsa -in domain.key -out domain_nopass.key

# 生成SSL证书请求
openssl req -new -key domain.key -out domain.csr

# 生成SSL证书链
openssl x509 -req -days 365 -in domain.csr -signkey domain.key -out domain.crt

# 查看SSL证书的有效期
openssl x509 -in domain.cer -noout -dates

一个SSL证书,需要包括几个步骤:

  • 生成RSA钥匙对,包括公有钥匙与私有钥匙(默认有一层密码做外部加密),.key格式
  • 根据RSA钥匙对生成服务器用的无密码私有钥匙,.key格式
  • 根据RSA钥匙对生成SSL证书请求,.csr格式,包括公有钥匙,加密算法,请求者签名。这个文件一般是申请者提交给CA的
  • 根据RSA钥匙对生成SSL证书链,.crt格式,服务器可用的公有整数。这个文件一般是CA颁发给申请者的。
server{
    listen 443 ssl;

    server_name testclient.com;

    ssl_certificate /usr/local/etc/nginx/conf/server.crt;
    ssl_certificate_key /usr/local/etc/nginx/conf/server.key;
    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Connection "";
        proxy_pass http://localhost:9596;
    }
}

nginx配置和普通的公认证书一样。

8 WebSocket

server {
    listen   80;

    server_name push.abc.com;

    location / {
            proxy_pass http://localhost:8080;
            # WebScoket Support
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
    }
}

对于代理后端的WebSocket应用,我们需要以上这样的配置。

9 FAQ

9.1 Permission denied

2021/06/05 20:38:18 [crit] 27810#0: *28 stat() "/var/www/abc/" failed (13: Permission denied), client: 14.157.94.16, server: _, request: "GET / HTTP/1.1", host: "47.118.30.60"

当nginx的错误日志报出Permission denied错误时,你先试试那个server的问题。然后一路用sudo -u user stat xxxx来测试是哪个目录出的问题。

另外,目录的问题出错了,一般是因为缺少x权限导致的。解决方法是先将nginx的执行用户(一般为nginx用户)加入到所在目录的用户组,然后对所有目录的全路径加入x权限。例如,对/var加x权限,对/var/www/加x权限,对/var/www/abc/加x权限。最后还有一种隐蔽的问题,当/var/www/abc/是一个软链,实际指向的是/home/abc/kk目录时,你就要记得对/home加入x权限,对/home/abc加入x权限,对/home/abc/kk目录加入x权限。

参考这里,和这里

10 总结

相关文章