Zabbix 监控 Nginx status 性能

其实zabbix对nginx的服务状态监控,网上有很多相关模板以及设置方法,根据自己的需求修改即可,后期我会写一些比较详细用于生产环境中的一些教程。

部署环境:

  • OS:CentOS 7.4
  • WEB: Nginx 1.3
  • 监控:Zabbix 3.4

先决条件:

《Centos 7 源码编译安装 Nginx》
https://www.renwole.com/archives/39

注意:主要是 –with-http_stub_status_module 模块。

1. 修改 nginx.conf

在 server 段 添加以下内容:

$ vim /usr/local/nginx/conf/nginx.conf
location /stub_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}

2. 重启Nginx服务并测试访问

$ systemctl restart nginx.service
$ curl http://127.0.0.1/stub_status
Active connections: 1
server accepts handled requests
304 304 304
Reading: 0 Writing: 1 Waiting: 0

访问返回数据说明Nginx配置正常.

3. 创建 Nginx 监控脚本

将以下代码复制保存为 nginx.sh

$ cd /usr/local/zabbix/bin
$ vim nginx.sh
#!/bin/bash

HOST="127.0.0.1"
PORT="80"
stub_status=stub_status
function check() {
/sbin/pidof nginx | wc -l
}
function active() {
/usr/bin/curl -s "http://$HOST:$PORT/${stub_status}/" 2>/dev/null| grep 'Active' | awk '{print $NF}'
}
function accepts() {
/usr/bin/curl -s "http://$HOST:$PORT/${stub_status}/" 2>/dev/null| awk NR==3 | awk '{print $1}'
}
function handled() {
/usr/bin/curl -s "http://$HOST:$PORT/${stub_status}/" 2>/dev/null| awk NR==3 | awk '{print $2}'
}
function requests() {
/usr/bin/curl -s "http://$HOST:$PORT/${stub_status}/" 2>/dev/null| awk NR==3 | awk '{print $3}'
}
function reading() {
/usr/bin/curl -s "http://$HOST:$PORT/${stub_status}/" 2>/dev/null| grep 'Reading' | awk '{print $2}'
}
function writing() {
/usr/bin/curl -s "http://$HOST:$PORT/${stub_status}/" 2>/dev/null| grep 'Writing' | awk '{print $4}'
}
function waiting() {
/usr/bin/curl -s "http://$HOST:$PORT/${stub_status}/" 2>/dev/null| grep 'Waiting' | awk '{print $6}'
}

case "$1" in
check)
check
;;
active)
active
;;
accepts)
accepts
;;
handled)
handled
;;
requests)
requests
;;
reading)
reading
;;
writing)
writing
;;
waiting)
waiting
;;

*)
echo $"Usage $0 {check|active|accepts|handled|requests|reading|writing|waiting}"
exit
esac

4. 给该脚本执行权限

$ chmod 755 nginx.sh

5. 创建 userparameter_nginx.conf 配置文件

$ cd /usr/local/zabbix/etc/zabbix_agentd.conf.d

添加以下内容:

$ vim userparameter_nginx.conf
UserParameter=nginx.status[*],/usr/local/zabbix/bin/nginx.sh $1

6. 重启 zabbix

$ systemctl restart zabbix-agent.service

7. 导入 zbx_nginx_templates.xml 模板

下载模板: https://www.renwole.com/zabbix/nginx/zbx_nginx_templates.xml

导入模板添加主机链接到 Template App Nginx 模板即可。

docker一键式安装nginx

一、准备Dockerfile文件

FROM hub.c.163.com/library/centos:latest

RUN echo "Asia/shanghai" > /etc/timezone;
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

COPY nginx-1.8.1.sh /usr/local/
COPY run.sh /usr/local

二、创建 nginx-1.8.1.sh 安装脚本

#!/bin/bash
#install nginx-1.8.1

#安装目录
INSTALL_DIR=/opt/
SRC_DIR=/opt/software

[ ! -d ${INSTALL_DIR} ] && mkdir -p ${INSTALL_DIR}
[ ! -d ${SRC_DIR} ] && mkdir -p ${SRC_DIR}

# Check if user is root
if [ $(id -u) != "0" ]; then
    echo "Error: You must be root to run this script!!"
    exit 1
fi

#安装依赖包
for Package in unzip wget gcc gcc-c++ autoconf automake zlib zlib-devel openssl openssl-devel pcre pcre-devel
do
    yum -y install $Package
done

