Nginx后端开发人员必学神器-并发编程经典之作剖析和名企热点面试

2021/12/10 7:21:55

本文主要是介绍Nginx后端开发人员必学神器-并发编程经典之作剖析和名企热点面试,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

概述

**本人博客网站 **IT小神 www.itxiaoshen.com

Nginx官网 最新版本为1.21.3

Nginx (engine x) 是一个开源的、高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务,由俄罗斯的程序设计师IgorSysoev所开发,官方测试nginx能够支撑5万并发连接,并且cpu、内存等资源消耗却非常低,运行非常稳定,支持热部署,几乎可以实现7*24小时不间断运行。

可以说只要有网站或者后台服务的企业就会需要用到Nginx,其使用极为广泛。

部署方式

预先构建好的包安装

#以RHEL/CentOS为例
#安装先决条件
sudo yum install yum-utils
#在新机器上第一次安装nginx之前,需要设置nginx包存储库。之后,您可以从存储库安装和更新nginx,要设置yum存储库,创建文件/etc/yum.repos.d/nginx
[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

#默认情况下,使用稳定nginx包的存储库。如果你想使用主线nginx包,请运行以下命令:
sudo yum-config-manager --enable nginx-mainline
#使用实例安装nginx:
sudo yum install nginx

image-20211006133414806

源码安装

如果一些特殊的功能是必需的,而不是包和端口提供的,nginx也可以从源文件编译。虽然这种方法更灵活,但对于初学者来说可能比较复杂,下面我们进行Nginx源码安装

#编译前提,需要安装必要的包
yum install gcc pcre-devel openssl-devel zlib-devel -y
#从Nginx官网下载源码文件
wget https://nginx.org/download/nginx-1.21.3.tar.gz
#解压下载的压缩文件
tar -xvf nginx-1.21.3.tar.gz
#进入解压的nginx目录
cd nginx-1.21.3
#参数使用示例(所有这些都需要在一行中输入),需要定制特别参数可以详细参考官网源代码安装说明,https://nginx.org/en/docs/configure.html
./configure --prefix=/usr/local/nginx \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_realip_module \
--with-http_stub_status_module \
--with-http_gzip_static_module \
--with-pcre \
--with-stream \
--with-stream_ssl_module \
--with-stream_realip_module
#配置完成后,使用make编译并安装nginx,-j 8 表示CPU八核,可以按照实际CPU核数定义,越多越快
make -j 8 && make install

image-20211006142949217

image-20211006143105583

修改/usr/local/nginx/conf/nginx.conf,将监听端口改为8082,

image-20211006144127074

启动nginx和访问本机的8082端口,http://localhost:8082,返回nginx欢迎页面,nginx部署完毕

image-20211006144252762

常用命令

cd /usr/local/nginx/sbin/
#启动nginx
./nginx
#停止nginx
./nginx -s stop
#安全退出nginx
./nginx -s quit
#重新加载配置文件启动nginx,相当于热启动
./nginx -s reload
#查看nginx进程
ps aux | grep nginx

Nginx常用特性

概述

关于nginx https://nginx.org/en/ ,详细可以查阅这个官网文档

image-20211006175317457

从HTTP缓存看Nginx进程架构

image-20211006180920191

nginx是C语言编写的,采用的多进程架构模式,一个master和多个worker(worker数量一般为cpu核数),Master负责管理worker进程,worker进程负责处理网络事件,整个框架被设计为一种依赖事件驱动、异步、非阻塞的模式。这样设计有点如下:

  • 可以充分利用多核机器,增强并发处理能力。
  • 多worker间可以实现负载均衡。
  • Master监控并统一管理worker行为。在worker异常后,可以主动拉起worker进程,从而提升了系统的可靠性。并且由Master进程控制服务运行中的程序升级、配置项修改等操作,从而增强了整体的动态可扩展与热更的能力。

如果有兴趣要深入了解nginx源码可研究nginx的upstream(工作方式)、线程池(线程管理,)、内存池(建立一个大块内存缓冲,不需要每次使用去申请)、网络IO、进程间通信如共享内存、epoll、多进程、日志处理、conf配置文件管理、原子操作、http模块、链表、红黑树以及进一步理解Nginx业务流程架构;Nginx提供高度灵活性,通常我们可以自己编写C模块嵌入到nginx程序中,可以基于nginx模块化开发实现类似限流、黑白名单、认证鉴权验证等功能。

Nginx官方功能和特性描述如下:

image-20211006145950083

image-20211006150051696

反向代理

正向代理

正向代理作用在客户端的,类似一个跳板机,代理访问外部资源,比如我们国内访问谷歌,直接访问访问不到,我们可以通过一个正向代理服务器,请求发到代理服,代理服务器能够访问谷歌,这样由代理去谷歌取到返回数据,再返回给我们,这样我们就能访问谷歌了。正向代理即是客户端代理, 代理客户端, 服务端不知道实际发起请求的客户端。

image-20211006190622482

反向代理

反向代理是作用在服务器端的,代理服务器对外就表现为一个服务器,实际运行方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端;反向代理即是服务端代理, 代理服务端, 客户端不知道实际提供服务的服务端

反向代理的作用:

  • 保证内网的安全,阻止web攻击,大型网站,通常将反向代理作为公网访问地址,Web服务器是内网。
  • 负载均衡,通过反向代理服务器来优化网站的负载。

image-20211006190137447

动静分离

Nginx本身就是一个非常强劲的静态资源服务器,如通过location配置实现动态页面转发后端tomcat服务器,在nginx站点下存放静态资源文件或站点,加快网站的解析速度,可以把动态页面和静态页面由不同的服务器来解析,加快解析速度,降低原来单个服务器的压力。 简单来说,就是使用正则表达式匹配过滤,然后交个不同的服务器。

image-20211006194339511

动静分离的原理很简单,通过location对请求url进行匹配即可,在/usr/local/nginx/html/(任意目录)下创建 /static/imgs 配置如下:

###静态资源访问
server {
  listen       80;
  server_name  static.itxiaoshen.com;
  location /static/imgs {
       root /usr/local/nginx/html/imgs;
       index  index.html index.htm;
   }
}
###动态资源访问
 server {
  listen       80;
  server_name  www.itxiaoshen.com;    
  location / {
     proxy_pass http://127.0.0.1:8080; //这里如果有多台也可以通过配置upstream然后在这里引用,详细可以看下一节
     index  index.html index.htm;
   }
}

负载均衡

Nginx提供丰富的负载均衡算法,包括轮询、加权轮询、最少连接、响应时间最小、iphash、urlhash。

image-20211007102601758

轮询(Round Robin)

nginx默认负载均衡算法,可以配合权重使用,默认情况权重是1。

upstream backend {
   #没有负载均衡算法定义则默认为Round Robin
   server backend1.example.com;
   server backend2.example.com;
}

加权轮询

upstream backend {
   server backend1.example.com weight=10;
   server backend2.example.com weight=10;
}

最少连接(Least Connections)

请求会转发到当前有效连接最少的服务器,可以配合权重使用。

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

IP哈希(IP Hash)

由请求客户端的IP地址决定请求发往哪台服务器,可以保证同一个IP地址的请求可以转发到同一台服务器。IPV4的前三位或者IPV6的全部地址参与哈希运算。

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

如果想要暂时移除当前负载服务器组中的某一台服务器,可以用down 参数标记服务器,已经通过IP哈希到当前服务器的请求会暂时保持,并自动被转移到组中的下一台服务器。

upstream backend {
    server backend1.example.com;
    server backend2.example.com;
    server backend3.example.com down;
}

URL哈希

请求会根据自定义的字符串、变量或者二者的组合作为key进行哈希,决定发往哪台服务器。比如按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。Nginx本身是不支持url_hash的,如果需要使用这种调度算法,必须安装Nginx 的hash软件包。

upstream backend {
    hash $request_uri consistent;
    hash_method crc32;
    server backend1.example.com;
    server backend2.example.com;
}

hash指令中consistent 参数是可选的,如果指定了consistent参数,负载均衡会使用一致性哈希中的ketama算法,请求会根据定义的哈希键值哈希均匀的发往组中的所有服务器上。给服务器组中增加新服务器或者移除要原来的服务器,只会有少数的哈希键会重新映射。

响应时间最小fair(第三方)

比上面两个更加智能的负载均衡算法。根据后端服务器的响应时间来分配请求,响应时间短的优先分配。Nginx本身是不支持fair的,如果需要使用这种调度算法,必须下载Nginx的upstream_fair模块。或者使用商业版本的Nginx Plus,此外在在upstream中每个主机后面配置 max_conns可以限制每个后端服务器的处理连接数。

upstream backend {    
    server server1;    
    server server2;    
    fair;    
}

缓存

nginx配置缓存的优点:可以在一定程度上,减少服务器的处理请求压力。比如对一些图片,css或js做一些缓存,那么在每次刷新浏览器的时候,就不会重新请求了,而是从缓存里面读取。这样就可以减轻服务器的压力。详细可参考官网说明

image-20211007115934714

nginx可配置的缓存有2种:

  • 客户端的缓存(一般指浏览器的缓存,Cache-Control)。网页缓存是由HTTP消息头中的"Cache-control"来控制的,常见的取值有private、no-cache、max-age、must-revalidate等,默认为private。
  • 服务端的缓存(使用proxy-cache实现的)。
worker_processes  1;
events {
  worker_connections  1024;
}
http {
  include       mime.types;
  default_type  application/octet-stream;
  sendfile        on;
  #tcp_nopush     on;
  #keepalive_timeout  0;
  keepalive_timeout  65;
  include nginx_proxy.conf;
  proxy_cache_path  /data/nuget-cache levels=1:2 keys_zone=nuget-cache:20m max_size=50g inactive=168h;
  #gzip  on;
  server {
    listen       8081;
    server_name  xxx.abc.com;
    location / {
      proxy_pass http://localhost:7878;
      add_header  Cache-Control  max-age=no-cache;
    }
    location ~* \.(css|js|png|jpg|jpeg|gif|gz|svg|mp4|ogg|ogv|webm|htc|xml|woff)$ {
      access_log off;
      add_header Cache-Control "public,max-age=30*24*3600";
      proxy_pass http://localhost:7878;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
      root   html;
    }
  }
}
  • 缓存文件放在哪儿?
#指定缓存位置、缓存名称、内存中缓存内容元数据信息大小限制、缓存总大小限制。缓存位置是一个目录应该先创建好,nginx并不会帮我们创建这个缓存目录
proxy_cache_path /data/nginx/cache  keys_zone=one:10m  max_size=10g;
#指定使用前面设置的缓存名称
proxy_cache  one;
  • 如何指定哪些请求被缓存?
    • Nginx默认会缓存所有get和head方法的请求结果,缓存的key默认使用请求字符串.
    • 自定义key,例如 proxy_cache_key “host request_uri$cookie_user”;
    • 指定请求至少被发送了多少次以上时才缓存,可以防止低频请求被缓存,例如:proxy_cache_min_uses 5;
    • 指定哪些方法的请求被缓存,例如 proxy_cache_methods GET HEAD POST;
  • 缓存有效期?
    • 默认情况下,缓存的内容是长期存留的,除非缓存的总量超出限制。可以指定缓存的有效期,例如:
      • 响应状态码为200 302时,10分钟有效 proxy_cache_valid 200 302 10m;
      • 对应任何状态码,5分钟有效 proxy_cache_valid any 5m;
  • 如何指定哪些请求不被缓存?
    • proxy_cache_bypass 该指令响应来自原始服务器而不是缓存,例如:proxy_cache_bypass $cookie_nocache \(arg_nocache\)arg_comment;
    • 如果任何一个参数值不为空或者不等0,nginx就不会查找缓存,直接进行代理转发。

Rewrite 配置功能

URL重写有利于网站首选域的确定,对于同一资源页面多条路径的301重定向有助于URL权重的集中,rewrite的组要功能是实现URL地址的重定向。Nginx的rewrite功能需要PCRE软件的支持,即通过perl兼容正则表达式语句进行规则匹配的。默认参数编译nginx就会支持rewrite的模块,但是也必须要PCRE的支持,rewrite是实现URL重写的关键指令,根据regex(正则表达式)部分内容,重定向到replacement,结尾是flag标记。详细可参考官网说明

image-20211007120455550

限流

概述

Nginx限流的实现主要是ngx_http_limit_conn_module和ngx_http_limit_req_module这两个模块,按请求速率限速模块使用的是漏桶算法,即能够强行保证请求的实时处理速度不会超过设置的阈值。Nginx官方版本限制IP的连接和并发分别有两个模块:zone 用来限制单位时间内的请求数,即速率限制,采用的漏桶算法 “leaky bucket”。limit_req_conn 用来限制同一时间连接数,即并发限制。

限流算法

令牌桶算法

算法思想:

  • 令牌以固定速率产生,并缓存到令牌桶中;
  • 令牌桶放满时,多余的令牌被丢弃;
  • 请求要消耗等比例的令牌才能被处理;
  • 令牌不够时,请求被缓存。

image-20211007123221870

漏桶算法

算法思想:

  • 水(请求)从上方倒入水桶,从水桶下方流出(被处理);
  • 来不及流出的水存在水桶中(缓冲),以固定速率流出;
  • 水桶满后水溢出(丢弃)。
  • 这个算法的核心是:缓存请求、匀速处理、多余的请求直接丢弃。相比漏桶算法,令牌桶算法不同之处在于它不但有一只“桶”,还有个队列,这个桶是用来存放令牌的,队列才是用来存放请求的。从作用上来说,漏桶和令牌桶算法最明显的区别就是是否允许突发流量(burst)的处理,漏桶算法能够强行限制数据的实时传输(处理)速率,对突发流量不做额外处理;而令牌桶算法能够在限制数据的平均传输速率的同时允许某种程度的突发传输。

image-20211007123402818

请求处理数限流

#先安装压测工具,当前我们java开发在windows熟悉的jmeter有相似的功能
yum install -y httpd-tools

#编辑 vim /usr/local/nginx/conf/nginx.conf
#将limit_req_zone放在http中
limit_req_zone $binary_remote_addr zone=MyReqZone:10m rate=1r/s;
#将limit_req放在server中
limit_req zone=MyReqZone burst=5 nodelay;
#更新nginx配置
./nginx -s reload
#执行访问测试
ab -c 1 -n 50 http://localhost:8082/
  • limit_req_zone
    • 第一个参数:\(binary_remote_addr,表示通过remote_addr这个标识来做限制,“binary_”的目的是缩写内存占用量,是限制同一客户端ip地址。使用这个标识来确认用户身份唯一性,\)binary_remote_addr变量的大小对于IPv4地址始终为4字节,对于IPv6地址始终为16字节。存储状态在64位平台上总是占用64字节。一个1兆字节的区域可以保留大约1.6万个64字节状态。如果区域存储已用尽,服务器将把错误返回给所有请求。
    • 第二个参数:zone=MyReqZone:10m表示生成一个大小为10M,名字为one的内存区域,用来存储访问的频次信息。
    • 第三个参数:rate=1r/s表示允许相同标识的客户端的访问频次,这里限制的是每秒1次,还可以有比如30r/m的。
  • limit_req
    • 第一个参数:zone=MyReqZone设置使用哪个配置区域来做限制,与上面limit_req_zone 里的name对应。
    • 第二个参数:burst=5,重点说明一下这个配置,burst爆发的意思,这个配置的意思是设置一个大小为5的缓冲区当有大量请求(爆发)过来时,超过了访问频次限制的请求可以先放到这个缓冲区内。
    • 第三个参数:nodelay,如果设置,超过访问频次而且缓冲区也满了的时候就会直接返回503,如果没有设置,则所有请求会等待排队。

image-20211007125528244

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IPS50iJd-1633588453688)(F:\creation\markdown\article\Nginx大厂面试需要掌握多少\Nginx大厂面试需要掌握多少.assets\image-20211007124839533.png)]

