阅读 1969

Nginx 无痛入门指南

在这个新的专题当中,笔者整理了Nginx的基本内容,以及在虚拟机环境下模拟部署高可用集群HA。我们主要从以下内容入手:

  1. Nginx是什么?它是用来做什么的?
  2. Nginx在Linux系统下的安装,包括反向代理,负载均衡,动静分离,配置高可用集群。

Nginx简介

Nginx (engine x)是一个高性能的HTTP和反向代理web服务器,同时也提供IMAP/POP3/SMTP服务。它的特点是:占用内存小,并发能力强

Nginx可以作为静态的web服务器,同时支持CGI协议的动态语言,比如perl,php。而JavaWeb项目则需要与tomcat配合完成。Nginx专门为了优化而开发,性能是最重要的考量。

Nginx支持热部署。可以在不间断服务的情况下,对软件版本进行升级。

反向代理

在介绍反向代理之前,首先介绍,什么是正向代理?

我们日常所使用的虚拟专用网络就属于正向代理。比如国内的客户想要直接访问Google官网是无法连接的,但如果该用户可以访问某服务器A,而A可以访问Google官网,那么客户可以将此服务器A设置为代理服务器,借助A服务器请求获得Google的响应报文,再转发给用户。此时这个服务器A称之为正向代理服务器。

也就是说正向代理,需要用户主动配置代理服务器。

而反向代理,客户是对代理无感知的。客户只需要向公开的URL向代理服务器发送请求,代理服务器再选择目标服务器处理请求并返回数据,如下图所示。用户并不关心是哪个具体的目标服务器替他完成了服务,虚线右侧的目标服务器对客户来说是透明的。

负载均衡

如果一辆货车很难拉动一大批货物,那就再叫上几辆车。

客服端发送多个请求到服务器,服务器处理请求,有些请求需要与数据库交互,直到服务器处理完毕之后,再将结果返回给客户端。

这种架构模式对于早期并发请求较少的情况下还是比较适合的,且维护成本也比较低。但是我们已经都知道:摩尔定律逐渐失效,硬件的性能提升已经跟不上我们实际的性能要求了。比如天猫双十一,其瞬时访问量都是非常庞大的,我们即使将单节点升级到现在的顶级配置也很难应对(这还没考虑到升级设备的成本)。因此现在的服务都逐渐采用集群方式。

所谓负载均衡,就是反向代理服务器将大批量的请求分散到集群的节点当中,以减少单点服务器的压力,避免服务器崩溃的现象。

动静分离

为了加快网站的解析速度,我们可以把动态页面和静态页面分散到不同的服务器进行解析,加快解析速度,降低单个服务器的压力。

Nginx安装

我们的主场仍旧是Linux系统。我们可以进入到nginx官网,下载对应的版本,然后发送到我们的云服务器/虚拟机当中安装,但是安装Nginx需要解决大量的依赖问题,在此不推荐用此方式。

笔者的虚拟机是CentOS 7.8 x86_64版本,在这里通过简单配置yum源的方式来安装(和笔者安装Docker的过程比较类似):

如果之前没有安装yum-utils,则需要先安装它:

$ yum install yum-utils
复制代码

进入到/etc/yum.reops.d/目录下,创建一个nginx的下载源配置文件:

$ vim nginx.repo
复制代码

在该文件中添加以下配置:

[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true

[nginx-mainline]
name=nginx mainline repo
baseurl=http://nginx.org/packages/mainline/centos/$releasever/$basearch/
gpgcheck=1
enabled=0
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
复制代码

配置完毕后,就可以使用yum命令安装nginx服务器了。

#centOS7默认是没有nginx安装包的。
$ yum search nginx
$ yum install nginx
复制代码

我们可以使用find命令查看nginx的安装路径:

$ find / | grep nginx.conf
复制代码

此外,Nginx官网提供了RHEL/CentOS, Debian, Ubuntu等快捷安装方式。

📦Nginx提供的官方安装教程(适用于Linux内核)

尝试启动Nginx

我们找到Nginx的启动脚本,然后启动Nginx服务(Nginx启动脚本在/usr/sbin目录下):

$ /usr/sbin/nginx
# 或者直接nginx也可以。
# 或者systemctl start nginx
# 查看进程中是否有nginx
$ ps -ef | grep nginx
复制代码

配置文件为/etc/nginx/nginx.conf,如果找不到nginx的配置路径,可以通过刚才的find命令搜索到。

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

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

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}
复制代码

