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 等都进行一致的高性能响应。
nginx实际把请求处理流程划分为了11个阶段,这样划分的原因是将请求的执行逻辑细分,各阶段按照处理时机定义了清晰的执行语义,开发者可以很容易分辨自己需要开发的模块应该定义在什么阶段。
由于 Nginx 把一个请求分成了很多阶段,第三方模块就可以根据自己的行为,挂载到不同阶段处理达到目的,OpenResty 也应用了同样的特性。不同的阶段,有不同的处理行为,这是OpenResty 的一大特色,OpenResty 处理一个请求的流程参考下图
你可以在你的 CentOS 系统中添加 openresty 仓库,这样就可以便于未来安装或更新我们的软件包(通过 yum update 命令)
运行下面的命令就可以添加我们的仓库:
sudo yum install yum-utils
sudo yum-config-manager --add-repo
https://openresty.org/package/centos/openresty.repo
然后就可以像下面这样安装软件包,比如 openresty
sudo yum install openresty
OpenResty插件分为自带插件以及第三方插件,如果是自带插件直接激活就可以,如果是第三方插件需要手动下载插件添加进去,这里我们以本地缓存插件安装举例
yum install -y make cmake gcc gcc-c++ autoconf automake libpng-devel libjpeg-
devel zlib libxml2-devel ncurses-devel bison libtool-ltdl-devel libiconv
libmcrypt mhash mcrypt pcre-devel openssl-devel freetype-devel libcurl-devel lua-
devel readline-devel curl wget
wget https://openresty.org/download/openresty-1.19.3.1.tar.gz
tar -zxvf openresty-1.19.3.1.tar.gz # 解压openresty
到 缓存插件地址 下载最新版 缓存插件
wget http://labs.frickle.com/files/ngx_cache_purge-2.3.tar.gz
tar -zxvf ngx_cache_purge-2.3.tar.gz #解压缓存插件
选择需要的插件启用, –with-Components 激活组件,–without 则是禁止组件 ,–add-module是安装第三方模块
./configure --prefix=/usr/local/openresty --with-luajit --without-
http_redis2_module --with-http_stub_status_module --with-http_v2_module --with-
http_gzip_static_module --with-http_sub_module --add-
module=/usr/local/openresty/modules/ngx_cache_purge-2.3 #配置缓存插件的源码路径
这里禁用了 redis组件 并且 安装了第三方缓存组件
出现如下界面表示编译成功
gmake && gmake install
vi /etc/profile ##加入path路径
export PATH=$PATH:/usr/local/openresty/nginx/sbin/
source /etc/profile ##生效配置
nginx -v
nginx version: openresty/1.17.8.2
查看安装的组件
nginx -V
我们在 conf 目录下创建一个 nginx.conf 文件 代码如下
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
server {
listen 9000;
location / {
default_type text/html;
content_by_lua '
ngx.say("Hello, World!
")
';
}
}
}
如果你熟悉 nginx 的配置,应该对以上代码就很熟悉。这里我们将 html 代码直接写在了配置文件中
nginx -c /usr/local/openresty/nginx/conf/nginx.conf
接下来我们可以使用 curl 来测试是否能够正常范围
> curl http://localhost:9000/
<p>Hello, World!</p>
、
我们在配置文件写的 html 已正常输出
主要帮助对http请求取参、取header头、输出等
商品详情页架构的要求->高可用 ,-> 高性能,-> 高并发 ;一般来说 业界分为 种主流的方案
很多中小型 电商的商品详情页 可能一分钟都没有一个访问,这种的话,就谈不上并发设计,一个tomcat 就能搞定。
还有一种中小型公司呢?虽然说公司不大,但是也是有几十万日活。然后几百万用户,这种公司,就是稍微大点的公司,他们的商品详情用,采取的方案可能是全局的一个静态页面这样子
就是我们有把商品详情页直接做成一个静态页面,然后这样子每次全量的更新,把数据全部静态放到nginx,里面,每次数据变化的时候,我们就通过一个Java服务去渲染这个数据,然后把这个静态页面推送到到文件服务器
这种方案的缺点,如果商品很多,那么渲染的时间会很长,达不到实时的效果
文件服务器性能高,tomcat性能差,压力都在Tomcat服务器了
只能处理一些静态的东西,如果动态数据很多,比如有库存的,你不可能说每次去渲染,然后推送到文件服务器,那不是更加慢?
下面这张图就是整体的思路
上图展示了核心思想主要有以下四步来完成
添加修改页面的时候生成静态页,这个地方生成的是一个通用的静态页,敏感数据比如 价格,商品名称等,通过占位符来替换,然后将生成的静态页的链接,以及敏感数据同步到redis中,如果只修改价格不需要重新生成静态页,只需要修改redis敏感数据即可。
这个的文件服务器泛指能够提供静态文件处理的文件服务器,nginx代理静态文件,tomcat,以及OSS等都算静态文件服务器,生成完静态文件后将文件推送到文件服务器,并将请求连接存放进redis中
Redis和nginx的速度很快,但是如果有人恶意请求不存在的请求会造成redis很大的开销,那么可以采用布隆过滤器将不存在的请求过滤出去。
因为java连接Reids进行操作并发性能很弱,相对于OpenResty来说性能差距很大,这里使用OpenResty,读取Redis中存放的URL以及敏感数据
从Redis获取到URL后lua脚本抓取模板页面内容,然后通过redis里面的敏感数据进行渲染然后返回前端,因为都是lua脚本操作性能会很高
下面我们就基于这个架构来安装和搭建所需要的环境
使用布隆过滤器来解决缓存穿透问题
布隆过滤器其本质就是一个只包含0和1的数组。具体操作当一个元素被加入到集合里面后,该元素通过K个Hash函数运算得到K个hash后的值,然后将K个值映射到这个位数组对应的位置,把对应位置的值设置为1。查询是否存在时,我们就看对应的映射点位置如果全是1,他就很可能存在(跟hash函数的个数和hash函数的设计有关),如果有一个位置是0,那这个元素就一定不存在
bloom filter之所以能做到在时间和空间上的效率比较高,是因为牺牲了判断的准确率、删除的便利性
很显然,过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。
另外,哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高
如何选择适合业务的 k 和 m 值呢,这里直接贴一个公式:
在redis布隆过滤器插件地址下载最新的release源码,在编译服务器进行解压编译
wget https://github.com/RedisBloom/RedisBloom/archive/v2.2.4.tar.gz
解压插件进行插件的编译
tar RedisBloom-2.2.5.tar.gz
cd RedisBloom-2.2.5
make
编译得到动态库 rebloom.so
启动redis时,如下启动即可加载bloom filter插件
配置文件形式配置
#在redis配置文件(redis.conf)中加入该模块即可
vim redis.conf
#添加
loadmodule /root/bloom/redisbloom-2.2.4/rebloom.so (前面为你自己的路径)
启动命令挂载
redis-server redis.conf --loadmodule /usr/rebloom/rebloom.so INITIAL_SIZE
1000000 ERROR_RATE 0.0001
#容量100万, 容错率万分之一, 占用空间是4m
docker run -d -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest
这里将本地编译好的redisbloom.so挂载到/etc/redis/redisbloom.so目录
配置docker-compose.yml文件
version: '2'
services:
redis01:
image: redis
hostname: redis01
container_name: redis01
networks:
docker-network:
ipv4_address: 172.18.0.2
ports:
- "7001:6379"
volumes:
- "/tmp/etc/redis/redis.conf:/etc/redis/redis.conf"
- "/tmp/data/redis/node1:/data"
- "/tmp/etc/redis/redisbloom.so:/etc/redis/redisbloom.so"
command:
redis-server /etc/redis/redis.conf
redis02:
image: redis
hostname: redis02
container_name: redis02
networks:
docker-network:
ipv4_address: 172.18.0.3
ports:
- "7002:6379"
volumes:
- "/tmp/etc/redis/redis.conf:/etc/redis/redis.conf"
- "/tmp/data/redis/node2:/data"
- "/tmp/etc/redis/redisbloom.so:/etc/redis/redisbloom.so"
command:
redis-server /etc/redis/redis.conf
redis03:
image: redis
hostname: redis03
container_name: redis03
networks:
docker-network:
ipv4_address: 172.18.0.4
ports:
- "7003:6379"
volumes:
- "/tmp/etc/redis/redis.conf:/etc/redis/redis.conf"
- "/tmp/data/redis/node3:/data"
- "/tmp/etc/redis/redisbloom.so:/etc/redis/redisbloom.so"
command:
redis-server /etc/redis/redis.conf
redis04:
image: redis
hostname: redis04
container_name: redis04
networks:
docker-network:
ipv4_address: 172.18.0.5
ports:
- "7004:6379"
volumes:
- "/tmp/etc/redis/redis.conf:/etc/redis/redis.conf"
- "/tmp/data/redis/node4:/data"
- "/tmp/etc/redis/redisbloom.so:/etc/redis/redisbloom.so"
command:
redis-server /etc/redis/redis.conf
redis05:
image: redis
hostname: redis05
container_name: redis05
networks:
docker-network:
ipv4_address: 172.18.0.6
ports:
- "7005:6379"
volumes:
- "/tmp/etc/redis/redis.conf:/etc/redis/redis.conf"
- "/tmp/data/redis/node5:/data"
- "/tmp/etc/redis/redisbloom.so:/etc/redis/redisbloom.so"
command:
redis-server /etc/redis/redis.conf
redis06:
image: redis
hostname: redis06
container_name: redis06
networks:
docker-network:
ipv4_address: 172.18.0.7
ports:
- "7006:6379"
volumes:
- "/tmp/etc/redis/redis.conf:/etc/redis/redis.conf"
- "/tmp/data/redis/node6:/data"
- "/tmp/etc/redis/redisbloom.so:/etc/redis/redisbloom.so"
command:
redis-server /etc/redis/redis.conf
networks:
docker-network:
ipam:
config:
- subnet: 172.18.0.0/16
gateway: 172.18.0.1
启动布隆过滤器集群
docker-compose up -d
配置集群
进入一个容器
docker exec -ti redis01 /bin/bash
执行创建集群命令
redis-cli --cluster create 172.18.0.2:6379 172.18.0.3:6379 172.18.0.4:6379 172.18.0.5:6379 172.18.0.6:6379 172.18.0.7:6379 --cluster-replicas 1
# 进入一个redis集群的节点内部
docker exec -ti redis01 /bin/bash
# 以集群方式登录172.18.0.2:3306节点
redis-cli -h 172.18.0.2 -c
# 在redis中添加一个布隆过滤器 错误率是0.01 数量是1万个
BF.RESERVE bf_test 0.01 10000 NONSCALING
# 在bf_test 的布隆过滤器添加一个key
BF.ADD bf_test key
# 验证布隆过滤器key是否存在
BF.EXISTS bf_test key
# 验证布隆过滤器key1是否存在
BF.EXISTS bf_test key1
1、引入Guava pom配置
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>29.0-jreversion>
dependency>
2、代码实现
public class BloomFilterTest {
int size=100000;
double fpp=0.03;
@Test
public void test1(){
BloomFilter<Integer> integerBloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);
//插入10W样本数据
for (int i = 0; i < size; i++) {
integerBloomFilter.put(i);
}
//用另外十万测试数据,测试误判
int count=0;
for (int i = 100001; i < size+100000; i++) {
if (integerBloomFilter.mightContain(i)){
count++;
System.out.println(i+"误判了");
}
}
System.out.println("总共的误判数="+count);
}
}
代码分析:
核心 BloomFilter.create 方法:
public static <T> BloomFilter<T> create(Funnel<? super T> funnel, int expectedInsertions, double fpp) {
return create(funnel, (long)expectedInsertions, fpp);
}
这里有四个参数:
咱们重点讲一下 fpp 参数
fpp误判率
情景一: fpp = 0.01
误判个数:947 占内存大小:9585058位数
情景二: fpp = 0.03 (默认参数)
误判个数:3033 占内存大小:7298440位数
总结
误判率能够经过 fpp 参数进行调节
fpp越小,须要的内存空间就越大:0.01须要900多万位数,0.03须要700多万位数。
fpp越小,集合添加数据时,就须要更多的hash函数运算更多的hash值,去存储到对应的数组下标里。(忘了去看上面的布隆过滤存入数据的过程)
上面使用Guava实现的布隆过滤器是把数据放在了本地内存中。分布式的场景中就不合适了,没法共享内存
还能够用Redis来实现布隆过滤器,这里使用Redis封装好的客户端工具Redisson
pom配置:
<dependency>
<groupId>org.redissongroupId>
<artifactId>redisson-spring-boot-starterartifactId>
<version>3.16.0version>
dependency>
java代码:
public class RedissonBloomFilter {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
config.useSingleServer().setPassword("1234");
RedissonClient redisson = Redisson.create(config);
RBloomFilter<Object> bloomFilter = redisson.getBloomFilter("phoneList");
bloomFilter.tryInit(1000000L,0.03);
bloomFilter.add("10086");
System.out.println(bloomFilter.contains("123456"));
System.out.println(bloomFilter.contains("10086"));
}
}
lua_resty_redis 它是一个基于rockspec API的为ngx_lua模块提供Lua redis客户端的驱动。
resty-redis-cluster模块地址:https://github.com/steve0511/resty-redis-cluster
将 resty-redis-cluster/lib/resty/ 下面的文件 拷贝到 openresty/lualib/resty
总共两个文件 rediscluster.lua , xmodem.lua
创建 RedisUtils.lua 的文件
--操作Redis集群,封装成一个模块
--引入依赖库
local redis_cluster = require "resty.rediscluster"
--配置Redis集群链接信息
local config = {
name = "testCluster", --rediscluster name
serv_list = { --redis cluster node list(host and port),
{ip="192.168.64.181", port = 7001},
{ip="192.168.64.181", port = 7002},
{ip="192.168.64.181", port = 7003},
{ip="192.168.64.181", port = 7004},
{ip="192.168.64.181", port = 7005},
{ip="192.168.64.181", port = 7006},
},
keepalive_timeout = 60000, --redis connection pool idle timeout
keepalive_cons = 1000, --redis connection pool size
connection_timout = 1000, --timeout while connecting
max_redirection = 5
}
--定义一个对象
local lredis = {}
--创建查询数据get()
function lredis.get(key)
local red = redis_cluster:new(config)
local res, err = red:get(key)
if err then
ngx.log(ngx.ERR,"执行get错误:",err)
return false
end
return res
end
-- 执行hgetall方法并封装成table
function lredis.hgetall(hash_key)
local red = redis_cluster:new(config)
local flat_map, err = red:hgetall(hash_key)
if err then
ngx.log(ngx.ERR,"执行hgetall错误:",err)
return false
end
local result = {}
for i = 1, #flat_map, 2 do
result[flat_map[i]] = flat_map[i + 1]
end
return result
end
-- 判断key中的item是否在布隆过滤器中
function lredis.bfexists(key,item)
local red = redis_cluster:new(config)
-- 通过eval执行脚本
local res,err = red:eval([[
local key=KEYS[1]
local val= ARGV[1]
local res,err=redis.call('bf.exists',key,val)
return res
]],1,key,item)
if err then
ngx.log(ngx.ERR,"过滤错误:",err)
return false
end
return res
end
return lredis
我们编写了lua脚本需要交给nginx服务器去执行,我们需要将创建一个lua目录,并在全局的nginx‘配置文件中配置lua目录,配置参数使用lua_package_path
# 配置redis 本地缓存
lua_shared_dict redis_cluster_slot_locks 100k;
# 配置lua脚本路径
lua_package_path "/usr/local/openresty/script/?.lua;;"; #注意后面是两个分号
完整的配置如下
user root;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
# lua脚本路径
lua_package_path "/usr/local/openresty/script/?.lua;;";
#gzip on;
include /usr/local/nginx/conf/conf.d/*.conf;
}
在 nginx配置文件嵌入lua脚本进行测试
server {
listen 9999;
charset utf-8;
location /test {
default_type text/html;
content_by_lua '
local lrredis = require("RedisUtils")
-- 尝试读取redis中key的值
local value = lrredis.get("key")
--判断key是否在bf_taxi的布隆过滤器中
local bfexist = lrredis.bfexists("bf_taxi","key")
local htest = lrredis.hgetall("h_test")
ngx.log(ngx.INFO, "key的值是",value)
ngx.log(ngx.INFO, "bf_taxi布隆过滤器key的状态",bfexist)
ngx.log(ngx.INFO, "h_test[url]的值是",htest["url"])
';
}
}
访问查看nginx日志打印
tail -f /usr/local/openresty/nginx/logs/error.log
nginx为了能够处理请求需要获取请求数据,需要将获取请求参数的lua脚本封装以下,创建RequestUtils.lua 的文件
--定义一个对象
local lreqparm={}
-- 获取请求参数的方法
function lreqparm.getRequestParam()
-- 获取请求方法 get或post
local request_method = ngx.var.request_method
-- 定义参数变量
local args = nil
if "GET" == request_method then
args = ngx.req.get_uri_args()
elseif "POST" == request_method then
ngx.req.read_body()
args = ngx.req.get_post_args()
end
return args
end
return lreqparm
server {
listen 9999;
charset utf-8;
location /testreq {
default_type text/html;
content_by_lua '
local lreqparm = require("RequestUtils")
local params = lreqparm.getRequestParam()
local title = params["title"]
if title ~= nil then
ngx.say("请求参数的Title是:
"..title)
return
end
ngx.say("没有输入title请求参数
"
)
';
}
}
访问http://192.168.64.150:9999/testreq?title=ceshi 测试
下载地址 https://github.com/ledgetech/lua-resty-http
将 lua-resty-http-master\lib\resty 下的所有文件复制到 openresty/lualib/resty
总共两个文件 http.lua , http_headers.lua
因为需要从远程服务器抓取远程页面的内容,需要用到http模块,将其封装起来,创建RequestHtml.lua
-- 引入Http库
local http = require "resty.http"
--定义一个对象
local lgethtml={}
function lgethtml.gethtml(requesturl)
--创建客户端
local httpc = http:new()
local resp,err = httpc:request_uri(requesturl,
{
method = "GET",
headers = {["User-Agent"]="Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36"}
})
--关闭连接
httpc:close()
if not resp then
ngx.say("request error:",err)
return
end
local result = {}
--获取状态码
result.status = resp.status
result.body = resp.body
return result
end
return lgethtml
模板文件我们用
server {
listen 9999;
charset utf-8;
location /testgetHtml {
default_type text/html;
content_by_lua '
local lgethtml = require("RequestHtml")
local url = "http://192.168.64.1:9001/template.html"
local result = lgethtml.gethtml(url);
ngx.log(ngx.INFO, "状态是",result.status)
ngx.log(ngx.INFO, "body是",result.body)
ngx.say(result.body)
';
}
}
访问http://192.168.64.181:9999//testgetHtml 测试
wget https://github.com/bungle/lua-resty-template/archive/v1.9.tar.gz tar -xvzf v1.9.tar.gz
解压后可以看到lib/resty下面有一个template.lua,这个就是我们所需要的,在template目录中还有两个lua文件,将这两个文件复到/usr/openResty/lualib/resty中即可。
local template = require "resty.template"
-- Using template.new
local html=[[<html>
<body>
<h1>{{message}}</h1>
</body>
</html>
]]
template.render(html, { message = params["title"] })
在nginx中配置文件中嵌入lua脚本执行测试
server {
listen 9999;
charset utf-8;
location /testtemplate {
default_type text/html;
content_by_lua '
local lreqparm = require("RequestUtils")
local template = require "resty.template"
local params = lreqparm.getRequestParam()
-- Using template.new
local html=[[
{{message}}
]]
template.render(html, { message = params["title"] })
';
}
}
访问 http://192.168.64.150:9999/testtemplate?title=123456
整个调用流程如下,需要使用lua脚本编写,整体流程分为7 步
上面我们将各个组件都给封装了,下面我们需要将各个组件组合起来
创建一个 requestTemplateRendering.lua 的lua脚本
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by baiyp.
--- DateTime: 2020/11/24 13:24
---
local template = require("resty.template")
local lrredis = require("RedisUtils")
local lgethtml = require("RequestHtml")
local lreqparm = require("RequestUtils")
--获取请求参数
local reqParams = lreqparm.getRequestParam()
-- 定义本地缓存
local html_template_cache = ngx.shared.html_template_cache
-- 获取请求ID的参数
local reqId = reqParams["id"];
ngx.log(ngx.INFO, "requestID:", reqId);
-- 校验参数
if reqId==nil then
ngx.say("缺少ID参数");
return
end
-- 布隆过滤器检查id是否存在
local bfexist = lrredis.bfexists("bf_taxi",reqId)
ngx.log(ngx.INFO, "布隆过滤器检验:", bfexist)
-- 校验key不存在直接返回
if bfexist==0 then
ngx.say("布隆过滤器校验key不存在...")
return
end
-- 拼接hget的key
local hgetkey = "hkey_".. reqId
-- 通过hget获取map的所有数据
local templateData = lrredis.hgetall(hgetkey);
if next(templateData) == nil then
ngx.say("redis没有存储数据...")
return
end
--获取模板价格数据
local amount = templateData["amount"]
ngx.log(ngx.INFO, "amount:", amount)
if amount == nil then
ngx.say("价格数据未配置");
return
end
-- 获取本地缓存对象
ngx.log(ngx.INFO, "开始从缓存中获取模板数据----")
local html_template = html_template_cache:get(reqId)
-- 判断本地缓存是否存在
if html_template == nil then
-- 获取模板url中的数据
ngx.log(ngx.INFO, "缓存中不存在数据开始远程获取模板")
local url = templateData["url"]
ngx.log(ngx.INFO, "从缓存中获取的url地址:", url)
if url == nil then
ngx.say("URL路径未配置");
return
end
-- 抓取远程url的html
ngx.log(ngx.INFO, "开始抓取模板数据:", url)
local returnResult = lgethtml.gethtml(url);
-- 判断抓取模板是否正常
if returnResult==nil then
ngx.say("抓取URL失败...");
return
end
-- 判断html状态
if returnResult.status==200 then
html_template = returnResult.body
--设置模板缓存为一小时
ngx.log(ngx.INFO, "将模板数据加入到本地缓存")
html_template_cache:set(reqId,html_template,60 * 60)
end
end
ngx.log(ngx.INFO, "缓存中获取模板数据结束----")
-- 模板渲染
--编译得到一个lua函数
local func = template.compile(html_template)
local context = {amount=amount}
ngx.log(ngx.INFO, "开始渲染模板数据")
--执行函数,得到渲染之后的内容
local content = func(context)
--通过ngx API输出
ngx.say(content)
在nginx主配置文件中添加模板缓存
lua_shared_dict html_template_cache 10m;
我们这里使用 content_by_lua_file 的方式执行脚本
server {
listen 8888;
charset utf-8;
#配置静态资源代理
location /static {
proxy_pass http://192.168.64.1:9001/static;
}
#删除本地缓存
location /delete {
default_type text/html;
content_by_lua '
local lreqparm = require("RequestUtils")
--获取请求参数
local reqParams = lreqparm.getRequestParam()
-- 定义本地缓存
local html_template_cache = ngx.shared.html_template_cache
-- 获取请求ID的参数
local reqId = reqParams["id"];
ngx.log(ngx.INFO, "requestID:", reqId);
-- 校验参数
if reqId==nil then
ngx.say("缺少ID参数");
return
end
-- 获取本地缓存对象
html_template_cache:delete(reqId);
ngx.say("清除缓存成功");
';
}
location /template {
default_type text/html;
content_by_lua_file
/usr/local/openresty/script/requestTemplateRendering.lua;
}
}
# 进入一个redis集群的节点内部
docker exec -ti redis01 /bin/bash
# 以集群方式登录172.18.0.2:3306节点
redis-cli -h 172.18.0.2 -c
# 在redis中添加一个布隆过滤器 错误率是0.01 数量是1万个
BF.RESERVE bf_taxi 0.01 10000 NONSCALING
# 在bf_test 的布隆过滤器添加一个key
BF.ADD bf_taxi 1
#检查数据是否存在
BF.EXISTS bf_taxi 1
# 添加URL以及价格
hset hkey_1 url http://192.168.64.1:9001/template.html amount 100.00
访问 http://192.168.64.181:8888/template?id=1 进行测试
Kong是一款基于OpenResty(Nginx + Lua模块)编写的高可用、易扩展的,由Mashape公司开源的API Gateway项目。Kong是基于NGINX和Apache Cassandra或PostgreSQL构建的,能提供易于使用的RESTful API来操作和配置API管理系统,所以它可以水平扩展多个Kong服务器,通过前置的负载均衡
配置把请求均匀地分发到各个Server,来应对大批量的网络请求。
API网关是一个服务器,是系统的唯一入口。从面向对象设计的角度看,它与外观模式类似。API网关封装了系统内部架构,为每个客户端提供一个定制的API。它可能还具有其它职责,如身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理。API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。通常,网关也
是提供REST/HTTP的访问API。
在微服务架构之下,服务被拆的非常零散,降低了耦合度的同时也给服务的统一管理增加了难度。如上图左所示,在旧的服务治理体系之下,鉴权,限流,日志,监控等通用功能需要在每个服务中单独实现,这使得系统维护者没有一个全局的视图来统一管理这些功能。API 网关致力于解决的问题便是为微服务纳管这些通用的功能,在此基础上提高系统的可扩展性。如右图所示,微服务搭配上 API 网关,可以使得服务本身更专注于自己的领域,很好地对服务调用者和服务提供者做了隔离。
目前,比较流行的网关有:Nginx 、 Kong 、Orange等等,还有微服务网关Zuul 、Spring Cloud Gateway等等
对于 API Gateway,常见的选型有基于 Openresty 的 Kong、基于 Go 的 Tyk 和基于 Java 的gateway,这三个选型本身没有什么明显的区别,主要还是看技术栈是否能满足快速应用和二次开发。
Kong主要有三个组件
Kong采用插件机制进行功能定制,插件集(可以是0或N个)在API请求响应循环的生命周期中被执行。插件使用Lua编写,目前已有几个基础功能:HTTP基本认证、密钥认证、CORS(Cross-Origin Resource Sharing,跨域资源共享)、TCP、UDP、文件日志、API请求限流、请求转发以及Nginx监
控。
Kong网关具有以下的特性
为了更好地理解系统,这是使用Kong网关的API接口的典型请求工作流程:
当Kong运行时,每个对API的请求将先被Kong命中,然后这个请求将会被代理转发到最终的API接口。在请求(Requests)和响应(Responses)之间,Kong将会执行已经事先安装和配置好的任何插件,授权您的API访问操作。Kong是每个API请求的入口点(Endpoint)。
首先我们创建一个Docker自定义网络,以允许容器相互发现和通信。在下面的创建命令中 kong- net 是我们创建的Docker网络名称。
docker network create kong-net
Kong 目前使用Cassandra(Facebook开源的分布式的NoSQL数据库) 或者PostgreSql,你可以执行以下命令中的一个来选择你的Database。请注意定义网络 --network=kong-net 。
docker run -d --name kong-database --network=kong-net -p 5432:5432 -v /tmp/data/kong:/var/lib/postgresql/data -e "POSTGRES_USER=kong" -e "POSTGRES_DB=kong" -e "POSTGRES_PASSWORD=kong" postgres:11.7
使用 docker run --rm 来初始化数据库,该命令执行后会退出容器而保留内部的数据卷(volume)
docker run --rm --network=kong-net -e "KONG_DATABASE=postgres" -e
"KONG_PG_HOST=kong-database" -e "KONG_PG_PASSWORD=kong" -e
"KONG_CASSANDRA_CONTACT_POINTS=kong-database" kong:latest kong migrations bootstrap
这个命令我们还是要注意的,一定要跟你声明的网络,数据库类型、host名称一致。同时注意Kong的版本号,本文是在 Kong 1.4.x 版本下完成的。
完成初始化数据库后,我们就可以启动一个连接到数据库容器的Kong容器,请务必保证你的数据库容器启动状态,同时检查所有的环境参数 -e 是否是你定义的环境
docker run -d --name kong \
--network=kong-net \
-e "KONG_DATABASE=postgres" \
-e "KONG_PG_HOST=kong-database" \
-e "KONG_PG_PASSWORD=kong" \
-e "KONG_CASSANDRA_CONTACT_POINTS=kong-database" \
-e "KONG_PROXY_ACCESS_LOG=/dev/stdout" \
-e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" \
-e "KONG_PROXY_ERROR_LOG=/dev/stderr" \
-e "KONG_ADMIN_ERROR_LOG=/dev/stderr" \
-e "KONG_ADMIN_LISTEN=0.0.0.0:8001, 0.0.0.0:8444 ssl" \
-p 8000:8000 \
-p 8443:8443 \
-p 8001:8001 \
-p 8444:8444 \
kong:latest
通过服务器执行curl访问尝试服务是否已经启动
curl http://localhost:8001/status
我们来看一个典型 Nginx 的配置对应在 Kong 上是怎么样的,下面是一个典型的 Nginx 配置
upstream passportUpstream {
server 192.168.64.1:9001 weight=10;
}
server {
listen 80;
location /hello {
proxy_pass http://passportUpstream;
}
}
下面我们开看看其对应 Kong 中的配置
# 配置 upstream
curl -X POST http://localhost:8001/upstreams --data "name=passportUpstream"
# 配置 target
curl -X POST http://localhost:8001/upstreams/passportUpstream/targets --data
"target=192.168.64.1:9001" --data "weight=10"
# 配置 service
curl -X POST http://localhost:8001/services --data "name=hello" --data "host=passportUpstream"
# 配置 route
curl -X POST http://localhost:8001/routes \
--data "paths[]=/hello" \
--data "service.id=bfdaf1ea-9b81-4f53-a16d-f7255e44264a"
curl -X POST http://localhost:8001/routes
--data "hosts[]=*.example.com,test.com,*.abc.com"
--data "service.id=8695cc65-16c1-43b1-95a1-5d30d0a50409"
这一切配置都是通过其 Http Restful API 来动态实现的,无需我们再手动的 reload Nginx.conf
在上述的配置中涉及到了几个概念: Upstream、Target、Service、Route 等概念,它们是 Kong的几个核心概念,也是我们在使用 Kong API 时经常打交道的,下面我们就其几个核心概念做一下简单的说明。
Upstream 对象表示虚拟主机名,可用于通过多个服务(目标)对传入请求进行负载均衡。例如:service.v1.xyz 为 Service 对象命名的上游 Host 是 service.v1.xyz 对此服务的请求将代理到上游定义的目标。
目标 IP地址/主机名,其端口表示后端服务的实例。每个上游都可以有多个 Target,并且可以动态添加 Target。
由于上游维护 Target 的更改历史记录,因此无法删除或者修改 Target。要禁用目标,请发布一个新的 Targer weight=0,或者使用 DELETE 来完成相同的操作
顾名思义,服务实体是每个上游服务的抽象,服务的示例是数据转换微服务,计费API等。
服务的主要属性是它的 URL(其中,Kong 应该代理流量),其可以被设置为单个JSON串或通过指定其 protocol, host,port 和path。
服务与路由相关联(服务可以有许多与之关联的路由),路由是 Kong 的入口点,并定义匹配客户端请求的规则。一旦匹配路由,Kong 就会将请求代理到其关联的服务。
路由实体定义规则以匹配客户端的请求。每个 Route 与一个 Service 相关联,一个服务可能有多个与之关联的路由,与给定路由匹配的每个请求都将代理到其关联的 Service 上。可以配置的字段有:
Service 和 Route 的组合(以及它们之间的关注点分离)提供了一种强大的路由机制,通过它可以在Kong 中定义细粒度的入口点,从而使基础架构路由到不同上游服务。
Consumer 对象表示服务的使用者或者用户,你可以依靠 Kong 作为主数据库存储,也可以将使用者列表与数据库映射,以保持Kong 与现有的主数据存储之间的一致性。
插件实体表示将在 HTTP请求/响应生命周期 期间执行的插件配置。它是为在 Kong 后面运行的服务添加功能的,例如身份验证或速率限制。
将插件配置添加到服务时,客户端向该服务发出的每个请求都将运行所述插件。如果某个特定消费者需要将插件调整为不同的值,你可以通过创建一个单独的插件实例,通过 service 和 consumer 字段指定服务和消费者。
配置服务
# 添加服务
curl -i -X POST http://192.168.64.181:8001/services/ -d 'name=test-service' -d 'url=http://116.62.213.90/1.html'
url 参数是一个简化参数,用于一次性添加 protocol,host,port 和 path。
添加路由
# 添加service 的路由
curl -i -X POST http://192.168.64.181:8001/routes/ -d 'methods=GET' -d 'paths=/service' -d 'service.id=65c11356-e86d-431e-a76a-aa7c3acd9aeb'
访问测试
# 通过如下URL访问
http://192.168.64.181:8000/service?a=123
配置 upstream 和 target
创建一个名称 load 的 upstream
curl -X POST http://192.168.64.181:8001/upstreams --data "name=load"
添加3000 端口的负载
curl -X POST http://192.168.64.181:8001/upstreams/load/targets --data "target=192.168.64.1:9001" --data "weight=10"
curl -X POST http://192.168.64.181:8001/upstreams/load/targets --data "target=192.168.64.1:9002" --data "weight=10"
如上的配置对应了 Nginx 的配置
upstream load {
server 192.168.64.181:9001 weight=10;
server 192.168.64.181:9002 weight=10;
}
配置service
host 的值便对应了 upstream 的名称,配置成功后会返回生成的 service 的 id
curl -X POST http://192.168.64.181:8001/services --data "name=load-service" -- data "host=load"
配置路由信息
这里的service.id 就是上面添加service
curl -X POST http://192.168.64.181:8001/routes --data "paths[]=/load" --data "service.id=3a6b839b-00c1-49b7-b271-8024a12d19be"
访问测试
http://192.168.64.181:8000/load/index
Kong 企业版提供了管理UI,开源版本是没有的。但是有很多的开源的管理 UI ,其中比较好用的是Konga。项目地址:https://github.com/pantsel/konga
使用 docker run --rm 来初始化数据库,该命令执行后会退出容器而保留内部的数据卷(volume)。
docker run --name konga --rm \
--network=kong-net \
pantsel/konga -c prepare -a postgres -u postgresql://kong:kong@kong-
database:5432/kong
docker run -p 1337:1337 -d \
--network=kong-net \
-e "DB_ADAPTER=postgres" \
-e "DB_HOST=kong-database" \
-e "DB_USER=kong" \
-e "DB_PASSWORD=kong" \
-e "DB_DATABASE=kong" \
-e "KONGA_HOOK_TIMEOUT=120000" \
-e "DB_PG_SCHEMA=public" \
-e "NODE_ENV=production" \
--name konga \
pantsel/konga
通过浏览器请求 http://IP:1337 检查是否能够访问,如果能够访问说明已经启动成功
在这个界面添加一个用户 登录就可以看到如下页面
点击Connections 来填加kong的信息
添加kong名称以及API地址就可以管理kong了
添加后点击’active’激活按钮就可以激活kong网关
然后就会出现如下界面管理可以通过菜单操作管理kong
限流的场景:服务提供的能力是有限的。为了防止大量的请求将服务拖垮,可以通过网关对服务的请求做限流。例如:某个服务1s只能处理100个请求,超过限流阈值的请求丢弃。
路由配置限流
为/load的路由添加限流插件
设置限流为每分钟2个
测试验证
多次访问 http://192.168.64.150:8000/load 发现已经被限流了
配置属性如下