连接数限流

#将limit_conn_zone放在http中,limit_conn perip 10:对应的key是 $binary_remote_addr,表示限制单个IP同时最多能持有10个连接。limit_conn perserver 100:对应的key是 $server_name,表示虚拟主机(server) 同时能处理并发连接的总数。注意,只有当 request header 被后端server处理后,这个连接才进行计数。
limit_conn_zone $binary_remote_addr zone=MyPerIp:10m;
limit_conn_zone $server_name zone=MyPerServer:10m;
#将limit_conn放在server中
limit_conn MyPerIp 10;
limit_conn MyPerServer 100;

Nginx的生态

image-20211006170918240

我们通常使用开源Nginx版本,基于Nginx开源版本至上还衍生其他版本,包括商业收费版本的Nginx Plus、淘宝开源的TEngine、OpenResty。

NGINX Plus

NGINX Plus官网 https://www.nginx.com/resources/datasheets/nginx-plus-datasheet/

NGINX Plus以先进的功能和屡获殊荣的支持扩展了NGINX开源,为客户提供了完整的应用交付解决方案。NGINX Plus将负载平衡器、内容缓存、web服务器、安全控制和丰富的应用程序监控和管理整合到一个易于使用的软件包中。

TEngine

TEngine官网 http://tengine.taobao.org/