Install_Nginx()
{
#更新版本信息
NGINX="nginx-1.8.1"
PCRE="pcre-8.35"
ZLIB="zlib1211"
OPENSSL="openssl-1.0.1i"

NGINXFEATURES="--prefix=${INSTALL_DIR}nginx 
--user=nginx 
--group=nginx 
--with-http_ssl_module 
--with-http_gzip_static_module 
--with-http_stub_status_module 
--with-http_realip_module 
--pid-path=/var/run/nginx.pid 
--with-pcre=${SRC_DIR}/${PCRE} 
--with-zlib=${SRC_DIR}/zlib-1.2.11 
--with-openssl=${SRC_DIR}/${OPENSSL}
"

cd ${SRC_DIR}
#下载所需安装包
echo 'Downloading NGINX'
if [ ! -f ${NGINX}.tar.gz ]
then
  wget -c http://nginx.org/download/${NGINX}.tar.gz
else
  echo 'Skipping: NGINX already downloaded'
fi

echo 'Downloading PCRE'
if [ ! -f ${PCRE}.tar.gz ]
then
  wget -c https://sourceforge.net/projects/pcre/files/pcre/8.35/${PCRE}.tar.gz
else
  echo 'Skipping: PCRE already downloaded'
fi

echo 'Downloading ZLIB'
if [ ! -f ${ZLIB}.zip ]
then
  wget -c http://zlib.net/${ZLIB}.zip
else
  echo 'Skipping: ZLIB already downloaded'
fi

echo 'Downloading OPENSSL'
if [ ! -f ${OPENSSL}.tar.gz ]
then
  wget -c http://www.openssl.org/source/${OPENSSL}.tar.gz
else
  echo 'Skipping: OPENSSL already downloaded'
fi

echo '----------Unpacking downloaded archives. This process may take serveral minutes---------'

echo "Extracting ${NGINX}..."
tar xzf ${NGINX}.tar.gz
echo 'Done.'

echo "Extracting ${PCRE}..."
tar xzf ${PCRE}.tar.gz
echo 'Done.'

echo "Extracting ${ZLIB}..."
unzip ${ZLIB}.zip
echo 'Done.'

echo "Extracting ${OPENSSL}..."
tar xzf ${OPENSSL}.tar.gz
echo 'Done.'

#添加用户
groupadd -r nginx
useradd -r -g nginx nginx

#编译
echo '###################'
echo 'Compile NGINX'
echo '###################'
cd ${SRC_DIR}/${NGINX}
./configure ${NGINXFEATURES}
make
make install
cd ../

mkdir -p ${INSTALL_DIR}/nginx/conf/vhosts

}

Install_Nginx

三、创建运行脚本 run.sh

#!/bin/bash
source /etc/profile

echo `pwd`

sh /usr/local/nginx1.8.sh

/usr/local/nginx/sbin/nginx

while true; do sleep 1; done

四、构建

docker build -t nginx:0.1 .

五、准备yaml文件 nginx.yaml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  minReadySeconds: 10
  template:
    metadata:
      labels:
        app: nginx
        version: V20170907134852
    spec:
      volumes:
        - name: logs
          hostPath:
            path: /data/log
        - name: jdk
          hostPath:
            path: /usr/local/java/jdk1.8.0_102
        - name: project
          hostPath:
            path: /data/appdeploy
      containers:
        - name: nginx
          image: nginx:0.1
          ports:
            - containerPort: 80
          volumeMounts:
          - mountPath: /data/log
            name: logs
          - mountPath: /usr/local/java/jdk1.8.0_102
            name: jdk
            readOnly: true
          - mountPath: /data/appdeploy
            name: project
          env:
            - name: DEPLOYMENT_DEMO_VER
              value: V20170907134852
          command:
            - /usr/local/run.sh
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  type: NodePort
  selector:
    app: nginx
  ports:
  - name: http
    port: 80
    nodePort: 80
    targetPort: 80
    protocol: TCP
  selector:
    app: nginx

六、启动yaml

kubectl create -f y nginx.yaml

大功告成!

通过配置nginx的proxy_set_header解决无法正确获取客户端访问ip地址总显示127.0.0.1

一、前言

为了防止本站资源(小木人印象www.xwood.net)被恶意下载,最近实现安全控制模块-通过分析用户访问IP地址在有效时间内的对本站资源合理下载量,作为黑名单规则,但是发现获取通过之前HttpClientIpUtils工具类获取的ip地址都是127.0.0.1,无法获取终端访问用户有效的ip地址,导致黑名单库无法创建。

二、解决方法

由于nginx配置服务端的反向代理导致,之前反向配置如下

location ^~/open-api/{
    proxy_pass   http://127.0.0.1:8080/openapi/;  
}

应该调整配置为如下(增加配置项proxy_set_header x-forwarded-for $remote_addr;)

location ^~/open-api/{
    proxy_pass   http://127.0.0.1:8080/openapi/;
    proxy_set_header x-forwarded-for  $remote_addr;
}

三、黑名单代码分享

1、访问客户端安全控制类ClientUserController,代码如下

public class ClientUserController {

    private static final Logger logger = Logger.getLogger(ClientUserController.class);
    private  static  ConcurrentMap<String,ClientUser>  downloadUsers=new ConcurrentHashMap<String,ClientUser>();
    private  static  List<String>  blackIplist=new CopyOnWriteArrayList<String>();

    //12小时最大下载量
    private  static   int   maxDayDownloadTimes=1000;

    //验证期限
    private  static   long  validTimeSec=12*60*60;

    public  static  void  register(String ip){

        if(StringUtils.isEmpty(ip)||"127.0.0.1".equalsIgnoreCase(ip))
            return ;

        if(!isPermission(ip))
            return ;

        if(downloadUsers.containsKey(ip)){
            downloadUsers.get(ip).setDownloadTimes(downloadUsers.get(ip).getDownloadTimes()+1);
            logger.info(" downloadUser login --------------:"+ip+" times----------------:"+downloadUsers.get(ip).toString());
        }else{
            downloadUsers.put(ip,new ClientUser(ip));
            logger.info(" New downloadUser  register --------------:"+ip+" times----------------:1");
        }

    }


