linux 下安装 lua 及 lua-cjson

安装 lua

yum -y install libtermcap-devel ncurses-devel libevent-devel readline-devel
wget -c https://www.lua.org/ftp/lua-5.1.4.tar.gz
tar xvzf lua-5.1.4.tar.gz
cd lua-5.1.4
make linux install

安装 lua-cjson

wget -c https://www.kyne.com.au/~mark/software/download/lua-cjson-2.1.0.tar.gz
tar zxvf lua-cjson-2.1.0.tar.gz
cd lua-cjson-2.1.0/
make 
make install

Linux下安装配置OpenResty,并测试在Nginx中使用Lua编程

一、简介

OpenResty,也被称为“ngx_openresty”,是一个以Nginx为核心同时包含很多第三方模块的Web应用服务器。借助于Nginx的事件驱动模型和非阻塞IO,可以实现高性能的Web应用程序。 OpenResty不是Nginx的分支,它只是一个软件包。主要有章亦春维护。

OpenResty默认集成了Lua开发环境,而且提供了大量组件如Mysql、Redis、Memcached等,使得在Nginx上开发Web应用更方便简单。

二、安装OpenResty

[root@hbase31 src]# wget https://openresty.org/download/openresty-1.11.2.5.tar.gz
[root@hbase31 src]# tar -zxvf openresty-1.13.6.1.tar.gz
[root@hbase31 openresty-1.13.6.1]# ./configure --prefix=/usr/local/openresty --user=www --group=www --with-http_stub_status_module --with-http_ssl_module --with-openssl=/usr/local/ssl --with-pcre=/usr/local/src/pcre-8.38 --add-module=/usr/local/src/ngx_cache_purge-2.3 --with-http_gzip_static_module --with-luajit
[root@hbase31 openresty-1.13.6.1]# make && make install

注:关于这里的编译参数可以认为是在Nginx的编译参数的基础上添加了其他组件的参数。如需查看更多参数可以使用以下命令:

[root@hbase31 openresty-1.13.6.1]# ./configure --help

配置nginx的启动脚本:

[root@hbase31 openresty-1.13.6.1]# vim /etc/init.d/nginx

添加如下内容:

#!/bin/bash
# nginx Startup script for the Nginx HTTP Server
# it is v.1.3.0 version.
# chkconfig: - 85 15
# description: Nginx is a high-performance web and proxy server.
#              It has a lot of features, but it's not for everyone.
# processname: nginx
# pidfile: /var/run/nginx.pid
# config: /usr/local/openresty/nginx/conf/nginx.conf
nginxd=/usr/local/openresty/nginx/sbin/nginx
nginx_config=/usr/local/openresty/nginx/conf/nginx.conf
nginx_pid=/usr/local/openresty/nginx/logs/nginx.pid
RETVAL=0
prog="nginx"
# Source function library.
.  /etc/rc.d/init.d/functions
# Source networking configuration.
.  /etc/sysconfig/network
# Check that networking is up.
[ ${NETWORKING} = "no" ] && exit 0
[ -x $nginxd ] || exit 0
# Start nginx daemons functions.
start() {
if [ -e $nginx_pid ];then
   echo "nginx already running...."
   exit 1
fi
   echo -n $"Starting $prog: "
   daemon $nginxd -c ${nginx_config}
   RETVAL=$?
   echo
   [ $RETVAL = 0 ] && touch /var/lock/subsys/nginx
   return $RETVAL
}
# Stop nginx daemons functions.
stop() {
        echo -n $"Stopping $prog: "
        killproc $nginxd
        RETVAL=$?
        echo
        [ $RETVAL = 0 ] && rm -f /var/lock/subsys/nginx $nginx_pid
}
reload() {
    echo -n $"Reloading $prog: "
    #kill -HUP `cat ${nginx_pid}`
    killproc $nginxd -HUP
    RETVAL=$?
    echo
}
# See how we were called.
case "$1" in
start)
        start
        ;;
stop)
        stop
        ;;
reload)
        reload
        ;;
restart)
        stop
        start
        ;;

status)
        status $prog
        RETVAL=$?
        ;;