Tengine是由淘宝网发起的Web服务器项目。它在Nginx的基础上,针对大访问量网站的需求,添加了很多高级功能和特性。Tengine的性能和稳定性已经在大型的网站如淘宝网,天猫商城等得到了很好的检验。它的最终目标是打造一个高效、稳定、安全、易用的Web平台。

从2011年12月开始,Tengine成为一个开源项目,Tengine团队在积极地开发和维护着它。Tengine团队的核心成员来自于淘宝、搜狗等互联网企业。Tengine是社区合作的成果,我们欢迎大家参与其中,贡献自己的力量。

TEngine特性:

  • 继承Nginx-1.18.0的所有特性,兼容Nginx的配置;
  • 支持HTTP的CONNECT方法,可用于正向代理场景;
  • 支持异步OpenSSL,可使用硬件如:QAT进行HTTPS的加速与卸载;
  • 增强相关运维、监控能力,比如异步打印日志及回滚,本地DNS缓存,内存监控等;
  • Stream模块支持server_name指令;
  • 更加强大的负载均衡能力,包括一致性hash模块、会话保持模块,还可以对后端的服务器进行主动健康检查,根据服务器状态自动上线下线,以及动态解析upstream中出现的域名;
  • 输入过滤器机制支持。通过使用这种机制Web应用防火墙的编写更为方便;
  • 支持设置proxy、memcached、fastcgi、scgi、uwsgi在后端失败时的重试次数;
  • 动态脚本语言Lua支持。扩展功能非常高效简单;
  • 支持按指定关键字(域名,url等)收集Tengine运行状态;
  • 组合多个CSS、JavaScript文件的访问请求变成一个请求;
  • 自动去除空白字符和注释从而减小页面的体积;
  • 自动根据CPU数目设置进程个数和绑定CPU亲缘性;
  • 监控系统的负载和资源占用从而对系统进行保护;
  • 显示对运维人员更友好的出错信息,便于定位出错机器;
  • 更强大的防攻击(访问速度限制)模块;
  • 更方便的命令行参数,如列出编译的模块列表、支持的指令等;
  • 支持Dubbo协议;
  • 可以根据访问文件类型设置过期时间;