    public  static  boolean  isPermission(String ip){

        if(StringUtils.isEmpty(ip)){
            logger.info(" downloadUser  isPermission  false,becase you  have't  clientIp <<<<<<<<<<<<<<<<<<<<<<<< ");
            return  false;
        }

        if("127.0.0.1".equalsIgnoreCase(ip)){
            logger.info(" downloadUser can't  get ip ; ======================================== 127.0.0.1 ");
            return true;
        }


        if(blackIplist.contains(ip)){
            logger.info(" downloadUser@"+ip+"@  is danger downloadUser  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!  ");
            logger.info(" downloadUser@"+ip+"@  is danger downloadUser  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!  ");
            logger.info(" downloadUser@"+ip+"@  is danger downloadUser  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!  ");
            return false;
        }

        if(downloadUsers.containsKey(ip)){

            ClientUser  checkClientUser=downloadUsers.get(ip);

            if(System.currentTimeMillis()-checkClientUser.getLastTime()>=validTimeSec){

                if(checkClientUser.getDownloadTimes()>=maxDayDownloadTimes){
                    blackIplist.add(ip);
                    logger.info(" downloadUser@"+ip+"@  add  to  blacklist !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!  ");
                    logger.info(" downloadUser@"+ip+"@  add  to  blacklist !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!  ");
                    logger.info(" downloadUser@"+ip+"@  add  to  blacklist !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!  ");
                    return false;
                }else{
                    downloadUsers.remove(ip);
                }

            }else{

                if(checkClientUser.getDownloadTimes()>=maxDayDownloadTimes){
                    blackIplist.add(ip);
                    logger.info(" downloadUser@"+ip+"@  add  to  blacklist !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!  ");
                    logger.info(" downloadUser@"+ip+"@  add  to  blacklist !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!  ");
                    logger.info(" downloadUser@"+ip+"@  add  to  blacklist !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!  ");
                    logger.info(" downloadUser@"+ip+"@  add  to  blacklist !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!  ");
                    logger.info(" downloadUser@"+ip+"@  add  to  blacklist !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!  ");
                    logger.info(" downloadUser@"+ip+"@  add  to  blacklist !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!  ");
                    return false;
                }

            }


        }

        return true;
    }


}

2、客户端用户类ClientUser,代码如下

public class ClientUser {

    private  String ip;

    private  Integer downloadTimes=1;

    private  Long  lastTime;

    public ClientUser() {
        super();
        lastTime=System.currentTimeMillis();
    }

    public ClientUser(String ip) {
        super();
        this.ip = ip;
        lastTime=System.currentTimeMillis();
    }

    public String getIp() {
        return ip;
    }

    public void setIp(String ip) {
        this.ip = ip;
    }

    public Integer getDownloadTimes() {
        return downloadTimes;
    }

    public void setDownloadTimes(Integer downloadTimes) {
        this.downloadTimes = downloadTimes;
    }

    public Long getLastTime() {
        return lastTime;
    }

    public void setLastTime(Long lastTime) {
        this.lastTime = lastTime;
    }


    public static  void  main(String[] args) throws Exception{
        ClientUser  u=new ClientUser();
        u.lastTime=System.currentTimeMillis();
        Thread.sleep(2000);
        System.out.println((System.currentTimeMillis()-u.lastTime)/1000);
    }

    @Override
    public String toString() {
        return "ClientUser [ip=" + ip + "]";
    }

    @Override
    public boolean equals(Object obj) {

        ClientUser _this=(ClientUser)obj;
        if(_this==null)
            return false;

        if(this.getIp().equalsIgnoreCase(_this.getIp()))
            return true;

        return false;
    }

}

nginx+lua 实现请求流量上报kafka

环境依赖

前面26、27、28讲到的博文环境即可,上报kafka ,只需在应用层nginx上操作(192.168.0.16,192.168.0.17)

请求上报kafka 其实很简单,大致思路是:

  • 下载lua-resty-kafka,提供lua 操作kafka的方法类库
  • lua 获取nginx 请求参数,组装上报对象
  • 上报对象 encode cjson 编码
  • lua kakfa 上报即可

代码实现

  • 引入 lua-resty-kafka 类库
yum install -y unzip
cd /usr/local/servers/ && wget https://github.com/doujiang24/lua-resty-kafka/archive/master.zip
unzip master.zip
cp -rf lua-resty-kafka-master/lib/resty/kafka /usr/local/test/lualib/resty/
/usr/local/servers/nginx/sbin/nginx -s reload
  • lua 获取请求,组装上报对象,encode对象并上报(注意:以下代码只对流量上报代码进行注释,其他代码参考 前面 28 “分发层 + 应用层” 双层nginx 架构 之 应用层实现)
vim /usr/local/test/lua/test.lua

代码如下:

// 引入 kafka 生产者 类库
local producer = require("resty.kafka.producer")
// 引入json 解析类库
local cjson = require("cjson")
// 构造kafka 集群节点 broker
local broker_list = {
    { host = "192.168.0.16", port = 9092},
    { host = "192.168.0.17", port = 9092},
    { host = "192.168.0.18", port = 9092}
}
// 定义上报对象
local log_obj = {}
// 自定义模块名称
log_obj["request_module"] = "product_detail_info"
// 获取请求头信息
log_obj["headers"] = ngx.req.get_headers()
// 获取请求uri 参数
log_obj["uri_args"] = ngx.req.get_uri_args()
// 获取请求body
log_obj["body"] = ngx.req.read_body()
// 获取请求的http协议版本
log_obj["http_version"] = ngx.req.http_version()
// 获取请求方法
log_obj["method"] = ngx.req.get_method()
// 获取未解析的请求头字符串
log_obj["raw_reader"] = ngx.req.raw_header()
// 获取解析的请求body体内容字符串
log_obj["body_data"] = ngx.req.get_body_data()
// 上报对象json 字符串编码
local message = cjson.encode(log_obj)

