Skip to content

一. 安装

1. CentOS 7 安装 Nginx

1
2
3
4
5
6
7
8
# 更新系统
sudo yum update -y
# Nginx 不在默认的 CentOS 仓库中,需要先安装 EPEL 仓库
sudo yum install epel-release -y
# 安装 nginx
sudo yum install nginx -y
# 启动 nginx
sudo systemctl enable nginx --bow

2. Ubuntu 安装 Nginx

1
2
3
4
5
6
# 更新系统
sudo apt update
# 安装 nginx
sudo apt install nginx -y
# 启动 nginx
sudo systemctl enable nginx --now
添加到 systemd 管理 sudo vim /etc/systemd/system/nginx.service

[Unit]
Description=A high performance web server and a reverse proxy server
Documentation=https://nginx.org/en/docs/
After=network.target

[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t -q -g 'daemon on; master_process on;'
ExecStart=/usr/sbin/nginx -g 'daemon on; master_process on;'
ExecReload=/usr/sbin/nginx -g 'daemon on; master_process on;' -s reload
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target

3. Docker 启动 Nginx

# 简单运行
docker run -d --name my-nginx -p 8081:80 nginx:1.26.2

# 挂载容器卷运行
docker run  -p 8080:80 --name nginx \
-v /tem/study-nginx/conf/nginx.conf:/etc/nginx/nginx.conf \
-v /tem/study-nginx/conf/conf.d:/etc/nginx/conf.d \
-v /tem/study-nginx/html:/usr/share/nginx/html \
-v /tem/study-nginx/logs:/var/log/nginx \
-d  nginx:1.26.2

二. 配置

1. Nginx 目录结构

/etc/nginx/                  # 配置文件目录
├── nginx.conf               # 主配置文件
├── conf.d/                  # 服务配置文件目录
├── sites-available/         # 可用的站点配置(Ubuntu/Debian)
└── sites-enabled/           # 启用的站点配置(Ubuntu/Debian)

/var/log/nginx/              # 日志文件目录
├── access.log               # 访问日志
└── error.log                # 错误日志

/usr/share/nginx/html/      # 默认网页根目录(CentOS/RHEL)
├── 50x.html                # 50x 错误页面
└── index.html              # 默认首页

/var/www/html/               # 默认网页根目录(Ubuntu/Debian)
/var/cache/nginx/            # 缓存目录
/run/nginx.pid               # PID 文件
/usr/sbin/nginx              # Nginx 可执行文件

nginx服务主要有两种类型进程,worker负责处理请求,master负责指挥协调worker;

2. nginx启动命令

1
2
3
4
5
#进入 /usr/local/nginx/sbin 下再执行
./nginx         启动
./nginx -s stop     快速停止
./nginx -s quit     优雅关闭,在退出前完成已经接受的请求
./nginx -s reload   重新加载配置

3. nginx.cnf 配置文件

3.1 nginx.conf 示例

user  nginx;
worker_processes  auto;     #工作进程的个数,个数小于等于处理器进程个数

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;   #每个worker进程的最大连接数,一般默认就行
}


http {
    include       /etc/nginx/mime.types;    #引入配置文件;mime.types用于文件类型的映射,它根据文件的后缀,去匹配对应的格式,格式则告诉browser该怎么解析这个文件;
    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  /var/log/nginx/access.log  main;
    access_log offl;    #停止记录访问日志

    sendfile        on;     #数据0拷贝,减少了文件加载到内存和传输到相应端口的一次过程;默认on;使用linux的 sendfile(socket, file, len)高效网络传输;不开启时,文件会先被加载到内存中,内存中文件再被传到网络接口 socket 发送。开启后文件直接会被传到网络接口 socket 进行传输;

    #tcp_nopush     on;

    keepalive_timeout  65;  #设置 Keep-Alive 连接超时时间

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;   #引入配置文件
}

3.2 nginx 负载均衡

3.1.1 负载均衡方案

nginx 支持 轮询、ip_hash、least_conn、url_hash、fair 这五种负载均衡方案 - 轮询:逐一转发,这种方式适用于无状态请求 - ip_hash:根据客户端的ip地址转发同一台服务器,可以保持会话 - least_conn:最少连接访问,需要根据第三方插件搭配使用 - url_hash:根据用户访问的 url 定向转发请求。很少使用,可以用于做资源隔离 - fair:根据后端服务器响应时间转发请求,需要根据第三方插件搭配使用

3.1.2 weight 权重

只有轮询方案才能配置 weight,指定轮询时机率,weight和访问比率成正比,用于后端服务器性能不均的情况。 - down:表示当前的server暂时不参与负载 - weight:默认为1.weight越大,负载的权重就越大 - backup: 其它所有的非backup机器down或者忙的时候,请求backup机器

3.1.3 示例

nginx.conf中配置

1
2
3
4
5
upstream httpd {
    server 192.168.1.180 weight=10 down;
    server 192.168.1.181 weight=1;
    server 192.168.1.182 weight=1 backup;
}

4. default.conf 配置文件