OpenResty

概述

OpenResty官网 http://openresty.org/cn/

OpenResty® 是一个基于Nginx 与 Lua 的高性能Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

OpenResty® 通过汇聚各种设计精良的 Nginx 模块(是国人章亦春发起和创建,目前主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。

OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。

OpenResty强大之处就是提供许许多多的组件,开箱即用

image-20211007105825045

通过git编译成机器码缓存起来,直接执行机器码,lua git效率更高,lua git将lua虚拟机vm嵌入到进程中,lua nginx可以在设置阶段中通过lua命令嵌入到nginx中并回显。Cosocket实际上也是些协程,采用异步事件网络IO,发送和接收数据是两个流程,Cosocket实现了一个同步非阻塞的模式

#在redis存储hello的String类型key,值为heihei,下面这句代码OpenResty->redis发送并挂起当前的协程,等redis server返回数据的时候激活挂起的协程返回数据
Local val = redis:get("hello")

在Open Resty四个部分,lua nginx module 、测试集、resty lrucache redis 、工具集。lua可以随心所欲的做复杂的访问控制和安全检查,防止一些注入和xss的攻击。比如可以在Open Resty实现黑白名单(黑名单数据可以存储在mysql或者redis中,通过lua访问后端存储服务)、身份授权,还有比如很多做电商行业通过在Open Resty中将一些并发访问极大的商品详细页嵌入到nginx实现,通过Open Resty以使用同步编程方式但实际上内部确是异步非阻塞的模式访问redis获取库存数据等并构建成静态页面返回给用户,lua_shared_dict、lua_resty_lrucache、lua_resty_lock 解决缓存失效风暴问题。

Nginx实质上也即是一个网关,Kong网关也是一款基于OpenResty(Nginx + Lua模块)编写的高可用、易扩展的,由Mashape公司开源的API Gateway项目。Kong是基于NGINX和Apache Cassandra或PostgreSQL构建的,能提供易于使用的RESTful API来操作和配置API管理系统,所以它可以水平扩展多个Kong服务器,通过前置的负载均衡配置把请求均匀地分发到各个Server,来应对大批量的网络请求。

配置文件与模块化设计

60ccf064d064e2d352b2a805ae31bde8

OpenResty中,Nginx 的模块化设计使得每一个 HTTP 模块可以仅专注于完成一个独立的、简单的功能,而一个请求的完整处理过程可以使由无数个 HTTP 模块共同合作完成。

  • init 阶段:
    • init_by_lua context:http
    • init_worker_by_lua context:http
  • rewrite/access阶段:
    • 请求类型:
      • http类型:
        • set_by_lua context:server,server if, location,location if
        • rewrite_by_lua context:http,server,location,location if
        • access_by_lua context:http,server,location, location if
      • https类型:
        • ssl_certificate_by_lua context:server
        • set_by_lua context:server,server if, location,location if
        • rewrite_by_lua context:http,server,location,location if
        • access_by_lua context:http,server,location, location if
  • content阶段
    • 响应生成方式
      • lua方式
        • content_by_lua context:location,location if
        • header_filter_by_lua context:http,server,location, location if
        • body_filter_by_lua context:http,server.location,location if
      • upstream方式
        • balancer_by_lua context:upstream
        • header_filter_by_lua context:http,server,location, location if
        • body_filter_by_lua context:http,server.location,location if
      • 其他方式(如 echo)
        • header_filter_by_lua context:http,server,location, location if
        • body_filter_by_lua context:http,server.location,location if
  • log 阶段
    • log_by_lua context:http,server,location,location if

c5a6e0098ec0a75d22627b90f9427eed

beddb076d1a44a0413f673716b1543c1

CentOS安装OpenResty

# add the yum repo:
wget https://openresty.org/package/centos/openresty.repo
sudo mv openresty.repo /etc/yum.repos.d/

# update the yum index:
sudo yum check-update
sudo yum install -y openresty
sudo yum install -y openresty-resty
#安装完找到/usr/local/openresty

image-20211007135633404

HelloWorld入门示例

image-20211007140434235

#进入/usr/local/openresty/nginx/conf目录,编辑下面这段嵌入一段简单lua脚本,可以执行代码块或者脚本文件
worker_processes  1;
error_log logs/error.log;
events {
    worker_connections 1024;
}
http {
    server {
        listen 8084;
        location / {
            default_type text/html;
            content_by_lua_block {
                ngx.say("<p>hello, world</p>")
            }
        }
    }
}
##进入到openresty的nginx bin目录
cd /usr/local/openresty/nginx/sbin
##启动openresty的nginx
./nginx
#访问nginx,出现我们使用lua代码块回显的内容,至此可以开启openresty的编程大门
curl http://localhost:8084

image-20211007140818835

大厂面试题

惊群效应是怎么产生的?Nginx解决思路?

由于worker都是由master进程fork产生,所以worker都会同时监听相同端口,有IO事件所有进程同时被唤醒然后挂起,但只有一个进程能否获取到,这样多个子进程在accept建立连接时会发生争抢,带来著名的“惊群”问题。

nginx采取在同一个时刻只有一个进程在监听端口的机制,实现如下ngx_shmtx_t 是共享变量、共享内存是多进程通信最快方式,所有worker进程使用同一把锁,采用无锁的CAS实现。

为什么Nginx使用多进程不使用多线程?

Nginx使用业务场景主要是一个web服务器,有处理无状态的如http请求,如果采用多线程就需要加锁和同步问题,采用多进程可以对每次请求做到最大的隔离。

  • 不采用多线程模型管理连接
    • 无状态服务,无需共享进程内存。
    • 采用独立的进程,可以让互相之间不会影响。一个进程异常崩溃,其他进程的服务不会中断,提升了架构的可靠性。
    • 进程之间不共享资源,不需要加锁,所以省掉了锁带来的开销。
  • 不采用多线程处理逻辑业务
    • 进程数已经等于核心数,再新建线程处理任务,只会抢占现有进程,增加切换代价。
    • 作为接入层,基本上都是数据转发业务,网络IO任务的等待耗时部分,已经被处理为非阻塞/全异步/事件驱动模式,在没有更多CPU的情况下,再利用多线程处理,意义不大。并且如果进程中有阻塞的处理逻辑,应该由各个业务进行解决,比如openResty中利用了Lua协程,对阻塞业务进行了优化。

Nginx热启动是如何实现?

我们知道使用nginx -s reload就可以不停止nginx服务情况下更新配置,master负责解析配置文件、监控worker进程,worker进程负责listen,一个worker进程同时监听多个server端口,worker之间是竞争的关系,通过master将内存配置结构更新写入共享内存中下一次就可以获取最新的配置信息。

Nginx支持配置热更新和程序热更新,Nginx热更配置时,可以保持运行中平滑更新配置,具体流程如下:

  • 更新nginx.conf配置文件,向master发送SIGHUP信号或执行nginx -s reload
  • Master进程使用新配置,启动新的worker进程
  • 使用旧配置的worker进程,不再接受新的连接请求,并在完成已存在的连接后退出

image-20211007112327121

谈谈Nginx的事件驱动模型?

Worker进程在处理网络事件时,依靠epoll模型,来管理并发连接,实现了事件驱动、异步、非阻塞等特性。通常海量并发连接过程中,每一时刻(相对较短的一段时间),往往只需要处理一小部分有事件的连接即活跃连接。基于以上情况,epoll通过将连接管理与活跃连接管理进行分离,实现了高效、稳定的网络IO处理能力。其中,epoll利用红黑树高效的增删查效率来管理连接,利用一个双向链表来维护活跃连接。

image-20211007113101508

Nginx如何实现高可用?

可以通过基于VRRP虚拟路由器冗余协议的keepalived方案实现多台nginx故障自动切换,核心也即是控制同一个VIP虚拟IP在多台主机漂移。

附带十个并发编程小面试题

多进程和多线程实现并发编程各自优势和劣势是什么?

  • 多进程实现并发编程强调的是稳定性,每个进程有自己独立的地址空间,一个进程挂了不影响其他的进程,但进程间的通信方式实现还是比较麻烦的,比如管道、有名管道、信号量、消息队列、信号、共享内存、套接字等。

  • 多线程实现并发编程主要是共享进程的地址空间,一个线程挂了或者写乱数据有可能影响其他线程甚至整个应用程序,也即是常说线程安全问题,多线程交换数据比较方便,线程之间的通信也可以直接通过内存来实现。多线程其实并不是多个线程一起执行,而是因为线程之间切换的速度非常的快,所以我们看起来像不间断的执行。

  • 并发编程充分利用多核CPU或者多处理器的计算能力,从设计上方便进行业务拆分以提升应用程序性能。并发指的是多个事情,在同一时间段内同时发生了; 而并行指的是多个事情在同一时间点上同时发生了;并发的多个任务之间是互相抢占资源的; 并行的多个任务之间是不互相抢占资源的,只有在多CPU的情况中,才会发生并行;否则,看似同时发生的事情,其实都是并发执行的。

协程为什么能够实现更高的并发?

  • 进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度;线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的);协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度;因此进程和线程切换代价比较大,都是需要在内核态切换,协程在用户态切换,不用进入内核态里,所以上下文切换成本基本就没有了,协程其实就是用户态的线程。协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任。。
  • 协程非常轻量,占用内存很小,一个协程内存通常只有几十kb,执行协程也只需要极少的栈内存(大概是4~5KB),可较为轻松创建和管理成千上万的并发协程任务。
  • 协程大部分都是基于非阻塞API。

