原理

Nginx进程模型

Nginx默认采用多进程工作方式,Nginx启动后,会运行一个master进程和多个worker进程。其中master充当整个进程组与用户的交互接口,同时对进程进行监护,管理worker进程来实现重启服务、平滑升级、更换日志文件、配置文件实时生效等功能。worker用来处理基本的网络事件,worker之间是平等的,他们共同竞争来处理来自客户端的请求。

在创建master进程时,先建立需要监听的socket(listenfd),然后从master进程中fork()出多个worker进程,如此一来每个worker进程多可以监听用户请求的socket。一般来说,当一个连接进来后,所有在Worker都会收到通知,但是只有一个进程可以接受这个连接请求,其它的都失败,这是所谓的惊群现象。nginx提供了一个accept_mutex(互斥锁),有了这把锁之后,同一时刻,就只会有一个进程在accpet连接,这样就不会有惊群问题了。

先打开accept_mutex选项,只有获得了accept_mutex的进程才会去添加accept事件。nginx使用一个叫ngx_accept_disabled的变量来控制是否去竞争accept_mutex锁。ngx_accept_disabled = nginx单进程的所有连接总数 / 8 -空闲连接数量,当ngx_accept_disabled大于0时,不会去尝试获取accept_mutex锁,ngx_accept_disable越大,于是让出的机会就越多,这样其它进程获取锁的机会也就越大。不去accept,每个worker进程的连接数就控制下来了,其它进程的连接池就会得到利用,这样,nginx就控制了多进程间连接的平衡。

每个worker进程都有一个独立的连接池,连接池的大小是worker_connections。这里的连接池里面保存的其实不是真实的连接,它只是一个worker_connections大小的一个ngx_connection_t结构的数组。并且,nginx会通过一个链表free_connections来保存所有的空闲ngx_connection_t,每次获取一个连接时,就从空闲连接链表中获取一个,用完后,再放回空闲连接链表里面。一个nginx能建立的最大连接数,应该是worker_connections * worker_processes。当然,这里说的是最大连接数,对于HTTP请求本地资源来说,能够支持的最大并发数量是worker_connections * worker_processes,而如果是HTTP作为反向代理来说,最大并发数量应该是worker_connections * worker_processes/2。因为作为反向代理服务器,每个并发会建立与客户端的连接和与后端服务的连接,会占用两个连接。

Nginx处理HTTP请求流程

http请求是典型的请求-响应类型的的网络协议。http是文件协议,所以我们在分析请求行与请求头,以及输出响应行与响应头,往往是一行一行的进行处理。通常在一个连接建立好后,读取一行数据,分析出请求行中包含的method、uri、http_version信息。然后再一行一行处理请求头,并根据请求method与请求头的信息来决定是否有请求体以及请求体的长度,然后再去读取请求体。得到请求后,我们处理请求产生需要输出的数据,然后再生成响应行,响应头以及响应体。在将响应发送给客户端之后,一个完整的请求就处理完了。

高可用

什么是高可用

高可用HA(High Availability)是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。如果一个系统能够一直提供服务,那么这个可用性则是百分之百,但是天有不测风云。所以我们只能尽可能的去减少服务的故障。

解决的问题

在生产环境上很多时候是以Nginx做反向代理对外提供服务,但是一天Nginx难免遇见故障,如:服务器宕机。当Nginx宕机那么所有对外提供的接口都将导致无法访问。

虽然我们无法保证服务器百分之百可用,但是也得想办法避免这种悲剧,今天我们使用keepalived来实现Nginx的高可用。

双机热备方案

这种方案是国内企业中最为普遍的一种高可用方案,双机热备其实就是指一台服务器在提供服务,另一台为某服务的备用状态,当一台服务器不可用另外一台就会顶替上去。

keepalived是什么?

Keepalived软件起初是专为LVS负载均衡软件设计的,用来管理并监控LVS集群系统中各个服务节点的状态,后来又加入了可以实现高可用的VRRP (Virtual Router Redundancy Protocol ,虚拟路由器冗余协议)功能。因此,Keepalived除了能够管理LVS软件外,还可以作为其他服务(例如:Nginx、Haproxy、MySQL等)的高可用解决方案软件