local uri_args = ngx.req.get_uri_args()
local product_id = uri_args["productId"]
local shop_id = uri_args["shopId"]
// 创建kafka producer 连接对象,producer_type = "async" 异步
local async_producer = producer:new(broker_list, {producer_type = "async"})
// 请求上报kafka,kafka 生产者发送数据,async_prodecer:send(a,b,c),a : 主题名称,b:分区(保证相同id,全部到相同的kafka node 去,并且顺序一致),c:消息(上报数据)
local ok, err = async_producer:send("access-log", product_id, message)
// 上报异常处理
if not ok then
   ngx.log(ngx.ERR, "kafka send err:", err)
   return
end
local cache_ngx = ngx.shared.test_cache
local product_cache_key = "product_info_"..product_id
local shop_cache_key = "shop_info_"..shop_id
local product_cache = cache_ngx:get(product_cache_key)
local shop_cache = cache_ngx:get(shop_cache_key)
if product_cache == "" or product_cache == nil then
      local http = require("resty.http")
      local httpc = http.new()

      local resp, err = httpc:request_uri("http://192.168.0.3:81",{
        method = "GET",
            path = "/getProductInfo?productId="..product_id
      })
      product_cache = resp.body
      cache_ngx:set(product_cache_key, product_cache, 10 * 60)
end
if shop_cache == "" or shop_cache == nil then
      local http = require("resty.http")
      local httpc = http.new()
      local resp, err = httpc:request_uri("http://192.168.0.3:81",{
        method = "GET",
            path = "/getShopInfo?shopId="..shop_id
      })
      shop_cache = resp.body
      cache_ngx:set(shop_cache_key, shop_cache, 10 * 60)
end
local product_cache_json = cjson.decode(product_cache)
local shop_cache_json = cjson.decode(shop_cache)
local context = {
      productId = product_cache_json.id,
      productName = product_cache_json.name,
      productPrice = product_cache_json.price,
      productPictureList = product_cache_json.pictureList,
      productSecification = product_cache_json.secification,
      productService = product_cache_json.service,
      productColor = product_cache_json.color,
      productSize = product_cache_json.size,
      shopId = shop_cache_json.id,
      shopName = shop_cache_json.name,
      shopLevel = shop_cache_json.level,
      shopRate = shop_cache_json.rate
}
local template = require("resty.template")
template.render("product.html", context)
  • 配置nginx DNS resolver实例,避免 DNS 解析失败
vim /usr/local/servers/nginx/conf/nginx.conf

在 http 部分添加以下内容,如下图:

resolver 8.8.8.8

未分类

配置nginx dns resolver
(注:以上操作 nginx 应用服务器(192.168.0.16,192.168.0.17)都需要进行)

  • 配置 kafka advertised.host.name 参数(避免通过机器名无法找到对应的机器)(所有kafka 节点都配置)

advertised.host.name = 本机ip

vim /usr/local/kafka/config/server.properties

未分类

配置advertised.host.name

  • nginx 校验 及 重载
/usr/local/servers/nginx/sbin/nginx -t && /usr/local/servers/nginx/sbin/nginx -s reload
  • 启动kafka(确保 zookeeper 已启动)
cd /usr/local/kafka && nohup bin/kafka-server-start.sh config/server.properties &
  • kafka 中创建 access-log 主题
cd cd /usr/local/kafka && bin/kafka-topics.sh –zookeeper my-cache1:2181,my-cache2:2181,my-cache3:2181 –topic access-log –replication-factor 1 –partitions 1 –create
  • 打开kafka consumer 查看数据
bin/kafka-console-consumer.sh –zookeeper my-cache1:2181,my-cache2:2181,my-cache3:2181 –topic access-log –from-beginning
  • 浏览器请求nginx

未分类

nginx请求

未分类

shell 打开kafka 消费端查看请求上报kafka 数据

完毕,利用nginx + lua 实现请求流量上报kafka就到这里了。

以上就是本章内容,如有不对的地方,请多多指教,谢谢!

nginx+lua在帐号系统中的应用

我们的帐号系统要应用到多个产品里,所以设计的系统需要满足高并发的特性。项目A更新密码,项目B就得下一次触发接口时,自动登出。

我们帐号系统没有使用Oauth2.0,而是采用了简单的JWT(Json Web Token)的方式来处理的用户认证。所以,帐号系统要提供一个验证用户密码修改的API。

这里就不展开讲jwt了。不了解的可以去google。jwt一共三段:xxx.yyy.zzz, 我们把重要的信息放在payload中,也就是yyy的位置,可以通过base64解码,类似于我们在session存的用户信息。payload也可以做加密处理。

payload一般里面会有一些默认的字段,sub代表用户主体,比如用户的id就可以赋值给sub,也可以是手机号。除了公共的字段,我们也可以定义私有字段,比如seq,可以用在单个应用内来处理单设备登录问题。这里我们要增加一个全局的字段表示密码的状态,或者说用户的状态,比如冻结用户,解冻,踢掉用户登录状态。我们先解决密码状态问题,增加一个字段passwd_seq,初始值1。每更新一次密码passwd_seq加一。所有应用内需要认证的接口都需要校验密码状态。所以都会调用该接口(/token)。失效后,返回401,重新登录。

初步调研A项目每日的接口调用次数10w(接口数百个),除了注册占比较低,忽略不计。就也是说/token接口至少会调用10w次一天。