下面两种访问数据的方式那种更快,为什么?

#第一种
for(i=0;i++;i<n)
    for(j=0;j++;j<n)
    	array[i][j] = 0;   
#第二种
for(i=0;i++;i<n)
    for(j=0;j++;j<n)
    	array[j][i] = 0;    

经过测试,第一种比第二种的执行时间要快好几倍甚至几十倍

  • 需要先确定下是属于行布局还是列布局的内存布局方式,二维数组内存是行布局,因为二维数组 array 所占用的内存是连续的,内存中的数组元素的布局顺序如下图所示

    image-20211005220807299

  • 第一种方式缓存行批量读取优势。用 array【i】【j】访问数组元素的顺序,正是和内存中数组元素存放的顺序一致。当 CPU 访问 array[0][0] 时,由于该数据不在 Cache 中,于是会「顺序」把跟随其后的 3 个元素从内存中加载到 CPU Cache,这样当 CPU 访问后面的 3 个数组元素时,就能在 CPU Cache 中成功地找到数据,这意味着缓存命中率很高,缓存命中的数据不需要访问内存,这便大大提高了代码的性能

  • 第一种方式充分利用缓存行大小64k的局部性原理。访问 array【i】【j】元素时,CPU 具体会一次从内存中加载多少元素到 CPU Cache 呢?这跟 CPU Cache Line 有关,它表示 CPU Cache 一次性能加载数据的大小,可以在 Linux 里通过 coherency_line_size 配置查看 它的大小,通常是 64 个字节。

