Nginx学习笔记

首先提醒一下,国内绝大多数程序员都把Nginx的读音搞错了(甚至包括很多国外的程序员),Nginx官方推荐的正确的读音应该是:Engine-X,而不是En-jingks.

Nginx分为主进程(master process)和工作进程(worker process),每个进程中只有一个线程(也可以配置线程池),通过IO多路复用(底层使用epoll/kqueue等技术)和事件循环达到高并发(这点跟Node.js比较相似)。主进程负责总体协调工作,比如在配置文件更新后重新应用配置、协调哪个worker process应该退役等等。工作进程的个数一般设置为CPU的个数。

还有个Nginx Plus,它是Nginx背后的商业公司,提供比Nginx开源版更多功能的Nginx软件,以及一些有关Nginx的技术支持服务,Nginx和Nginx Plus的对比在这里。

安装Nginx

通过Yum安装

首先安装epel-release源:

sudo yum install epel-release

然后安装Nginx:

sudo yum install nginx

最后启动Nginx:

sudo systemctl start nginx

查看安装之后的文件路径:

 rpm -ql nginx

在笔者的CentOS/7虚拟机上,Nginx的配置文件位于/etc/nginx/nginx.conf,默认静态资源目录为/usr/share/nginx/html/

对于不同的操作系统或安装方式,在安装完Nginx之后,配置文件放置的位置可能不一样,因此如果一个地方找不到,不妨试试其他目录。以下是几个比较常见的地方:

  • /etc/nginx/nginx.conf
  • /usr/local/nginx/conf/nginx.conf
  • /usr/local/etc/nginx/nginx.conf

默认静态资源文件目录:

  • /usr/share/nginx/html

通过Docker安装

Docker官网提供了Nginx镜像,运行Docker:

$ sudo docker run -p 8081:80  -d nginx

如果要对外提供静态资源文件:

$ sudo docker run -p 8081:80 -v /some/content:/usr/share/nginx/html:ro -d nginx

此时,将本机静态资源文件夹/some/content映射成了nginx的默认静态资源文件夹,然后将Docker的80端口映射到了宿主机的8081端口,此时访问http://localhost:8081/便可以访问宿主机中/some/content/下的静态文件了。

当然,要指定静态资源文件,更好的方式是通过Dockerfile将资源拷贝到Docker容器中,通常前端项目便可以通过这种方式构建Docker镜像:

FROM nginx
COPY /some/content /usr/share/nginx/html

另外,如果要定制化Nginx的配置,也可以通过Dockerfile:

FROM nginx
COPY nginx.conf /etc/nginx/nginx.conf

以上Dockerfile将宿主机的nginx.conf文件拷贝到Docker容器中。

更多安装方式请参考Nginx官网。

Nginx命令行

可以通过发信号的方式对Nginx进行控制,发信号有两种方式,一种是通过nginx命令,一种是通过kill,详细请参考官网,这里列出通过该nginx控制Nginx的功能,命令格式:

nginx -s 

其中,SIGNAL取值和功能如下:

  • quit – 安全地关闭
  • reload – 重新加载配置文件
  • reopen – 重新打开日志文件
  • stop – 直接关闭

配置文件

Nginx的主配置文件nginx.conf通过上下文context组织结构,context中可以包含配置项directive,而directive又可以反过来包含context

Nginx包含以下顶层的context

  • events - 通用的与连接相关的配置
  • http - http流量相关配置
  • mail - 邮件流量相关配置
  • stream - TCP/UDP流量相关配置

位于顶层context之外的directive也被称为在main context中。

Virtual Server(Server Block)

在每个流量配置context中(http,mailstream),都可以通过server配置多个虚拟服务器(Virtual Server),server下的配置基于context的不同而不同。比如对于httpserver下可以配置多个location来处理不同的URL,而对于mailstreamserver中则可以配置端口或者Socket。

示例配置文件

user nobody; # a directive in the 'main' context

events {
    # configuration of connection processing
}

http {
    # Configuration specific to HTTP and affecting all virtual servers  

    server {
        # configuration of HTTP virtual server 1       
        location /one {
            # configuration for processing URIs starting with '/one'
        }
        location /two {
            # configuration for processing URIs starting with '/two'
        }
    } 
    
    server {
        # configuration of HTTP virtual server 2
    }
}