故障转移机制

Keepalived高可用服务之间的故障切换转移,是通过VRRP 来实现的。
在 Keepalived服务正常工作时,主 Master节点会不断地向备节点发送(多播的方式)心跳消息,用以告诉备Backup节点自己还活着,当主 Master节点发生故障时,就无法发送心跳消息,备节点也就因此无法继续检测到来自主 Master节点的心跳了,于是调用自身的接管程序,接管主Master节点的 IP资源及服务。而当主 Master节点恢复时,备Backup节点又会释放主节点故障时自身接管的IP资源及服务,恢复到原来的备用角色。

实现过程

安装keepalived

1
sudo apt install keepalived

修改主机(192.168.16.128)keepalived配置文件

yum方式安装的会生产配置文件在/etc/keepalived下:

1
vi keepalived.conf

keepalived.conf:

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
#检测脚本
vrrp_script chk_http_port {
script "/usr/local/src/check_nginx_pid.sh" #心跳执行的脚本,检测nginx是否启动
interval 2 #(检测脚本执行的间隔,单位是秒)
weight 2 #权重
}
#vrrp 实例定义部分
vrrp_instance VI_1 {
state MASTER # 指定keepalived的角色,MASTER为主,BACKUP为备
interface ens33 # 当前进行vrrp通讯的网络接口卡(当前centos的网卡) 用ifconfig查看你具体的网卡
virtual_router_id 66 # 虚拟路由编号,主从要一直
priority 100 # 优先级,数值越大,获取处理请求的优先级越高
advert_int 1 # 检查间隔,默认为1s(vrrp组播周期秒数)
#授权访问
authentication {
auth_type PASS #设置验证类型和密码,MASTER和BACKUP必须使用相同的密码才能正常通信
auth_pass 1111
}
track_script {
chk_http_port #(调用检测脚本)
}
virtual_ipaddress {
192.168.16.130 # 定义虚拟ip(VIP),可多设,每行一个
}
}

virtual_ipaddress 里面可以配置vip,在线上通过vip来访问服务。

interface需要根据服务器网卡进行设置通常查看方式ip addr

authentication配置授权访问后备机也需要相同配置

修改备机keepalived配置文件

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
#检测脚本
vrrp_script chk_http_port {
script "/usr/local/src/check_nginx_pid.sh" #心跳执行的脚本,检测nginx是否启动
interval 2 #(检测脚本执行的间隔)
weight 2 #权重
}
#vrrp 实例定义部分
vrrp_instance VI_1 {
state BACKUP # 指定keepalived的角色,MASTER为主,BACKUP为备
interface ens33 # 当前进行vrrp通讯的网络接口卡(当前centos的网卡) 用ifconfig查看你具体的网卡
virtual_router_id 66 # 虚拟路由编号,主从要一直
priority 99 # 优先级,数值越大,获取处理请求的优先级越高
advert_int 1 # 检查间隔,默认为1s(vrrp组播周期秒数)
#授权访问
authentication {
auth_type PASS #设置验证类型和密码,MASTER和BACKUP必须使用相同的密码才能正常通信
auth_pass 1111
}
track_script {
chk_http_port #(调用检测脚本)
}
virtual_ipaddress {
192.168.16.130 # 定义虚拟ip(VIP),可多设,每行一个
}
}

检测脚本:

1
2
3
4
5
6
7
8
9
#!/bin/bash
#检测nginx是否启动了
A=`ps -C nginx --no-header |wc -l`
if [ $A -eq 0 ];then #如果nginx没有启动就启动nginx
systemctl start nginx #重启nginx
if [ `ps -C nginx --no-header |wc -l` -eq 0 ];then #nginx重启失败,则停掉keepalived服务,进行VIP转移
killall keepalived
fi
fi

脚本授权:chmod 775 check_nginx_pid.sh

说明:脚本必须通过授权,不然没权限访问啊,在这里我们两条服务器执行、VIP(virtual_ipaddress:192.168.16.130),我们在生产环境是直接通过vip来访问服务。

模拟nginx故障:

修改两个服务器默认访问的Nginx的html页面作为区别。

首先访问192.168.16.130,通过vip进行访问,页面显示192.168.16.128;说明当前是主服务器提供的服务。