Fibonacci数列F(n)=F(n-1)+F(n-2),实现F(n)函数?

定义:f ( 0 ) = f ( 1 ) = 1 , f ( n ) = f ( n − 1 ) + f ( n − 2 ) ( n ≥ 2 )

image-20211005223827497

第一种:递归普通方法,能体现递归思维能力,算法时间复杂度是指数级增加,根据递归数而定。

int Fibnacci(int n)
{
    if(n == 0){
        return 1;
    }else if(n == 1){
        return 1;
    }else {
        return Fibnacci(n-1) + Fibnacci(n-2);
    }
}

第二种: 记忆化搜索优化后的递归算法,开辟一块空间,利用-1赋值方式,采用记忆化搜索的功能,此外动态规划基础也是记忆化搜索。

image-20211005224417471

我们从节点5开始,依次来到节点4,节点3,节点2,节点1(上图红色节点)。并且在递归返回过程中,节点2,节点3的值被计算出。此时递归返回来到节点4,f ( 4 ) = f ( 3 ) + f ( 2 ) f(4)=f(3)+f(2)f(4)=f(3)+f(2),如果是一般的递归,此时还需要重新计算f ( 2 ) f(2)f(2);但在记忆化搜索中,f ( 2 ) f(2)f(2)的值已经被m e m o memomemo记录下来了,实际并不需要继续向下递归重复计算,函数可以直接返回。最后,递归返回来到了节点5,本应重复计算的f ( 3 ) f(3)f(3)由于m e m o memomemo的记录,也不需要重复计算了。所以,记忆化搜索可以起到剪枝的作用,对于已经保存的中间结果 ,由于其记忆能力,并不需要重新递归计算了。