stream {
    # Configuration specific to TCP/UDP and affecting all virtual servers
    server {
        # configuration of TCP virtual server 1 
    }
}

配置继承

有些directive可以位于多种context中, 此时便形成了一种继承关系,即子context将继承父context中的directive配置,当然子context也可以通过显式配置的方式覆盖继承自父context的配置。

另外,主配置文件nginx.conf可以通过include来包含其他配置文件,一个常用的实践是nginx.conf只负责配置nginx本身,而将server相关的配置放到各自的配置文件中,然后在nginx.conf文件中include这些文件。

虚拟服务器选择

Nginx处理HTTP请求原理:先根据server中的listenserver_name等判断该请求应该由哪个server来处理,然后通过匹配server下的location来决定应该由哪个location来处理,服务器配置示例如下:

server {
    listen      80;
    server_name example.org www.example.org;
    ...
}

需要注意一下几点:

  • 如果没有listen,那么Nginx默认监听所有网卡的80端口(以root启动nginx),或者默认监听所有网卡的8000端口(非root启动)
  • server_name的前后可以使用通配符*来匹配任意字符,但是*不能出现在域名中间
  • 可以使用~server_name进行正则匹配。

Nginx通过将server_name与请求中的Host头信息进行匹配,通过以下顺序,第一个满足匹配条件的胜出:

  • 精确匹配
  • *开头的最长server_name
  • *结尾的最长server_name
  • 第一个匹配的正则表达式
  • 如果以上都没有匹配到,则匹配标有default_server的server
  • 如果以上都没有匹配到,则选择第一个server

Location匹配

在Nginx决定了由哪个server处理请求之后,将进一步将请求URL与该server中的各个location匹配,以最终决定由哪个location来处理该请求。

location分为两

  • 前缀字符串,示例:
location /some/path/ {
    ...
}
  • 正则表达式,示例:
location ~ \.html? {
    ...
}

匹配规则如下:

  • 先将请求URL与所有location进行前缀匹配
  • 如果某个前缀命中的的location=修饰符,则选择之并停止匹配
  • 如果最长前缀的location^~修饰符,则停止匹配,并选择之
  • 保存最长的前缀匹配location
  • 进行正则表达式匹配,根据location出现的顺序进行匹配
  • 找到第一个匹配成功的正则表达式location,选择之,停止匹配(请注意,只要有正则表达式匹配上,那么先前保存的最长前缀匹配将失效)
  • 如果没有匹配上的正则表达式,那么匹配先前保存的最长前缀匹配

神奇的斜杠/

请参考笔者另一篇文章。

Location配置

location中可以有一下配置:

  • root:指定静态资源服务目录
  • return:直接返回一个http状态码,默认情况下nginx在返回状态码的同时会有默认的response body,比如:
location /wrong/url {
    return 404;
}

也可以返回301并重定向到指定URL:

location /permanently/moved/url {
    return 301 http://www.example.com/moved/here;
}
  • index:当location时一个目录时,设置该目录默认的index文件
  • try_files:依次访问列表中的资源,如果都没有成功则返回最后的资源:
location / {
    try_files $uri $uri.html $uri/ /fallback/index.html;
}
  • rewrite:对URL进行重写,比如通过301重定向,不过要重定向用return更高效
  • error_page:对应访问资源所得的的http状态码,设置应该返回的错误页面:
    error_page 404 /another/whoops.html;

此时并不会有浏览器端的redirect,而是直接将错误页面内容随着404返回(内部redirect,即如果/another/whoops.html是一个location,那么该location指向的资源将返回)。

  • proxy_pass:设置需要代理的URL

内部重定向

  • requests redirected by the error_page, index, random_index, and try_files directives;
  • requests redirected by the “X-Accel-Redirect” response header field from an upstream server;
  • subrequests formed by the “include virtual” command of the ngx_http_ssi_module module, by thengx_http_addition_module module directives, and by auth_request and mirror directives;
  • requests changed by the rewrite directive.

服务静态文件

先将静态资源拷贝到/data/www目录下,然后在http中配置:

server {
    location / {
        root /data/www;
    }
}