通过观察该配置文件的最后一行,笔者注意到此下载版本中,nginx的配置被分散到了两个路径下:/etc/nginx/conf.d/*.conf

我们再去浏览此/cet.nginx/conf.d/目录下,初始只有一个文件(/etc/nginx/conf.d/default.conf),通过listen能够观察到Nginx的默认启动端口是80。

server {
    listen       80;
    server_name  localhost;

    #charset koi8-r;
    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}
复制代码

我们在浏览器中输入对应的url(如果监听端口默认为80,我们可以不主动地输入端口号)来访问nginx服务器,其中hadoop102是笔者已经配置好的主机名。

hadoop102
复制代码

当看到这个页面时,就说明我们在虚拟机中启动的Nginx服务器成功了。

配置Linux防火墙规则

在Linux的firewalld防火墙启动状态下,如果不进行任何配置,我们是无法访问到80端口的。我们首先使用firewall命令查看目前开放的端口:

$ firewall-cmd --list-all
复制代码

我们设置80端口号是开放的:

$ firewall-cmd --add-service=http --permanent
$ firewall-cmd --add-port=80/tcp --permanent
#重启firewall-cmd --reload
$ firewall-cmd --reload
复制代码

Nginx常用命令

查看Nginx的版本号:

$ nginx -v
#笔者的安装版本是:
#nginx version: nginx/1.18.0
复制代码

启动Nginx服务:

$ nginx
复制代码

停止Nginx服务:

$ nginx -s stop
复制代码

重加载Nginx(我们在开篇就提到过,Nginx支持热部署,因此只需要重新刷新Nginx服务就可以):

$ nginx -s reload
复制代码

Nginx配置文件

我们首先要对下面三个路径比较清晰:

启动脚本 -> /usr/sbin/nginx
主配置文件 -> /etc/nginx/nginx.conf
副配置文件-> /etc/nginx/conf.d/default.conf
复制代码

Nginx配置文件主要由三大块构成:全局块,events块,http块。其中,http块为核心,具体又可细分。:

全局块
events块
http块
  ┗ http全局块
  ┗ server块
      ┗ 全局server块
      ┗ location块
复制代码

全局块

作用域为文件开始到events块之前的部分。这里的配置会影响到nginx服务器整体的运行

其中,work_process表示可并发处理的请求数量。这个配置取决于机器实际的硬件/软件配置。

work_process 1
复制代码

events块

这里的配置会影响到Nginx服务器和用户的网络连接

其中,worker_connections表示最大的连接数,它会影响到Nginx的总体性能,所以应当灵活配置。

http块

这一块是配置的主要内容,http块还包含了两部分:http全局块,server块。其中nginx默认将server块单独迁移到了另外一个配置文件中。

http全局块包括引入文件(如引入server块所在的配置文件),MIME-TYPE定义,日志自定义,连接超时时间,单链接请求数上限等内容。

server块

server块和虚拟主机由密切的联系,而从客户的角度而言,虚拟主机和独立的主机是没有区别的。它主要是为了节省互联网服务器硬件的成本。

一个http块,可以包含多个server块。每个server块都相当于是一个虚拟主机。而每个server块又分为全局server块,和一个(或者多个)location块

下面代表其中一个虚拟主机:启动在本地,并开启80端口。

listen			80;
server_name		localhost;
复制代码

location块配置了请求url和资源的映射(我们在后续的实例中会进行更详细的配置),原配置文件中其实有大量有关location的注释内容,但是我们基本都可以推测出其大致功能。

location / {
	root html;
	index index.html index.htm
}
复制代码

配置反向代理

我们在Nginx简介中介绍过了反向代理的作用。我们主要依赖http block -> servr block ->location block来配置具体的反向代理。对于动态请求,我们常使用proxy_pass映射到指定主机号的对应端口,对于静态请求,我们也可以使用root将本机设为静态资源服务器,用于返回静态页面(详见配置动静分离的章节)。我们还需要对location的uri匹配部分做一个基本的了解,才能避免意外的404问题。

关于Location的配置指令

实际上location可以包含以下形式:

location [ = | ~ | ~* | ^~ ] uri{
	....
}
复制代码
  • =表示最严格的精确匹配。比如配置location如下:

    # www.nginxText.com/hi/a.html => 127.0.0.1:9998/hello/a.html
    location = /hi/a.html {
    	proxy_pass http://127.0.0.1:9998;
    }
    复制代码
  • ^~表示在正则匹配之前进行前缀匹配。

    # www.nginxText.com/hi/a.html => 127.0.0.1:9998/helloworld/a.html
    location ^~ /hi {
    	proxy_pass http:127.0.0.1:9998/helloworld
    }
    复制代码
  • ~表示请求的uri包含了区分大小写的正则匹配

    # www.nginxText.com/hello/a.html => 127.0.0.1:9998/hello/a.html
    location ~ /hello/ {
    	#不允许在proxy_pass配置uri
    	proxy_pass http:127.0.0.1:9998
    }
    复制代码
  • ~*表示请求的uri包含了不区分大小写的正则匹配。

    # www.nginxText.com/hello/cafe.jpg => 127.0.0.1:9998/hello/cafe.jpg
    # www.nginxText.com/hello/cafe.JPG => 127.0.0.1:9998/hello/cafe.jpg
    location ~* \.(jpg|png)$ {
    	#不允许在proxy_pass配置uri
    	proxy_pass http:127.0.0.1:9998
    }
    复制代码
  • 若不带任何符号,则表示在正则匹配之后进行前缀匹配。

匹配顺序

匹配顺序如下(图源):

(location `=` ) 
(location `完整路径` )  
(location `^~` 路径)  
(location `~`,`~*` 正则顺序) 
(location 部分起始路径) 
(location `/`)
复制代码

注意以下点:

  1. 前缀匹配遵循最长匹配原则。

  2. 在进行正则匹配(~~*)时,配置的proxy_pass不允许包含uri。否则会报错:

    "proxy_pass" cannot have URI part in location given by regular expression
    复制代码

有关更多的location匹配细则参考这里

反向代理一个Tomcat服务器

根据配置反向代理,我们主要想达到以下的效果:

打开浏览器,输入www.nginxtest.com访问我们的Nginx服务器,Nginx服务器将请求转发到虚拟机系统下的Tomcat服务器。

首先,进入到物理机(笔者的物理机是Window10系统)的C:\Windows\System32\drivers\etc目录下,修改hosts文件,在文件末尾追加以下行:

{虚拟机ip地址} www.nginxtest.com
复制代码

随后在浏览器中直接输入www.nginxtest.com,默认访问80端口,即可访问到虚拟机中启动的Nginx服务。

笔者在这里使用Docker启动一个tomcat容器,端口映射为0.0.0.0:10000->8080。现在要访问Tomcat服务,我们需要输入:

www.nginxtest.com:10000/
复制代码

我们希望仅输入www.nginxtest.com即可访问到Tomcat服务器,因此需要配置Nginx的反向代理服务。

我们打开Nginx配置文件,在server域中添加下面的location配置,表示我们将:

www.nginxtest.com/helloworld映射到127.0.0.1:10000/helloworld。(tomcat会默认返回/helloworld/index.jsp

location /helloworld {
	proxy_pass http://127.0.0.1:10000;
}
复制代码

随后刷新Nginx服务器:

$ nginx -s reload
复制代码

我们在浏览器输入www.nginxtest.com时,就可以让Nginx通过反向代理映射到虚拟机实际运行的Tomcat服务器了。

根据URI匹配不同的Tomcat服务器

通过配置反向代理,我们希望实现以下效果:

www.nginxtest.com/hi/* -> 127.0.0.1:10000/hi/*

www.nginxtest.com/hello/* ->127.0.0.1:9998/hello/*
复制代码

当路径为/hi/..时,将此请求转发到127.0.0.1:10000的对应的/hi/..下的资源;当路径为/hello/..时,将此请求转发到127.0.0.1:10000的对应的/hello/..下的资源;

笔者在这里利用Docker的Tomcat镜像生成了一个新的实例,端口映射为0.0.0.0:9998->8080

我们打开default.conf,进行如下配置:

location ~ /hi/ {
	proxy_pass http://127.0.0.1:10000;
}

location ~ /hello/ {
	proxy_pass http://127.0.0.1:9998;
}
复制代码

注意,这次的配置中多了一个~符号,表示正则匹配。这个配置告诉Nginx:所有包含/hi/的请求uri都会转发到10000端口的Tomcat服务器;所有包含/hello/的uri都会转发到9998端口的Tomcat服务器。注意每个配置项后面都要带上;符号。

配置负载均衡

我们想要实现一个这样的效果:Tomcat1(10000端口)和Tomcat2(9998端口)都具备/helloworld/b.html资源(为了观察到效果,可以在b.html中做下区分标记)。当客户端不断请求此资源的uri时,希望Nginx能够根据相应策略作负载均衡。

最基本的配置

打开/etc/nginx/nginx.conf主配置文件的http块下,配置动态服务器组:

#upstream 后面加上自己配置的服务器组名字。
upstream dynamic {
    server localhost:10000; # docker->tomcat
    server localhost:9998; # docker->tomcat
}
复制代码

保存退出,进入到/etc/nginx/conf.d/default.confserver块下,在proxy_pass项中配置服务器组名。

location = /helloworld/b.html {
	proxy_pass http://dynamic;
}
复制代码

这个配置是一个精确匹配。表示当访问/helloworld/b.html这个资源时,将由Nginx根据负载均衡策略决定访问Tomcat1,或者是Tomcat2。

通过nginx -s reload保存配置,然后在浏览器中输入:www.nginxTest/helloworld/b.html,并反复刷新,可以观察到每次的内容是不一样的,则证明Nginx的负载均衡策略生效了。

Nginx中的负载均衡策略

以上仅是最基本的复杂均衡配置,实际上要根据服务器的配置来采取不同的策略。Nginx支持6种负载均衡策略,但是有2种需要依赖第三方。在这里详细介绍常用的四种方式。

参数 方式
不做任何配置,默认轮询方式。
weight 基于轮询方式,配置server配置项的后面表示权重。
ip_hash 依据ip分配方式,解决session跨域问题。
least_conn 最少连接方式
fair(第三方) 响应时间方式
url_hash(第三方) 依据URL分配方式

轮询(默认)方式

轮询是upstream默认的负载均衡策略,请求时按照时间顺序分配到各服务器。轮询策略可配置以下参数:

参数 作用
fail_timeout 设置超时时间,与max_fails结合使用。
max_fails 设置在fail_timeout参数设置的时间内最大失败次数,如果在这个时间内,所有针对该服务器的请求都失败了,那么认为该服务器会被认为是停机(down)了。
fail_time 服务器会被认为停机的时间长度,默认为10s。
backup 标记该服务器为备用服务器。当主服务器停止时,请求会被发送到它这里。
down 标记服务器永久停机了。

轮询策略适用于各个服务器之间配置相当,无状态(不需要coookie,session)的服务。

权重方式

在不做任何配置的情况下,每个server的权重默认均为1。

权重越大的server,承担流量的概率也会越高。常用于服务器配置差距较大的情况。我们会让性能更好的服务器来承受更大的流量。

#upstream 后面加上自己配置的服务器组名字。
upstream dynamic {

	# 假定此服务器性能更好,则上调它的访问权重。
    server localhost:10000 weight=2; 
    
    # 如果20秒内有10个请求都失败了,则该server会被停用。
    server localhost:9998 max_fails=10 fail_timeout=20; 
}
复制代码

ip_hash方式

session用于记录客户<->服务器的一个会话,因此有状态的服务不能够直接跨服务器。我们使用ip_hash来基于客户端的ip进行分配,来保证同一个客户的请求只会发送到指定的服务器,来保证session会话状态。

#upstream 后面加上自己配置的服务器组名字。
upstream dynamic {
	#每个访客分配到固定的服务器中。
	ip_hash; 
	
	# ip_hash方式可以基于轮询一起使用。
    server localhost:10000 weight=2; 
    server localhost:9998 max_fails=10 fail_timeout=20; 
}
复制代码

注意以下几点:

  • 如果使用ip_hash,则不能再分配备用服务器backup(合情合理,因为即使备用的服务器也没有记录其它服务器的session信息,没有意义)。

  • 在Nginx版本1.3.1之前,不能在ip_hash策略中配置权重(weight)。

  • 当有服务器需要剔除,必须手动down掉。

least_conn方式

把请求转发给连接数较少的后端服务器。轮询算法是把请求平均的转发给各个后端,使它们的负载大致相同;但是,有些请求占用的时间很长,会导致其所在的后端负载较高。这种情况下,least_conn这种方式就可以达到更好的负载均衡效果。

#upstream 后面加上自己配置的服务器组名字。
upstream dynamic {
	#基于连接数分配。
	least_conn
	
    server localhost:10000 weight=2; 
    server localhost:9998 max_fails=10 fail_timeout=20; 
}
复制代码

适用于不同请求的处理时间差异较明显的情况。

配置动静分离

动静分离,就是将静态资源(图片,视频,css,js等文件)和动态资源(jsp,php)区别开。动态分离并不是为了单纯的将这两种资源区别开,而是使用Nginx处理静态资源,由Tomcat(或其它服务器)处理动态资源。

除此之外,通过配置反向代理和expires参数,我们可以将一些不会经常变动的静态资源直接发送到对方浏览器中保存起来。

用户下次通过浏览器再访问此资源时,浏览器只需要做一件事:在一定时间内(取决于expires),浏览器向原资源服务器发送简单请求,仅对比最后修改时间来判断此静态资源是否发生变化。

服务器正常情况下会发送两种状态码:

  1. 304 => 文件没有变动,浏览器直接使用缓存的资源。
  2. 200 => 文件发生变动,浏览器成功下载到新的文件。

目前的主流做法是,将所有的静态文件单独分离出来保存到一个单独的域名中。也有部分开发者选择将动态文件和静态文件混合在一起发布。

动静分离的配置

在动静分离之前,笔者先在虚拟机的/usr/images/目录下存放一些.jpg,.png等格式的图像文件,并配置本地作为静态资源的仓库。

配置方式一:前缀匹配

location ^~ /images {
	root /usr;
	autoindex on;
}
复制代码

由于我们直接使用本地文件夹,因此在这里配置的是root。因此所有前缀满足/images/...的资源都会映射到本机的/usr/iamges/...路径下。

当配置autoindex on;时,我们可以在浏览器中输入www.nginxTest.com/images/直接查看此路径下所有的文件。为了避免中文文件显示乱码,建议在server块中配置charset utf-8

配置方式二:正则匹配

另一种思路是碰到所有后缀名为.jpg, .JPG, .png, .PNG等文件的uri时,使用本机的静态资源库。由于这里不确定前缀名,这里将autoindex on;选项去掉。

location ~* \.(jpg|png|gif|jepg)$ {
	root /usr;
}
复制代码

如果静态资源存放在另一个服务器,同样可以使用proxy_pass进行反向代理。

设置expires缓存

设置expires比较简单,我们在location内部直接配置即可。语法如下:

expires 30s;   #缓存30秒
expires 30m;   #缓存30分钟   
expires 2h;    #缓存2小时
expires 30d;   #缓存30天
复制代码

我们直接在刚才的正则匹配中加入这段配置,刷新Nginx,在浏览器中输入uri,并查看response header

location ~* \.(jpg|png|gif|jepg)$ {
	#缓存1天
	expires 1d
	root /usr;
}
复制代码

笔者通过Postman工具查看,并标注了重点部分。

局域网*下搭建高可用集群

笔者出于学习目的,仅演示虚拟机环境下的局域网搭建一个简单的高可用集群(HA),而在实际部署环境中,我们至少要准备两台云服务器搭建VPC,还需要另购一个公网IP用作VIP(Virtual IP)。

在学习了Nginx之后,我们能够绘出这样的拓扑结构了:通过动静分离将静态资源单独保管到资源服务器中;通过负载均衡使不同的服务器之间能够协同工作......尤其是笔者刚刚学会使用Docker快速部署服务,感觉技能树一下点亮了不少。

如图展示了一个完全是围绕着一个Nginx服务器构建的星型拓扑结构,这要我们必须面临一个fatal的问题:如果Nginx本身宕机了,那么所有的请求就全部失效了。所以很明显,当前这种做法,是存在风险的。

高可用模式的配置

为了避免将所有的鸡蛋放到一个篮子里,最直观,最显著的方法就是:准备一个(或多个)Nginx的Backup(后备服务器),当Master因意外宕机的时候,Backup能够快速代替Master进行工作。

我们需要使用一款keepalived来实现Nginx的主备模式。同时还对用户隐藏细节,使用户能够按统一的接口来访问

在配置高可用模式之前

我们首先罗列一下配置高可用模式所需要的清单:

  1. 两台服务器(笔者使用两台虚拟机代替)
  2. 使用keepalived软件。
  3. 供keepalived进行检测目标Nginx是否运行的可执行脚本。
  4. 需要一个虚拟IP地址(用于对用户提供统一的访问)。
  5. 禁用SElinux,清除iptables规则,关闭防火墙

keepalived检测Nginx的流程

keepalived集群内部有多个Nginx服务器,在这个集群运作时,其中只有一个Nginx是处于工作状态,即Master(主机),其余的Nginx都处于Backup(候选)状态。每个主机内的keepalived监视本机的Nginx运行的状态。

不过无论是Master还是Backup在进行反向代理工作,对客户来说是不可见的。这主要通过提供一个统一的VIP(Virtual IP)来实现。所以无论是谁在运作,客户都只通过这一个VIP来请求服务。

keepalived基于arp广播模式工作,因此所有的keepalived处于一个局域网当中。(如果是多个云服务器互备则要配置在同一个虚拟私有云VPC内)

每个主机的keepalived和只本机的Nginx绑定。keepalived会通过一个脚本来检测本地的Nginx是否处于正常状态(或者称进行健康检查)。当keepalived检测到Nginx处于不可重启的状态中,则会将本身的keepalived进程一同中断(表示本机的"Nginx was died")。

集群中各个keepalived会进行心跳检测(通过组播形式实现)。其它的keepalived"感知"不到Master的keepalived(说明Master Nginx宕机了)时,便会从Backup中选出一个候补获取VIP的使用权,之后客户输入的路径将全部由此候选的Nginx负责。

对于实际工作的Nginx节点,我们是可以通过ip addr检查到网口是绑定了VIP的。而处于候选状态的节点,ip addr是没有绑定VIP的。

keepalived的配置可以有两种形式:

  1. 抢占式(默认),主节点出现故障之后,由备机工作。当主节点重新启动时,备机将重新处于候选状态。
  2. 非抢占式,主节点出现故障之后,由备机工作。当主节点重新启动时,备机继续工作。

配置另一个虚拟机的Nginx

我们之前都是在主机"hadoop102"中配置的Nginx反向代理/负载均衡/动静分离,所以在hadoop102的Nginx中,有很多配置都指向"localhost","127.0.0.1"。

而对于新的主机"hadoop101"而言,这些配置就要相应地替换成主机hadoop102或者它的ip地址了。我们直接拷贝之前hadoop102主机的配置文件,修正nginx.confdefault.conf的一些细节:

upstream dynamic{
    #由于这次我们需要通讯,所以要配置max_fails和fail_timeout。
	server hadoop102:10000 max_fails=10 fail_timeout=10s;
	server hadoop102:9998 max_fails=10 fail_timeout=10s;
}	
复制代码

default.conf中做以下修改,静态资源的目录不再位于本机,所以需要proxy_pass进行代理。

location ~* \.(jpg|png|gif|jepg)$ {
	expires 1d;
	proxy_pass http://hadoop102;
}
复制代码

正常情况下,静态资源服务器是会单独在另一个服务器中的,不过这里的hadoop102的Nginx既作为资源服务器,又作为keepalived集群的备用机。

在MASTER工作的情况下,处于BACKUP状态的Nginx也仍然正常工作。

安装keepalived

注意!!请谨慎阅读以下内容的每一处细节,因为稍有不慎就会导致keepalived无法正常运行。

其中keepalived有多种渠道安装,也可以直接使用yum源进行安装。两个服务器都需要安装此软件。

$ yum install keepalived -y
复制代码

我们可以使用find命令来查找到keepalived配置文件所在的目录(实际它在/etc/keepalived/目录下):

$ find / | grep keepalived.conf 
复制代码

除此之外,另外一个虚拟机也需要安装一个Nginx服务器,过程参见上文。

编写检测脚本

笔者在健康检测脚本中的设定是:keepalived在Nginx处于关闭状态时仍然会尝试再次开启Nginx服务,直到2秒后发现Nginx仍无法启动时才会判定为dead.

keepalived使用脚本来实现检测目标Nginx是否正常运作。当脚本检测到Master Nginx宕机时,要切换到Backup Nginx上。

#!/bin/bash
A=`ps -C nginx --no-header |wc -l`
if [ $A -eq 0 ];then
    systemctl start nginx
    sleep 2
    if [ `ps -C nginx --no-header |wc -l` -eq 0 ];then
        killall keepalived
    fi
fi
复制代码

❗编写脚本注意的问题:

  1. 命令模式输入set ff检查文件的编码格式。如果显示文件的编码格式为doc,则需要更改为set off=unix

  2. 这个脚本需要被设置为具备x权限,可以使用chmod a+x进行更改。

笔者将这个文件保存为/usr/local/src/nginx_check.sh。最好手动执行一遍这个脚本来排查问题,以免稍后的keepalived不生效。

配置keepalived【谨慎配置】

keepalived的配置文件为/etc/keepalived/keepalive.conf默认情况下有非常多的默认配置。

从这个实例来说,我们只需要用到以下三个部分global_defs(全局定义),vrrp_script(用于检测脚本,需要手动添加),vrrp_instance(配置虚拟IP)。

对于用不上,或者不清楚其功能的配置项,强烈建议全部删去,否则非常容易导致keepalived不生效。

我们首先对主节点Master为例进行配置:

在此,我们不需要通过Mail的形式收发消息。因此在global_defs部分中,我们只需要保留router_id项,来为为自己的主机命名。

# 配置此主机的主机名。相当于在配置/etc/hosts
global_defs {
   router_id r1
}
复制代码

原配置文件中没有vrrp_script选项,需要我们手动添加上去。script的路径是刚才创建脚本的绝对路径。可能会有差异,请仔细甄别。

# vrrp_script后面的名称可以自定义(此处为check),但是要和稍后track_script块配置对应。
vrrp_script check {
   script  "/usr/local/src/nginx_check.sh"
   interval 4
}
复制代码

❗注意,这个interval的数值要比脚本中的睡眠时间sleep要大,否则脚本会执行失败。(笔者的脚本中,睡眠时间为2,因此在这里配置为了4)

其中,在vrrp_instance中我们需要进行5处更改,笔者已经通过注释的方式给出。实际的更改值要取决于自己的网络环境,比如VIP的选择。在虚拟机环境下,我们只需要选任意一个没有被占用的内网IP即可。而如果是云服务器构建的HA集群,要向服务提供商(腾讯云,华为云)另行购买一个HAVIP

笔者的物理机和宿主机集群通过NAT方式连接,并处在192.168.229.0/24网络。宿主机的系统均为CentOS 7系统,因此网卡口名称均为ens33。这些都取决于个人配置,不可直接粘贴。

vrrp_instance VI_1 {
    #1 可设置BACKUP和MASTER,取决于设置。
    state MASTER
    #2 配置成自己的网卡口!centOS7一般都叫ens33.
    #  可以使用ip addr来查看。
    interface ens33
    
    # 注意,同一个集群下的virtual_router_id必须保持一致!
    virtual_router_id 51
    
    #3 保证BACKUP的优先级要比MASTER的优先级低。
    priority 100
    
    # 设置每1秒组播心跳消息,证明keepalived还存活。
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass 1111
    }
    #4 这一块需要自己手动添加。注意要写到virtual_ipaddress项的上面!
    #  内部的脚本名称实际上取决于你刚才在vrrp_script的配置名称
    track_script {
        check
    }
    
    #5 无论是BACKUP/MASTER,为了让用户通过统一域名访问,因此都要选择统一的VIP。
    #  这个IP根据你的局域网IP号而定。
    virtual_ipaddress {
        192.168.229.171
    }
}
复制代码

对于配置为BACKUP的主机,只需要把state一项修改即可。

启动keepalived集群

在确保以上的配置就绪之后,我们通过systemctl命令启动nginxkeepalived

$ systemctl start nginx
$ systemctl start keepalived.service
复制代码

检查Nginx和keepalived服务是否正常启动:

$ systemctl status nginx
$ systemctl status keepalived.service
复制代码

在集群启动后,正在处于工作状态的服务器是可以通过ip addr检查到ens33网口绑定了VIP的。其它正在运行但处于候选状态的Nginx主机则没有绑定VIP。

如果不慎出了错误!

一定要去日志中寻找线索,获取在浏览器端开启调试模式检查错误码。

#nginx错误日志默认位置
cat /var/log/nginx/error.log
复制代码

笔者遇到并解决了以下错误:

(13: Permission denied) while connecting to upstream
复制代码

解决方案:可能是由于selinux防火墙开启引发的问题,检查防火墙状态:

$ sestatus -v
复制代码

这里笔者姑且采用直接的方案:关闭selinux防火墙。

# SELINUX = disabled
$ vim /etc/selinux/config
# 重启生效
$ reboot
复制代码

另外,由于我们对upstream的每个节点都设置了超时时间,因此如果因为高并发(或其它原因)导致所有节点全部处于down状态的话,日志文件会打印以下错误:

...no live upstreams while connecting to upstream...
复制代码

它表示没有一个存活的上游节点,但它并不是根本问题。我们需要继续跟踪日志,追查是什么原因导致所有的节点全部down掉。此刻在浏览器端,我们可以在调试模式下检测到502错误。

公网环境下的高可用方案

阿里云:阿里云服务器不再支持单独购买IP,因此在公网环境下不能通过keepalived的方式进行热备。不过阿里云似乎直接提供负载均衡服务?(要恰饭的嘛)

腾讯云和华为云的实现原理类似,需要为多个云服务器创建一个虚拟私有云VPC,然后再另行购买用于浮动的HAVIP,进行绑定。

不同厂商间提供的解决方案稍有区别,因此要根据自己之后的实际情况查找具体方案。笔者目前没有在公网搭建HA的强烈需求(主要是穷),因此暂时不会再深入研究公网环境下的HA搭建了(老实说,笔者认为这些应该是运维的工作......)。笔者在此罗列一些帖子供日后参考:

Nginx补充

Master与Worker

我们使用ps命令查看进程时,可以留意到:Nginx服务实际上有两个进程:

$ ps -ef | grep nginx
#root       2016      1  0 10:49 ?        00:00:00 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
#nginx      2017   2016  0 10:49 ?        00:00:00 nginx: worker process
复制代码

worker进程的数量实际上可以有多个的。正是由我们当时在/etc/nginx/nginx.conf中配置的worker_process数量所决定的(默认为1,实际数量取决于机器性能)。当外部访问到Nginx服务器的时候,worker进程之间通过竞争的方式来获取此连接。

这种一个Master管理多个Worker的优势是什么呢?

  1. 基于这种模式,我们可以使用nginx -s reload对Nginx进行热部署:它是使得正在处理请求的worker不会被打断已有的工作,而空闲的worker则立刻刷新了配置
  2. 每个worker是独立的进程(非线程)。因此节省了各种同步锁造成的开销。此外,即便是个别worker因异常而关闭,其它worker仍然能够正常运行,提高了Nginx整体的稳定性,降低了风险。

Nginx 的高性能依赖于 Linux 2.6 内核的 epoll 或是 BSD 内核的 kqueue 提供高效的网络套接字状态轮询服务【时间复杂度为 O(1) 】。在没有这两个服务的内核上则退化成为性能低下的 select 【*nix ,Windows 都有,时间复杂度为 O(n) 】. Windows 没有 epoll 和 kqueue,Nginx 在 Windows 上用 select 表现自然不佳。

另一个NoSQL数据库软件 Redis 也是同样的原因,导致它在Windows运行的性能和*nix平台相比要低。

我们应该设置多少个Worker数量最合适呢?一般情况下,我们通常将这个数值设置为和物理机的CPU数量相等。

worker_connection

首先提一个问题:基于Http 1.1协议,一个用户发送一个请求到一个Worker中,会占用Worker的几个连接数呢?答案是2个或者4个。

当访问静态资源,Nginx直接从本地返回资源时,需要占用两个连接。

当Nginx进行反向代理的时候,则需要占用4个连接。

因此假设每个worker的连接数值为worker_connection,则实际的并发访问量(指同时处理Client的请求数)是:

  1. (worker_connection * worker_process) / 2 => 作为静态服务器

  2. (worker_connection * worker_process) / 4 => 作为反向代理服务器