int dp[] = new int[100];
int Fibnacci(int n)
{
    if (n == 0 || n == 1)
        return 1;                       
    if (dp[n] != -1){
        return dp[n];
    }else{
        dp[n] = Fibnacci(n-1) + Fibnacci(n-2);
        return dp[n];
    }
}

第三种:降低复杂度,o(n)复杂度,倒着去推,从0开始计算,记忆化

int dp[] = new int[100];
int Fibnacci(int n)
{
    if (n == 1 || n == 2){
        dp[n] = n;       
    }
    else
    {
        if (dp[n] == -1){
            dp[n] = Fibnacci(n-1) + Fibnacci(n-2);
        }
    }
    return dp[n];
}

第四种:最优方法也即是数学公式计算,变成O(1)的时间复杂度:F(n)=(1/√5)*{[(1+√5)/2]^n - [(1-√5)/2]^n},如果能回答出这个那就非常不错了

image-20211005222244466

哈希表和二叉树相比,各自优缺点是什么?

  • CRUD的复杂度上红黑树为O(logn),而哈希表为O(1)
  • 哈希表做遍历性能就不太好,不能支持范围查询,关键字相邻但值却不一定相邻,而红黑树平衡比较麻烦,树的高度也可能高,通常使用B和B+树

解决哈希表冲突有哪些方法?各自优缺点是什么?

  • 开散列哈希数组列表存放有冲突的元素也叫链表法,闭散列一组哈希函数;链表法简单,闭散列要求对哈希函数设计难些;闭散列持久化优势大,大存储和分布式系统容灾比较好。
  • 哈希函数要尽量减少冲突,运算速度够快,考虑位运算和数学知识,从数学上模一个质数比如31,还要考虑业务的热度是否均匀;哈希表要考虑扩容,装载因子,哈希槽和存放元素比例,动态扩容,单机内还是跨服务器,只要是要标识数据是在老表还是新表可以解决动态扩容提供服务问题。
  • 开放散列(open hashing)/ 拉链法(针对桶链结构)
    • 优点
      • 对于记录总数频繁可变的情况,处理的比较好(也就是避免了动态调整的开销)。
      • 由于记录存储在结点中,而结点是动态分配,不会造成内存的浪费,所以尤其适合那种记录本身尺寸(size)很大的情况,因为此时指针的开销可以忽略不计了。
      • 删除记录时,比较方便,直接通过指针操作即可。
    • 缺点
      • 存储的记录是随机分布在内存中的,这样在查询记录时,相比结构紧凑的数据类型(比如数组),哈希表的跳转访问会带来额外的时间开销。
      • 如果所有的 key-value 对是可以提前预知,并之后不会发生变化时(即不允许插入和删除),可以人为创建一个不会产生冲突的完美哈希函数(perfect hash function),此时封闭散列的性能将远高于开放散列。
      • 由于使用指针,记录不容易进行序列化(serialize)操作。
  • 封闭散列(closed hashing)/ 开放定址法
    • 优点
      • 记录更容易进行序列化(serialize)操作。
      • 如果记录总数可以预知,可以创建完美哈希函数,此时处理数据的效率是非常高的。
    • 缺点
      • 存储记录的数目不能超过桶数组的长度,如果超过就需要扩容,而扩容会导致某次操作的时间成本飙升,这在实时或者交互式应用中可能会是一个严重的缺陷。
      • 使用探测序列,有可能其计算的时间成本过高,导致哈希表的处理性能降低。
      • 由于记录是存放在桶数组中的,而桶数组必然存在空槽,所以当记录本身尺寸(size)很大并且记录总数规模很大时,空槽占用的空间会导致明显的内存浪费。
      • 删除记录时,比较麻烦。比如需要删除记录a,记录b是在a之后插入桶数组的,但是和记录a有冲突,是通过探测序列再次跳转找到的地址,所以如果直接删除a,a的位置变为空槽,而空槽是查询记录失败的终止条件,这样会导致记录b在a的位置重新插入数据前不可见,所以不能直接删除a,而是设置删除标记。这就需要额外的空间和操作。
  • 哈希表相对于其他数据结构的优缺点
    • 优点
      • 记录数据量很大的时候,处理记录的速度很快,平均操作时间是一个不太大的常数。
    • 缺点
      • 好的哈希函数(good hash function)的计算成本有可能会显著高于线性表或者搜索树在查找时的内部循环成本,所以当数据量非常小的时候,哈希表是低效的。
      • 哈希表按照 key 对 value 有序枚举(ordered enumeration, 或者称有序遍历)是比较麻烦的(比如:相比于有序搜索树),需要先取出所有记录再进行额外的排序。
      • 哈希表处理冲突的机制本身可能就是一个缺陷,攻击者可以通过精心构造数据,来实现处理冲突的最坏情况。即:每次都出现冲突,甚至每次都出现多次冲突(针对封闭散列的探测),以此来大幅度降低哈希表的性能。这种攻击也被称为基于哈希冲突的拒绝服务攻击(Hashtable collisions as DOS attack)。