此时访问http://localhost/,如果/data/www下有index.html文件,那么将直接返回index.html文件内容。

另外,还可以通过index直接指定默认的index文件:

location / {
    root /data/www;
    index index.html index.php;
}

当前很多单页面应用采用了HTML5的History API,在使用Nginx时,由于所有的URL都将被Nginx处理,但是单页面有只有一个页面,也即所有URL都将路由到同一个页面,比如Index.html,再有浏览器根据URL做客户端路由。此时可以通过try_files对Nginx进行配置:

server {
  ...
  location / {
    try_files $uri $uri/ /index.html;
  }
}

在上例中,Nginx会先尝试访问原URL资源,如果资源不存在则返回index.html。

try_files的工作机制是会依次访问参数中的资源直到正常返回,需要注意,最后的参数资源必须存在,不然nginx将报错。更多详情请参考Nginx官网或这里。

负载均衡

首先在http中定义多台机器组,组名为backend:

    upstream backend {
        server backend1.example.com;
        server backend2.example.com;
        server 192.0.0.1 backup;
    }

然后就可以在server中引用backend了,完整例子:

http {
    upstream backend {
        server backend1.example.com;
        server backend2.example.com;
        server 192.0.0.1 backup;
    }
    
    server {
        location / {
            proxy_pass http://backend;
        }
    }
}

配置负载均衡方式

upstream中可以配置以哪种方式分发请求:

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

Nginx有以下方式分发请求:

  • Round Robin(默认) - 依次轮训
  • Least Connection - 连接最少的节点胜
  • IP Hash - 通过IP地址的hash值进行路由,可以保证来源相同的请求总是路由到同一个节点,进而支持sticky session
  • 通用Hash - 通过自定义算法计算Hash值来决定路由

每种方式都有其自定义的配置,更多详情请参考Nginx官网。

Health Check

如果upstream服务器返回不正常,那么Nginx将在一段时间内不再向该服务代理请求:

upstream backend {
    server backend1.example.com;
    server backend2.example.com max_fails=3 fail_timeout=30s;
    server backend3.example.com max_fails=2;
}

上例中,对于backend2.example.com,如果超过了3(max_fails)次返回失败,那么Nginx将在30秒内(fail_timeout)不再路由到该节点,30秒后再次启用该节点。

默认情况下,max_fails=1,fail_timeout=10s。

配置HTTPS

server {
    listen              443 ssl;
    server_name         www.example.com;
    ssl_certificate     www.example.com.crt;
    ssl_certificate_key www.example.com.key;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2; #可选
    ssl_ciphers         HIGH:!aNULL:!MD5; #可选
    #...
}

对于wildcard证书,可以将证书放在server块之外:

ssl_certificate     common.crt;
ssl_certificate_key common.key;

server {
    listen          443 ssl;
    server_name     www.example.com;
    #...
}

server {
    listen          443 ssl;
    server_name     www.example.org;
    #...
}

Basic Authentication

http {
    server {
        listen 192.168.1.23:8080;
        root   /usr/share/nginx/html;

        location /api {
            api;
            satisfy all;

            deny  192.168.1.2;
            allow 192.168.1.1/24;
            allow 127.0.0.1;
            deny  all;

            auth_basic           “Administrator’s area;
            auth_basic_user_file /etc/apache2/.htpasswd; 
        }
    }
}

更多详情,参考这里。

最佳实践

设置worker_processes

通常的做法是将worker_processes设置成与CPU的数量相同,也可以设置为auto让Nginx自动为你决定。

worker_processes auto; 

停用access_log

启动access_log后Nginx记录每一次请求,这将增加磁盘空间并给Nginx带来额外负担,如果你确定不需要access_log,可以:

access_log off;

access_log可以指定格式,但是error_log不能。

去掉Nginx版本号

server_tokens off; #为了安全

基于server配置gzip

配置gzip是把双刃剑,不配吧数据量太大,配了吧有安全风险。因此推荐的实践是只针对静态资源文件使用gzip,并且基于server单独配置:

server {
    listen         80;
    server_name    example1.com;
    gzip           on;
    gzip_types text/html text/css image/jpg image/jpeg image/png image/svg;
}

