一. 阅读前提
本文建立在前一篇的 [Nginx实战01-入门篇] 之上, 有兴趣可以花5分钟的时间看一下入门篇
https://blog.csdn.net/weixin_43273174/article/details/105844831
- 本文的受众对象
- 阅读过 [Nginx实战01-入门篇]
- 运维工程师
- 架构师
- 对Nginx技术有浓厚兴趣的小伙伴
- 内容概要
- 实战中对于Nginx负载均衡及反向代理的应用
- 服务集群
- 以案例的形式介绍一些实用的转发策略
- HTTP/HTTPS
- TCP
- URL转发
- URL重写
- 静态资源转发(前端页面部署)
- 实战中对于Nginx负载均衡及反向代理的应用
- 那么闲话不多说,开始我们的中级篇
二. 实战中对于Nginx负载均衡及反向代理的应用
1. 服务集群,(本地集群)
上一篇文章中说到: 负载均衡与反向代理
- 负载均衡
- 为了降低后台服务器的压力,我们需要将压力分摊到更多的服务器中,我将这种做法称作负载均衡
- 反向代理
- 暴露一个公网的ip,请求通过公网入口转发到内网端口的实现
接下来我会用一个实例来简单的演示负载均衡的实现
- 材料准备
- 一台linux虚拟机
- 一个springboot应用(什么应用都可以),接下来简称这个springboot应用为sb.jar将这个下面附上源码
package com.gralves.loadbalance.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* @author 老周啊啊
* @date 2020年4月30日14:28:53
*/
@Slf4j
@RestController
public class LoadBalanceTests {
private static final ConcurrentLinkedQueue<String> TICKETS = new ConcurrentLinkedQueue<>();
/*设置队列长度*/
/*可通过--ticketAmount参数指定队列长度,即token数量*/
@Value("${ticketAmount:200}")
private Integer ticketAmount;
/*初始化队列长度并加入票据*/
@PostConstruct
public void init() {
for (int i = 0; i < ticketAmount; i++) {
TICKETS.add(UUID.randomUUID().toString());
}
}
/**
* 队列中弹出一个UUID票据,为空则返回异常
*
* @return 票据/Exception
* @author 老周啊啊
* @date 2020年4月30日14:15:56
*/
@GetMapping("takeoutTicket")
public String takeoutTicket() throws Exception {
String ticket = TICKETS.poll();
if (Objects.nonNull(ticket)) {
return ticket;
}
throw new Exception("队列为空");
}
}
该代码示例以一个队列的形式取令牌,由上述代码,可以看到,队列中默认的令牌数量为200,所以当取出令牌数超过200时,则跑出异常,以为这请求失败
- 首先以单进程的方式调用接口,获取令牌
# 启动sb.jar,并指定启动端口为8081
java -jar sb.jar --server.port=8081 --ticketAmount=200
使用jmeter进行接口请求测试,请求linux服务器的ip:8081/takeoutTicket
- 当请求数200时,通过下图可以看到,当请求取出令牌数等于队列长度时,请求正常
- 当请求数+1,则返回失败,我们以队列的长度模拟服务器的负载能力,则认为单台服务器的负载为200
在实际的生产环境中,单台服务器的负载能力始终是有限的,所以我们通常会通过增加机器的操作来增加服务的负载能力
配置Nginx负载均衡
- 首先附上配置,然后再进行解释
http {
include 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 logs/access.log main;
sendfile on;
keepalive_timeout 65;
#新增一个负载均衡器,负载均衡器命名方式可以自定义,此处叫localProxy,需要与下面server中proxy_pass的配置对应
upstream localProxy {
# 配置两台服务器,第一个为前面启动的8081端口的Java进程所在的服务器ip:端口,由于我们模拟的是伪分布式,所以ip都是127.0.0.1
server 127.0.0.1:8081;
server 127.0.0.1:8080;
}
# 请求转发代码块
server {
# 监听8888端口
listen 8888;
# 进入改代码块的域名要求为localhost,如果有多个server代码块监听相同端口,则server_name要求不同,
# 这样才能进入不同的转发逻辑
server_name localhost;
# uri 匹配模式为 "/",表示所有的请求都进入下面的转发代码块
location / {
# 所有的请求都会进入localProxy负载均衡器
proxy_pass http://localProxy;
proxy_redirect default;
}
}
}
- 完成以上配置后,检查nginx配置并重载配置
# 检查
nginx -t
# 配置重载
nginx -s reload
- 上述配置可见,对外暴露一个8888端口,请求将分发到localProxy的负载均衡器并由两个java进程处理请求
- 设置两台java应用的负载能力都是200
# 启动sb.jar,并指定启动端口为8081,设置负载为200
java -jar sb.jar --server.port=8081 --ticketAmount=200
# 启动sb.jar,并指定启动端口为8080,设置负载为200
java -jar sb.jar --server.port=8080 --ticketAmount=200
-
使用Jmeter进行测试,设置并发为400,下图可见,添加负载均衡器后,该接口的负载能力增加到了400,那么负载均衡的目的已经达到了
-
至此已经完成了使用Nginx进行简单的负载均衡
负载均衡器的进阶应用-分发策略
- 轮询策略
- 负载均衡器对于后端服务器的分发策略默认为轮询,所有请求都按照时间顺序分配到不同的服务上,如果服务Down掉,可以自动剔除,配置参考
upstream proxy-server {
server localhost:8081;
server localhost:8080;
}
- 权重策略
- 指定每个服务的权重比例,weight和访问比率成正比,通常用于后端服务机器性能不统一,将性能好的分配权重高来发挥服务器最大性能,如下配置后8080服务的访问比率会是8081服务的二倍。
upstream proxy-server {
server localhost:8081 weight=1;
server localhost:8080 weight=2;
}
- iphash策略
- 每个请求都根据访问ip的hash结果分配,经过这样的处理,每个访客固定访问一个后端服务,如下配置(ip_hash可以和weight配合使用).
upstream proxy-server {
iphash;
server localhost:8081 weight=1;
server localhost:8080 weight=2;
}
- 最少连接策略
- 将请求分配到连接数最少的服务上。
upstream proxy-server {
least_conn;
server localhost:8081 weight=1;
server localhost:8080 weight=2;
}
- fair策略
- 按后端服务器的响应时间来分配请求,响应时间短的优先分配。
upstream proxy-server {
server localhost:8081 weight=1;
server localhost:8080 weight=2;
fair;
}
三. 以案例的形式介绍一些实用的转发策略
在开始介绍转发策略之前,先简单的介绍一下Nginx的代码结构
- nginx配置文件结构
- 主要便于后续代码展示,不至于造成看不懂代码配置该放在什么地方的尴尬窘境
... #全局块
events { #events块
...
}
# TCP代码块
stream {
# stream 全局块
server {
# server代码块
}
}
# http块
http
{
... #http全局块
server #server块
{
... #server全局块
location [PATTERN] #location块
{
...
}
}
server
{
...
}
... #http全局块
}
- Nginx全局变量介绍
$args : #这个变量等于请求行中的参数,同$query_string
$content_length : 请求头中的Content-length字段。
$content_type : 请求头中的Content-Type字段。
$document_root : 当前请求在root指令中指定的值。
$host : 请求主机头字段,否则为服务器名称。
$http_user_agent : 客户端agent信息
$http_cookie : 客户端cookie信息
$limit_rate : 这个变量可以限制连接速率。
$request_method : 客户端请求的动作,通常为GET或POST。
$remote_addr : 客户端的IP地址。
$remote_port : 客户端的端口。
$remote_user : 已经经过Auth Basic Module验证的用户名。
$request_filename : 当前请求的文件路径,由root或alias指令与URI请求生成。
$scheme : HTTP方法(如http,https)。
$server_protocol : 请求使用的协议,通常是HTTP/1.0或HTTP/1.1。
$server_addr : 服务器地址,在完成一次系统调用后可以确定这个值。
$server_name : 服务器名称。
$server_port : 请求到达服务器的端口号。
$request_uri : 包含请求参数的原始URI,不包含主机名,如:”/foo/bar.php?arg=baz”。
$uri : 不带请求参数的当前URI,$uri不包含主机名,如”/foo/bar.html”。
$document_uri : 与$uri相同。
if判断指令
语法为if(condition){…},对给定的条件condition进行判断。如果为真,大括号内的rewrite指令将被执行,if条件(conditon)可以是如下任何内容:
- 当表达式只是一个变量时,如果值为空或任何以0开头的字符串都会当做false
- 直接比较变量和内容时,使用=或!=
- 正则表达式匹配,*不区分大小写的匹配,!~区分大小写的不匹配
- -f和!-f用来判断是否存在文件
- -d和!-d用来判断是否存在目录
- -e和!-e用来判断是否存在文件或目录
- -x和!-x用来判断文件是否可执行
关于if指令的一些操作示例,一下代码中
//如果UA包含"MSIE",rewrite请求到/msid/目录下
if ($http_user_agent ~ MSIE) {
rewrite ^(.*)$ /msie/$1 break;
}
//如果cookie匹配正则,设置变量$id等于正则引用部分
if ($http_cookie ~* "id=([^;]+)(?:;|$)") {
set $id $1;
}
//如果提交方法为POST,则返回状态405(Method not allowed)。return不能返回301,302
if ($request_method = POST) {
return 405;
}
//限速,$slow可以通过 set 指令设置
if ($slow) {
limit_rate 10k;
}
//如果请求的文件名不存在,则反向代理到localhost 。这里的break也是停止rewrite检查
if (!-f $request_filename){
break;
proxy_pass http://127.0.0.1;
}
//如果query string中包含"post=140",永久重定向到example.com
if ($args ~ post=140){
rewrite ^ http://example.com/ permanent;
}
//防盗链
location ~* \.(gif|jpg|png|swf|flv)$ {
valid_referers none blocked oss.hdyl.net.cn
if ($invalid_referer) {
return 404;
}
}
常用正则
. : 匹配除换行符以外的任意字符
? : 重复0次或1次
+ : 重复1次或更多次
* : 重复0次或更多次
\d : 匹配数字
^ : 匹配字符串的开始
$ : 匹配字符串的介绍
{n} : 重复n次
{n,} : 重复n次或更多次
[c] : 匹配单个字符c
[a-z] : 匹配a-z小写字母的任意一个
转发与重写
- 什么是转发(forward)
请求转发时客户端和浏览器只发出一次请求,Servlet、HTML、JSP或其它信息资源,由第二个信息资源响应该请求,在请求对象request中,保存的对象对于一个每个信息资源是共享的。
- 什么是重写(rewrite)
请求重写是两次HTTP请求,服务器端在响应第一次请求的时候,让浏览器再向另外一个URL发出请求,从而达到转发的目的。
- 两者之间的区别
两者之间最明显的区别在于转发时会丢失request的所有信息,重写属于页面级别的重定向,相当于是告诉浏览器,客户端重新发起一次请求
举个例子
小明找小红借钱,小红没钱,然后小红找小王借钱,整个过程结束后,小明只发起了一次请求,这就是转发
小明找小红借钱,小红没钱,然后小红让小明去找小王借钱,整个过程中,小明将发起两次请求,这个过程就是重写(重定向)
1. 转发
1.1 转发语法
proxy_pass <ip>:<port>;
转发关键字 ip : 端口
1.1.1 转发关键字使用域
# 1. server代码块
server {
...
proxy_pass <ip>:<port>;
}
# 2. location代码块
location ... {
...
proxy_pass <ip>:<port>;
}
# 3. if代码块
if(true){
...
proxy_pass <ip>:<port>;
}
以下演示案例皆在[location]代码块下进行演示, [if]代码块可在[location]代码块中嵌套执行
1.2 全匹配模式
# 匹配 $uri = /goods/add,
# 例如访问 https://domain.com/goods/add,则请求将进入该代码块
location = /goods/add {
# 转发请求到 127.0.0.1:8080
# 127.0.0.1 为接受请求的服务ip,端口为服务的端口,实际按照真实的ip:port进行配置
proxy_pass http://127.0.0.1:8080;
}
1.3 正则匹配模式
正则匹配语法
location <match pattern> <regex>
关键字 匹配模式 正则表达式
match pattern 匹配模式
- = 表示精确匹配
- ^~ 表示uri以某个常规字符串开头,不是正则匹配
- ~ 表示区分大小写的正则匹配;
- ~* 表示不区分大小写的正则匹配
- / 通用匹配, 如果没有其它匹配,任何请求都会匹配到
- 优先级
= > 完整路径 > ^~ > ~,~* > 前缀匹配 > /
1.3.1 前缀匹配
使用前缀匹配模式的注意事项
-
若配置location ^~ /goods , 表示匹配/goods** , 则满足一下条件的请求会进入相应的location
- /goods/add
- /goodsStock
- /goodsStock/increase
- 只要前缀满足/goods即可
-
若配置location ^~ /goods/ , 表示匹配/goods/** , 则满足一下条件的请求会进入相应的location
- /goods/add
- /goods/delete
- 需要前缀满足/goods/
# 匹配 $uri 前缀为 /goods,
# 例如访问 https://domain.com/goods/add,则请求将进入该代码块
location ^~ /goods {
# 转发请求到 127.0.0.1:8080
# 127.0.0.1 为接受请求的服务ip,端口为服务的端口,实际按照真实的ip:port进行配置
proxy_pass http://127.0.0.1:8080;
}
1.3.2 正则匹配示例
location ~* \.(gif|jpg|jpeg)$ {
# 匹配所有以 gif,jpg或jpeg 结尾的请求
proxy_pass http://127.0.0.1:8081;
}
location ^~ /\w+/test/ {
# 匹配所有/*/test/**为前缀的请求
proxy_pass http://127.0.0.1:8082;
}
1.4 RestFul匹配
RestFul匹配的使用在if代码块中进行演示,便于大家理解
# 匹配根目录,所有的请求都会进入次代码块
location / {
# 当请求uri匹配前缀==/goods/add**==,并且请求方式为POST请求,则进行请求转发
# 若请求uri匹配前缀==/goods/add**==,但请求方式不为POST,则不进行请求转发,继续进行后续的匹配逻辑,直至找到何时的匹配规则
if ($uri ~ POST-/goods/add){
proxy_pass http://127.0.0.1:8080;
}
}
2. 重写
2.1 重写语法
rewrite <regex> <replacement> [flag];
重写关键字 正则表达式 替换字符串 转发标记
2.2 rewrite关键字使用域
# 1. server代码块
server {
...
rewrite <regex> <replacement> [flag];
}
# 2. location代码块
location ... {
...
rewrite <regex> <replacement> [flag];
}
# 3. if代码块
if(true){
...
rewrite <regex> <replacement> [flag];
}
转发标记详解
last ---> 本条规则匹配完成后,继续向下匹配新的location URI规则, 浏览器地址栏URL地址不变
break ---> 本条规则匹配完成即终止,不再匹配后面的任何规则, 浏览器地址栏URL地址不变
redirect ---> 返回302临时重定向,浏览器地址会显示跳转后的URL地址
permanent ---> 返回301永久重定向,浏览器地址栏会显示跳转后的URL地址
重写示例
对形如/images/ef/uh7b3/test.png的请求,重写到/data?file=test.png,于是匹配到location /data,先看/data/images/test.png文件存不存在,如果存在则正常响应,如果不存在则重写tryfiles到新的image404 location,直接返回404状态码。
http {
# 定义image日志格式
log_format imagelog '[$time_local] ' $image_file ' ' $image_type ' ' $body_bytes_sent ' ' $status;
# 开启重写日志
rewrite_log on;
server {
root /home/www;
location / {
# 重写规则信息
error_log logs/rewrite.log notice;
# 注意这里要用‘’单引号引起来,避免{}
rewrite '^/images/([a-z]{2})/([a-z0-9]{5})/(.*)\.(png|jpg|gif)$' /data?file=$3.$4;
# 注意不能在上面这条规则后面加上“last”参数,否则下面的set指令不会执行
set $image_file $3;
set $image_type $4;
}
location /data {
# 指定针对图片的日志格式,来分析图片类型和大小
access_log logs/images.log mian;
root /data/images;
# 应用前面定义的变量。判断首先文件在不在,不在再判断目录在不在,如果还不在就跳转到最后一个url里
try_files /$arg_file /image404.html;
}
location = /image404.html {
# 图片不存在返回特定的信息
return 404 "image not found\n";
}
}
3. HTTP/HTTPS
前置知识
- HTTP请求默认解析端口为80
- HTTPS请求默认解析端口为443
问题
- 假设现在有一个域名aaa.bbb.ccc
- 要求使用这个域名既可以使用http协议又可以使用https协议
- http://aaa.bbb.ccc
- https://aaa.bbb.ccc
- 要求使用这个域名既可以使用http协议又可以使用https协议
- 下面来实现以下这个需求
- 前提
- 一个域名
- 解析域名到指定的Nginx服务器ip
- SSL证书并解析域名
- 下载SSL证书Nginx版
代码实现与注释详解
server {
# 开放80 端口监听
listen 80;
# 开放443 端口监听
listen 443 ssl;
# 虚拟主机,匹配合法host进入相应server逻辑代码块
server_name aaa.bbb.ccc;
# 设置字符集
charset utf-8;
# 配置Nginx访问日志,最后一级目录目录必须真实存在,否则检查配置的时候会报错
access_log /data/nginx/logs/80/access.log;
# 配置Nginx异常日志,最后一级目录目录必须真实存在,否则检查配置的时候会报错
error_log /data/nginx/logs/80/error.log;
# 配置SSL证书文件
ssl_certificate /etc/nginx/ssl/domain.pem;
# 配置SSL秘钥文件
ssl_certificate_key /etc/nginx/ssl/domain.key;
client_max_body_size 50m;
location / {
proxy_pass http://local_proxy;
proxy_redirect default;
}
}
4. TCP
基于长连接的转发使用stream代码块实现
- 常用的使用场景
- 连接内网数据库
- 资源反向代理
长连接的转发比较简单,直接附上配置解析
stream {
# 配置虚拟服务器节点
server {
# 监听13306端口,即对外暴露13306端口
listen 13306;
# 连接超时时间
proxy_connect_timeout 10s;
# 代理超时时间
proxy_timeout 300s;
# 转发ip:端口
# 此处演示的是转发阿里云RDS VPC网络连接地址
proxy_pass xxxx.rds.aliyuncs.com:3306;
}
}
- 配置完成后重载配置
nginx -t
nginx -s reload
- 之后通过nginx所在的ip:13306端口即可连接RDS的内网地址,实现公网转发内网的需求
5. 静态资源转发
本节内容建立在以上4节内容的基础之上,如有疑问,请先阅读上面的四个小节
转发示例
- 设定页面在/usr/local/static/h5/目录下,页面入口均为为index.html,目录结构如下
- /usr/local/static/h5/
// 淘宝活动页
- taobaoActivity
- css
- favicon.ico
- js
- index.html
// 官网页,vue工程,需要配置路由
- portal
- css
- favicon.ico
- js
- index.html
示例
server {
# 监听8909端口
listen 8909;
# 设置domain
server_name domain.com;
charset utf-8;
client_max_body_size 50m;
# 当uri匹配前缀/h5/taobaoActivity/时,请求将转发至Nginx本机的/usr/local/static/h5/taobaoActivity/目录
# 若请求 https://domain.com/h5/taobaoActivity/index.html
# 则该请求的内容为目录配置中的淘宝活动页
location /h5/taobaoActivity/ {
root /usr/local/static/;
index index_real.html index.htm;
}
# 当uri匹配前缀/h5/portal/时,请求将转发至Nginx本机的/usr/local/static/h5/portal/目录
# 若请求 https://domain.com/h5/portal
# 则该请求的内容为目录配置中的官网页
location /h5/portal/ {
alias /usr/local/static/h5/portal/;
index index.html index.htm;
# try_files取代了一部分rewrite的功能
# try_files执行的最终逻辑为:
# 1.将index.html拼接在$uri之后,则$uri=/h5/portal/index.html
# 2.当$uri=/h5/portal/index.html,在alias的别名真实路径,最终的请求将转发至/usr/local/static/h5/portal/index.html,即上述文件目录中的官网页路径
try_files $uri $uri/ index.html;
}
}
补充:alias与root的区别
- root和alias都可以定义在location模块中,都是用来指定请求资源的真实路径
location /i/ {
root /data/w3;
}
- 请求 http://foofish.net/i/top.gif 这个地址时,那么在服务器里面对应的真正的资源是 /data/w3/i/top.gif文件
- 注意:真实的路径是root指定的值加上location指定的值.
- alias指定的路径是location的别名,不管location的值怎么写,资源的真实路径都是alias指定的路径
location /i/ {
alias /data/w3/;
}
-
同样请求 http://foofish.net/i/top.gif 时,在服务器查找的资源路径是: /data/w3/top.gif
-
1、 alias 只能作用在location中,而root可以存在server、http和location中。
-
2、alias 后面必须要用 “/” 结束,否则会找不到文件,而 root 则对 ”/” 可有可无。