0。前话 俄罗斯年轻程序员IgorSysoev为了解决所谓C10Kproblem,也就是以前的WebServer不能支持超10k并发请求的问题,在2002年开启了新的WebServer的开发。 Nginx2004年在2clauseBSD证书下发布于众,根据2021年3月WebServer的调查,Nginx持有35。3的市场占有率,为4。196亿网站提供服务。 感谢DigitalOcean公司的NGINXConfig项目,提供了很多写好的Nginx模板供下载,这样就可以在不理解Nginx配置的情况下复制粘贴配置Nginx。 这里不是说复制粘贴是不对的,而是如果只是复制粘贴并不理解的话,迟早会出问题。所以,你必须理解Nginx的配置,通过学习本文,你能够:理解工具生成或者别人配置的Nginx。从0到1配置Web服务器、反向代理服务器和负载均衡服务器。优化Nginx获取最大性能。 学习本文需要有一定的Linux基础,会执行例如ls、cat等Linux命令,还需要你对前后端有一定的了解,不过这些对前端或者后端程序员都很容易。1。Nginx基本介绍 Nginx是一个高性能的Web服务器,着眼于高性能、高并发和低资源消耗。尽管Nginx作为一个Web服务器被大家所熟知,它另外的一个核心功能是反向代理。 Nginx不是市场上唯一的Web服务器,它最大的竞争对手ApacheHTTPServer(httpd)在1995年就发布了。人们在选择Nginx作为Web服务器时候,基于下面两点考虑:支持更高的并发。用更少的硬件资源提供静态文件服务。 Nginx和Apache谁更好的争论没有意义,如果想了解更多Nginx和Apache的区别可以参考JustinEllingwood的文章。 关于Nginx对请求处理的新特点,引用Justin的文章解释如下: Nginx在Apache之后出现,更多认识到网站业务扩大之后面临的并发性问题,所以从一开始就设计为异步、非阻塞和事件驱动连接处理的算法。 Nginx工作时候会设定worker进程(workerprocess),每一个worker进程都能够处理数千个连接。worker进程通过fastlooping的机制来不断轮询处理事件。将具体处理请求的工作和连接解耦能够让每一个worker进程仅当新的事件触发的时候将其与一个连接关联。 Nginx基本工作原理图: Nginx之所以能够在低资源消耗的情况下高性能提供静态文件服务,是因为它没有内置动态编程语言处理器。当一个静态文件请求到达后,Nginx就是简单的响应请求文件,并没有做什么额外的处理。 这不是说Nginx不能够整合动态编程语言处理器,它可以将请求任务代理到独立的进程上,例如PHPFPM、Node。js或者Python。一旦第三方进程处理完请求,再将响应代理回客户端,工作如图: 2。怎么安装nginx Nginx的安装网上示例很多,这里以Ubuntu为例:更新源sudoaptupdatesudoaptupgradey安装sudoaptinstallnginxy复制代码 这种方式安装Nginx成功之后,Nginx会注册为systemd系统服务,查看服务:sudosystemctlstatusnginx如果没有注册为systemd服务,可以用service查看试下sudoservicenginxstatus复制代码 Nginx的配置文件经常放在etcnginx目录中,默认的配置端口是80,如果启动成功,可以访问得到页面: 恭喜!Nginx安装成功了!3。Nginx配置文件管理 Nginx为静态或者动态文件提供服务,具体怎么样提供服务是由配置文件设置的。 Nginx的配置文件以。conf结尾,常常位于etcnginx目录中。访问etcnginx目录:cdetcnginxlslhdrwxrxrx2rootroot4。0KApr212020conf。drwrr1rootroot1。1KFeb42019fastcgi。confrwrr1rootroot1007Feb42019fastcgiparamsrwrr1rootroot2。8KFeb42019koiutfrwrr1rootroot2。2KFeb42019koiwinrwrr1rootroot3。9KFeb42019mime。typesdrwxrxrx2rootroot4。0KApr212020modulesavailabledrwxrxrx2rootroot4。0KApr1714:42modulesenabledrwrr1rootroot1。5KFeb42019nginx。confrwrr1rootroot180Feb42019proxyparamsrwrr1rootroot636Feb42019scgiparamsdrwxrxrx2rootroot4。0KApr1714:42sitesavailabledrwxrxrx2rootroot4。0KApr1714:42sitesenableddrwxrxrx2rootroot4。0KApr1714:42snippetsrwrr1rootroot664Feb42019uwsgiparamsrwrr1rootroot3。0KFeb42019复制代码 该目录中的etcnginxnginx。conf就是Nginx的主配置文件。如果你打开这个配置文件,会发现很多内容,不要害怕,本文就是一点一点地要学会它。 在进行配置文件修改的时候,不建议直接修改etcnginxnginx。conf,可以将之备份之后再修改:重命名文件sudomvnginx。confnginx。conf。backup新建配置文件sudotouchnginx。conf复制代码4。Nginx配置为一个基本的WebServer 这一部分,将会从零一步步学习Nginx配置文件的书写,目的是了解Nginx配置文件的基本语法和基本概念。4。1写第一个配置文件 vimetcnginxnginx。conf打开配置文件并更新内容:events{}http{server{listen80;servernamelocalhost;return200Bonjour,monami!;配置重定向return302https:www。baidu。comrequesturi;}}复制代码 重启Nginx并访问,你会得到如下信息:curlihttp:127。0。0。1HTTP1。1200OKServer:nginx1。18。0(Ubuntu)Date:Sat,19Feb202208:31:59GMTContentType:textplainContentLength:21Connection:keepaliveBonjour,monami!复制代码4。2校验、重载Nginx配置文件 Nginx的配置文件是否正确可以通过t参数校验:sudonginxtnginx:theconfigurationfileetcnginxnginx。confsyntaxisoknginx:configurationfileetcnginxnginx。conftestissuccessful复制代码 如果有相关的语法错误,上述命令输出结果会有相关提示。 如果你想改变Nginx的相关状态,例如重启、重载等,可以有三种办法。一是通过s(signal)参数向Nginx发送信号;二是使用系统服务管理工具systemd或者service等;三是使用kill命令对Linux进程操作。 向Nginx发送信号 nginx信号:nginxsreloadquitstopreopen,分别表示重载配置文件、优雅停止Nginx、无条件停止Nginx和重新打开log文件。 所谓的优雅停止Nginx,是指处理完目前的请求再停止;而无条件停止Nginx,相当于kill9,进程直接被杀死。 系统服务管理Nginx使用systemctlsudosystemctlstartrestartstopnginx或者使用servicesudoservicenginxstartrestartstop复制代码 kill命令杀死进程并手动启动杀死主进程及各子进程sudokillTERMMASTERPID指定配置文件启动Nginxsudousrsbinnginxcetcnginxnginx。conf复制代码4。3理解Nginx配置文件中的Directives和Contexts Nginx的配置文件虽然看起来只是简单的配置文本,但它是包含语法的。实际上配置文件中的内容都是Directives。Directives分为两种:SimpleDirectivesBlockDirectives SimpleDirectives:包含名称和空格,以分号(;)结尾。例如listen、return等。 BlockDirectives:包裹在{}中,{}由SimpleDirectives组成,称之为Contexts。 Nginx配置中核心的Contexts:events{}:总体配置nginx如何处理请求,只能在配置文件中出现一次。http{}:配置nginx如何处理http或者https请求,只能在配置文件中出现一次。server{}:内嵌在http{}中,用来配置一个独立主机上指定的虚拟主机。http{}可以配置多个server{},表示多个虚拟主机。main:上述3个Contexts之外的配置都在该Contex上。 在主机上设置不同的虚拟主机(多个server{}、相同servername),监听不同的端口(listen不同):http{server{listen80;servernamelocalhost;return200hellofromport80!;}server{listen8080;servernamelocalhost;return200hellofromport8080!;}}复制代码 不同的虚拟主机,监听同一个端口(多个server{}、不同servername),监听同一个端口(listen相同): 这种情况必须用域名,Nginx会将请求头中Host信息取出来和服务端配置servername做匹配,匹配到哪个就就进入到那个处理块中。http{server{listen8088;servernamelibrary。test;return200yourlocallibrary!;}server{listen8088;servernamelibrarian。library。test;return200welcomedearlibrarian!;}}复制代码 当访问不同的域名时,会返回不同的结果:curlihttp:library。test:8088HTTP1。1200OKServer:nginx1。18。0(Ubuntu)Date:Sun,20Feb202208:02:20GMTContentType:applicationoctetstreamContentLength:21Connection:keepaliveyourlocallibrary!curlihttp:librarian。library。test:8088HTTP1。1200OKServer:nginx1。18。0(Ubuntu)Date:Sun,20Feb202208:04:26GMTContentType:applicationoctetstreamContentLength:24Connection:keepalivewelcomedearlibrarian!复制代码 这样能成功的提前是指定的域名解析到同一个IP,或者在本地的hosts文件中配置好域名进行本地测试:172。19。146。188library。testlibrarian。library。test复制代码 注意,这里return这个Directive后面跟两个参数,一个是状态码,一个是返回的文本信息,文本信息要用引号引起来。4。4使用Nginx作为静态文件服务器 更新Nginx配置文件如下:events{}http{server{listen8088;servernamelocalhost;rootusrsharenginxhtml;}}复制代码 这里对Nginx默认的展示页面做了修改,在文件usrsharenginxhtmlassetsmystyle。css写入p{background:red;}并在html文件中引入该css,这样正常情况段落的背景会变成红色。 访问页面,展示的是index。html,但是段落的背景色没有生效。debug一下css文件:curlihttp:fengmengzhao。hypc:8088assetsmystyle。cssHTTP1。1200OKServer:nginx1。18。0(Ubuntu)Date:Sun,20Feb202208:43:58GMTContentType:textplainContentLength:27LastModified:Sun,20Feb202208:38:54GMTConnection:keepaliveETag:6211fe1e1bAcceptRanges:bytesp{background:red;}复制代码 注意,这里响应头信息ContentType是textplain,而不是textcss。也就是说Nginx将css文件做为一个普通的文本提供服务,而没有当做stylesheet,浏览器自然就不会渲染样式。 本文会在本地hosts文件增加域名解析,所以会在示例中看到对域名请求。在操作本文示例时,要根据自己环境对ip(域名)或者端口做相应修改。4。5Nginx中处理静态文件类型解析 实际上这里涉及到Nginx对静态文件类型解析的处理,默认不进行任何设置情况下,Nginx认为文本文件的类型是textplain。 修改配置文件如下:events{}http{types{texthtmlhtml;textcsscss;}server{listen8088;servernamelocalhost;rootusrsharenginxhtml;}}复制代码 重新访问页面,样式正常,mystyle。css文件的response头ContentType为textcss: 这里在http{}中引入了types{},通过文件的后缀映射文件的类型。需要注意,如果没有types{},nginx会认为。html文件的类型是texthtml,但是一旦引入types{},nginx只会解析定义的类型映射。所以这里引入types{}后,不能只定义css的类型映射,同样要显式定义html的类型映射,否则nginx会将html解析为普通文本文件。4。6Nginx子配置引入 手动在http{}中增加types{}来映射文件类型对于小项目还可以,对大型项目来说手动配置就太繁琐了,Nginx提供了默认的解析映射(常常在etcnginxmime。types文件中),可以通过include语法将子配置引入配置文件中。 修改配置如下:events{}http{includeetcnginxmime。types;server{listen80;servernamelocalhost;rootusrsharenginxhtml;}}复制代码 重启Nginx,自定义的css文件能够正常展示。5。Nginx的动态路由 上面的示例非常简单,访问root定义目录下的文件,存在就返回,不存在就返回默认404页面。 接下来学习Nginx的location动态路由用法,包括重定向、重写和tryfilesDirective。 所谓的动态路径就是用户访问的路径到达Nginx后,Nginx如何匹配访问内容。 LocationMatches 修改配置文件,如下:events{}http{server{设置默认的ContentTypetexthtml,否则将以流的方式下载defaulttypetexthtml;设置字符编码为utf8,否则页面会乱码charsetutf8;listen80;servernamelocalhost;前缀匹配,示例:http:fengmengzhao。hypc:8088agathalocationagatha{return200前缀匹配MissMarple。HerculePoirot。;}完全匹配,示例:http:fengmengzhao。hypc:8088agathalocationagatha{return200完全匹配MissMarple。HerculePoirot。;}正则匹配,默认大小写敏感,示例:http:fengmengzhao。hypc:8088agatha01234正则匹配的优先级要高于前缀匹配,低于优先前缀匹配locationagatha〔09〕{return200正则匹配,大小写敏感MissMarple。HerculePoirot。;}正则匹配,大小写不敏感,示例:http:fengmengzhao。hypc:8088AGatHa01234locationagatha〔09〕{return200正则匹配,大小写不敏感MissMarple。HerculePoirot。;}优先前缀匹配,示例:http:fengmengzhao。hypc:8088Agatha01234在前缀匹配前加即可转化为优先前缀匹配locationAgatha{return200优先前缀匹配MissMarple。HerculePoirot。;}}}复制代码 匹配规则总结: 匹配 关键字 完全 优先前缀 正则 或者 前缀 None 如果一个请求满足多个配置的匹配,正则匹配的优先级大于前缀匹配,而优先前缀匹配的优先级大于正则匹配,完全匹配优先级最高。 nginx中的变量(Variables) 设置变量:setlt;variablenamevariablevalue;setnameFarhansetage25setisworkingtrue复制代码 变量类型:StringIntegerBoolean 除了自定义变量外,nginx有内置的变量,参考nginx。orgendocsvar。 例如,如下配置中使用内置变量:events{}http{server{listen80;servernamelocalhost;return200HosthosturiArgsargs;}}curlhttp:localhostuser?nameFarhanHostlocalhostURIuserArgsnameFarhan复制代码 上面使用了host、uri和args内置变量,分别表示主机名、请求相对路径和请求参数。变量可以作为值赋值给自定义变量,例如:events{}http{server{listen80;servernamelocalhost;setnameargname;argquerystringnamereturn200Namename;}}复制代码 上面出现了arg内置变量,使用arg可以获取args变量中指定的querystring。 重定向(Redirects)和重写(Rewrites) nginx中的重定向和其他平台上见到的重定向一样,response返回3xx的状态码和location头信息。如果是在浏览器中访问,浏览器会自动重新发起location指定的请求,地址栏url也会发生改变。 重定向示例:events{}http{includeetcnginxmime。types;server{listen80;servernamelocalhost;rootusrsharenginxhtml;locationindexpage{return307https:fengmengzhao。github。io;}locationaboutpage{return307https:fengmengzhao。github。ioabout;}}}curlIhttp:localhostaboutpageHTTP1。1307TemporaryRedirectServer:nginx1。18。0(Ubuntu)Date:Mon,21Feb202211:47:42GMTContentType:texthtml;charsetutf8ContentLength:180Connection:keepaliveLocation:https:fengmengzhao。github。ioabout复制代码 重写(Rewrites)和重定向不一样,重写内部转发了请求,地址栏不会发生改变。示例如下:events{}http{includeetcnginxmime。types;server{listen80;servernamelocalhost;rootusrsharenginxhtml;rewriteimageassetsgenerate。png;}}curlihttp:localhostimageHTTP1。1200OKServer:nginx1。18。0(Ubuntu)Date:Mon,21Feb202211:56:42GMTContentType:imagepngContentLength:144082LastModified:Sun,20Feb202208:35:21GMTConnection:keepaliveETag:6211fd49232d2AcceptRanges:bytesWarning:Binaryoutputcanmessupyourterminal。UseoutputtotellWarning:curltooutputittoyourterminalanyway,orconsideroutputWarning:FILEtosavetoafile。复制代码 如果在浏览器上访问http:fengmengzhao。hypc:8088image,即可展示图片。 tryfiles尝试多个文件 tryfiles示例:events{}http{includeetcnginxmime。types;server{listen80;servernamelocalhost;rootusrsharenginxhtml;tryfilesassetsxxx。jpgnotfound;locationnotfound{return404sadly,youvehitabrickwallbuddy!;}}}复制代码 示例查找assetsxxx。jpg文件,如果不存在就查找notfound路径。 tryfiles常常和uri内置变量一起使用:events{}http{includeetcnginxmime。types;server{listen80;servernamelocalhost;rootusrsharenginxhtml;tryfilesurinotfound;当访问http:localhost返回404这里表示,当访问uri文件不存在时,尝试uri作为一个目录访问tryfilesuriurinotfound;locationnotfound{return404sadly,youvehitabrickwallbuddy!;}}}复制代码6。Nginx的日志 日志位置(常常在varlognginx):lslhvarlognginxrwr1wwwdataadm0Apr2507:34access。logrwr1wwwdataadm0Apr2507:34error。log复制代码 删除日志文件并reopenNginx:deletetheoldfilessudormvarlognginxaccess。logvarlognginxerror。logcreatenewfilessudotouchvarlognginxaccess。logvarlognginxerror。logreopenthelogfilessudonginxsreopen复制代码 这里如果采用上面删除文件后再创建文件的方法清空日志,就需要nginxsreopen重载Nginx,否则新的日志文件不会被写入日志,因为Nginx的输出流指向还是之前删除的日志文件。实际上这里想清空日志文件可以采用echovarlognginxaccess。log的方法,这样就不用reopenNginx了。 访问Nginx并查看日志:curlIhttp:localhostHTTP1。1200OKServer:nginx1。18。0(Ubuntu)Date:Sun,25Apr202108:35:59GMTContentType:texthtmlContentLength:960LastModified:Sun,25Apr202108:35:33GMTConnection:keepaliveETag:608529d53c0AcceptRanges:bytessudocatvarlognginxaccess。log192。168。20。20〔25Apr2021:08:35:590000〕HEADHTTP1。12000curl7。68。0复制代码 默认情况下,任何访问的日志都会记录在access。log文件中,也可以通过accesslogDirective来自定义路径:events{}http{includeetcnginxmime。types;server{listen80;servernamelocalhost;location{日志会在默认配置日志文件输出return200thiswillbeloggedtothedefaultfile。;}locationadmin{日志会输出在varlogsnginxadmin。log文件中accesslogvarlogsnginxadmin。log;return200thiswillbeloggedinaseparatefile。;}locationnologging{禁止日志输出accesslogoff;return200thiswillnotbelogged。;}}}复制代码 在location{}中可以自定义access。log的路径,也可以用accesslogoff来关闭log输出。 同样,errorlog也可以自定义Nginx的error。log路径:events{}http{includeetcnginxmime。types;server{listen80;servernamelocalhost;errorlogvarlogerror。log;return后面只能跟两个参数,这里是为了让Nginx报错,输出错误日志return200。。。。。。;}}复制代码 使用nginxsreload重载Nginx:sudonginxsreloadnginx:〔emerg〕invalidnumberofargumentsinreturndirectiveinetcnginxnginx。conf:14复制代码 访问错误日志文件,有同样的错误信息:sudocatvarlognginxerror。log2021042508:35:45〔notice〕41694169:signalprocessstarted2021042510:03:18〔emerg〕84348434:invalidnumberofargumentsinreturndirectiveinetcnginxnginx。conf:14复制代码 Nginxerror日志信息是有级别的:debug:能帮忙排查哪里出错了。info:可以了解但是不必要的信息。notice:比info更值得了解的信息,但不知道也没什么。warn:意料之外的事情发生了,哪里出问题了,但还能工作。error:什么失败了的信息。crit:严重问题,急需解决。alert:迫在眉睫。emerg:系统不稳定,十万火急。 默认情况下,Nginx记录所有级别的Error信息,可以通过errorlog第二个参数覆写。如果要设置最低级别的日志输出为warn,更新配置文件如下:events{}http{includeetcnginxmime。types;server{listen80;servernamelocalhost;errorlogvarlogerror。logwarn;return200。。。。。。;}}复制代码 重载Nginx并查看日志:catvarlognginxerror。log2021042511:27:02〔emerg〕1276912769:invalidnumberofargumentsinreturndirectiveinetcnginxnginx。conf:16复制代码 这里可以看到,没有输出之前的〔notice〕日志了。7。Nginx作为反向代理服务器7。1什么是反向代理? 所谓的反向代理,首先是一种代理,是客户端和服务端之外的第三方。把正向代理(Forwardproxy)和反向代理(Reverseproxy)比较起来看就很容易理解。 正向代理一般代理的是客户端,用户(客户端)是知道代理存在(一般是客户端配置的)。客户端对目标服务的请求会经由代理转发并将目标服务响应返回给客户端。常见的VPN代理、浏览器(设置)代理、Git(设置)代理和Fiddler抓包软件等都是正向代理。 本文中所述的目标服务、被代理的上游服务、被代理的服务、服务端均指代proxypass配置的被代理的服务。代理服务、代理服务的服务端指代的是Nginx提供的代理服务。 正向代理示意图: 反向代理一般代理的是服务端,客户端直接和代理服务打交道(如果有反向代理的话),而对被代理的服务一无所知。客户端请求到达代理服务之后,代理服务再将请求转发到被代理的服务并将响应返回给客户端。 反向代理示意图: 上面二图,可以理解蓝色背景的服务是相互知晓的。 Nginx作为反向代理时,处在客户端和服务端之间。客户端发送请求到Nginx(反向代理),Nginx将请求发送给服务端。一旦服务端处理完请求,会将结果返回给Nginx,Nginx再将结果返回给客户端。在这整个过程中,客户端并不知道实际上谁处理了请求(真正的处理请求并产生响应,而不是代理)。7。2反向代理基本原理 笔者刚接触反向代理的时候,感觉这是一个很神奇的事情。进行简单的配置就能将第三方的网站代理到自己的主机上吗? 实际上,不尽然。有些网站能够将主页代理过来,但功能不能完全使用;有些代理过来样式、图片等加载会出问题。只有理解了个中原理,才能够解释各种各样的情况。 所谓的反向代理就是将客户端发送来的请求转发给实际处理请求服务端(proxypass指定的服务端),服务端响应之后,再将响应代理回客户端。 既然是代理,就不仅仅简单的只做转发,在代理收到客户端请求后,准备转发到指定代理服务端之前,会对请求的header信息进行重写,例如重写规则如下(反向代理header重写章节会对规则做详细介绍):值为空的header不会进行转发;header的key中包含有下划线的不会进行转发。默认改写Host和Connection两个header,分别为:Host:proxyhost、Connection:close。 如果代理服务器只是转发,还要什么代理?就像生活中的代理一样,会提供增值服务,什么事情都帮你搞定。 反向代理就是将客户端的请求,重写header信息之后,在代理服务的服务端转发请求到被代理服务,被代理服务处理请求将响应返回给代理服务,代理服务进而转发响应回客户端。 代理服务转发的请求是代理服务端重新发起,因此在客户端的浏览器或者Fiddler工具进行网络抓包是抓不到的。要看具体的代理发起网络请求需要用Wireshark工具抓包代理服务器对应的网卡。 别理解复杂了,就是客户端代理服务被代理服务。Nginx的反向代理默认不会改变响应的内容,被代理服务响应页面的绝对引用(assetsimageabc。jpg)、相对引用(assetsimageabc。jpg)或者图床引用(https:image。comimageabc。jpg)代理回客户端的时候不会发生改变。这些引用在客户端解析html时候会重新发起请求,如果请求指向了代理服务,会同样进行请求代理服务被代理服务这个流程。 表示请求,表示响应。 有些时候代理之后之所以情况变得复杂,是因为被代理服务存在重定向或者权鉴的约束产生的,而代理的过程就是请求代理服务被代理服务这么简单,并且不会改变被代理服务的响应内容。7。3反向代理基本配置 看一个简单的反向代理配置:events{}http{includeetcnginxmime。types;server{listen80;servernamelocalhost;location{proxypasshttps:bbs。tianya。cn;}}}复制代码 代理后页面如下: 因为是http反向代理了https,运营商竟然还在右下角插入了广告(https:bbs。tianya。cn不会被插入广告)。 proxypass能够简单的将客户端请求转发给第三方服务端并反向代理响应结果返回给客户端。 这只是简单的代理,如果你要反向代理一个接口并且使用WebSocket,那么就要覆写header信息:WebSocket需要http1。1,默认是http1。0proxyhttpversion1。1;覆写headerUpgrade为httpupgrade的值,该值为Nginx获取客户端请求过来的Upgrade头信息值proxysetheaderUpgradehttpupgrade;覆写headerConnection为upgradeproxysetheaderConnectionupgrade;复制代码7。4Nginx反向代理地址匹配规则 客户端发送给Nginx的请求,究竟Nginx会怎样拼接到proxypass指定的上游服务呢?Nginx有一定的规则:如果proxypass代理的上游服务是域名加端口(没有端口时默认端口为80或者443),那么客户端请求的代理路径会直接拼到上游服务地址上。示例,proxypasshttp:redis。cn就只是对域名(和端口)的代理。如果proxypass代理的上游服务有请求路径,那么客户端请求的代理路径将会是把客户端请求路径裁剪掉匹配路径后再拼到上游服务地址上。示例,proxypasshttp:redis。cn或者proxypasshttp:redis。cncommands是有请求路径的代理。 上面1、2分别定义为情况1和情况2,下面中有引用。events{}http{includeetcnginxmime。types;server{listen8088;servernamelocalhost;location{情况1,客户端路径和代理路径映射:http:fengmengzhao。hypc:8088commandshttp:redis。cncommandsproxypasshttp:redis。cn;}locationredis{情况1,客户端路径和代理路径映射:http:fengmengzhao。hypc:8088rediscommandshttp:redis。cnrediscommandsproxypasshttp:redis。cn;}locationredis{情况2,客户端路径和代理路径映射:http:fengmengzhao。hypc:8088rediscommandshttp:redis。cncommandsproxypasshttp:redis。cn;}locationredis{情况2,客户端路径和代理路径映射:http:fengmengzhao。hypc:8088rediscommandshttp:redis。cncommandsproxypasshttp:redis。cn;}locationrediscommands{情况2,客户端路径和代理路径映射:http:fengmengzhao。hypc:8088rediscommandshttp:redis。cncommandshttp:fengmengzhao。hypc:8088rediscommandskeys。htmlhttp:redis。cncommandskeys。htmlproxypasshttp:redis。cncommands;}locationrediscommands{情况2,客户端路径和代理路径映射:http:fengmengzhao。hypc:8088rediscommandskeys。htmlhttp:fengmengzhao。hypc:8088commandskeys。htmlproxypasshttp:redis。cncommands;}locationrediscommands{情况2,客户端路径和代理路径映射:http:fengmengzhao。hypc:8088rediscommandskeys。htmlhttp:fengmengzhao。hypc:8088commandskeys。htmlproxypasshttp:redis。cncommands;}locationrediscommands{情况2,客户端路径和代理路径映射:http:fengmengzhao。hypc:8088rediscommandskeys。htmlhttp:fengmengzhao。hypc:8088commandskeys。htmlproxypasshttp:redis。cncommands;}}}复制代码 总结客户端请求和代理端转发请求的对应关系,如下: 匹配路径 proxypass 客户端请求 代理后请求 redis。cn redis redis。cn redis redis redis。cn redis。cn redis redis。cn redis redis redis。cn rediscommands commands redis redis。cn redis redis redis。cn rediscommands commands rediscommands redis。cncommands rediscommands commands rediscommands redis。cncommands rediscommandskeys。html commandskeys。html rediscommands redis。cncommands rediscommands commands rediscommands redis。cncommands rediscommandskeys。html commandskeys。html rediscommands redis。cncommands rediscommands commands rediscommands redis。cncommands rediscommandskeys。html commandskeys。html rediscommands redis。cncommands rediscommands commands rediscommands redis。cncommands rediscommandskeys。html commandskeys。html 表格中为空表示只有域名端口的访问,没有请求路径。 代理后的请求在客户端看不到网络请求,可以用tcpdump抓包代理服务所在主机的网卡生成。cap文件,并在Wireshark中查看具体请求。 tcpdump监听命令:172。19。146。188是Nginx代理IP;121。42。46。75是被代理上游服务IP,也就是redis。cn域名的解析IPech0是172。19。146。188使用的网卡IPsudotcpdumpieth0tcpport8088andhost172。19。146。188orhost121。42。46。75c100nvvvwoptnginx2。cap复制代码 启动后,访问代理服务,数据包经过网卡eth0就会被捕捉到。将nginx2。cap文件在Wireshark中打开即可查看具体网络包。 以下表请求为demo,抓包获取代理请求。 请求如下: 匹配路径 proxypass 客户端请求 代理后请求 rediscommands redis。cncommands rediscommandskeys。html commandskeys。html 抓取请求包如图: 7。5反向代理header重写 Nginx在服务端代理的请求和客户端发的请求不是完全相同的,主要的不同在于请求的header信息,Nginx会对客户端发过来的请求的header进行修改,规则如下:Nginx删除空值的header。Nginx这样做是因为空值的Header发送服务端也没有意义,当然利用这一点,如果想让代理不发送某个header信息,可以在配置中用proxysetheader覆写header值为空。Nginx默认header的名称中如果包含下划线是无效header。这个行为也可以通过配置文件中设置underscoresinheaderson来开启,否则任何含有的header信息都不会被代理到目标上游服务。代理的Host头信息会被覆写为变量proxyhost,该变量是被代理上游服务的IP(或域名)加端口,其值在proxypass中定义。代理的Connection头信息会被覆写为close,该请求头告诉被代理上游服务,一旦服务端响应代理请求,该连接就会被关闭,不会被持久化(persistent)。 第3点的Host头信息覆写在Nginx的反向代理中是比较重要的,Nginx定义不同的变量代表不同的值:proxyhost:上面提过了,是默认反向代理覆写的header,其值是proxypass定义的上游服务IP和端口。httphost:是Nginx获取客户端请求的Host头。Nginx使用http作为前缀加上客户端header名称的小写,并将符号用替换拼接后就代表客户端实际请求的头信息。Host:常常和httphost一样,但是会将httphost转化为小写(域名情况)并去除端口。如果httphost不存在或者是空的情况,host的值等于Nginx配置中servername的值。 Nginx可以通过proxysetheader来覆写客户端发送过来请求的header再转发。除了上面说的Host头比较重要,经常用到的header还有:XForwardedProto:配置值schema。告诉上游被代理服务,原始的客户端请求是http还是https。XRealIP:配置值remoteaddr。告诉代理服务客户端的IP地址,辅助代理服务做出某种决定或者日志输出。XForwardedFor:配置值proxyaddxforwardedfor。包含请求经过每一次代理的IP。7。6反向代理试试,tcpdump抓包解析,探个中究竟 笔者也一直在理解这个Host在http请求中的作用,正常当一个http请求发送之后,tcp连接已经指定了IP和端口,那还需要Host头信息做什么呢? 首先,MDNWebDocs对Host头的说明: 所有HTTP1。1请求报文中必须包含一个Host头字段。对于缺少Host头或者含有超过一个Host头的HTTP1。1请求,可能会收到400(BadRequest)状态码。 那Nginx反向代理默认对Host头覆写为proxyhost的作用是什么,如果改写为其他会怎么样?用tcpdump工具抓包一探究竟。 看示例,反向代理http:redis。cn,配置如下(情况一):events{}http{includeetcnginxmime。types;server{listen8088;servernamelocalhost;location{proxypasshttp:redis。cn;}}复制代码 最普通的反向代理设置,没有进行任何header覆写。用tcpdump工具监控网卡:先用ping或者nslookup找到redis。cn的IP,这里找到是121。42。46。75这里host121。42。46。75,代表过滤指定IP的包。不过滤的话包会很多,不太好看c100捕捉到100个包,会自动退出并生产文件需要将cap文件Wireshark中打开sudotcpdumpieth0host121。42。46。75c100nvvvwoptnginxredis1。cap复制代码 这时候访问http:fengmengzhao。hypc:8088,代理页面很正常: Nginx服务端的tcpdump包也抓到了: 用Wireshark查看包请求: 修改Nginx配置proxysetheaderHosthttphost(情况二):events{}http{includeetcnginxmime。types;server{listen8088;servernamelocalhost;location{proxypasshttp:redis。cn;proxysetheaderHosthttphost;}}复制代码 访问http:fengmengzhao。hypc:8088,代理页面: 这是什么页面?如果直接用redis。cn的IP地址http:121。42。46。75访问,得到同样的页面。为什么? 看看抓到的包情况: 从tcpdump抓包来看,该响应是正常从服务端响应的。那为何不同的Host头返回的页面会不同呢? 情况二设置proxysetheaderhttphost之后Nginx代理请求的Host为客户端请求的Host(fengmengzhao。hypc:8088),而情况一的Host为上游被代理服务的Host(redis。cn)。可能在redis。cn该域名对应的主机121。42。46。75不止提供一个80端口的服务。 这种在一个主机上提供多个域名服务(端口相同)称之为虚拟主机。理解Nginx配置文件中的Directives和Contexts章节中提到的Nginx可以设置不同域名同一端口的虚拟主机就可以实现这种情况。另外,Apache也支持配置不同域名的虚拟主机。这两种情况,归根结底都是在请求到达服务端后,服务端会获取请求中的Host头信息并匹配到不同的虚拟服务。 所以,Nginx反向代理中对Host头信息的覆写要看上游被代理服务是否有特殊需要到该信息。如果没有特殊实现上需要,默认的proxyhost就可以;如果是特殊的实现机制,就要小心对待。 这里的特殊需要是例如上面虚拟主机那种情况,Host头信息在HTTP1。1中是必须带的。7。7反向代理处理相对路径问题 基于上面讲解的对反向代理的理解,我们处理一下实际工作中遇到的问题,增加对Nginx反向代理的认识。 假设被代理的上游服务是一个简单的静态页面(http:127。0。0。1:80),页面中引用了两个相同的图片,分别是绝对引用assetsgenerate。png和相对引用assetsgenerate。png。我们进行如下的反向代理配置:events{}http{includeetcnginxmime。types;server{listen8088;servernamelocalhost;locationstatic{proxypasshttp:127。0。0。1;}}复制代码 这时候,访问http:fengmengzhao。hypc:8088static会发现其中绝对引用(assetsgenerate。png)的图片加载失败,通过浏览器网络查看,其客户端加载的请求是http:fengmengzhao。hypc:8088assetsgenerate。png。该请求在我们的配置中会默认寻找root匹配(一般默认是usrsharenginxhtml路径),找不到对应的资源。 实际上不管是绝对应用还是相对应用我们想让客户端的请求都是http:fengmengzhao。hypc:8088staticassetsgenerate。png,这里可以看到,如果采用上面的代理方式,并且上游服务有绝对路径的引用,就会出现加载异常的情况。示例: 这里我们也可以看出来,Nginx反向代理默认对响应的内容是不会修改的,目标服务中相对路径或者绝对路径的引用反向代理之后返回给客户端的跟直接访问目标服务端响应是一样的。 怎么样解决呢,有如下方案: 1)。如果目标上游服务可以修改,可以将所有的绝对路径的引用改为相对路径引用。一级目录静态文件引用assetsgenerate。png要改为。assetsgenerate。png或者assetsgenerate。png;二级目录静态文件引用要改为。。xxxassetsgenerate。png。总之,页面上绝对路径的引用要改为相对路径的引用。 2)。可以将不能正常代理的图片添加代理,如下配置:events{}http{includeetcnginxmime。types;server{listen8088;servernamelocalhost;locationstatic{proxypasshttp:127。0。0。1;}locationassets{proxypasshttp:127。0。0。1assets;}}复制代码 这样绝对引用http:fengmengzhao。hypc:8088assetsgenerate。png就能够代理到http:127。0。0。1assetsgenerate。png,就能够正常加载图片了。 3)。放弃子目录的方案,用独立域名就没问题了,配置如下:events{}http{includeetcnginxmime。types;server{listen8088;servernamestatic。fengmengzhao。hypc;location{proxypasshttp:127。0。0。1;}}复制代码 这样访问http:static。fengmengzhao。hypc:8088就能够成功代理http:127。0。0。1了。 4)。Nginx重写目标服务端响应内容 文中强调过多次,Nginx反向代理默认是不会修改目标服务端响应内容的。但Nginx也支持对响应内容进行修改,需要开启Nginx的ngxhttpsubmodule。 可以通过nginxV查看是否包含httpsubmodule就知道当前Nginx是否有ngxhttpsubmodule模块。 开启ngxhttpsubmodule模块后,修改配置如下:events{}http{includeetcnginxmime。types;server{listen8088;servernamelocalhost;locationstatic{subfiltersrcassetssrc。assets;subfilteronceoff;proxypasshttp:127。0。0。1;}}复制代码 通过上面的任意方法,可以获取正确的代理响应: 这里要注意一个点,当你的访问路径是http:fengmengzhao。hypc:8088static(情况一),其响应html中有引用assetsgenerate。png,对该generate。png的请求路径是:http:fengmengzhao。hypc:8088assetsgnerate。png。而当你的访问路径是http:fengmengzhao。hypc:8088static(情况二),其响应html同样引用assetsgenerate。png,对图片的请求会变为:http:fengmengzhao。hypc:8088staticassetsgenerate。png。情况二访问路径和情况一的区别是URI的最后有没有跟,如果有结尾的话,认为当前访问是一个目录,所以其相对引用就从当前地址栏中的路径开始;如果没有结尾的话,认为当前访问是一个文件,其相对路径就是文件所在的路径,也就是URI往前数有出现那个层级,在这里就是根目录,所以情况一虽然是相对引用,但是请求路径还是从根目录开始。8。Nginx作为一个负载均衡服务器 学习完反向代理,就很容易理解基于反向代理做进一步的负载均衡了。 配置示例:events{}http{upstreambackendservers{serverlocalhost:3001;serverlocalhost:3002;serverlocalhost:3003;}server{listen80;servernamelocalhost;location{proxypasshttp:backendservers;}}}复制代码 upstream{}可以包含多个服务并且作为一个上游服务被引用。 测试负载均衡:whilesleep0。5;docurlhttp:localhost;doneresponsefromserver2。responsefromserver3。responsefromserver1。responsefromserver2。responsefromserver3。responsefromserver1。responsefromserver2。responsefromserver3。responsefromserver1。responsefromserver2。复制代码9。优化Nginx性能 本文介绍三个方面优化Nginx的性能:根据主机参数调优WorkerProcesses及WorkerConnections配置、缓存静态文件和响应数据压缩。9。1怎么设置工作进程数(WorkerProcesses)和工作连接数(WorkerConnections 文章开始的时候已经提到过,Nginx会设置Worker进程并在进程间进行切换,能够同时并发处理成千上万个请求。可以通过status命令查看Worker进程数:sudosystemctlstatusnginxnginx。serviceAhighperformancewebserverandareverseproxyserverLoaded:loaded(libsystemdsystemnginx。service;enabled;vendorpreset:enabled)Active:active(running)sinceSun2021042508:33:11UTC;5h54minagoDocs:man:nginx(8)Process:22610ExecReloadusrsbinnginxgdaemonon;masterprocesson;sreload(codeexited,status0SUCCESS)MainPID:3904(nginx)Tasks:3(limit:1136)Memory:3。7MCGroup:system。slicenginx。service3904nginx:masterprocessusrsbinnginxgdaemonon;masterprocesson;22611nginx:workerprocess22612nginx:workerprocess也可以通过ps查看进程能够看到master进程是各个Worker进程的父进程psefgrepnginx复制代码 这里可以看到有1个master进程和2个Worker进程。Worker进程数在Nginx中很容易配置:一般情况,主机有多少核,就设置Worker进程的个数为多少workerprocesses2;根据主机cpu核心数的不同自动设置Worker进程的个数workerprocessesauto;events{}http{server{listen80;servernamelocalhost;return200workerprocessesandworkerconnectionsconfiguration!;}}复制代码 假设说主机有4个核心,workerprocesses如果配置为4,表示每一个Worker理论上能够利用100的cpu。workerprocesses如果配置为8,表示一个Worker理论上能够利用50的cpu,意味着当主机cpu满负荷运转时Worker每运行1分钟就需要等待一分钟。所以,workerprocesses不是配置的越大越好,数量如果超出主机cpu核心数,就会有时间浪费在操作系统级别对进程的调度。 可以很方便的通过nproc命令查看主机的cpu核心数:nproc4复制代码 workerprocessesauto配置会根据主机cpu核心数的不同自动设置Worker进程的个数。如果你的主机只用来运行Nginx,可以这样配置;如果主机上还有其他服务部署,要斟酌合理分配资源。 workerconnections表示一个Worker进程能够处理的最大连接数,该参数跟主机cpucore个数和一个core能打开的文件个数有关(该值可以通过命令ulimitn查询)。ulimitn1024复制代码 workerconnections设置:workerprocessesauto;events{workerconnections1024;}http{server{listen80;servernamelocalhost;return200workerprocessesandworkerconnectionsconfiguration!;}}复制代码 注意,这里本文中第一次使用到events这个Context。9。2怎样缓存静态文件 不管使用Nginx提供什么样的服务,总是有一些静态文件(js或者css等)是不经常发生改变的,可以将它们缓存起来提高Nginx的性能。Nginx对静态文件的缓存配置非常方便:workerprocessesauto;events{workerconnections1024;}http{includeenvnginxmime。types;server{listen80;servernamelocalhost;rootusrsharenginxhtml;正则匹配,大小写不敏感以。css或者。js或者。jpg结尾的匹配location。(cssjsjpgpng){accesslogoff;addheaderCacheControlpublic;addheaderPragmapublic;addheaderVaryAcceptEncoding;1M代表一个月expires1M;}}}复制代码 像之前反向代理设置中的proxysetheader给可以给代理到后端的请求增加header一样,使用addheader可以给response增加header。 CacheControl头信息设置为public,是在告诉client该请求内容可以被缓存。Pragma是CacheControl的oldversion。 Vary头信息设置为AcceptEncoding,后续详解。 expiresdirective表示Nginx缓存响应的时间,可以帮助很方便设置响应Expires头信息,其值可以是1M(1month)、10m10minutes或者24h24hours等。 CacheControl告诉客户端,该response在服务端缓存,客户端可以以任意的形式缓存。另外根据Nginx的expires设置的缓存时间,增加CacheControl:maxage2592000,这里CacheControl:maxage代表该response在maxage时间内不会刷新。2592000单位是秒,等于expire设置的1M(一个月,30x24x36002592000)。 重启Nginx之后,测试请求的响应信息:curlIhttp:fengmengzhao。hypc:8088assetsgenerate。pngHTTP1。1200OKServer:nginx1。18。0(Ubuntu)Date:Tue,01Mar202205:04:17GMTContentType:imagepngContentLength:144082LastModified:Sun,20Feb202208:35:21GMTConnection:keepaliveETag:6211fd49232d2Expires:Thu,31Mar202205:04:17GMT注意这个时间和下面的比较CacheControl:maxage2592000CacheControl:publicPragma:publicVary:AcceptEncodingAcceptRanges:bytes复制代码 这里可以看到,response中已经增加了CacheControl头信息,说明配置已经生效。至于Nginx服务端有没有缓存响应,可以用tcpdump抓包看一看,这里不再演示。 需要注意的是,如果在浏览器上访问http:fengmengzhao。hypc:8088assetsgenerate。png,第一次返回的是200状态码,表示是服务端成功返回。第二次返回的是304状态码,表示浏览器根据第一次response头信息CacheControl:public的指示,第二次访问的时候,直接使用客户端缓存。也可以通过F12打开控制台,勾选NetworkDisableCache选项,这样浏览器端就不使用缓存。9。3怎样压缩响应(response) 压缩配置:workerprocessesauto;events{workerconnections1024;}http{includeenvnginxmime。types;开启gzip,默认只对html进行压缩gzipon;不是设置的越大越好,一般设置为14gzipcomplevel3;对css和js文件进行压缩gziptypestextcsstextjavascript;server{listen80;servernamelocalhost;rootusrsharenginxhtml;location。(cssjsjpg){accesslogoff;addheaderCacheControlpublic;addheaderPragmapublic;addheaderVaryAcceptEncoding;expires1M;}}}复制代码 默认nginx会对html文件进行gzip压缩,如果要对其他类型文件压缩,需要设置gziptypestextcsstextjavascript;。 gzipcomplevel不是设置的越大越好,一般设置为14。 服务端设置gzip之后,要想真正的压缩传输到客户端,客户端需要增加header信息AcceptEncoding:gzip才能完成服务端到客户端的压缩传输。 客户端请求没有AcceptEncoding:gzip的示例:curlIhttp:localhostmini。min。cssHTTP1。1200OKServer:nginx1。18。0(Ubuntu)Date:Sun,25Apr202116:30:32GMTContentType:textcssContentLength:46887LastModified:Sun,25Apr202108:35:33GMTConnection:keepaliveETag:608529d5b727Expires:Tue,25May202116:30:32GMTCacheControl:maxage2592000CacheControl:publicPragma:publicVary:AcceptEncodingAcceptRanges:bytes复制代码 客户端请求设置AcceptEncoding:gzip的示例:curlIHAcceptEncoding:gziphttp:localhostmini。min。cssHTTP1。1200OKServer:nginx1。18。0(Ubuntu)Date:Sun,25Apr202116:31:38GMTContentType:textcssLastModified:Sun,25Apr202108:35:33GMTConnection:keepaliveETag:W608529d5b727Expires:Tue,25May202116:31:38GMTCacheControl:maxage2592000CacheControl:publicPragma:publicVary:AcceptEncodingContentEncoding:gzip复制代码 注意,这里response的header中有Vary:AcceptEncoding信息,该头信息告诉客户端,根据客户端设置的AcceptEncoding头信息的不同,服务端响应会发生变化。 对比压缩前后传输内容的大小:cdmkdircompressiontestcdcompressiontestcurlhttp:localhostmini。min。cssuncompressed。csscurlHAcceptEncoding:gziphttp:localhostmini。min。csscompressed。csslslhrwrwr1vagrantvagrant9。1KApr2516:35compressed。cssrwrwr1vagrantvagrant46KApr2516:35uncompressed。css复制代码 没压缩的版本大小是46k,而压缩后的版本大小是9。1k。10。理解Nginx整个配置文件 完整nginx配置文件:userwwwdata;workerprocessesauto;pidrunnginx。pid;includeetcnginxmodulesenabled。conf;events{workerconnections768;multiaccepton;}http{BasicSettingssendfileon;tcpnopushon;tcpnodelayon;keepalivetimeout65;typeshashmaxsize2048;servertokensoff;servernameshashbucketsize64;servernameinredirectoff;includeetcnginxmime。types;defaulttypeapplicationoctetstream;SSLSettingssslprotocolsTLSv1TLSv1。1TLSv1。2TLSv1。3;DroppingSSLv3,ref:POODLEsslpreferservercipherson;LoggingSettingsaccesslogvarlognginxaccess。log;errorlogvarlognginxerror。log;GzipSettingsgzipon;gzipvaryon;gzipproxiedany;gzipcomplevel6;gzipbuffers168k;gziphttpversion1。1;gziptypestextplaintextcssapplicationjsonapplicationjavascripttextxmlapplicationxmlapplicationxmlrsstextjavascript;VirtualHostConfigsincludeetcnginxconf。d。conf;includeetcnginxsitesenabled;}mail{Seesampleauthenticationscriptat:http:wiki。nginx。orgImapAuthenticateWithApachePhpScriptauthhttplocalhostauth。php;pop3capabilitiesTOPUSER;imapcapabilitiesIMAP4rev1UIDPLUS;server{listenlocalhost:110;protocolpop3;proxyon;}server{listenlocalhost:143;protocolimap;proxyon;}}复制代码 上文中已经讲解过的配置,不再做重复说明。 userwwwdata;设置Nginx进程的用户,这里会涉及到权限问题,如果用户为wwwdata读取没有权限的目录,就不能正常的提供服务,这时候查看Nginx的error日志,就会报权限相关的错。 pidrunnginx。pid;设置nginx进程的processid。 includeetcnginxmodulesenabled。conf;设置include指定目录中任何。conf结尾的配置文件。该目录用来加载nginx的动态模块(本文中并没有涉及)。 在http{}下,有基本的优化设置,如下:sendfileon;:禁止静态文件buffering。tcpnopushon;:允许在一个响应包中发送头信息。tcpnodelayon;:静态文件快传中禁用NaglesAlgorithm。 keepalivetimeout设置httpconnection的连接时间。typeshashmaxsize设置Hashmap的大小。 SSL的配置在本文中不做讲解。 mailContext可以将Nginx配置为一个邮件服务端,本文仅讨论Nginx作为web服务端,所以不做说明。 重点看一下如下配置:VirtualHostConfigsincludeetcnginxconf。d。conf;includeetcnginxsitesenabled;复制代码 该配置表示Nginx会加载etcnginxconf。d和etcnginxsitesenabled目录内匹配的配置。这样,一般认为这两个目录就是放置Nginx配置的最好的选择,实际上并不是。 有另外一个目录etcnginxsitesavailable,该目录用来存放Nginx的虚拟主机(也就是server{}块)配置。etcnginxsitesenabled目录用来存放符号链接指向目录etcnginxsitesavailable中配置。例如:lnlhetcnginxsitesenabledlrwxrwxrwx1rootroot34Apr2508:33defaultetcnginxsitesavailabledefault复制代码 这样通过符号链接的方式可以激活或者禁用etcnginxsitesavailable目录中的配置。符号链接unlink和创建的命令如下:删除符号链接,用rm也可以sudounlinketcnginxsitesenableddefault创建符号链接,第一个参数是被链接的文件,第二个参数是创建符号链接的路径也就是,链接某个文件到某个符号链接上sudolnsetcnginxsitesavailablenginxhandbook。confetcnginxsitesenablednginxhandbook复制代码引用www。freecodecamp。orgnewsthengserverfault。comquestions9www。cnblogs。comskychengpstackoverflow。comquestions1www。jscape。comblogbid87tarunlalwani。compostnginxwww。digitalocean。comcommunitytstackoverflow。comquestions4blog。csdn。netgui951753a 链接:https:juejin。cnpost7070269868553011230