server {
    listen         443 ssl;
    server_name    example2.com;
    gzip           off;
}

注意add_header

add_header用于向最终返回给客户端的response中添加HTTP Header信息,需要注意的是add_header并不享受Nginx的继承机制,意味着如果子context中有add_header,那么它将覆盖所有的父context中的add_header配置。比如,在http中配置了3个add_header,然后在server中配置了1个add_header,那么server中的add_header会将http中的所有3个add_header给覆盖掉。

防止加入iframe

某些钓鱼网站会通过iframe的方式将你的网站加入钓鱼网站,此时我们可以通过Nginx配置声明自己的网站不应该被放入iframe中,在server中配置:

add_header X-Frame-Options DENY;

启用XSS过滤器

以下配置中,Nginx会通知浏览器启用XSS过滤器,虽然对于多数浏览器来说这个是默认设置:

add_header X-XSS-Protection "1; mode=block";

调优

  • Nginx中,当使用sendfile函数时,TCP_NOPUSH才起作用,因为在sendfile时,Nginx会要求发送某些信息来预先解释数据,这些信息其实就是报头内容,典型情况下报头很小,而且套接字上设置了TCP_NODELAY。有报头的包将被立即传输,在某些情况下(取决于内部的包计数器),因为这个包成功地被对方收到后需要请求对方确认。这样,大量数据的传输就会被推迟而且产生了不必要的网络流量交换。而通过设置TCP_NOPUSH=on,表示将所有HTTP的header一次性发出去,参考这里。
  • Nginx的TCP_NODELAY只有在配置长连接时才起作用,因为长连接可能引起小包的阻塞,配置TCP_NODELAY可以避免该阻塞,参考这里
  • Use the tcp_nopush directive together with the sendfile on;directive. This enables NGINX to send HTTP response headers in one packet right after the chunk of data has been obtained by sendfile().
  • 在 nginx 中,tcp_nopush 配置和 tcp_nodelay “互斥”。
  • 默认情况下,nginx已经自动开启了对client连接的keep alive支持(同时client发送的HTTP请求要求keep alive)。
  • 增大TCP的listen queue:sudo sysctl -w net.core.somaxconn=4096或者永久修改/etc/sysctl.confnet.core.somaxconn = 4096,然后修改Nginx:
server {
    listen 80 backlog=4096;
    # ...
}
  • 默认nginx访问后端都是用的短连接(HTTP1.0),一个请求来了,Nginx 新开一个端口和后端建立连接,后端执行完毕后主动关闭该链接)。
  • location值的最后含有斜杠,比如/hello/,然后该location有设置的proxy_pass,那么当客户端访问不带最后斜杠的/hello时,Nginx将301重定向到带有斜杠的/hello/,参考这里。
  • 在代理后端(比如tomcat)时,为了减少TCP的time_wait连接,通常需要在upstream中设置keepalive,其表示缓存在nginx中的链接数,下次可以直接用。另外,还需要设置HTTP的keep-alive,综合起来:
upstream http_backend {
    server 127.0.0.1:8080;

    keepalive 16;
}

server {
    ...

    location /http/ {
        proxy_pass http://http_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection ""; #对于HTTP 1.1 不设置Connection,默认持久连接
        ...
    }
}

  • worker_rlimit_nofiles vs worker_connections

worker_rlimit_nofiles adjusts system limit on number of open
files in nginx worker, while worker_connections is number of
connections nginx will allow.

System limit on number of open files must be larger than number of
worker_connections as any connection opens at least one file
(usually two - connection socket and either backend connection
socket or static file on disk).


root和alias的区别

  • root不做替换,而是直接将location添加到root的末尾
  • alias会做替换,即将location的值替换成root的值
location /static/ {
    root /var/www/app/static/;
    autoindex off;
}

以上匹配结果为:/var/www/app/static/static

location /static/ {
    alias /var/www/app/static/;
    autoindex off;
}

以上匹配结果为:/var/www/app/static,参考这里。


proxy_pass末尾的斜杠问题

  • 参考官网和笔者另一篇文章。

HTTP 重定向到HTTPS

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 301 https://$host$request_uri;
}

你可能感兴趣的:(Nginx学习笔记)