*)
        echo $"Usage: $prog {start|stop|restart|reload|status|help}"
        exit 1
esac
exit $RETVAL

添加可执行权限:

[root@hbase31 openresty-1.13.6.1]# chmod a+x /etc/init.d/nginx

启动nginx:

[root@hbase31 openresty-1.13.6.1]# service nginx start

三、在Nginx中使用Lua脚本

[root@hbase31 vhost]# cd /usr/local/openresty/nginx/conf
[root@hbase31 conf]# mkdir lua vhost

(1)测试在Nginx中使用Lua脚本

[root@hbase31 nginx]# vim /usr/local/openresty/nginx/conf/vhost/lua.conf

其内容如下:

server {
    server_name localhost;
    listen 3000;
    index index.html index.htm index.jsp;

    location / {
        root /usr/local/openresty/nginx/html;
    }

    location /lua {
        default_type text/plain;
        content_by_lua 'ngx.say("hello,lua!")';
    } 

    limit_conn perip 1000;
    access_log logs/access_rua.log;
}

测试是否可以访问:

[root@hbase31 nginx]# service nginx reload

然后访问:http://192.168.1.31:3000/lua

如果输出以下内容则证明在Nginx中可以执行Lua脚本:

hello,lua!

(2)在Nginx中使用Lua脚本访问Redis

i)连接Redis集群,然后添加测试参数:

192.168.1.30:7000> set '123' '456'

ii)添加连接Redis的Lua脚本:

[root@hbase31 nginx]# vim /usr/local/openresty/nginx/conf/lua/redis.lua

其内容如下:

local redis = require "resty.redis"
local conn = redis.new()
conn.connect(conn, '192.168.1.30', '7000')
local res = conn:get("123")
if res==ngx.null then
    ngx.say("redis集群中不存在KEY——'123'")
    return
end
ngx.say(res)

iii)在上面的lua.conf配置文件中添加以下location:

    location /lua_redis {
        default_type text/plain;
        content_by_lua_file /usr/local/openresty/nginx/conf/lua/redis.lua;
    }
1
2
3
4
    location /lua_redis {
        default_type text/plain;
        content_by_lua_file /usr/local/openresty/nginx/conf/lua/redis.lua;
    }

iv)测试是否可以访问:

[root@hbase31 nginx]# service nginx reload

然后访问:http://192.168.1.31:3000/lua_redis

如果输出以下内容则证明可以访问redis:

456

Openresty最佳案例 | 第2篇:Lua入门

什么是lua

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

Lua 是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组,由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo所组成并于1993年开发。
—摘抄 http://www.runoob.com/lua/lua-tutorial.html

环境搭建

注意: 在上一篇文章中,OpenResty已经有了Lua的环境,这里安装的是单独的Lua环境,用于学习和开发Lua。大多数的电脑是Windowds版本的电脑,Windows版本下载地址http://luaforge.net/projects/luaforwindows/。

Linux和Mac电脑下载地址:http://luajit.org/download.html,安装命令如下:

wget http://luajit.org/download/LuaJIT-2.1.0-beta1.tar.gz

tar -xvf LuaJIT-2.1.0-beta1.tar.gz
cd LuaJIT-2.1.0-beta1
make
sudo make install

使用IDEA开发的同学,可以通过安装插件的形式来集成Lua的环境,插件名为EmmyLua,安装插件后,在Idea的右侧栏就会出现Lua的图标,点击图标,就会出现运行Lua代码的窗口。建议使用该插件,可以免去安装Lua环境的麻烦。

第一个Lua程序

安装好环境后,我采用EmmyLua插件的形式,对Lua的入门语法进行一个简单的讲解。
打开EmmyLua的终端,在终端上输入:

print("hi you")

按ctrl+enter,终端显示:

hi you

Lua基本数据类型

lua的基本数据类型有nil、string、boolean、number、function类型。

nil 类型

nil类似于Java中的null ,表示空值。变量第一次赋值为nil。

local num
print(num)
num=100
print(num)

终端输出:

nil

100

number (数字)

Number 类型用于表示实数,和 Java里面的 double 类型很类似。可以使用数学函数
math.floor(向下取整) 和 math.ceil(向上取整) 进行取整操作。