我在自己的电脑上测试。配置如截图:

未分类

redis压测数据:

$ redis-benchmark -t set,get
====== SET ======
  100000 requests completed in 2.65 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1

32.18% <= 1 milliseconds
98.80% <= 2 milliseconds
99.62% <= 3 milliseconds
99.71% <= 4 milliseconds
99.79% <= 5 milliseconds
99.93% <= 6 milliseconds
99.95% <= 10 milliseconds
99.95% <= 11 milliseconds
99.96% <= 12 milliseconds
99.97% <= 13 milliseconds
99.98% <= 14 milliseconds
99.99% <= 15 milliseconds
99.99% <= 16 milliseconds
100.00% <= 16 milliseconds
37664.79 requests per second

====== GET ======
  100000 requests completed in 2.60 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1

34.93% <= 1 milliseconds
99.62% <= 2 milliseconds
99.91% <= 3 milliseconds
100.00% <= 3 milliseconds
38491.14 requests per second

nginx+lua读写redis接口测试,单核测试。

测试的nginx配置文件如下:

worker_processes  1;   # we could enlarge this setting on a multi-core machine
error_log  logs/error.log warn;

events {
    worker_connections  2048;
}

http {
    lua_package_path 'conf/?.lua;;';

    server {
        listen       8080;
        server_name  localhost;

        #lua_code_cache on;

        location /test {

            access_by_lua_block {
                local jwt = require "resty.jwt"
                local foo = require "foo"

                local err_msg = {
                   x_token = {err = "set x-token in request, please!"},
                   payload = {err = "payload not found"},
                   user_key = {err = "用户配置信息有问题"},
                   password = {err = "密码已修改,请重新登录"},
                   ok = {ok = "this is my own error page content"},
                }

                -- 判断token是否传递
                local req_headers = ngx.req.get_headers()
                if req_headers.x_token == nil then
                   foo:response_json(422, err_msg.x_token)
                   return
                end

                local jwt_obj = jwt:load_jwt(req_headers.x_token)

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

        red:set_timeout(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

        -- 请注意这里 auth 的调用过程
        local count
        count, err = red:get_reused_times()
        if 0 == count then
            ok, err = red:auth("test123456")
            if not ok then
            ngx.say("failed to auth: ", err)
            return
            end
        elseif err then
            ngx.say("failed to get reused times: ", err)
            return
        end

                if jwt_obj.payload == nil then
                   foo:response_json(422, err_msg.payload)
                   return
                end
                local sub = jwt_obj.payload.sub
                user_key, err = red:get('user:' .. sub)

                if user_key == ngx.null then
                    foo:response_json(500, err_msg.user_key)
                    return
                elseif tonumber(user_key) > 3  then
                   foo:response_json(401, err_msg.password)
                   return
                else
                   foo:response_json(200, err_msg.ok)
                   return
                end

        -- 连接池大小是200个,并且设置最大的空闲时间是 10 秒
        local ok, err = red:set_keepalive(10000, 200)
        if not ok then
            ngx.say("failed to set keepalive: ", err)
            return
        end
    }
        }
    }
}

上面的配置文件代码格式化,目前没找到合适的工具.

测试结果如下:

$   ab -c10 -n5000 -H 'x-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vYWNjb3VudC1hcGkuc3VwZXJtYW4yMDE0LmNvbS9sb2dpbiIsImlhdCI6MTUwNTQ2Njg5OSwiZXhwIjoxNTA1NDcwNDk5LCJuYmYiOjE1MDU0NjY4OTksImp0aSI6ImJ0TWFISmltYmtxSGVUdTEiLCJzdWIiOjIsInBydiI6Ijg3MTNkZTA0NTllYTk1YjA0OTk4NmFjNThlYmY1NmNkYjEwMGY4NTUifQ.yiXqkHBZrYXuxtUlSiy5Ialle--q_88G32lxUsDZO0k'  http://127.0.0.1:8080/token
This is ApacheBench, Version 2.3 <$Revision: 1757674 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 500 requests
Completed 1000 requests
Completed 1500 requests
Completed 2000 requests
Completed 2500 requests
Completed 3000 requests
Completed 3500 requests
Completed 4000 requests
Completed 4500 requests
Completed 5000 requests
Finished 5000 requests


Server Software:        openresty/1.11.2.5
Server Hostname:        127.0.0.1
Server Port:            8080

Document Path:          /token
Document Length:        175 bytes

Concurrency Level:      10
Time taken for tests:   0.681 seconds
Complete requests:      5000
Failed requests:        0
Non-2xx responses:      5000
Total transferred:      1655000 bytes
HTML transferred:       875000 bytes
Requests per second:    7344.73 [#/sec] (mean)
Time per request:       1.362 [ms] (mean)
Time per request:       0.136 [ms] (mean, across all concurrent requests)
Transfer rate:          2374.13 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   0.2      1       4
Processing:     0    1   0.4      1       5
Waiting:        0    1   0.4      1       4
Total:          1    1   0.5      1       6

Percentage of the requests served within a certain time (ms)
  50%      1
  66%      1
  75%      1
  80%      1
  90%      2
  95%      2
  98%      3
  99%      4
 100%      6 (longest request)

单核每秒的qps在7k以上(几乎没优化lua代码)。php之前的测试数据在60左右(大家可以实际测试下)。

看到这个比例。单机单核每日的请求量最大上面是604,800k,每天可以处理6亿个请求。

比如我们优化后再测试,nginx上的lua_code_cache开启,同时开启了2个worker, 测试结果如下:

 ab -c10 -n5000 -H 'x-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vYWNjb3VudC1hcGkuc3VwZXJtYW4yMDE0LmNvbS9sb2dpbiIsImlhdCI6MTUwNTQ2Njg5OSwiZXhwIjoxNTA1NDcwNDk5LCJuYmYiOjE1MDU0NjY4OTksImp0aSI6ImJ0TWFISmltYmtxSGVUdTEiLCJzdWIiOjIsInBydiI6Ijg3MTNkZTA0NTllYTk1YjA0OTk4NmFjNThlYmY1NmNkYjEwMGY4NTUifQ.yiXqkHBZrYXuxtUlSiy5Ialle--q_88G32lxUsDZO0k'  http://127.0.0.1:8080/token
This is ApacheBench, Version 2.3 <$Revision: 1757674 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 500 requests
Completed 1000 requests
Completed 1500 requests
Completed 2000 requests
Completed 2500 requests
Completed 3000 requests
Completed 3500 requests
Completed 4000 requests
Completed 4500 requests
Completed 5000 requests
Finished 5000 requests


Server Software:        openresty/1.11.2.5
Server Hostname:        127.0.0.1
Server Port:            8080

Document Path:          /token
Document Length:        175 bytes

Concurrency Level:      10
Time taken for tests:   0.608 seconds
Complete requests:      5000
Failed requests:        0
Non-2xx responses:      5000
Total transferred:      1655000 bytes
HTML transferred:       875000 bytes
Requests per second:    8217.29 [#/sec] (mean)
Time per request:       1.217 [ms] (mean)
Time per request:       0.122 [ms] (mean, across all concurrent requests)
Transfer rate:          2656.18 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   0.2      1       2
Processing:     0    1   0.2      1       2
Waiting:        0    1   0.2      1       2
Total:          1    1   0.3      1       3

Percentage of the requests served within a certain time (ms)
  50%      1
  66%      1
  75%      1
  80%      1
  90%      1
  95%      1
  98%      2
  99%      3
 100%      3 (longest request)

除了测试用具ab,还可以用Python写的boom或者go写的hey。可以去github下。其他的两个用具测试结果要比ab工具更稳定。

项目的部署工具可以选用walle开源项目(https://github.com/meolu/walle-web),但是不支持docker部署方式,docker一般部署有两种方式,把代码包到docker image或者做目录映射。我基于walle v1.2.0打了一个patch。 如下:

我们项目的开发部署环境可以使用:openresty image ,其实我们可以把这个项目clone下来。做些处理,或者直接继承这个image。

开发的项目最好使用绝对路径引入lua文件。

一般的项目路径如下:

.
├── config
│   ├── domain
│   │   └── nginx_token.conf
│   ├── nginx.conf
│   └── resources.properties.json
├── lua
│   ├── init.lua
│   └── token_controller.lua
└── lualib
    └── luka
        └── response.lua

5 directories, 6 files

感觉lua这种脚本语言还是不错的,在性能上不比编译型语言差多少,但开发效率却高出不少。后期准备把laravel写的那些中间件都改为lua+nginx,各种各样的校验的事交给lua处理会提升不少性能,甚至,某些不复杂的接口也可以改用lua编写。

lua真是个不错的家伙。借助nginx的高性能,让它成为运行最快的脚本语言。开发web 应用目前已经非常方便了。openresty提供了不少库,比如:mysql,redis,memcache,jwt,json等等吧。

Nginx配置调优

nginx运行进程个数

worker_processes  4;

表示启动nginx process数. 一般建议设置为cpu核心数或者核心数*2

cpu亲和力

如果cpu为四核心,设置进程数为4

worker_processes  4;
worker_cpu_affinity 0001 0010 0100 1000;

如果cpu为八核心,设置进程数为8

worker_processes  8;
worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000;

此配置项是将nginx每个进程绑定到固定的cpu核心上.不会造成进程频繁地切换cpu而占用更多的资源

nginx打开文件的最大个数

worker_rlimit_nofile 102400;

在修改这个参数之前同时需要修改操作系统限制的最大打开文件数量

ulimit -n       #显示当前系统限制同时打开文件数量
ulimit -n 102400    #临时修改系统限制
vim /etc/security/limits.conf       #修改系统配置文件
* soft nofile 102400
* hard nofile 102400

Nginx事件处理模型

events {
    use epoll;
    worker_connections  10240;
}
  • use:使用的事件模型,网上表示epoll处理效率高,具体也不是很清楚
  • worker_connections:单进程的最大可连接数

连接超时时间

tcp在建立连接之时需要进行三次握手,断开时四次挥手。如果需要不停的加载资源,极大的占用了系统资源与时间。
keepalive选项则是连接建立之后,进行一段时间的等待,减少了握手与挥手的时间损耗。
然而长连接却也占用了系统资源,这个选项的参数设置也是需要根据实际情况来调整

keepalive_timeout  65;
tcp_nodelay on;
client_header_timeout 15;
client_body_timeout 15;
send_timeout 15;
  • keepalived_timeout 客户端连接保持会话超时时间,超过这个时间,服务器断开这个链接
  • tcp_nodelay;也是防止网络阻塞,不过要包涵在keepalived参数才有效
  • client_header_timeout 客户端请求头读取超市时间,如果超过设个时间没有发送任何数据,nginx将返回request time out的错误
  • client_body_timeout 客户端求主体超时时间,超过这个时间没有发送任何数据,和上面一样的错误提示
  • send_timeout 响应客户端超时时间,这个超时时间仅限于两个活动之间的时间,如果超哥这个时间,客户端没有任何活动,nginx关闭连接

上传文件大小限制

http {
    ...
    clinet_max_body_size 10m;
    }

gzip压缩

使用gzip压缩可以给我们节省带宽成本,提高传输速度,但是也会占用cpu资源

nginx需要启用with-http_gzip_static_module才可以使用

gzip on;
gzip_min_length  1k;
gzip_buffers     4 32k;
gzip_http_version 1.1;
gzip_comp_level 9;
gzip_types  text/css text/xml application/javascript;
gzip_vary on;
  • gzip on; #开启压缩功能
  • gzip_min_length 1k; #设置允许压缩的页面最小字节数,页面字节数从header头的Content-Length中获取,默认值是0,不管页面多大都进行压缩,建议设置成大于1K,如果小与1K可能会越压越大。
  • gzip_buffers 4 32k; #压缩缓冲区大小,表示申请4个单位为32K的内存作为压缩结果流缓存,默认值是申请与原始数据大小相同的内存空间来存储gzip压缩结果。
  • gzip_http_version 1.1; #压缩版本(默认1.1,前端为squid2.5时使用1.0)用于设置识别HTTP协议版本,默认是1.1,目前大部分浏览器已经支持GZIP解压,使用默认即可
  • gzip_comp_level 9; #压缩比例,用来指定GZIP压缩比,1压缩比最小,处理速度最快,9压缩比最大,传输速度快,但是处理慢,也比较消耗CPU资源。
  • gzip_types text/css text/xml application/javascript; #用来指定压缩的类型,‘text/html’类型总是会被压缩。
  • gzip_vary on; #vary header支持,改选项可以让前端的缓存服务器缓存经过GZIP压缩的页面,例如用Squid缓存经过nginx压缩的数据

缓存调优

主要是针对图片,css,js这种元素修改机会较少的情况使用。
特别是图片,占用带宽大。

我们列出匹配这些的url

location ~ .*.(gif|jpg|jpeg|png|bmp|swf)$
      {
      expires      30d;
      }
location ~ .*.(js|css)?$
      {
      expires      10d;
      }

expires参数表示缓存时间。

Nginx lua 接收 GET/POST 请求

有时候写一些简单的api 接口如果不想再起个php-fpm ,完全可以通过nginx加lua在nginx 直接实现。

当然前提是你必须给nginx安装 lua-nginx-module 模块。

下面是nginx 的配置及lua 代码:

lua_need_request_body on;
location = /api {
    default_type text/plain;
    content_by_lua_block {
        ngx.req.read_body()
        // 获取Post 数据
        local post = ngx.req.get_post_args()
        // 获取Querystring 数据
        local get = ngx.req.get_uri_args() 
        ngx.say(post.name)
        ngx.say(post.gender)
        ngx.say(get.type)
    }

}

content_by_lua_block 中放lua 代码

打开命令行模拟请求测试一下

curl  -XPOST 'http://test-lua.loc/mm?type=good' -d "name=shanhuhai" -d "gender=1"

nginx 防御ddos

防御DDOS是一个系统工程,攻击花样多,防御的成本高瓶颈多,防御起来即被动又无奈。DDOS的特点是分布式,针对带宽和服务攻击,也就 是四层流量攻击和七层应用攻击,相应的防御瓶颈四层在带宽,七层的多在架构的吞吐量。对于七层的应用攻击,我们还是可以做一些配置来防御的,例如前端是 Nginx,主要使用nginx的http_limit_conn和http_limit_req模块来防御。 ngx_http_limit_conn_module 可以限制单个IP的连接数,ngx_http_limit_req_module 可以限制单个IP每秒请求数,通过限制连接数和请求数能相对有效的防御CC攻击。下面是配置方法:

一. 限制每秒请求数

ngx_http_limit_req_module模块通过漏桶原理来限制单位时间内的请求数,一旦单位时间内请求数超过限制,就会返回503错误。配置需要在两个地方设置:

  • nginx.conf的http段内定义触发条件,可以有多个条件
  • 在location内定义达到触发条件时nginx所要执行的动作

例如:

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s; //触发条件,所有访问ip 限制每秒10个请求
    ...
    server {
        ...
        location  ~ .php$ {
            limit_req zone=one burst=5 nodelay;   //执行的动作,通过zone名字对应
               }
           }
     }

参数说明:

$binary_remote_addr  二进制远程地址
zone=one:10m    定义zone名字叫one,并为这个zone分配10M内存,用来存储会话(二进制远程地址),1m内存可以保存16000会话
rate=10r/s;     限制频率为每秒10个请求
burst=5         允许超过频率限制的请求数不多于5个,假设1、2、3、4秒请求为每秒9个,那么第5秒内请求15个是允许的,反之,如果第一秒内请求15个,会将5个请求放到第二秒,第二秒内超过10的请求直接503,类似多秒内平均速率限制。
nodelay         超过的请求不被延迟处理,设置后15个请求在1秒内处理。

二.限制IP连接数

http {
    limit_conn_zone $binary_remote_addr zone=addr:10m; //触发条件
    ...
    server {
        ...
        location /download/ {
            limit_conn addr 1;    // 限制同一时间内1个连接,超出的连接返回503
                }
           }
     }

三.白名单设置

http_limit_conn和http_limit_req模块限制了单ip单位时间内的并发和请求数,但是如果Nginx前面有lvs或者 haproxy之类的负载均衡或者反向代理,nginx获取的都是来自负载均衡的连接或请求,这时不应该限制负载均衡的连接和请求,就需要geo和map 模块设置白名单:

geo $whiteiplist  {
        default 1;
        10.11.15.161 0;
    }
map $whiteiplist  $limit {
        1 $binary_remote_addr;
        0 "";
    }
limit_req_zone $limit zone=one:10m rate=10r/s;
limit_conn_zone $limit zone=addr:10m;

geo模块定义了一个默认值是1的变量whiteiplist,当在ip在白名单中,变量whiteiplist的值为0,反之为1
如果在白名单中–> whiteiplist=0 –> $limit=”” –> 不会存储到10m的会话状态(one或者addr)中 –> 不受限制
反之,不在白名单中 –> whiteiplist=1 –> $limit=二进制远程地址 –>存储进10m的会话状态中 –> 受到限制

四.测试

使用ab命令来模拟CC攻击,http_limit_conn和http_limit_req模块要分开测试,同时注意 http_limit_conn模块只统计正在被处理的请求(这些请求的头信息已被完全读入)所在的连接。如果请求已经处理完,连接没有被关闭时,是不会 被统计的。这时用netstat看到连接数可以超过限定的数量,不会被阻止。

ab -n 请求数 -c 并发 http://10.11.15.174/i.php

如果被阻止前台会返回503,同时在nginx的error_log中会看到如下错误日志:

被限制连接数:

2015/01/28 14:20:26 [error] 4107#0: *65525 limiting connections by zone "addr", client: 10.11.15.161, server: , request: "GET /i.php?=PHPE9568F35-D428-11d2-A769-00AA001ACF42 HTTP/1.1", host: "10.11.15.174", referrer: "http://10.11.15.174/i.php"

被限制请求数:

2015/01/28 14:18:59 [error] 4095#0: *65240 limiting requests, excess: 5.772 by zone "one", client: 10.11.15.161, server: , request: "GET /i.php?=PHPE9568F34-D428-11d2-A769-00AA001ACF42 HTTP/1.1", host: "10.11.15.174", referrer: "http://10.11.15.174/i.php"

五.其它一些防CC的方法

1、Nginx模块 ModSecurity、http_guard、ngx_lua_waf

  • ModSecurity 应用层WAF,功能强大,能防御的攻击多,配置复杂
  • ngx_lua_waf 基于ngx_lua的web应用防火墙,使用简单,高性能和轻量级
  • http_guard 基于openresty

2、软件+Iptables

  • fail2ban 通过分析日志来判断是否使用iptables拦截
  • DDoS Deflate 通过netstat判断ip连接数,并使用iptables屏蔽

开头说过抗DDOS是一个系统工程,通过优化系统和软件配置,只能防御小规模的CC攻击,对于大规模攻击、四层流量攻击、混合攻击来说,基本上系统和应用软件没挂,带宽就打满了。下面是我在工作中使用过的防御DDOS的方式:

  1. 高防服务器和带流量清洗的ISP 通常是美韩的服务器,部分ISP骨干供应商有流量清洗服务,例如香港的PCCW。通常可以防御10G左右的小型攻击
  2. 流量清洗服务 例如:akamai(prolexic),nexusguard 我们最大受到过80G流量的攻击,成功被清洗,但是费用非常贵
  3. CDN 例如:百度云加速。

linux下nginx不支持中文URL路径的解决方案

未分类

1、确定你的系统是UTF编码

[root@localhost ~]# echo $LAGN
en_US.UTF-8

2、nginx配置文件里默认编码设置为utf-8

server
{
listen 80;
server_name .inginx.com ;
index index.html index.htm index.php;
root /usr/local/nginx/html/inginx.com;
charset utf-8;
}

3、将非UTF-8的文件名转换为UTF-8编码

做法很简单,把文件名都修改成utf8编码就可以了!

安装convmv,由他去转换编码:

yum install convmv -y
convmv -f GBK -t UTF8 -r --notest 目标路径

其中-f是源编码,-t是目标编码,-r是递归处理目录,–notest是不移动,实际上对文件进行改名。

nginx 解决微信文章图片防盗链

解决图片防盗链的方法就是要把请求 头的 referer 去掉就可以。

我们可以用 nginx 反向代理微信图片的链接,并把请求的 referer 去掉。

首先找到微信图片服务器的 IP

$ ping mmbiz.qpic.cn
PING ssd.tcdn.qq.com (110.81.153.148): 56 data bytes
64 bytes from 110.81.153.148: icmp_seq=0 ttl=56 time=10.590 ms
64 bytes from 110.81.153.148: icmp_seq=1 ttl=56 time=8.083 ms
64 bytes from 110.81.153.148: icmp_seq=2 ttl=56 time=10.891 ms
64 bytes from 110.81.153.148: icmp_seq=3 ttl=56 time=10.202 ms
^C
--- ssd.tcdn.qq.com ping statistics ---
4 packets transmitted, 4 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 8.083/9.941/10.891/1.100 ms

配置微信图片反向代理

    location ~ /mmbiz_(.*)/ {
        proxy_pass         http://59.57.18.143;

        proxy_set_header   Host             "mmbiz.qpic.cn";
        proxy_set_header   Referer          "";
    }

移除微信图片的域名

使用 sub_filter 移除微信图片的域名

sub_filter "http://mmbiz.qpic.cn" "";
sub_filter_once off;

到这儿以微信文章图片防盗链。