自旋锁有哪些特点,不适用于哪些场景?

  • 特点
    • 自旋锁顾名思义就是「线程循环地去获取锁」,一直占用 CPU 的时间片去循环获取锁,直到获取到为止。非自旋锁,也就是普通锁。获取不到锁,线程就进入阻塞状态。等待 CPU 唤醒,再去获取。
    • 对性能要求较高的情况下,在多核cpu上,一般要求你锁住那段代码运行时间比较短,并发高,微观上有可能改一点时间片就切换出去了,互斥锁锁住时间长没有关系。自旋锁一直询问,互斥锁排队等待结果通知,自旋锁线程不会切换,互斥锁线程会切换,有代价。互斥锁休眠等待和自旋锁忙等待。
    • 死循环然后利用cpu提供pause指令,可以省电。
    • 阻塞 & 唤醒线程都是需要资源开销的,如果线程要执行的任务并不复杂。这种情况下,切换线程状态带来的开销比线程执行的任务要大;而很多时候,我们的任务往往比较简单,简单到线程都还没来得及切换状态就执行完毕。这时我们选择自旋锁明显是更加明智的。所以,自旋锁的好处就是「用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销」
  • 不适用场景
    • 并发高,经常出现自旋不成功。
    • 线程执行的同步任务过于复杂,耗时比较长

读写锁用于解决什么问题?读优先和写优先是指什么?

  • 读写锁适合于对数据结构的读次数比写次数多得多的情况.因为,读模式锁定时可以共享,以写 模式锁住时意味着独占,所以读写锁又叫共享-独占锁.
  • 读写锁是用来解决读者写者问题的,读操作可以共享,写操作是排他的,读可以有多个在读,写只有唯一个在写,同时写的时候不允许读
  • 如果你能区分对于一个共享数据是读还是写就可以使用读写锁,分为两把锁,读锁共享锁,多个资源可以同时访问,写锁是独占锁,比较适合读多写少的场景。优先会导致其他饿死。ReentranctReadWriteLock公平读写锁。弄一个队列排队效率第一点但不会饿死。可以用互斥锁也可以用自旋锁。
  • 读优先,即使写者发出了请求写的信号,但是只要还有读者在读取内容,就还允许其他读者继续读取内容,直到所有读者结束读取,才真正开始写。
  • 写优先,如果有写者申请写文件,在申请之前已经开始读取文件的可以继续读取,但是如果再有读者申请读取文件,则不能够读取,只有在所有的写者写完之后才可以读取

怎样将文件快速发送客户端?

  • 内存拷贝次数太多了,磁盘文件先拷贝操作系统高速缓冲区,再从高速缓冲区拷贝用户态的内存如32k的缓冲区,然后再拷贝操作系统内核socket缓冲区,然后再拷贝网卡上,四次拷贝。
  • 切换次数多,掉一次read或者send都是两次切换。
  • 零拷贝 sendfile ,拷贝次数减少了,切换次数也减少了,充分使用内核tcp缓冲区一般是1M多可配。

image-20211005231948486

相比堆,为什么栈上分配的对象速度更快?

  • 每个线程都有一个独立栈,都有函数在使用调用栈,栈是每个线程独立不需要加锁。中的数据占内存大小在编译时是确定的,比如一个int类型就占4B,所以变量地址好计算,所以分配和销毁和访问速度都比较快.
  • 内存预分配,比如8M已经分配好了,栈的问题生命周期比较有限,函数退出就没有了,大小受限就比如递归函数递归多层会报栈溢出。
  • 的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享,基本数据类型存储在“栈”中,对象引用类型实际存储在“堆”中,在栈中只是保留了引用内存的地址值栈里放的是地址,堆里可以放数据也可以放地址(想象下堆里的东西也有可能指向别的地方);但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

后续我们再找时间深入分析Nginx源码、Nginx的C模块化开发、Nginx高阶及高可用实战,OpenResty编程实战。



这篇关于Nginx后端开发人员必学神器-并发编程经典之作剖析和名企热点面试的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程