local order = 3.99
local score = 98.01
print(math.floor(order))
print(math.ceil(score))

输出:

3

99

string 字符串

Lua 中有三种方式表示字符串:
1、使用一对匹配的单引号。例:’hello’。
2、使用一对匹配的双引号。例:”abclua
3.字符串还可以用一种长括号(即[[ ]]) 括起来的方式定义

ocal str1 = 'hello world'
local str2 = "hello lua"
local str3 = [["addname",'hello']]
local str4 = [=[string have a [[]].]=]
print(str1) -->output:hello world
print(str2) -->output:hello lua
print(str3) -->output:"addname",'hello'
print(str4) --

table (表)

Table 类型实现了一种抽象的“关联数组”。“关联数组”是一种具有特殊索引方式的数组,索引通常是字符串(string) 或者 number 类型,但也可以是除 nil 以外的任意类型的值。

local corp = {
    web = "www.google.com", --索引为字符串,key = "web",
                              -- value = "www.google.com"
    telephone = "12345678", --索引为字符串
    staff = {"Jack", "Scott", "Gary"}, --索引为字符串,值也是一个表
    100876, --相当于 [1] = 100876,此时索引为数字
            -- key = 1, value = 100876
    100191, --相当于 [2] = 100191,此时索引为数字
    [10] = 360, --直接把数字索引给出
    ["city"] = "Beijing" --索引为字符串
}

print(corp.web) -->output:www.google.com
print(corp["telephone"]) -->output:12345678
print(corp[2]) -->output:100191
print(corp["city"]) -->output:"Beijing"
print(corp.staff[1]) -->output:Jack
print(corp[10]) -->output:36

function(函数)

在 Lua 中,函数 也是一种数据类型,函数可以存储在变量中,可以通过参数传递给其他函
数,还可以作为其他函数的返回值。

local function foo()
   print("in the function")
   --dosomething()
   local x = 10
   local y = 20
   return x + y
end
local a = foo --把函数赋给变量
print(a())

--output:
in the function
30

表达式

~= 不等于

未分类

  • a and b 如果 a 为 nil,则返回 a,否则返回 b;
  • a or b 如果 a 为 nil,则返回 b,否则返回 a。
local c = nil
local d = 0
local e = 100
print(c and d) -->打印 nil
print(c and e) -->打印 nil
print(d and e) -->打印 100
print(c or d) -->打印 0
print(c or e) -->打印 100
print(not c) -->打印 true
print(not d) --> 打印 false

在 Lua 中连接两个字符串,可以使用操作符“..”(两个点).

print("Hello " .. "World") -->打印 Hello World
print(0 .. 1) -->打印 01

控制语句

单个 if 分支 型

x = 10
if x > 0 then
    print("x is a positive number")
end

两个分支 if-else 型

x = 10
if x > 0 then
    print("x is a positive number")
else
    print("x is a non-positive number")
end

多个分支 if-elseif-else 型:

score = 90
if score == 100 then
    print("Very good!Your score is 100")
elseif score >= 60 then
    print("Congratulations, you have passed it,your score greater or equal to 60")
    --此处可以添加多个elseif
else
    print("Sorry, you do not pass the exam! ")
end

for 控制结构

Lua 提供了一组传统的、小巧的控制结构,包括用于条件判断的 if 用于迭代的 while、repeat
和 for,本章节主要介绍 for 的使用.

for 数字型

for 语句有两种形式:数字 for(numeric for) 和范型 for(generic for) 。
数字型 for 的语法如下:

for var = begin, finish, step do
--body
end

实例1:

for i = 1, 5 do
    print(i)
end
-- output:
1 2 3 4 5

实例2:

for i = 1, 10, 2 do
    print(i)
end
-- output:
1 3 5 7 9

for 泛型

泛型 for 循环通过一个迭代器(iterator) 函数来遍历所有值:

-- 打印数组a的所有值

local a = {"a", "b", "c", "d"}
for i, v in ipairs(a) do
    print("index:", i, " value:", v)
end

-- output:
index: 1 value: a
index: 2 value: b
index: 3 value: c
index: 4 value: d