server {        #virtual host虚拟主机,可以有多个。在这个版本及以后,通常在nginx.cnf中用include引入虚拟主机;一个server代表一个主机服务,server之间互不干扰,
    listen       80;    #服务占用端口号
    listen  [::]:80;
    server_name  localhost; #主机名、域名,匹配分先后顺序,前面的匹配上就不会再往下匹配了

    #access_log  /var/log/nginx/host.access.log  main;

    location / {    #对应uri,对请求路径做映射;格式:请求路径   实际资源存放路径;
    #proxy_pass http://192.168.0.104;   #反向代理到的地址
    rewrite ^/([0-9]+).html$  /index.jsp?num=$1  break; #把地址栏参数改为类似restful风格,如:/2.html
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
    upstream name{  #配置负载均衡服务器,参数:weight是权重,权重越高,被分配到的请求比例就越高;down代表不参与负载均衡;backup标记为备份,类似备胎作用,正常的服务器忙时或者挂了时才会处理请求;不配置权重时,默认是轮询,但轮询不会保存会话。
    server 192.168.0.105 weight=8 down;
    server 192.168.0.106 weight=4;
    server 192.168.0.106 weight=1 backup;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;    #若请求出错,则转到相应的错误页面提示页面
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

4.2 location

4.2.1 location 前缀

通过 location 前缀来确定匹配规则 - "=" 精准匹配,不是以指定模式开头 - "^~" 非正则匹配,匹配以指定模式开头的 location - "~" 正则匹配,区分大小写 - "~*" 正则匹配,不区分大小写 - "/" 通用匹配,任何请求都会匹配到

4.2.2 location 匹配顺序

  • 多个正则 location 直接按书写顺序匹配,成功后就不会继续往后面匹配
  • 普通(非正则)location 会一直往下,直到找到匹配度最高的(最大前缀匹配)
  • 当普通 location 与正则 location 同时存在,如果正则匹配成功,则不会再执行普通匹配
  • 所有类型 location 存在时,“=”匹配 > “^~”匹配 > 正则匹配 > 普通(最大前缀匹配),如 4.2.1 所示

4.3 alias与root

root用来设置根目录,而alias在接受请求的时候在路径上不会加上location。

1
2
3
4
location /css {
    alias /usr/local/nginx/static/css;
    index index.html index.htm;
}

  1. alias指定的目录是准确的,即location匹配访问的path目录下的文件直接是在alias目录下查找的;
  2. root指定的目录是location匹配访问的path目录的上一级目录,这个path目录一定要是真实存在root指定目录下的;
  3. 使用 alias 标签的目录块中不能使用rewrite的break(具体原因不明);另外,alias指定的目录后面必须要加上"/"符号!!
  4. alias虚拟目录配置中,location匹配的path目录如果后面不带"/",那么访问的url地址中这个path目录后 面加不加"/"不影响访问,访问时它会自动加上"/"; 但是如果location匹配的path目录后面加上"/",那么访问的url地址中这个path目录必须要加上"/",访问时它不会自动加上"/"。如果不加上"/",访问就会失败!
  5. root 目录配置中,location匹配的path目录后面带不带"/",都不会影响访问。

5. rewrite

rewrite 是实现 URL 重写的关键指令,把地址栏参数改为类似 restful 风格,如:/2.html;

5.1 rewrite 格式

格式 rewrite <regex> <replacement> [flag]; - 关键字:其中关键字 error_log 不能改变; - regex:perl 兼容正则表达式语句进行规则匹配; - replacement:将正则匹配的内容替换成 replacement; - flag 标记:rewrite 支持的 flag 标记;

rewrite 参数的标签段所在位置:server,location,if;

5.2 flag 标记说明

  • last 本条规则匹配完成后,继续向下匹配新的 location URI 规则
  • break 本条规则匹配完成即终止,不再匹配后面的任何规则
  • redirect 返回 302 临时重定向,浏览器地址会显示跳转后的 URL 地址
  • permanent 返回 301 永久重定向,浏览器地址栏会显示跳转后的 URL 地址

5.3 示例

# 把 /12.html 转换成 /index.jsp?pageNum=12 
rewrite ^/([0-9]+).html$ /index.jsp?pageNum=$1 break;

6. 防盗链

防盗链用来防止其他网站直接引用你网站的资源。网页在第二个请求开始,就会在请求头中携带 Referer 参数,Referer 中记录着当前请求是从哪个域名发出的。nginx 防盗链的作用就是通过 HTTP 请求头里的 Referer 来判断资源访问来源,从而控制是否允许访问。

6.1 格式

valid_referers none | blocked | server_names | domain_name ....;
- none, 允许没有 Referer 的请求访问。 - blocked,允许 Referer 头域的值被防火墙或者代理服务器删除或伪装的情况下访问。这种情况该头域的值不以 “http://”“https://” 开头。 - server_names ,设置一个或多个域名 ,允许 Referer 的值与该域名匹配的请求。

6.2 示例

# 主机 180 代理到 181 时,会携带 reference 参数。验证请求中 reference 是否是 192.168.1.180,若不是则返回 403 ,若是则正确访问。
#如下配置应配置在 181 主机中的 location 块里面
location / {
    valid_referers none *.moloom.com *.moloom.cn 192.168.1.180;
    if ($invalid_referer) { # $invalid_referer Nginx 内置变量,表示是否是非法来源
        return 403;
    }
    root   /usr/share/nginx/html;
    index  index.html index.htm;
}

7. Keepalived

Keepalived 基于 VRRP(Virtual Router Redundancy Protocol,虚拟路由冗余协议) 实现高可用(HA)。基于 ipvsadm 实现负载均衡

VRRP 的 RFC 3768 规定使用 224.0.0.18 作为组播地址,端口号 112(IP 协议号)。 所有支持 VRRP 的设备(如路由器、防火墙)默认监听该地址。 组播地址是保留的(224.0.0.0/24 为本地网络控制用途),不会与普通单播 IP 冲突。

Keepalived 的 master 节点每隔 advert_int 秒向 224.0.0.18 发送 VRRP 广播包。Backup 节点 监听 224.0.0.18,如果超时(默认 3 秒)未收到广播,则发起选举。选举规则如下:只有单台 master,master 宕机,backup 接管 VIP,master 恢复,不管 BACKUP 是否配置了 nopreempt ,master必接管 VIP!若多个 BACKUP 之间,有某个设备宕机了又重新恢复,才会根据当前所接管 VIP 的设备的 nopreempt 配置来决定是否被优先级更高的设备接管 VIP。

有三台设备,主要配置如下: master(192.168.1.180),state MASTER,priority 100,nopreempt node1(192.168.1.181),state BACKUP,priority 90,nopreempt node2(192.168.1.182),state BACKUP,priority 80,nopreempt node3 (192.168.1.183),state BACKUP,priority 70

  • master 宕机 → node1 接管 VIP
  • node1 宕机 → node2 接管 VIP
  • node2 宕机 → node3 接管 VIP
  • node2 恢复 → node2 抢占接管 VIP
  • node1 恢复 → node2 持续接管 VIP(配置了非抢占)
  • master 恢复 → master 接管 VIP(master 高于 backup!)

nopreempt 配置决定该设备是否能被抢占 VIP,设置了该配置的 BACKUP 节点,只能被 MASTER 抢占 VIP,不能被高优先级的 BACKUP 抢占。

7.1 安装

每个节点都需要安装!

# 更新apt
sudo apt update
# 安装 keepalived
sudo apt install -y keepalived
# 启用内核转发
ansible master,node1,node2 -m shell -a 'sed "/^#net.ipv4.ip_forward=1/s/^#//" -i /etc/sysctl.conf && sudo sysctl -p' --become -K

#配置完配置文件后执行如下命令

# 启动 keepalived
sudo systemctl enable --now keepalived
# 检查状态
sudo systemctl status keepalived

# 查看组播情况
sudo tcpdump -i enp1s0 -nn host 224.0.0.18

若网卡没有启动组播(MULTICAST)则用 sudo ip link set enp1s0 multicast on 启动。

防火墙放行 VRRP 协议

# UFW(Ubuntu)
sudo ufw allow proto vrrp
sudo ufw allow to 224.0.0.18

# firewalld(RHEL/CentOS)
sudo firewall-cmd --add-rich-rule='rule protocol value="vrrp" accept' --permanent
sudo firewall-cmd --reload

# iptables(通用)
sudo iptables -A INPUT -p vrrp -j ACCEPT
sudo iptables -A INPUT -d 224.0.0.18 -j ACCEPT

7.2 默认的 keepalived 配置文件

keepadlived 配置文件路径 /etc/keepalived/keepalived.conf

! Configuration File for keepalived

global_defs {
   notification_email {
     acassen
   }
   notification_email_from Alexandre.Cassen@firewall.loc
   smtp_server 192.168.200.1
   smtp_connect_timeout 30
   router_id master     # 主机名 
   #vrrp_strict
}

vrrp_instance VI_1 {
    state MASTER    # 只启到标识作用,对 keepalived 无用
    interface enp1s0    #绑定的网卡
    virtual_router_id 77    # 虚拟路由 ID,主备节点必须相同
    nopreempt       # 不启用抢占模式
    priority 100    # 优先级,由优先级区分 master 和 backup。数值越大,优先级越高
    advert_int 1    # 心跳间隔(秒)
    authentication {
        auth_type PASS
        auth_pass 123456     # 密码,主备节点需一致
    }
    virtual_ipaddress {
        192.168.200.11/24   # 虚拟 IP(VIP),根据网络配置修改
    }
}

# 如果不需要负载均衡,直接删除 virtual_server 配置
virtual_server 10.10.10.2 1358 {
    delay_loop 6
    lb_algo rr
    lb_kind NAT
    persistence_timeout 50
    protocol TCP

    sorry_server 192.168.200.200 1358

    real_server 192.168.200.2 1358 {
        weight 1
        HTTP_GET {
            url {
              path /testurl3/test.jsp
              digest 640205b7b0fc66c1ea91c463fac6334d
            }
            connect_timeout 3
            retry 3
            delay_before_retry 3
        }
    }
}

7.3 MASTER 的配置

! Configuration File for keepalived

global_defs {
   notification_email {
     acassen
   }
   notification_email_from Alexandre.Cassen@firewall.loc
   #smtp_server 192.168.200.1
   # smtp_connect_timeout 30
   router_id master     # 主机名
   #vrrp_strict
}

vrrp_instance VI_1 {
    state MASTER    # 只启到标识作用,对 keepalived 无用
    interface enp1s0    #绑定的网卡
    virtual_router_id 77    # 虚拟路由 ID,主备节点必须相同
    nopreempt
    priority 100    # 优先级,由优先级区分 master 和 backup。数值越大,优先级越高
    advert_int 1    # 心跳间隔(秒)
    authentication {
        auth_type PASS
        auth_pass 1234567     # 密码,主备节点需一致
    }
    virtual_ipaddress {
        192.168.1.240/24    # 虚拟 IP(VIP),根据网络配置修改,可以有多个,但是主备节点都需一致
    }
}

7.4 BACKUP 的配置

vrrp_instance VI_1 {
    state BACKUP
    interface enp1s0    #绑定的网卡
    virtual_router_id 77    # 虚拟路由 ID,主备节点必须相同
    priority 90     # 优先级,由优先级区分 master 和 backup。数值越大,优先级越高
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass 1234567
    }
    virtual_ipaddress {
        192.168.1.240/24    # 虚拟 IP(VIP),根据网络配置修改,可以有多个,但是主备节点都需一致
    }
}

8. nginx 流量限制

8.1 漏桶算法

漏桶算法 (Leaky Bucket Algorithm),以固定速率处理请求,每秒放行的请求数=rate+burst。

1
2
3
4
# nodelay 会将超过 burst 数量的请求给直接拒绝。不配置它,超出的请求会等待
# burst 允许的突发请求数(缓冲队列大小)
limit_req_zone $binary_remote_addr zone=name:10m rate=2r/s;
limit_req zone=name [burst=number] [nodelay];

8.1.1 不同URL不同限流策略

map $uri $limit_rate {
    ~^/api/v1/ 10r/s;
    ~^/api/v2/ 20r/s;
    default 5r/s;
}

server {
    limit_req_zone $binary_remote_addr zone=dynamic:10m rate=$limit_rate;

    location /api/ {
        limit_req zone=dynamic burst=20;
        proxy_pass http://backend;
    }
}

8.1.2 白名单设置

geo $limit {
    default 1;
    10.0.0.0/8 0;
    192.168.0.0/24 0;
}

map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}

limit_req_zone $limit_key zone=ip_limit:10m rate=10r/s;

8.2 令牌桶算法

令牌桶算法(Token Bucket Algorithm)是一种广泛应用于网络流量整形和速率限制的算法,与漏桶算法(Leaky Bucket)齐名,是两种最常用的流量控制算法之一。

系统以固定速率向桶中添加令牌,每个请求/数据包需要消耗一个令牌才能被处理,当桶中有足够令牌时,请求被立即处理,若无足够令牌时,请求被延迟或丢弃。

1
2
3
4
5
6
# burst 令牌桶的容量
# limit_rate 限制传输速率
# limit_rate_after  在传输多少容量后再限速

limit_rate 1k;
limit_rate_after 1m;

8.3 计数器算法

计数器算法用于进行并发数的限制。

1
2
3
limit_conn_zone $binary_remote_addr zone=two:10m rate=10r/s;

limit_conn two 1;

9. nginx 日志

默认的日志配置:来一个请求就追加到日志文件里,请求一多,很容易影响性能。

access log 日志性能优化

日志格式:access_log path [format [buffer=23k] [gzip [=5]] [flush=30s] [if=condition] ]; - format:日志的打印格式,用 log_format 定义 - buffer:设置日志写入的缓冲区大小,减少磁盘 I/O 操作,缓冲区满才会写入磁盘(崩溃会导致缓冲区内日志消失) - gzip:日志文件在写入时直接压缩,值范围(1-9 ),9是最大压缩率。需要 Nginx 支持 zlib 库,增加CPU开销,节省磁盘空间 - flush:缓冲区内日志每隔 flush 时间久写入磁盘 - if:满足条件的请求才记录

log_format  main  '$remote_addr - $remote_user [$time_local] "$request_uri" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
map $status $loggable {
  ~^[23]  0;  # 2xx/3xx 状态码不记录
  default 1;
}

# 默认配置
access_log /var/log/nginx/access.log main;

# 
access_log /var/log/nginx/access.log main buffer=32k flush=60m gzip if=$loggable;

error log 日志

错误日志格式:error_log path [level]; 错误日志有以下日志级别:

  • debug:调试信息(极详细,仅开发使用)。
  • info:一般信息。
  • notice:需要注意的事件。
  • warn:警告(不影响运行)。
  • error:错误(默认推荐级别)。
  • crit:严重错误。
  • alert:需立即处理的错误。
  • emerg:紧急情况(服务不可用)。
1
2
3
4
5
6
7
# 关闭日志
error_log /dev/null;

# 将错误日志发送到远程 Syslog 服务器(如 ELK/EFK 栈)
error_log syslog:server=10.0.0.1:514,facility=local7,tag=nginx_error warn;
# 直接输出到 stdout/stderr,由 Docker/K8s 收集
error_log stderr warn;

logrotate 日志分割

在宿主机上执行。nginx 容器内日志分割

vim /etc/logrotate.d/nginx

注释版
/mohome/nginx/logs/*.log {  # 路径必须对应挂载在宿主机上的位置
    daily          # 按天切割
    rotate 7       # 保留7天
    compress       # 压缩旧日志
    delaycompress  # 延迟一天压缩
    missingok      # 文件不存在时不报错
    notifempty     # 空文件不切割
    sharedscripts  # 所有日志完成后执行脚本
    postrotate
        #[ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
        # 向容器内的 nginx 主进程发送 USR1 信号
        docker kill -s USR1 mohome-homepage-1 2>/dev/null || true
    endscript
}

无注释版
/mohome/nginx/logs/*.log {
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    sharedscripts
    postrotate
        docker kill -s USR1 mohome-homepage-1 2>/dev/null || true
    endscript
}

1
2
3
4
5
6
7
8
# 手动测试
sudo logrotate -vf /etc/logrotate.d/nginx

#  检查定时任务是否存在
ls /etc/cron.daily/logrotate  # 检查是否存在

# 确保 logrotate 和 Nginx 有读写权限
sudo chown -R 101:101 /mohome/nginx/logs/  # 101 是 nginx 容器的默认 UID

三. lua 脚本

Lua 是由巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组于1993年开发的一种轻量、小巧的脚本语言,用标准 C 语言编写,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

Lua插件: https://github.com/EmmyLua/IntelliJ-EmmyLua https://emmylua.github.io/zh_CN/

1. lua 基础语法

1.1 hello word

print("hello world!")

1.2 保留关键字

and       break     do    else      elseif      end       false    for       function  if      in        local     nil      not      or      repeat    return    then     true      until    while

1.3 注释

1
2
3
4
5
6
7
8
-- 两个减号是行注释

--[[

 这是块注释
 这是块注释.

 --]]

1.4 变量

1.4.1 数字类型

Lua的数字只有double型,64bits

你可以以如下的方式表示数字

num = 1024

num = 3.0

num = 3.1416

num = 314.16e-2

num = 0.31416E1

num = 0xff

num = 0x56

1.4.2 字符串

可以用单引号,也可以用双引号。 也可以使用转义字符‘\n’ (换行), ‘\r’ (回车), ‘\t’ (横向制表), ‘\v’ (纵向制表), ‘\’ (反斜杠), ‘\”‘ (双引号), 以及 ‘\” (单引号)等等

1
2
3
4
5
6
7
8
9
-- 下面的四种方式定义了完全相同的字符串(其中的两个中括号可以用于定义有换行的字符串)
a = 'alo\n123"'

a = "alo\n123\""

a = '\97lo\10\04923"'

a = [[alo
123"]]

1.4.3 空值

C 语言中的 NULL 在 Lua 中是 nil,比如你访问一个没有声明过的变量,就是 nil

1.4.4 布尔类型

只有nil和false是 false,数字0,‘’空字符串(’\0’)都是true

1.4.5 作用域

lua中的变量如果没有特殊说明,全是全局变量,那怕是语句块或是函数里!!! 变量前加 local 关键字的是局部变量。

1.5 控制语句

1.5.1 while循环

1
2
3
4
5
6
local i = 0
local max = 10
while i <= max do
print(i)
i = i +1
end

1.5.2 if-else

local function main()

local age = 140
local sex = 'Male'
  if age == 40 and sex =="Male" then
    print(" 男人四十一枝花 ")
  elseif age > 60 and sex ~="Female" then
    print("old man!!")
  elseif age < 20 then
    io.write("too young, too simple!\n")
  else
  print("Your age is "..age)
  end
end

-- 调用
main()

1.5.3 for循环

1
2
3
4
sum = 0
for i = 100, 1, -2 do
    sum = sum + i
end

1.6 函数 function

方法1

1
2
3
4
5
6
7
-- 方法
function myPower(x,y)
  return      y+x
end
-- 调用 方法
power2 = myPower(2,3)
print(power2)

方法2

function newCounter()
   local i = 0
   return function()     -- anonymous function
        i = i + 1
        return i
    end
end

c1 = newCounter()
print(c1())  --> 1
print(c1())  --> 2
print(c1())

1.6.1 返回值

1
2
3
4
5
6
7
8
9
name, age,bGay = "yiming", 37, false, "yimingl@hotmail.com"
print(name,age,bGay) 


function isMyGirl(name)
  return name == 'xiao6' , name
end
local bol,name = isMyGirl('xiao6')
print(name,bol)

1.7 Table

key,value的键值对 类似 map

1
2
3
4
5
6
7
local function main()
dog = {name='111',age=18,height=165.5}
dog.age=35
print(dog.name,dog.age,dog.height)
print(dog)
end
main()

1.8 数组

1
2
3
4
5
local function main()
arr = {"string", 100, "dog",function() print("wangwang!") return 1 end}
print(arr[4]())
end
main()

1.9 for循环

1
2
3
4
arr = {"string", 100, "dog",function() print("wangwang!") return 1 end}
for k, v in pairs(arr) do
   print(k, v)
end

1.10 成员函数

1
2
3
4
5
6
7
8
local function main()
person = {name='旺财',age = 18}
  function  person.eat(food)
    print(person.name .." eating "..food)
  end
person.eat("骨头")
end
main()

2. Openresty Nginx + Lua

Nginx是一个主进程配合多个工作进程的工作模式,每个进程由单个线程来处理多个连接。 在生产环境中,我们往往会把cpu内核直接绑定到工作进程上,从而提升性能。

Nginx是一个主进程配合多个工作进程的工作模式,每个进程由单个线程来处理多个连接。

在生产环境中,我们往往会把cpu内核直接绑定到工作进程上,从而提升性能。

安装

预编译安装

以CentOS举例 其他系统参照:http://openresty.org/cn/linux-packages.html

你可以在你的 CentOS 系统中添加 openresty 仓库,这样就可以便于未来安装或更新我们的软件包(通过 yum update 命令)。运行下面的命令就可以添加我们的仓库:

 yum install yum-utils

 yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

然后就可以像下面这样安装软件包,比如 openresty:

 yum install openresty

如果你想安装命令行工具 resty,那么可以像下面这样安装 openresty-resty 包:

 sudo yum install openresty-resty

源码编译安装

下载

http://openresty.org/cn/download.html

最小版本基于nginx1.21

./configure

然后在进入 openresty-VERSION/目录, 然后输入以下命令配置:

./configure

默认, --prefix=/usr/local/openresty 程序会被安装到/usr/local/openresty目录。

依赖 gcc openssl-devel pcre-devel zlib-devel

安装:yum install gcc openssl-devel pcre-devel zlib-devel postgresql-devel

您可以指定各种选项,比如

```lua ./configure --prefix=/opt/openresty \

        --with-luajit \

        --without-http_redis2_module \

        --with-http_iconv_module \

        --with-http_postgres_module

```

试着使用 ./configure --help 查看更多的选项。

make && make install

服务命令

启动

Service openresty start

停止

Service openresty stop

检查配置文件是否正确

Nginx -t

重新加载配置文件

Service openresty reload

查看已安装模块和版本号

Nginx -V

测试lua脚本

1
2
3
4
5
6
7
8
在Nginx.conf 中写入
   location /lua {

        default_type text/html;
        content_by_lua '
           ngx.say("<p>Hello, World!</p>")
         ';
      }

lua-nginx-module

创建配置文件lua.conf

   server {
        listen       80;
        server_name  localhost;

   location /lua {

        default_type text/html;

        content_by_lua_file conf/lua/hello.lua;

         }
}

在Nginx.conf下引入lua配置

include lua.conf;

创建外部lua脚本

conf/lua/hello.lua

内容:

ngx.say("<p>Hello, World!</p>")

获取Nginx uri中的单一变量

    location /nginx_var {

         default_type text/html;

        content_by_lua_block {

            ngx.say(ngx.var.arg_a)

        }
    }

获取Nginx uri中的所有变量

local uri_args = ngx.req.get_uri_args()  

for k, v in pairs(uri_args) do  

    if type(v) == "table" then  

        ngx.say(k, " : ", table.concat(v, ", "), "<br/>")  

    else  

        ngx.say(k, ": ", v, "<br/>")  

    end  
end

在处理http请求时还可以使用

  • set_by_lua

修改nginx变量

  • rewrite_by_lua

修改uri

  • access_by_lua

访问控制

  • header_filter_by_lua

修改响应头

  • boy_filter_by_lua

修改响应体

  • log_by_lua

日志

代码热部署

lua_code_cache off

获取Nginx请求头信息

local headers = ngx.req.get_headers()                         

ngx.say("Host : ", headers["Host"], "<br/>")  

ngx.say("user-agent : ", headers["user-agent"], "<br/>")  

ngx.say("user-agent : ", headers.user_agent, "<br/>")

for k,v in pairs(headers) do  

    if type(v) == "table" then  

        ngx.say(k, " : ", table.concat(v, ","), "<br/>")  

    else  

        ngx.say(k, " : ", v, "<br/>")  

    end  

end  

获取post请求参数

ngx.req.read_body()  

ngx.say("post args begin", "<br/>")  

local post_args = ngx.req.get_post_args()  

for k, v in pairs(post_args) do  

    if type(v) == "table" then  

        ngx.say(k, " : ", table.concat(v, ", "), "<br/>")  

    else  

        ngx.say(k, ": ", v, "<br/>")  

    end  
end

http协议版本

ngx.say("ngx.req.http_version : ", ngx.req.http_version(), "<br/>")

请求方法

ngx.say("ngx.req.get_method : ", ngx.req.get_method(), "<br/>")  

原始的请求头内容

ngx.say("ngx.req.raw_header : ",  ngx.req.raw_header(), "<br/>")  

body内容体

ngx.say("ngx.req.get_body_data() : ", ngx.req.get_body_data(), "<br/>")

Nginx缓存

Nginx全局内存缓存

lua_shared_dict shared_data 1m;

local shared_data = ngx.shared.shared_data  



local i = shared_data:get("i")  

if not i then  

    i = 1  

    shared_data:set("i", i)  

    ngx.say("lazy set i ", i, "<br/>")  
end  


i = shared_data:incr("i", 1)  

ngx.say("i=", i, "<br/>")

lua-resty-lrucache

Lua 实现的一个简单的 LRU 缓存,适合在 Lua 空间里直接缓存较为复杂的 Lua 数据结构:它相比 ngx_lua 共享内存字典可以省去较昂贵的序列化操作,相比 memcached 这样的外部服务又能省去较昂贵的 socket 操作

https://github.com/openresty/lua-resty-lrucache

引用lua文件

1
2
3
                content_by_lua_block {
                require("my/cache").go()
            }

自定义函数

local _M = {}


lrucache = require "resty.lrucache"

c, err = lrucache.new(200)  -- allow up to 200 items in the cache
ngx.say("count=init")


if not c then
    error("failed to create the cache: " .. (err or "unknown"))
end

function _M.go()

count = c:get("count")


c:set("count",100)
ngx.say("count=", count, " --<br/>")


if not count then  


    c:set("count",1)

    ngx.say("lazy set count ", c:get("count"), "<br/>")  

else


c:set("count",count+1)



ngx.say("count=", count, "<br/>")
end


end
return _M

打开lua_code_cache

lua-resty-redis访问redis

https://github.com/openresty/lua-resty-redis

常用方法

1
2
3
4
5
local res, err = red:get("key")

local res, err = red:lrange("nokey", 0, 1)

ngx.say("res:",cjson.encode(res))

创建连接

1
2
3
red, err = redis:new()

ok, err = red:connect(host, port, options_table?)

timeout

red:set_timeout(time)

keepalive

red:set_keepalive(max_idle_timeout, pool_size)

close

ok, err = red:close()

pipeline

1
2
3
red:init_pipeline()

results, err = red:commit_pipeline()

认证

1
2
3
4
5
6
7
8
    local res, err = red:auth("foobared")

    if not res then

        ngx.say("failed to authenticate: ", err)

        return
end
  local redis = require "resty.redis"
                local red = redis:new()

                red:set_timeouts(1000, 1000, 1000) -- 1 sec

  local ok, err = red:connect("127.0.0.1", 6379)
 if not ok then
                    ngx.say("failed to connect: ", err)
                    return
                end

                ok, err = red:set("dog", "an animal")
                if not ok then
                    ngx.say("failed to set dog: ", err)
                    return
                end

                ngx.say("set result: ", ok)

                local res, err = red:get("dog")
                if not res then
                    ngx.say("failed to get dog: ", err)
                    return
                end

                if res == ngx.null then
                    ngx.say("dog not found.")
                    return
                end


              ngx.say("dog: ", res)

redis-cluster支持

https://github.com/steve0511/resty-redis-cluster

redis2-nginx-module

redis2-nginx-module是一个支持 Redis 2.0 协议的 Nginx upstream 模块,它可以让 Nginx 以非阻塞方式直接防问远方的 Redis 服务,同时支持 TCP 协议和 Unix Domain Socket 模式,并且可以启用强大的 Redis 连接池功能。

test

location = /foo {

default_type text/html;

     redis2_query auth 123123;

     set $value 'first';

     redis2_query set one $value;

     redis2_pass 192.168.199.161:6379;

 }

get

location = /get {

default_type text/html;

     redis2_pass 192.168.199.161:6379;

     redis2_query auth 123123;

     set_unescape_uri $key $arg_key;  # this requires ngx_set_misc

     redis2_query get $key;

}

set

# GET /set?key=one&val=first%20value

location = /set {

default_type text/html;

redis2_pass 192.168.199.161:6379;

redis2_query auth 123123;


     set_unescape_uri $key $arg_key;  # this requires ngx_set_misc

     set_unescape_uri $val $arg_val;  # this requires ngx_set_misc

     redis2_query set $key $val;

 }

pipeline

     set $value 'first';

     redis2_query set one $value;

     redis2_query get one;

     redis2_query set one two;

     redis2_query get one;

redis2_query del key1;

list

1
2
3
4
5
6
7
    redis2_query lpush key1 C;

    redis2_query lpush key1 B;

    redis2_query lpush key1 A;

redis2_query lrange key1 0 -1;

集群

upstream redis_cluster {

     server 192.168.199.161:6379;

     server 192.168.199.161:6379;

 }

location = /redis {

default_type text/html;

         redis2_next_upstream error timeout invalid_response;

         redis2_query get foo;

         redis2_pass redis_cluster;
   }

lua-resty-mysql

https://github.com/openresty/lua-resty-mysql

local mysql = require "resty.mysql"
                local db, err = mysql:new()
                if not db then
                    ngx.say("failed to instantiate mysql: ", err)
                    return
                end

                db:set_timeout(1000) -- 1 sec


                local ok, err, errcode, sqlstate = db:connect{
                    host = "192.168.44.211",
                    port = 3306,
                    database = "zhangmen",
                    user = "root",
                    password = "111111",
                    charset = "utf8",
                    max_packet_size = 1024 * 1024,
                }


                ngx.say("connected to mysql.<br>")



                local res, err, errcode, sqlstate = db:query("drop table if exists cats")
                if not res then
                    ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".")
                    return
                end


                res, err, errcode, sqlstate =
                    db:query("create table cats "
                             .. "(id serial primary key, "
                             .. "name varchar(5))")
                if not res then
                    ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".")
                    return
                end

                ngx.say("table cats created.")



                res, err, errcode, sqlstate =
                    db:query("select * from t_emp")
                if not res then
                    ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".")
                    return
                end

                local cjson = require "cjson"
                ngx.say("result: ", cjson.encode(res))


                local ok, err = db:set_keepalive(10000, 100)
                if not ok then
                    ngx.say("failed to set keepalive: ", err)
                    return
                end

模板实时渲染 lua-resty-template

https://github.com/bungle/lua-resty-template

如果学习过JavaEE中的servlet和JSP的话,应该知道JSP模板最终会被翻译成Servlet来执行;

而lua-resty-template模板引擎可以认为是JSP,其最终会被翻译成Lua代码,然后通过ngx.print输出。

lua-resty-template大体内容有:

l 模板位置:从哪里查找模板;

l 变量输出/转义:变量值输出;

l 代码片段:执行代码片段,完成如if/else、for等复杂逻辑,调用对象函数/方法;

l 注释:解释代码片段含义;

l include:包含另一个模板片段;

l 其他:lua-resty-template还提供了不需要解析片段、简单布局、可复用的代码块、宏指令等支持。

基础语法

l {(include_file)}:包含另一个模板文件;

l { var }:变量输出;

l {{ var }}:变量转义输出;

l {% code %}:代码片段;

l {# comment #}:注释;

l {-raw-}:中间的内容不会解析,作为纯文本输出;

lua代码热加载

在http模块中加入

lua_code_cache off;

reload后Nginx会提示影响性能,记得在生产环境中关掉。

1569585068623

测试

一、初始化

1
2
3
4
5
6
7
8
-- Using template.new
local template = require "resty.template"
local view = template.new "view.html"
view.message = "Hello, World!"
view:render()

-- Using template.render
-- template.render("view.html", { message = "Hel11lo, Worl1d!" })

二、执行函数,得到渲染之后的内容

1
2
3
local func = template.compile("view.html")  
local content = func(context)  
ngx.say("xx:",content) 

模板文件存放位置

nginx.conf中配置
set $template_root /usr/local/openresty/nginx/tmp;

resty.template.html

local template = require("resty.template")
local html = require "resty.template.html"

template.render([[
<ul>
{% for _, person in ipairs(context) do %}
    {*html.li(person.name)*} --
{% end %}
</ul>
<table>
{% for _, person in ipairs(context) do %}
    <tr data-sort="{{(person.name or ""):lower()}}">
        {*html.td{ id = person.id }(person.name)*}
    </tr>
{% end %}
</table>]], {
    { id = 1, name = "Emma"},
    { id = 2, name = "James" },
    { id = 3, name = "Nicholas" },
    { id = 4 }
})

模板内容

1
2
3
4
5
6
<!DOCTYPE html>
<html>
<body>
  <h1>{{message}}</h1>
</body>
</html>

多值传入

1
2
3
4
5
6
7
template.caching(false)
local template = require("resty.template")
local context = {
    name = "lucy",
    age = 50,
}
template.render("view.html", context)

模板内容

1
2
3
4
5
6
7
<!DOCTYPE html>
<html>
<body>
  <h1>name:{{name}}</h1>
  <h1>age:{{age}}</h1>
</body>
</html>

模板管理与缓存

模板缓存:默认开启,开发环境可以手动关闭

template.caching(true)

模板文件需要业务系统更新与维护,当模板文件更新后,可以通过模板版本号或消息通知Openresty清空缓存重载模板到内存中

template.cache = {}

完整页面

local template = require("resty.template")
template.caching(false)
local context = {
    title = "测试",
    name = "lucy",
    description = "<script>alert(1);</script>",
    age = 40,
    hobby = {"电影", "音乐", "阅读"},
    score = {语文 = 90, 数学 = 80, 英语 = 70},
    score2 = {
        {name = "语文", score = 90},
        {name = "数学", score = 80},
        {name = "英语", score = 70},
    }
}

template.render("view.html", context)

模板

{(header.html)}  
   <body>  
      {# 不转义变量输出 #}  
      姓名:{* string.upper(name) *}<br/>  
      {# 转义变量输出 #}  
      简介:{{description}}
           简介:{* description *}<br/>  
      {# 可以做一些运算 #}  
      年龄: {* age + 10 *}<br/>  
      {# 循环输出 #}  
      爱好:  
      {% for i, v in ipairs(hobby) do %}  
         {% if v == '电影' then  %} - xxoo

              {%else%}  - {* v *} 
{% end %}  

      {% end %}<br/>  

      成绩:  
      {% local i = 1; %}  
      {% for k, v in pairs(score) do %}  
         {% if i > 1 then %},{% end %}  
         {* k *} = {* v *}  
         {% i = i + 1 %}  
      {% end %}<br/>  
      成绩2:  
      {% for i = 1, #score2 do local t = score2[i] %}  
         {% if i > 1 then %},{% end %}  
          {* t.name *} = {* t.score *}  
      {% end %}<br/>  
      {# 中间内容不解析 #}  
      {-raw-}{(file)}{-raw-}  
{(footer.html)}  

layout 布局统一风格

使用模板内容嵌套可以实现全站风格同一布局

lua

local template = require "resty.template"

一、

1
2
3
4
5
6
7
local layout   = template.new "layout.html"

layout.title   = "Testing lua-resty-template"

layout.view    = template.compile "view.html" { message = "Hello, World!" }

layout:render()

二、

1
2
3
4
5
6
7
8
9
template.render("layout.html", {

  title = "Testing lua-resty-template",

  msg = "type=2",

  view  = template.compile "view.html" { message = "Hello, World!" }

})

三、

此方式重名变量值会被覆盖

1
2
3
4
5
6
7
8
9
local view     = template.new("view.html", "layout.html")

view.title     = "Testing lua-resty-template"

view.msg = "type=3"

view.message   = "Hello, World!"

view:render()

四、

可以区分一下

local layout   = template.new "layout.html"

layout.title   = "Testing lua-resty-template"

layout.msg = "type=4"

local view     = template.new("view.html", layout)

view.message   = "Hello, World!"

view:render()

layout.html

<!DOCTYPE html>

<html>

<head>

​    <title>{{title}}</title>

</head>

<h1>layout</h1>

<body>

​    {*view*}

</body>

</html>

view.html·

msg:{{message}}

多级嵌套

lua

local view     = template.new("view.html", "layout.html")

view.title     = "Testing lua-resty-template"

view.message   = "Hello, World!"

view:render()

view.html

{% layout="section.html" %}

msg:{{message}}

section.html

​ {*view*} - sss

layout.html

{{title}}

layout {{msg}}

​ {*view*}

Redis缓存+mysql+模板输出

lua

  cjson = require "cjson"
sql="select * from t_emp"


local redis = require "resty.redis"
                local red = redis:new()

                red:set_timeouts(1000, 1000, 1000) -- 1 sec

  local ok, err = red:connect("127.0.0.1", 6379)
 if not ok then
                    ngx.say("failed to connect: ", err)
                    return
                end



                local res, err = red:get(sql)
                if not res then
                    ngx.say("failed to get sql: ", err)
                    return
                end

                if res == ngx.null then
                    ngx.say("sql"..sql.." not found.")


--mysql查询
local mysql = require "resty.mysql"
                local db, err = mysql:new()
                if not db then
                    ngx.say("failed to instantiate mysql: ", err)
                    return
                end

                db:set_timeout(1000) -- 1 sec


                local ok, err, errcode, sqlstate = db:connect{
                    host = "192.168.44.211",
                    port = 3306,
                    database = "zhangmen",
                    user = "root",
                    password = "111111",
                    charset = "utf8",
                    max_packet_size = 1024 * 1024,
                }

                ngx.say("connected to mysql.<br>")

 res, err, errcode, sqlstate =
                    db:query(sql)
                if not res then
                    ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".")
                    return
                end
          --ngx.say("result: ", cjson.encode(res))

      ok, err = red:set(sql, cjson.encode(res))
                if not ok then
                    ngx.say("failed to set sql: ", err)
                    return
                end

                ngx.say("set result: ", ok)

                    return
                end

local template = require("resty.template")
template.caching(false)
local context = {
    title = "测试",
    name = "lucy",
    description = "<script>alert(1);</script>",
    age = 40,
    hobby = {"电影", "音乐", "阅读"},
    score = {语文 = 90, 数学 = 80, 英语 = 70},
    score2 = {
        {name = "语文", score = 90},
        {name = "数学", score = 80},
        {name = "英语", score = 70},
    },
zhangmen=cjson.decode(res)
}

template.render("view.html", context)

模板

{(header.html)}  
   <body>  
      {# 不转义变量输出 #}  
      姓名:{* string.upper(name) *}<br/>  
      {# 转义变量输出 #}  

      年龄: {* age + 10 *}<br/>  
      {# 循环输出 #}  
      爱好:  
      {% for i, v in ipairs(hobby) do %}  
         {% if v == '电影' then  %} - xxoo

              {%else%}  - {* v *} 
{% end %}  

      {% end %}<br/>  

      成绩:  
      {% local i = 1; %}  
      {% for k, v in pairs(score) do %}  
         {% if i > 1 then %}{% end %}  
         {* k *} = {* v *}  
         {% i = i + 1 %}  
      {% end %}<br/>  
      成绩2  
      {% for i = 1, #score2 do local t = score2[i] %}  
         {% if i > 1 then %}{% end %}  
          {* t.name *} = {* t.score *}  
      {% end %}<br/>  
      {# 中间内容不解析 #}  
      {-raw-}{(file)}{-raw-}  




掌门:
{* zhangmen *}



   {% for i = 1, #zhangmen do local z = zhangmen[i] %}  
         {* z.deptId *},{* z.age *},{* z.name *},{* z.empno *},<br>
      {% end %}<br/>  

{(footer.html)}  

Lua 开源项目

WAF

https://github.com/unixhot/waf

https://github.com/loveshell/ngx_lua_waf

l 防止 SQL 注入,本地包含,部分溢出,fuzzing 测试,XSS/SSRF 等 Web 攻击

l 防止 Apache Bench 之类压力测试工具的攻击

l 屏蔽常见的扫描黑客工具,扫描器

l 屏蔽图片附件类目录执行权限、防止 webshell 上传

l 支持 IP 白名单和黑名单功能,直接将黑名单的 IP 访问拒绝

l 支持 URL 白名单,将不需要过滤的 URL 进行定义

l 支持 User-Agent 的过滤、支持 CC 攻击防护、限制单个 URL 指定时间的访问次数

l 支持支持 Cookie 过滤,URL 与 URL 参数过滤

l 支持日志记录,将所有拒绝的操作,记录到日志中去

Kong 基于Openresty的流量网关

https://konghq.com/

https://github.com/kong/kong

Kong 基于 OpenResty,是一个云原生、快速、可扩展、分布式的微服务抽象层(Microservice Abstraction Layer),也叫 API 网关(API Gateway),在 Service Mesh 里也叫 API 中间件(API Middleware)。

Kong 开源于 2015 年,核心价值在于高性能和扩展性。从全球 5000 强的组织统计数据来看,Kong 是现在依然在维护的,在生产环境使用最广泛的 API 网关。

Kong 宣称自己是世界上最流行的开源微服务 API 网关(The World’s Most Popular Open Source Microservice API Gateway)。

核心优势:

l 可扩展:可以方便的通过添加节点水平扩展,这意味着可以在很低的延迟下支持很大的系统负载。

l 模块化:可以通过添加新的插件来扩展 Kong 的能力,这些插件可以通过 RESTful Admin API 来安装和配置。

l 在任何基础架构上运行:Kong 可以在任何地方都能运行,比如在云或混合环境中部署 Kong,单个或全球的数据中心。

APISIX

ABTestingGateway

https://github.com/CNSRE/ABTestingGateway

ABTestingGateway 是一个可以动态设置分流策略的网关,关注与灰度发布相关领域,基于 Nginx 和 ngx-lua 开发,使用 Redis 作为分流策略数据库,可以实现动态调度功能。

ABTestingGateway 是新浪微博内部的动态路由系统 dygateway 的一部分,目前已经开源。在以往的基于 Nginx 实现的灰度系统中,分流逻辑往往通过 rewrite 阶段的 if 和 rewrite 指令等实现,优点是性能较高,缺点是功能受限、容易出错,以及转发规则固定,只能静态分流。ABTestingGateway 则采用 ngx-lua,通过启用 lua-shared-dict 和 lua-resty-lock 作为系统缓存和缓存锁,系统获得了较为接近原生 Nginx 转发的性能。

l 支持多种分流方式,目前包括 iprange、uidrange、uid 尾数和指定uid分流

l 支持多级分流,动态设置分流策略,即时生效,无需重启

l 可扩展性,提供了开发框架,开发者可以灵活添加新的分流方式,实现二次开发

l 高性能,压测数据接近原生 Nginx 转发

l 灰度系统配置写在 Nginx 配置文件中,方便管理员配置

l 适用于多种场景:灰度发布、AB 测试和负载均衡等