这个时候192.168.16.128主服务器执行命令:

systemctl stop nginx; #停止nginx

再次访问vip(192.168.16.130)发现这个时候页面显示的还是:192.168.16.128,这是脚本里面自动重启。

现在直接将192.168.16.128服务器关闭,在此访问vip(192.168.16.130)现在发现页面显示192.168.16.129这个时候keepalived就自动故障转移了,一套企业级生产环境的高可用方案就搭建好了。

keepalived中还有许多功能比如:邮箱提醒等

动静分离

静态资源部署在nginx服务器

nginx来当做静态资源服务器

来自80端口的请求会去nginx服务器根路径下的data文件夹查找

http://xxx.xxx.xxx.xx/static/a/logo.png 会请求根路径下 /data/static/a/logo.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
http {
include mime.types;
default_type application/octet-stream;


server {
listen 80;
server_name xxx.xxx.xxx.xxx;

#charset koi8-r;

#access_log logs/host.access.log main;

location /static/ {
root /data/;
}

}
}

转发到静态资源服务器

转发时使用负载均衡配置

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
http {
include mime.types;
default_type application/octet-stream;
upstream stack_pools {
server 172.25.254.134:80 weight=5;
}
upstream dynamic_pools {
server 172.25.254.135:80 weight=5;
}
server {
listen 80;
server_name xxx.xxx.xxx.xx;
location / {
root html;
index index.html index.htm;
proxy_pass http://dynamic_pools;
}
location /image/ {
proxy_pass http://stack_pools;
}
location /dynamic/ {
roxy_pass http://dynamic_pools;
}
}
}

负载均衡

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
http {
upstream myServer{
ip_hash;
server localhost:1111 weight=10;
server localhost:2222;
fair;
}
listen port;
server_name xxx.xxx.xxx.xx;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
proxy_pass http://myServer;
}
}

轮询策略

每个请求按时间的顺序逐一分配到不同德尔服务器中,如果服务器挂了,可以自动剔除。

权重策略

权重越高,被分配的请求越多

ip hash

每个请求按访问ip的hash结果分配,这样每个访客固定访问一个服务器后,可以解决session问题。

fair 第三方

根据请求的响应时间来分配,响应时间短的先分配