lua的入门就到这里,因为lua语法虽少,但细节有很多,不可能花很多时间去研究这个。入个门,遇到问题再去查资料就行了。另外需要说明的是本文大部分内容为复制粘贴于OPenResty 最佳实践,感谢原作者的开源电子书,让我获益匪浅。更多内容请参考:

lua入门教程:http://www.runoob.com/lua/lua-tutorial.html

OPenResty 最佳实践: https://moonbingbing.gitbooks.io/openresty-best-practices/content/index.html

Lua之异常捕获

如果需要在 Lua 中处理错误,必须使用函数 pcall(protected call)来包装需要执行的代码。 pcall 接收一个函数和要传递给后者的参数,并执行,执行结果:有错误、无错误;返回值 true 或者或 false, errorinfo。

pcall本质上是使用“保护模式”来运行第一参数,因此可以捕获运行过程中的错误。

给一个Cjson的例子:

local json = require("cjson")
local str  = [[ {"key:"value"} ]]

local t    = json.decode(str)
ngx.say(" --> ", type(t))

可以看到,需要解析的str是不完整的,缺少一个”,如果运行,将直接返回500内部错误。这显然不是我们想看到的。为了捕获这个问题,我们将decode封装一层,然后使用pcall来调用。

local json = require("cjson")
local str  = [[ {"key:"value"} ]]

function _json_decode(str)
    return json.decode(str)
end

function json_decode(str)
    local ok, t = pcall(_json_decode, str)
    if not ok then
        return nil    
    else
        return t
    end
end

local t = json_decode(str)

ngx.say(" --> ", type(t))

我们将json.decode封装起来,并使用pcall函数来调用,同样传入错误的字符串,函数会判断实行有异常,然后返回nil。这样我们就有了处理异常的机制。

golang基于redis lua封装的优先级去重队列

前言:

 前两天由于某几个厂商的api出问题,导致后台任务大量堆积,又因为我这边任务流系统会重试超时任务,所以导致队列中有大量的重复任务。这时候我们要临时解决两个事情,一件事情,让一些高质量的任务优先执行; 另一件事情, 要有去重。 rabbitmq不能很好的针对这类情况去重、分优先级。  

 这时候我又想到了我最爱的redis…   去重?  list + set 就可以解决, 优先级,zset + zrange + zrem 也可以解决…  但问题这几个命令非原子,那么怎么让他们原子?   写模块 or redis lua script .   首先在 redis 4.x 写了个简单的module,但写完了发现一件颇为重要的事情,我们线上的是3.2 ….  然后又花了点时间改成redis lua的版本。项目本身的功能实现很简单,复杂的是创意 !!! 

项目名: redis_unique_queue, 项目地址,https://github.com/rfyiamcool/redis_unique_queue

主要功能介绍:

使用redis lua script 封装的去重及优先级队列方法, 达到了组合命令的原子性和节省来往的io请求的目的.

去重队列:

不仅能保证FIFO, 而且去重.

优先级去重队列:

按照优先级获取任务, 并且去重.

使用方法:

# xiaorui.cc

PriorityQueue

NewPriorityQueue(priority int, unique bool, r *redis.Pool)

Push(q string, body string, pri int) (int, error)

Pop(q string) (resp string, err error)
UniqueQueue

NewUniqueQueue(r *redis.Pool) *UniqueQueue

UniquePush(q string, body string) (int, error)

UniquePop(q string) (resp string, err error)

more..

下面是优先级去重队列的例子:

package main

// xiaorui.cc

import (
    "fmt"

    "github.com/rfyiamcool/redis_unique_queue"
)

func main() {
    fmt.Println("start")
    redis_client_config := unique_queue.RedisConfType{
        RedisPw:          "",
        RedisHost:        "127.0.0.1:6379",
        RedisDb:          0,
        RedisMaxActive:   100,
        RedisMaxIdle:     100,
        RedisIdleTimeOut: 1000,
    }
    redis_client := unique_queue.NewRedisPool(redis_client_config)

    qname := "xiaorui.cc"
    body := "message from xiaorui.cc"

    u := unique_queue.NewPriorityQueue(3, true, redis_client)
    // 3: 3个优先级,从1-3级
    // true: 开启unique set

    u.Push(qname, body, 2)
    // 2, 优先级

    fmt.Println(u.Pop(qname))
}

单单使用 去重队列的例子:

package main

import (
    "fmt"

    "github.com/rfyiamcool/redis_unique_queue"
)

func main() {
    fmt.Println("start")
    redis_client_config := unique_queue.RedisConfType{
        RedisPw:          "",
        RedisHost:        "127.0.0.1:6379",
        RedisDb:          0,
        RedisMaxActive:   100,
        RedisMaxIdle:     100,
        RedisIdleTimeOut: 1000,
    }
    redis_client := unique_queue.NewRedisPool(redis_client_config)


    qname := "xiaorui.cc"
    u := unique_queue.NewUniqueQueue(redis_client)
    for i := 0; i < 100; i++ {
        u.UniquePush(qname, "body...")
    }

    fmt.Println(u.Length(qname))

    for i := 0; i < 100; i++ {
        u.UniquePop(qname)
    }

    fmt.Println(u.Length(qname))


    fmt.Println("end")
}

需要改进地址也是很多, 比如 加入批量操作, 对于redis连接池引入方法改进等.

END.

openresty通过lua增加随机traceid

在没有引入zipkin(或者阿里的鹰眼,百度的华佗)这种trace系统的时候,排查问题的一般思路都是按照请求链路来寻找问题源。因此如果能在请求链路中有一个唯一的标识就最好了,而在nginx/openresty做接入层的架构中,可以通过lua脚本生成一个随机traceid。

随机数的生成原理,都是先初始化一个随机数种子,由于伪随机数的特性,种子的随机性就显得格外重要,而一般种子的生成都是通过时间的倒序来选取

lua 随机数生成方法

首先我们看下通常lua的随机数生成方法

math.randomseed(tonumber(tostring(os.time()):reverse():sub(1,6)))
math.random(m,n)

通过时间字符串的逆序初始化随机种子,这里注意到有个sub函数做了截断,是因为

math.randomseed will call the underlying C function srand which takes an unsigned integer valueLua will cast the value of the seed to this format. In case of an overflow the seed will actually become a bad seed, without warning

所以需要避免出现高类型向低类型转换的溢出问题

common 方法的问题

但上面的方法有个问题,就是os.time()函数返回的是秒(10位整数), 所以在做web请求的traceid时很容易就出现重复,影响问题追踪的效率,而lua如果要以毫秒为单位的时间来初始化随机种子,需要引入socket等外部模块,对于openresty来说一般都是封装好的,不方便去做这种定制

利用openresty 与lua生成traceid

幸运的是,nginx-lua中有个函数ngx.now会返回一个浮点数(当然在lua中统一为number类型),3位小数即为毫秒位,所以问题就变得简单了

access_by_lua_block {
    math.randomseed(tonumber(tostring(ngx.now()*1000):reverse():sub(1,9)))
    local randvar = string.format("%.0f",math.random(1000000000000000000,9223372036854775807))
    ngx.req.set_header("traceid", randvar)
}

通过ngx.now()*1000拿到毫秒数据转换为字符串取反,这样毫秒数据的变化才能显出效果; unsigned int(64bit机器下为4byte) 最大值为10位数,我们取前9位避免数据溢出带来的转换问题; lua在显示大于64bit的数据时会自动用科学技术法表示,所以我们需要通过string.format函数来将其转换为19位数字,然后通过ngx.req.set_header添加到请求头中去。

Nginx 安装 Lua 支持

Nginx 支持 Lua 需要安装 lua-nginx-module 模块,一般常用有 2 种方法:

  • 编译 Nginx 的时候带上 lua-nginx-module 模块一起编译

  • 使用 OpenResty: Nginx + 一些模块,默认启用了 Lua 支持

OpenResty is just an enhanced version of Nginx by means of addon modules anyway. You can take advantage of all the exisitng goodies in the Nginx world.

OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

OpenResty® 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。

OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。

OpenResty

OpenResty 的安装很方便,Mac 里使用 brew 安装,对于一些常见的 Linux 发行版本,OpenResty® 提供 官方预编译包,CentOS 使用 yum,Ubuntu 使用 apt-get,具体请参考 https://openresty.org/cn/installation.html,以下以 Mac 和 CentOS 7 中安装 OpenResty 为例。

Mac 使用 OpenResty

  • 终端执行 brew install homebrew/nginx/openresty 把 OpenResty 安装到 /usr/local/Cellar/openresty

  • 终端执行 openresty -V 可以从输出信息中发现 Nginx 的配置文件为 /usr/local/etc/openresty/nginx.conf

  • 启动 Nginx,2 种启动方式都可以

    • sudo openresty (openresty 其实就是 nginx 的软连接)

    • sudo nginx (把 /usr/local/Cellar/openresty/1.11.2.5/nginx/sbin 添加到 PATH 里,注意不同版本时的路径不同)

    • 查看是否启动了 nginx: ps -ef | grep nginx

  • 测试是否支持 Lua

1、/usr/local/etc/openresty/nginx.conf 中添加

location /lua {
    default_type 'text/html';
    content_by_lua 'ngx.say("hello world");';
}

2、nginx -t 测试配置没问题,然后 nginx -s reload 重新加载配置 (nginx -s stop 关闭 Nginx)

3、curl http://localhost/lua 输出 hello world 则说明 Nginx 支持 Lua

CentOS 7 使用 OpenResty

  • 终端执行下面 3 条命令把 OpenResty 安装到 /usr/local/openresty
sudo yum install yum-utils
sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
sudo yum install openresty
  • Nginx 的配置文件位于 /usr/local/openresty/nginx/conf/nginx.conf (openresty -V 中没有指定)

  • 启动 Nginx,2 种启动方式都可以

    • sudo openresty

    • sudo nginx

    • 查看是否启动了 nginx: ps -ef | grep nginx

  • 测试是否支持 Lua: 参考上面的方法

编译 Nginx + Lua

编译 Nginx 需要先准备好下面的这些工具,如果不确定是否已安装,可以在编译的时候根据出现的错误提示再进行安装

  • yum install -y gcc g++ gcc-c++

  • yum -y install zlib zlib-devel openssl openssl–devel pcre pcre-devel

Nginx 支持 Lua 需要依赖 LuaJIT-2.0.4.tar.gz,ngx_devel_kit,lua-nginx-module,下面介绍具体的编译过程 (都下载到 /root 目录)

1、下载安装 LuaJIT-2.0.4.tar.gz

wget -c http://luajit.org/download/LuaJIT-2.0.4.tar.gz
tar xzvf LuaJIT-2.0.4.tar.gz
cd LuaJIT-2.0.4
make install PREFIX=/usr/local/luajit
# 添加环境变量
export LUAJIT_LIB=/usr/local/luajit/lib
export LUAJIT_INC=/usr/local/luajit/include/luajit-2.0

2、下载解压 ngx_devel_kit

wget https://github.com/simpl/ngx_devel_kit/archive/v0.3.0.tar.gz
tar -xzvf v0.3.0.tar.gz

3、下载解压 lua-nginx-module

wget https://github.com/openresty/lua-nginx-module/archive/v0.10.8.tar.gz
tar -xzvf v0.10.8.tar.gz

4、下载安装 nginx-1.10.3.tar.gz

wget http://nginx.org/download/nginx-1.10.3.tar.gz
tar -xzvf nginx-1.10.3.tar.gz
cd nginx-1.10.3
# 注意ngx_devel_kit和lua-nginx-module 以实际解压路径为准
./configure --add-module=/root/ngx_devel_kit-0.3.0 --add-module=/root/lua-nginx-module-0.10.8
make -j2
make install

5、支持 Nginx 被安装到了 /usr/local/nginx,配置文件为 /usr/local/nginx/conf/nginx.conf

6、验证

  • 将 nginx 做成命令: ln -s /usr/local/nginx/sbin/nginx /usr/bin/nginx

  • /usr/local/nginx/conf/nginx.conf 中添加 Lua 测试代码

location /lua {
    default_type 'text/html';
    content_by_lua 'ngx.say("hello world");';
}
  • 启动 Nginx: sudo nginx

  • curl http://localhost/lua 输出 hello world 则说明 Nginx 支持 Lua

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等等吧。

Debian/Ubuntu 下 Nginx+Lua 环境搭建

前言

  • 关于lua的特性不再赘述;
  • 以下步骤均使用了apt-get进行操作,免去了诸如ng-lua组件手动加载等繁琐的步骤,妄图使用其他方式安装的请移步官方文档:https://github.com/openresty/lua-nginx-module#installation
  • lua在web上基于lua-nginx-module运作,目前还没有Apache的支持组件,所以妄图使用Apache的可以撤了;

本人环境供参考:

Distributor ID: Debian
Description:    Debian GNU/Linux 8.8 (jessie)
Release:    8.8
Codename:   jessie

注:不确定wheezy版本的Linux下luajit是否可用。

搭建步骤

1、首先确定自己的APT源是否可用,是否足够新.

本人最开始使用了一个较为老旧的dotdeb版本,导致apt-cache查询不到lua-nginx-module,甚至一度尝试手动加载该模块,由此浪费了许多时间;
本人使用的apt source list供参考:

sudo vi /etc/apt/sources.list

>
deb http://mirrors.aliyun.com/dotdeb jessie all
deb http://mirrors.aliyun.com/debian jessie main contrib non-free
deb http://mirrors.aliyun.com/debian jessie-updates main contrib non-free
deb http://mirrors.aliyun.com/debian-security jessie/updates main contrib non-free

需要注意的是:添加源的时候需要注意完整性,本人最开始图省事,仅用了一个all,发现apt并不能找到luajit模块,又浪费了很多时间。

2、没有nginx的先安装nginx;

不再赘述

sudo apt-get install nginx-full

3、安装lua及相关组件

不再赘述

apt-get install lua5.2 lua5.2-doc liblua5.2-dev

4、安装luajit

sudo apt-get install luajit

关于JIT :

通常,程序有两种运行方式:静态编译与动态直译。
静态编译的程序在执行前全部被翻译为机器码,而动态直译执行的则是一句一句边运行边翻译。
即时编译(Just-In-Time Compiler)则混合了这二者,一句一句编译源代码,但是会将翻译过的代码缓存起来以降低性能损耗。

此外,使用luajit可以大大提高lua的运行效率,由此也被官方钦定。

It is highly recommended to use OpenResty releases which integrate Nginx, ngx_lua, LuaJIT 2.1, as well as other powerful companion Nginx modules and Lua libraries.

5、安装nginx的lua模块 lua-nginx-module

sudo apt-get install libnginx-mod-http-lua

需要注意的是:如果前面apt源不够新或不够全,很可能会在安装此模块的时候出现找不到luajit依赖项的情况,此时请寻找新的可靠的源并确保完整性,不要在这里浪费时间。

6、配置nginx

nginx的配置中,核心在于content_by_lua_file。

关于 content_by_lua_file 的官方文档: https://github.com/openresty/lua-nginx-module#content_by_lua_file

本人的配置代码供参考:

server {
            listen 80;
            server_name ebid.xxxx.com;
            root /home/separes/xxx;

            location / {
                    lua_code_cache off;  // 缓存
                    content_by_lua_file /home/separes/xxx/index.o;
            }
}

需要注意的是:这里的nginx缓存是默认开启的,推荐调试及开发环境中手动关闭 lua_code_cache。

7、其它

这里推荐几个组件

// cjson
sudo apt-get install lua-cjson

// lyaml
sudo luarocks install lyaml

// dbi
sudo luarocks install luadbi-mysql MYSQL_INCDIR=/usr/include/mysql
//实测后面必须指定MYSQL路径,指向系统中的mysql根目录,匹配mysql.c文件

需要注意的是,使用 luarocks/apt-get 安装或升级 lua-DBI 的时候,需要注意新版本的DBI并不完全向下兼容,其中dbi参数由全局变量改为了局部变量,如果在以前的代码中使用过,需要重新进行声明,否则该参数会报错。

文档:

Lua官方文档: https://www.lua.org/manual/5.3

lua-nginx-module官方文档: https://github.com/openresty/lua-nginx-module

环境搭建至此为止。