让我们继续聊聊架构和Nginx吧.
Web站点架构 首先还是来从大的方面来说说. 我们之前其实是说过的, LVS是工作在传输层的, 所以对应用层的协议报文是没有什么处理能力, 而Nginx是工作在应用层的, 所以和LVS相比拥有更加高层的能力. 而且不仅如此, 我们的LVS仅仅是一个调度器, 最终和客户端进行通信的主机还是我们的后端主机, 也就是那些real server. 但是Nginx是不一样的, 之前也提到过, Nginx会自己做请求, 将资源抓取到自己的本地, 接着在组装响应报文发送给客户端. 这个就是我们说的反向代理 . 对于Nginx而言, 后端的那些服务器都被称为upstream server, 也就是上游服务器 .
而且不仅如此, 我们说过存在动态和静态的资源, 这样Nginx还可以将我们对于静态的动态的资源分开, 分别 进行调度. 对于静态内容的服务器集群, 我们还可以省去考虑session保持的问题. 仅仅使用最简单的轮询算法都是OK的.
另外, 我们之前还说过, 现代互联网系统是相当依赖于缓存的. 而Nginx也是拥有缓存功能的, 因为将资源赚取过来之后, 在一点时间内只要还有请求, 且请求同一资源, 那么就可以不需要进行后端的请求, 而可以直接进行响应. 这对于硬盘而言是个很大的减负了, 有的时候还可以使得响应数量翻倍. 但是同样对于这么一个缓存节点而言, 一旦他挂了, 那么整个系统也就相继崩掉了. 这个就是雪崩效应
但是这个缓存的存储要比后端更快速, 因为存储的方式是经过hash计算的, 然后以K-V方式存储的. 而从upstream server取数据的时候, 由于资源存储在文件系统上, 会进行树的检索, 显然没有K-V方式的快.
显然, 这个前端的Nginx负载均衡节点是个高压力的节点. 所以一般都会对这么一个节点做高可用. 而且,我们还可以结合DNS进行内容分发来减少压力. 如果还是不行, 那么就可以考虑在前端再加上一个LVS来抗压力, 当然这么一个LVS还是要做冗余备份. 这么算来节点的数量已经非常多了. 所以在系统变的越来越大的时候, 就要先考虑如何将我们的功能该进行拆分, 要不然系统的一个节点会导致全局不可用. 所以可以根据不同的功能来进行拆分.
而且, 考虑一个电商网站. 他需要存储大量的图片资源(几十万+), 这些图片我们还需要进行分布式的存储 . 而结构化的数据, 我们需要存储到数据库中, 这个时候我们还要做主从复制, 读写分离 等.这些都是后话了.
Nginx的代理功能 Nginx的代理功能依靠一个模块:
我们之前说定义location里面加上了root
来指定从本地的文件系统取得哪些资源, 但是如果不是从本地取资源的话, 也可以转交给后端的server来应付, 这个指令就是:
1 2 3 location / { proxy_pass http://192.168.206.100:8080; }
除了可以直接这样转发出去 ,我们还可以自行来改变HTTP报文的头部信息. 我们提到过nginx是自行再次封装报文进行的反向代理. 比如, 我们后台响应的服务器是需要进行日志记录的, 但是来源IP显然是我们的负载均衡衡器nginx. 显然是没有意义的, 这个时候, 第一种方法就是让Nginx去记录日志, 另一种就是添加自定义的头部, 从而使后端服务器了解到真是的客户端IP是多少.
官网的doc给了一个例子:
1 2 3 4 5 location / { proxy_pass http://localhost:8000; proxy_set_header Host $host ; proxy_set_header X-Real-IP $remote_addr ; }
第二个选项有时也是一个很重要的头部信息, 因为说不定我们的后端会开多个虚拟主机呢. 如果使用IP, 就访问不到了. 现在我们就简单的试一下:
后端主机开启httpd:
1 2 3 4 5 [root@VM-node2 html] forum index.html [root@VM-node2 html] index.html [root@VM-node2 html]
在前端Nginx主机配置:
1 2 3 4 location / { rewrite ^/bbs/(.*)$ /forum/$1 break ; proxy_pass http://192.168.206.21; }
如果在代理至上游服务器的时候也进行了URL重写的话, 发送给后面的请求就是已经重写之后的. 我们来验证一下:
1 2 3 4 5 6 7 8 9 10 11 C:\Users\lenovo λ curl http://192.168.206.9 <h1>It works! (From node2)</h1> C:\Users\lenovo λ curl http://192.168.206.9/bbs/ <h1>Forum</h1> C:\Users\lenovo λ curl http://192.168.206.9/forum/ <h1>Forum</h1>
这个是直接写location的URL, 但是如果改成模式匹配就不能这么写了:
1 2 3 location ~* \.txt$ { proxy_pass http://192.168.206.21/forum; }
结果在验证的时候:
1 2 3 [root@VM-node1 ~] nginx: [emerg] "proxy_pass" cannot have URI part in location given by regular expression, or inside named location, or inside "if" statement, or inside "limit_except" block in /etc/nginx/nginx.conf:58 nginx: configuration file /etc/nginx/nginx.conf test failed
这个时候, 仅仅只能写upstream server的地址了.
现在查看一下日志, 所有的来源IP都是我们的前端代理主机, 这样是没有什么信息的, 所有我们使用内嵌的变量来加上自定义的HTTP头部:
1 2 3 4 location ~* \.txt$ { proxy_pass http://192.168.206.21; proxy_set_header X-Real-IP $remote_addr ; }
这个时候记录还是没有变化的, 因为我们还需要修改一下httpd的日志记录:
1 LogFormat "%{X-Real-IP}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
这个时候访问:
1 2 192.168.206.9 - - [19/Oct/2017:03:06:46 +0800] "GET /test.txt HTTP/1.0" 304 - "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36" 192.168.206.1 - - [19/Oct/2017:03:08:01 +0800] "GET /test.txt HTTP/1.0" 304 - "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36"
就改变了.
如果我们的后端服务器迟迟不给回应, 怎么办? 我们由一个超时时长超过了就不再等待了:
1 proxy_connect_timeout time;
默认的值是60s.
另外, 我们还可以隐藏头部:
1 proxy_hide_header field;
而且, 我们还可以对于返回响应的头部报文, 还可以自定义添加头部:
1 2 3 4 5 location / { rewrite ^/bbs/(.*)$ /forum/$1 break ; proxy_pass http://http; add_header X-Via "Justin13" ; }
这样受到的报文就会带上我们的自定义头:
1 2 3 4 5 6 7 8 9 10 11 12 C:\Users\lenovo λ curl http://192.168.206.9/ -I HTTP/1.1 200 OK Server: nginx/1.12.1 Date: Wed, 18 Oct 2017 15:24:52 GMT Content-Type: text/html; charset=UTF-8 Content-Length: 32 Connection: keep-alive Last-Modified: Mon, 16 Oct 2017 11:20:12 GMT ETag: "7f47-20-55ba830a82afc" Accept-Ranges: bytes X-Via: Justin13
很简单吧, 这其实就是nginx对于代理转发的基本指令和注意点了.
Nginx的代理缓存 之前说过的Nginx除了能够把资源从后端请求过来, 还可以进行本地的缓存, 从而加速下一次的访问, 关于代理缓存的设定现在就来看一下吧.
先来看看几个重要的选项,
proxy_cache zone | off
;
这个选项就是用来定义是否开启缓存的, 这里的zone就是在后面要定义的路径中必须携带的参数, 其实就是一段命名空间用来存放你的缓存的.
proxy_cache_methods GET | HEAD | POST ...
;
指明什么样的请求方法才会进行缓存, 默认是GET和HEAD.
proxy_cache_min_uses number
;
意思就是说当资源比请求多少次之后才会进行缓存, 默认就是一次.
proxy_cache_path path [levels=levels] [use_temp_path=on|off] keys_zone=name:size [inactive=time] [max_size=size] [manager_files=number] [manager_sleep=time] [manager_threshold=time] [loader_files=number] [loader_sleep=time] [loader_threshold=time] [purger=on|off] [purger_files=number] [purger_sleep=time] [purger_threshold=time]
;
这应该是最重要的选项了(?) 指明缓存存储的路径, 挨? 不是说是键值对存储吗? 其实缓存最终还是存储在磁盘上的啊, 但是他的目录层级非常少, 所以寻找起来不会这么费时, 这个层级一般都是2层, 当然1层和3层的也有, 就在选项中的levels中定义. 一个示例:
1 proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=one:10m;
这样的话, 存储的结果就像这样:
1 /data/nginx/cache/c/29/b7f54b2df7773722d382f4809d65029c
来看一下, 里面的levels写法, 每一个冒号就是开启一个新目录层级的意思, 数字就是该目录的长度. 其实就是想之前我们说过的哈希桶那样了. 最后的那个文件其实就是URL的MD5摘要.
这个选项只能使用在server中.
proxy_cache_purge string ...
;
该选项是为了手动清理缓存 存在的, 因为nginx在缓存尚未失效前都不会删除Cache. 除非是缓存过期或者说到达最大值, 这个时候Cache Manager会进行LRU清理. 这个时候如果后端更新了资源, 但是用户访问的时候得到的还是旧的资源. 所以我们可以使用该选项定义一个修剪方法, 该方法请求的资源都会被从缓存中删除.
一个示例是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 proxy_cache_path /data/nginx/cache keys_zone=cache_zone:10m; map $request_method $purge_method { PURGE 1; default 0; } server { ... location / { proxy_pass http://backend; proxy_cache cache_zone; proxy_cache_key $uri ; proxy_cache_purge $purge_method ; } }
proxy_cache_revalidate on | off
;
该选项挺有用的. 在缓存有效期到期的时候, Manager没有删除掉缓存. 如果用户请求该资源, 明知道有效期到期, Nginx用不用呢? 要知道, 这些资源不一定是真的过期了 , 因为有可能后端没有更新. 这个时候就会需要这个选项了, 如果开启, Nginx就会去询问后端, 资源是否更新过? 这样就可以达到节省后端的带宽的目的. 如果请求中携带了: “If-Modified-Since” 和“If-None-Match” 这样的, 也会进行验证.
proxy_cache_use_stale error | timeout | invalid_header | updating | http_500 | http_502 | http_503 | http_504 | http_403 | http_404 | http_429 | off ...
;
这个选项就是在缓存已经过期, 但是这个时候后端的上游服务器出毛病了, 这个时候用不用呢 ? 怎么返回给客户端呢?
proxy_cache_valid [code ...] time
;
缓存的时间由后端的服务器决定, 但是我们也可以不遵从他的指示, 你说你的图片缓存10分钟, 可以哪有这么快就会改变啊, 所以我偏偏缓存10天. 这个时候就可以使用该选项了:
1 2 proxy_cache_valid 200 302 10m; proxy_cache_valid 404 1m;
如果后端返回了200或者302, 我就保存10min 如果是404, 我就缓存1min.
选项就这么多, 接下来我们就实际跑一个试试就知道了:
1 2 3 4 5 6 7 8 9 location / { proxy_cache mycache; proxy_cache_valid 200 30m; proxy_cache_valid 301 302 10m; proxy_cache_valid any 1m; proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504; rewrite ^/bbs/(.*)$ /forum/$1 break ; proxy_pass http://192.168.206.21; }
接着在http中定义:
1 proxy_cache_path /cache/nginx levels=1:1 keys_zone=mycache:36m;
我们创建目录, 并且改变属组. 因为这个目录的缓存写入是Worker进程定义的:
1 2 3 4 5 6 7 8 [root@VM-node1 ~] mkdir : created directory ‘/cache’mkdir : created directory ‘/cache/nginx’[root@VM-node1 ~] [root@VM-node1 ~] [root@VM-node1 ~] [root@VM-node1 nginx] [root@VM-node1 nginx]
显然这个目录是空的.
接着我们随便的访问一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [root@VM-node1 nginx] 2 4 6 a b [root@VM-node1 nginx] 4 ./4/4 4 ./4 4 ./6/0 4 ./6 4 ./2/5 4 ./2 4 ./a/a 4 ./a 4 ./b/7 4 ./b 20 .
看, 缓存已经建立了. ‘接着我们修改后端的资源:
再次访问:
1 2 3 4 5 6 7 C:\Users\lenovo λ curl http://192.168.206.9/ <h1>It works! (From node2)</h1> C:\Users\lenovo λ curl http://192.168.206.9/ <h1>It works! (From node2)</h1>
访问的就是缓存了. 你可以尝试着把缓存删除掉, 结果就会发生改变. 但是这不是好的建议.
出去玩了一会, 回来的时候试了一下, 页面得到了刷新.
upstream 我们说过Nginx可以进行负载均衡, 支持多种调度方法. 现在就是应用的时候了, 如果我们把之前的特定的proxy_pass
主机搞成一个集群, 不就可以实现动态调度了吗? 这就是upstream指令所定义的.
upstream可以把诸多主机定义在一起, 但是该选项只能用在http中. 在upstream中使用server指令来定义主机, 还可以传递参数. 现在我们一边搭建一边介绍好了.
首先为了负载均衡的效果, 我们需要把缓存功能关闭. 接着开启第三台主机充当后端的上游服务器.
1 2 3 4 5 6 7 8 9 10 11 12 upstream http { server 192.168.206.21; server 192.168.206.22; } ...(omitted) location / { rewrite ^/bbs/(.*)$ /forum/$1 break ; proxy_pass http://http; }
接着重载配置, 访问试试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 C:\Users\lenovo λ curl http://192.168.206.9/ <h1>This is node2(NEW)</h1> C:\Users\lenovo λ curl http://192.168.206.9/ <h1>It works! (From node3)</h1> C:\Users\lenovo λ curl http://192.168.206.9/ <h1>This is node2(NEW)</h1> C:\Users\lenovo λ curl http://192.168.206.9/ <h1>It works! (From node3)</h1>
我们还可以为特定的上游服务器加上权重, 比如:
1 2 3 4 upstream http { server 192.168.206.21 weight=2; server 192.168.206.22; }
接着效果就会发生改变:
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 C:\Users\lenovo λ curl http://192.168.206.9/ <h1>It works! (From node3)</h1> C:\Users\lenovo C:\Users\lenovo 2.168.206.9/ λ curl http://192.168.206.9/ <h1>This is node2(NEW)</h1> C:\Users\lenovo λ curl http://192.168.206.9/ <h1>This is node2(NEW)</h1> C:\Users\lenovo λ curl http://192.168.206.9/ <h1>It works! (From node3)</h1> C:\Users\lenovo λ curl http://192.168.206.9/ <h1>It works! (From node3)</h1> C:\Users\lenovo λ curl http://192.168.206.9/ <h1>This is node2(NEW)</h1> C:\Users\lenovo λ curl http://192.168.206.9/ <h1>This is node2(NEW)</h1> C:\Users\lenovo λ curl http://192.168.206.9/ <h1>It works! (From node3)</h1>
由此可见, 默认使用的调度算法是rr. 其实这个我们也是可以改的呀, 只要在upstream的头部加上算法就行了, 例如:
1 2 3 4 5 6 upstream http { ip_hash; server 192.168.206.21 weight=2; server 192.168.206.22; }
源地址哈希, 这样的话就会变成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 C:\Users\lenovo λ curl http://192.168.206.9/ <h1>It works! (From node3)</h1> C:\Users\lenovo λ curl http://192.168.206.9/ <h1>It works! (From node3)</h1> C:\Users\lenovo λ curl http://192.168.206.9/ <h1>It works! (From node3)</h1> C:\Users\lenovo λ curl http://192.168.206.9/ <h1>It works! (From node3)</h1> C:\Users\lenovo λ curl http://192.168.206.9/ <h1>It works! (From node3)</h1>
除此之外, server后面还可以跟上很多参数, 例如:
**weight: ** 权重.
**max_failure: ** 最大的失败尝试次数, 默认是1次.
**fail_timeout: ** 最大的失败超时时长, 默认是10s
**backup: ** 带这个标记的服务器被认定为备用服务器, 只有当主服务器宕机才会被调度.
down: 带有这个标记的服务器不会得到调度.
upstream同样可以定义我们之前说过的Session绑定. Nginx位于应用层, 于是能够识别请求, Session的绑定的原理是Cookie, 所以我们只要能够绑定Cookie, 就能够建立session了. 是这样用的:
1 2 3 4 5 6 upstream backend { server backend1.example.com; server backend2.example.com; sticky cookie srv_id expires=1h domain=.example.com path=/; }
sticky
指令第一种应用: 能够实现Cookie的绑定.
第二种就是路由携带:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 map $cookie_jsessionid $route_cookie { ~.+\.(?P<route>\w+)$ $route ; } map $request_uri $route_uri { ~jsessionid=.+\.(?P<route>\w+)$ $route ; } upstream backend { server backend1.example.com route=a; server backend2.example.com route=b; sticky route $route_cookie $route_uri ; }
除了这两种, 还有一个炒鸡厉害的 – learn 学习:
1 2 3 4 5 6 7 8 9 upstream backend { server backend1.example.com:8080; server backend2.example.com:8081; sticky learn create=$upstream_cookie_examplecookie lookup=$cookie_examplecookie zone=client_sessions:1m; }
Nginx可以通过分析客户端和上游服务器数据传输来维护一个session表.
接着Nginx还可以和后端的主机实现持久连接进行保活, 使用keepalive
关键字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 upstream memcached_backend { server 127.0.0.1:11211; server 10.0.0.2:11211; keepalive 32; } server { ... location /memcached/ { set $memcached_key $uri ; memcached_pass memcached_backend; } }
不过, 一般情况下, 我们不会对http做持久连接. 这样有可能会影响我们的并发性能. 一般都是数据查询这样的会选择长连接.
我们之前还说过, nginx支持应用层的健康状态检查, 也就是health_check
:
1 2 3 4 location / { proxy_pass http://backend; health_check; }
一般, 我们要单独建立一个location用来执行健康状态检查, 而且, 要明确的把日志记录关闭.
health_check也支持很多选项:
interval : 检查间隔
fails : 几次认定是不健康
passes : 几次认定成是健康
uri : 请求哪些资源
match : 返回什么认定是健康.
fastcgi 关于fastcgi的相关不再赘述, 因为之前已经说过了. 现在就直接来进行Nginx和php-fpm的连接吧.
首先, 这里的实验假定php-fpm和Nginx是一个主机了, 如果是不同的主机的话, 需要改变监听地址的哦. 默认是127.0.0.1:9000端口.
1 2 3 4 5 6 7 8 9 10 11 [root@VM-node1 ~] [root@VM-node1 ~] State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 128 *:8080 *:* LISTEN 0 128 *:80 *:* LISTEN 0 128 *:22 *:* LISTEN 0 100 127.0.0.1:25 *:* LISTEN 0 128 *:443 *:* LISTEN 0 128 127.0.0.1:9000 *:* LISTEN 0 128 :::22 :::* LISTEN 0 100 ::1:25 :::*
确定已经启动了, 接着就是主要的步骤了, 配置Nginx吧:
1 2 3 4 5 6 7 location ~ \.php$ { root /usr/share/nginx/html; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name ; include fastcgi_params; }
这个是默认的设置. 当然你需要改一下fastcgi_param
的脚本路径. 这个是常识了, 就这样的带过去了.
我们的fastcgi模块也提供了缓存功能, 基本上的关键字和之前说的那个没什么不同, 基本上是一样的.(只要把之前的proxy_换成fastcgi _就行了)
生产环境配置(2016) 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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 user nobody nobody; worker_processes 4; worker_rlimit_nofile 51200; error_log logs/error.log notice; pid /var/run/nginx.pid; events { use epoll; worker_connections 51200; } http { server_tokens off; include mime.types; proxy_redirect off; proxy_set_header Host $host ; proxy_set_header X-Real-IP $remote_addr ; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ; client_max_body_size 20m; client_body_buffer_size 256k; proxy_connect_timeout 90; proxy_send_timeout 90; proxy_read_timeout 90; proxy_buffer_size 128k; proxy_buffers 4 64k; proxy_busy_buffers_size 128k; proxy_temp_file_write_size 128k; default_type application/octet-stream; charset utf-8; client_body_temp_path /var/tmp/client_body_temp 1 2; proxy_temp_path /var/tmp/proxy_temp 1 2; fastcgi_temp_path /var/tmp/fastcgi_temp 1 2; uwsgi_temp_path /var/tmp/uwsgi_temp 1 2; scgi_temp_path /var/tmp/scgi_temp 1 2; ignore_invalid_headers on; server_names_hash_max_size 256; server_names_hash_bucket_size 64; client_header_buffer_size 8k; large_client_header_buffers 4 32k; connection_pool_size 256; request_pool_size 64k; output_buffers 2 128k; postpone_output 1460; client_header_timeout 1m; client_body_timeout 3m; send_timeout 3m; log_format main '$server_addr $remote_addr [$time_local] $msec+$connection ' '"$request" $status $connection $request_time $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"' ; open_log_file_cache max=1000 inactive=20s min_uses=1 valid=1m; access_log logs/access.log main; log_not_found on; sendfile on; tcp_nodelay on; tcp_nopush off; reset_timedout_connection on; keepalive_timeout 10 5; keepalive_requests 100; gzip on; gzip_http_version 1.1; gzip_vary on; gzip_proxied any; gzip_min_length 1024; gzip_comp_level 6; gzip_buffers 16 8k; gzip_proxied expired no-cache no-store private auth no_last_modified no_etag; gzip_types text/plain application/x-javascript text/css application/xml application/json; gzip_disable "MSIE [1-6]\.(?!.*SV1)" ; upstream tomcat8080 { ip_hash; server 172.16.100.103:8080 weight=1 max_fails=2; server 172.16.100.104:8080 weight=1 max_fails=2; server 172.16.100.105:8080 weight=1 max_fails=2; } server { listen 80; server_name www.magedu.com; root /data/webapps/htdocs; access_log /var/logs/webapp.access.log main; error_log /var/logs/webapp.error.log notice; location / { location ~* ^.*/favicon.ico$ { root /data/webapps; expires 180d; break ; } if ( !-f $request_filename ) { proxy_pass http://tomcat8080; break ; } } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } server { listen 8088; server_name nginx_status; location / { access_log off; deny all; return 503; } location /status { stub_status on; access_log off; allow 127.0.0.1; allow 172.16.100.71; deny all; } } }