反向代理

  • proxy_pass 包含 URI 路径(如 /api):

    无论末尾是否有斜杠,均删除 location 匹配部分,拼接剩余路径。

    例:location /webapp + proxy_pass http://xxx/api → /api/剩余路径。

  • proxy_pass 不包含 URI 路径(如 http://xxx):

    末尾无斜杠 → 直接拼接完整请求 URI(http://xxx/完整路径)。

    末尾有斜杠 → 替换 location 匹配部分,拼接剩余路径(http://xxx/剩余路径)。

简单反向代理

把本机 80 端口的请求转发到其他的服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
http {
include mime.types;
default_type application/octet-stream;

server {
listen 80;
server_name xxx.xxx.xxx.xxx;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root xxx.xxx.xxx.xxx:port;
index index.html index.htm;
}

}
}

通过正则匹配路径转发

根据路径中不同字段转,把 80 端口的请求,转发到不同的服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
http {
include mime.types;
default_type application/octet-stream;

server {
listen 80;
server_name xxx.xxx.xxx.xxx;

location ~ /api/ {
proxy_pass xxx.xxx.xxx.xxx:port;
}
location ~ /server/ {
proxy_pass xxx.xxx.xxx.xxx:port;
}

}
}

配置文件

位置

1
/etc/nginx/nginx.conf

配置结构

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
...              #全局块

events { #events块
...
}

http #http块
{
... #http全局块
server #server块
{
... #server全局块
location [PATTERN] #location块
{
...
}
location [PATTERN]
{
...
}
}
server
{
...
}
... #http全局块
}

全局块

1
2
# 值越大,可支持的并发数量越多
worker_processes 1

events块

主要涉及Nginx服务器和用户的网络连接

http块

最常用的配置,代理,缓存,日志定义等绝大多数功能和第三方模块的配置都在这里

http全局块

http全局配置包括文件引入,MIME-TYPE定义,日志自定义,链接超时时间,单链接请求上限等

server块

这块和虚拟主机有密切的关系,虚拟主机从用户的角度看,一台独立的硬件主机是完全一样的,该技术的产生是为了节约互联网服务器硬件成本

每个http块又可以包括多个server块,每个server块就相当于一个虚拟主机

每个server块也分为全局server块,以及可以同时包含多个location块

1
2
3
4
5
6
7
8
server {

# 监听80端口
listen 80;

# 主机名称
server_name localhost;
}
全局server块

最常见的配置是本虚拟主机的监听配置和本虚拟主机的名称或IP配置。

location块

主要作用是基于Nginx服务器接受到的请求字符串(例如,server_name/uri-string),对虚拟主机名称(也可以是ip别名)之外的字符串(例如 前面的uri-string)进行匹配,对待定的请求进行处理,地址定向,数据缓存和应答控制等功能,还有许多第三方模块的配置也在这里进行

详细配置

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
########### 每个指令必须有分号结束。#################
#user administrator administrators; #配置用户或者组,默认为nobody nobody。
#worker_processes 2; #允许生成的进程数,默认为1
#pid /nginx/pid/nginx.pid; #指定nginx进程运行文件存放地址
error_log log/error.log debug; #制定日志路径,级别。这个设置可以放入全局块,http块,server块,级别以此为:debug|info|notice|warn|error|crit|alert|emerg
events {
accept_mutex on; #设置网路连接序列化,防止惊群现象发生,默认为on
multi_accept on; #设置一个进程是否同时接受多个网络连接,默认为off
#use epoll; #事件驱动模型,select|poll|kqueue|epoll|resig|/dev/poll|eventport
worker_connections 1024; #最大连接数,默认为512
}
http {
include mime.types; #文件扩展名与文件类型映射表
default_type application/octet-stream; #默认文件类型,默认为text/plain
#access_log off; #取消服务日志
log_format myFormat '$remote_addr–$remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for'; #自定义格式
access_log log/access.log myFormat; #combined为日志格式的默认值
sendfile on; #允许sendfile方式传输文件,默认为off,可以在http块,server块,location块。
sendfile_max_chunk 100k; #每个进程每次调用传输数量不能大于设定的值,默认为0,即不设上限。
keepalive_timeout 65; #连接超时时间,默认为75s,可以在http,server,location块。

upstream mysvr {
server 127.0.0.1:7878;
server 192.168.10.121:3333 backup; #热备
}
error_page 404 https://www.baidu.com; #错误页
server {
keepalive_requests 120; #单连接请求上限次数。
listen 4545; #监听端口
server_name 127.0.0.1; #监听地址
location ~*^.+$ { #请求的url过滤,正则匹配,~为区分大小写,~*为不区分大小写。
#root path; #根目录
#index vv.txt; #设置默认页
proxy_pass http://mysvr; #请求转向mysvr 定义的服务器列表
deny 127.0.0.1; #拒绝的ip
allow 172.18.5.54; #允许的ip
}
}
}

location匹配优先级

匹配优先级从上之下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
location = / {
#规则A
}
location = /login {
#规则B
}
location ^~ /static/ {
#规则C
}
location ~ .(gif|jpg|png|js|css)$ {
#规则D
}
location ~* .png$ {
#规则E
}
location / {
#规则F
}

常用命令

使用nginx命令需要进入到nginx可执行文件目录 /usr/sbin/

nginx document

查看版本

1
nginx -v

启动nginx

1
./nginx

快速关闭

1
nginx -s stop

安全关闭

1
nginx -s quit

重新加载配置文件

1
nginx -s reload

nginx安装

命令安装

ubuntu

1
2
sudo apt update
sudo apt install nginx

安装成功后会默认启动在80端口,直接访问会显示nginx主页

查看可执行文件

1
which nginx

查看安装位置

1
whereis nginx

修改防火墙规则

UFW 全称为Uncomplicated Firewall,是Ubuntu 系统上默认的防火墙组件, 为了轻量化配置iptables 而开发的一款工具。

启用防火墙

1
sudo ufw enable

允许80端口访问

1
sudo ufw allow 80

基本概念

Nginx是什么

Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器。最高支持50000的并发链接。

正向代理

正向代理的用途:

(1)访问原来无法访问的资源,如google

(2)可以做缓存,加速访问资源

(3)对客户端访问授权,上网进行认证

(4)代理可以记录用户访问记录(上网行为管理),对外隐藏用户信息

反向代理

反向代理(Reverse Proxy)实际运行方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器

反向代理的作用:

(1)保证内网的安全,阻止web攻击,大型网站,通常将反向代理作为公网访问地址,Web服务器是内网

(2)负载均衡,通过反向代理服务器来优化网站的负载

负载均衡

增加服务器的数量,在大量请求的时候,把请求平均分发到不同的服务器

动静分离

在请求资源的时候,区分静态资源(js,html.css)和动态资源(serverlet,jsp),把不同资源的请求转发到不同的服务器

useEffect指南

每次渲染时,UseEffect 都是一个新的自己

每次重新渲染的时,useEffect 都会重新执行,而且如果没有添加依赖项数组的时候,每个 useEffect 都是全新的

而且 useEffect 内部用到的变量都是属于,当前此次执行时捕获到的变量,所以如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Counter() {
const [count, setCount] = useState(0);

function handleAlertClick() {
setTimeout(() => {
alert("You clicked on: " + count);
}, 3000);
}

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={handleAlertClick}>Show alert</button>
</div>
);
}

点击 alert 按钮之后快速点击 click 按钮,在定时器结束之后并不会弹出修改后的值,而是弹出在点击 alert 时 useEffect 捕获到的状态

如果你想读取一个过去或是未来的值,你可以用一个 Ref 去保存,因此对数据的操作不是在 react 的默认行为中,这样虽然显得代码不够干净,但可以确认是需要这样做。

useEffect 中的清理

React 只会在浏览器绘制后运行 effects。这使得你的应用更流畅因为大多数 effects 并不会阻塞屏幕的更新。Effect 的清除同样被延迟了。上一次的 effect 会在重新渲染后被清除

执行的过程应该是: 渲染新 UI -> 清除旧的 Effect -> 执行新的 Effect

清除函数只能读取到在旧的 Effect 执行时捕获到的变量

Effect 依赖

React 它统一描述了初始渲染和之后的更新,React 会根据我们当前的 props 和 state 同步到 DOM,useEffect 使你能够根据 props 和 state 同步 React tree 之外的东西,也包括常说的副作用。

如果你试图写一个 effect 会根据是否第一次渲染而表现不一致,可能违背了 React 初衷

但是如果诚实的告诉了 Effect 依赖,还可能存在问题:

1
2
3
4
5
6
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);

由于依赖项的改变定时器被清除

有两种方法可以解决,使用 setState 回调函数,或者使用 useReducer, 如果 useReducer 还需要依赖其他的内部变量,可以吧 useReducer 放到函数组建内部定义

有的时候可能会觉得一个函数不会变换,所以没有加到依赖中,但事实上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function SearchResults() {
const [query, setQuery] = useState("react");

function getFetchUrl() {
return "https://hn.algolia.com/api/v1/search?query=" + query;
}

async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}

useEffect(() => {
fetchData();
}, []);
}

函数依赖的参数可能在其他的方法中变化

简单处理的话可以吧相关的函数都放到 useEffect 中,而依赖项只是简单的 query

另外也可以吧 getFetchUrl 放到组件外部,把 query 当做参数传入,或者使用 useCallback 包裹 getFetchUrl

竞态问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Article extends Component {
state = {
article: null,
};
componentDidMount() {
this.fetchData(this.props.id);
}
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.fetchData(this.props.id);
}
}
async fetchData(id) {
const article = await API.fetchArticle(id);
this.setState({ article });
}
// ...
}

如果 componentDidUpdate 中的请求比 componentDidMount 中的请求慢,那么更新中的请求数据会被初始化的请求数据覆盖

我们想做的是,可以打断旧的更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Article({ id }) {
const [article, setArticle] = useState(null);

useEffect(() => {
let didCancel = false;

async function fetchData() {
const article = await API.fetchArticle(id);
if (!didCancel) {
setArticle(article);
}
}
fetchData();
return () => {
didCancel = true;
};
}, [id]);
}
  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信