高性能Web网关:OpenResty 基础讲解

一:概述

OpenResty是由国人章亦春开发的一个基于Nginx的可伸缩的Web平台。

        openresty 是一个基于 nginx 与 lua 的高性能 web 平台,其内部集成了大量精良的 lua 库、第三方模块以及大数的依赖项。用于方便搭建能够处理超高并发、扩展性极高的动态 web 应用、web 服务和动态网关。
        openresty 通过汇聚各种设计精良的 nginx 模块,从而将 nginx有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。
        openresty 的目标是让你的 Web 服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型(多reactor 模型),不仅仅对 HTTP 客户端请求(stream),甚至于对远程后端诸如MySQL、PostgreSQL、Memcached 以及 Redis etcd kafka grpc等都进行一致的高性能响应(upstream)。

让我们先复习一下Nginx

Nginx采用的是master-worker模型,也就是一个master进程管理多个worker进程,基本的时间处理都放在worker进程中,master进程负责全局初始化以及对worker进行的管理。

OpenResty中,每个worker进程使用一个LuaVM,当请求被分配到worker时,将在这个LuaVM中创建一个coroutine协程,协程之间数据隔离,每个协程都具有独立的全局变量。

二:应用场景

在请求真正到达上游服务之前,Lua 可以随心所欲的做复杂的访问控制和安全检测;

随心所欲的操控响应头里面的信息;

从外部存储服务(比如 Redis,Memcached,MySQL,Postgres)中获取后端信息,并用这些信息来实时选择哪一个后端来完成业务访问;

在内容 handler 中随意编写复杂的 Web 应用,使用同步但依然非阻塞的方式,访问后端数据库和其他存储;

在 rewrite 阶段,通过 Lua 完成非常复杂的 URL dispatch用 Lua 可以为 nginx 子请求和任意 location,实现高级缓存机制;

三:Nginx的模块

        nginx 采用模块化设计,使得每一个 http 模块可以仅专注于完成一个独立的、简单的功能,而一个请求的完整处理过程可以由无数个 http 模块共同合作完成。为了灵活有效地指定下一个http 处理模块是哪一个;http 框架依据常见的的处理流程将处理阶段划分为 11 个阶段,其中每一个阶段都可以由任意多个http 模块流水式地处理请求。
        openresty 将 lua 脚本嵌入到 nginx 阶段处理的末尾模块下;这样以来并不会影响 nginx 原有的功能,而是在 nginx 基础上丰富它的功能;
        嵌入 lua 的优点是:使用 openresty 开发,不需要重新编译,直接修改 lua 脚本,重新启动即可

Lua模块的指令顺序

init_by_lua:在 nginx 重新加载配置文件时,运行里面 lua 脚本,常用于全局变量的申请。例如 lua_shared_dict 共享内存的申请,只有当 nginx 重启后,共享内存数据才清空,这常用于统计。

set_by_lua:设置一个变量,常用与计算一个逻辑,然后返回结果,该阶段不能运行Output API、Control API、Subrequest API、Cosocket API

rewrite_by_lua:在 access 阶段前运行,主要用于 rewrite url;

access_by_lua:主要用于访问控制,这条指令运行于 nginx access 阶段的末尾,因此总是在 allow 和 deny 这样的指令之后运行,它们同属 access 阶段。可用来判断请求是否具备访问权限;

content_by_lua:阶段是所有请求处理阶段中最为重要的一个,运行在这个阶段的配置指令一般都肩负着生成内容(content)并输出HTTP 响应。

header_filter_by_lua:一般只用于设置 Cookie 和 Headers 等。

body_filter_by_lua:一般会在一次请求中被调用多次,因为这是实现基于 HTTP1.1 chunked 编码的所谓“流式输出”的。

log_by_lua:该阶段总是运行在请求结束的时候,用于请求的后续操作,如在共享内存中进行统计数据,如果要高精确的数据统计,应该使用 body_filter_by_lua

Lua嵌入的原理:

cosocket:

openresty 为 nginx 添加的最核心的功能就是 cosocket;自cosocket 加入,可以在 http 请求处理中访问第三方服务;
cosocket 主要依据 nginx 中的事件机制(reactor)和 lua 的协程结合后实现了非阻塞网络 io;在业务逻辑使用层面上可以通过同步非阻塞的方式来写代码;

        对于reactor中新的连接进来,由于是非阻塞的,刚连接进来并不知道是否是连接成功,他会一直到添加到epoll红黑树中去,并注册写事件,当触发写事件,我们才知道客户端连接成功了。而对于lua的协程来说,他是同步的,会一直阻塞等待连接成功。因此将这俩一结合,实现了非阻塞的网络IO。当代码运行到阻塞连接的时候,lua协程会让出操作,然后运行其他程序,当连接成功,会再次恢复操作。这样就形成同步非阻塞的方式了。

        引入 cosocket 后,nginx 中相当于有了多条并行同步逻辑线(lua 协程),nginx 中单线程负责唤醒或让出其中 lua 协程;唤醒或让出依据来源于协程运行的条件是否得到满足;

四:实现重定向和黑名单

1:nginx.conf

openresty -p . -c conf/nginx.conf        //启动
openresty -p . -s stop                    //关闭

        在下面的代码实现中一直遵循上面的11个阶段执行 。当碰到启动不了openresty的时候,多看看他的出错日志即可。

         我们在这个conf文件中实现了黑名单功能,以及重新定向功能。我们在location / 中实现了最基础的黑名单功能,他的局限就在于,每次添加黑名单就需要我们重新启动openresty,十分不方便。并且在这里还实现了重新定向的功能,跳到百度或者自己写的代码中。

worker_processes  4;events{worker_connections  1024;
}http{error_log ./logs/error.log info;            #在这里我们需要自己手动创建logslua_shared_dict bklist 1m;                      #由于每次都连接数据库很浪费资源,于是我们使用共享内存,这里使用的是字典init_worker_by_lua_file ./appp/init_worker.lua;  #我们在启动openrest的时候,把他在init_worker的时候加入进去。server{listen 8989;location / {rewrite_by_lua_block {          --执行重定向功能local args = ngx.req.get_uri_args()     -- 获取参数if args["jump"] == "1" thenreturn ngx.redirect("http://baidu.com")elseif args["jump"]=="2" thenreturn ngx.redirect("/jump_here")       -- 重定向到/jump_hereend}access_by_lua_block {           -- 访问控制local black_list = {                --创建黑名单,但是在这里我们每次添加新的黑名单["192.168.137.1"] = true        --就需要重新启动,很麻烦,因此我们选择使用数据库-redis}if black_list[ngx.var.remote_addr] thenngx.exit(403)end}content_by_lua_block {          -- 内容生成器ngx.say("hello", "\t", ngx.var.remote_addr)}}location /jump_here{content_by_lua_block {ngx.say("jump_here", "\t", ngx.var.remote_addr)}body_filter_by_lua_block {          --修改body的内容local chunk = ngx.arg[1]ngx.arg[1] = chunk:gsub("jump_here", "xinqingpu")}log_by_lua_block {local request_method = ngx.var.request_methodlocal request_uri = ngx.var.request_urilocal msg = string.format("request_method:%s\trequest_uri:%s", request_method, request_uri)ngx.log(ngx.INFO,msg)        --输出到日志中去}}location /back1 {access_by_lua_file ./appp/back1.lua;   --加载我们自己写的文件:黑名单文件content_by_lua_block {ngx.say("back1", "\t", ngx.var.remote_addr)}}location /back2 {access_by_lua_file ./appp/back2.lua;  content_by_lua_block {ngx.say("back2", "\t", ngx.var.remote_addr)}}}
}

2:完善黑名单的功能

        由于上面的黑名单十分垃圾,所以我们采用数据库:redis的方式。我们将黑名单存储到数据库中,这样我们就不需要重新启动了,但是还有一个问题,就是我们每次访问都需要加载数据库,会很浪费时间,以及影响性能。

--这个模块是使用Redis的,这样只需要我们将黑名单的参数添加到redis中即可,不用重启了
local redis = require "resty.redis" --加载Redis的模块local red = redis:new() --创建一个Redis对象local ok ,err = red:connect("127.0.0.1",6379) --连接Redis
if not ok thenreturn ngx.exit(301)        --exit里面的参数只要大于200,就会被nginx认为是错误的,会打断全部的断点。
end                             --而小于200只会打断当前所在的断点。
--断点就是跳出当前的运行位置,如果全部打断,那就打断全部的阶段,打断一个的话,跳出当前的阶段进入到下一个中,因为有11个阶段
local ip = ngx.var.remote_addrlocal exists,err = red:sismember("black_list",ip)  --访问哪个表,传入参数if exists == 1 then             --在黑名单中return ngx.exit(403)
end

3:使用共享内存完美解决

        因为nginx运行在共享内存中,所以我们把数据也存到共享内存中,提高速度,并减少redis的访问次数。

--这个模块在开始的时候就加载了
if ngx.worker.id() ~= 0 then        --在这里我们要先判断worker的数量,因为在这个位置我们还没将master进行fork,所以work的数量为0return                            --我们指的当前位置是在11个阶段当中的位置
endlocal redis = require "resty.redis"
local bklist = ngx.shared.bklist                --加载一下共享内存local function update_blacklist()               --更新黑名单,我们这里是使用共享内存访问redis,每几秒刷新一下。local red = redis:new()local ok, err = red:connect("127.0.0.1", 6379)if not ok thenreturnendlocal black_list,err = red:smembers("black_list")bklist:flush_all()                          --先清空一下共享内存for _, v in pairs(black_list) do            --加载共享内存bklist:set(v, true)endngx.timer.at(5, update_blacklist)           --每5秒刷新一次,将此函数加载到ngx的定时器中。
endngx.timer.at(5, update_blacklist)               --开始调用

 我们调用的是下面的函数库,上面的是用来初始化所有的worker进程的。


local bklist = ngx.shared.bklistlocal ip = ngx.var.remote_addrif bklist:get(ip) then          --如果我们的ip被查到在黑名单中就报错return ngx.exit(403)
end

 五:实现负载均衡

        我们还可以使用openresty来实现负载均衡,其中我们先使用最简单的方式,也就是手写需要转发的ip地址,然后下面需要我们使用lua代码动态加载服务器或者转发的地址。

worker_processes 4;events {worker_connections 1024;
}http {error_log ./logs/error.log info;upstream ups {server 192.168.137.132:8100;        --他的后面还是别的服务器server 192.168.137.132:8200;server 192.168.137.132:8300;}server {listen 8999;location / {proxy_pass http://ups;}}
}#下面全都是tcp裸连接
stream {upstream tcp_ups {server 127.0.0.1:8888;        --我们可以把我们需要连接的服务器,放在这里,我们通过tcp进行连接这里,会连接到我们需要的服务器。}server {listen 9000;proxy_pass tcp_ups;proxy_protocol on;            --我们采用这个,实现ip地址的保留}#为了更加方便的添加转发服务器,我们将服务器放到数据库中server {listen 9001;content_by_lua_file ./app/proxy.lua;    --这里是动态加载我们的服务器}
}

 下面的代码是我们转发过来的地址,我们会接收转发过来的ip地址。我们会发现ip地址丢失了。这里的丢失是我们客户端的ip丢失了。

worker_processes 4;events {worker_connections 1024;
}http {error_log ./logs/error2.log info;server {listen 8100;location / {content_by_lua_block {local headers = ngx.req.get_headers()local cjson = require "cjson"ngx.say(cjson.encode(headers))ngx.say(8100,"\t",ngx.var.remote_addr)}}}server {listen 8200;location / {content_by_lua_block {local headers = ngx.req.get_headers()local cjson = require "cjson"ngx.say(cjson.encode(headers))ngx.say(8200,"\t",ngx.var.remote_addr)}}}server {listen 8300;location / {content_by_lua_block {local headers = ngx.req.get_headers()local cjson = require "cjson"ngx.say(cjson.encode(headers))ngx.say(8300,"\t",ngx.var.remote_addr)}}}}

        下面就是我们lua的代码了,我们可以动态的加载不同的服务器了,不需要我们手动写道openresty中了,并且我们通过这个代码,还可以做到保留原客户端的ip地址不丢失。

--这里的nginx既有服务端也有客户端的性质,所以有两个套接字,一个用于连接redis,一个用于接收客户端的数据
local sock, err = ngx.req.socket()          --创建nginx的套接字,用来连接redisif err thenngx.log(ngx.INFO, err)
endlocal upsock, okupsock = ngx.socket.tcp()                   --创建客户端连接上来的套接字,客户端是tcp,所以这里也是tcp的套接字ok, err = upsock:connect("127.0.0.1", 8989)     --这里是redis的端口,通过tcp套接字来连接这个redis。
if not ok thenngx.log(ngx.INFO, "connect error:"..err)
endupsock:send(ngx.var.remote_addr .. '\n')            --由于会丢失ip,我们在这里写下客户端源地址,并发送给redislocal function handle_upstream()local datafor i=1, 1000 dolocal reader = upsock:receiveuntil("\n", {inclusive = true})        --全都是存的服务器data, err, _ = reader()if err thensock:close()upsock:close()returnendsock:send(data)         --我们从redis中获取一个服务器,然后我们向这个服务器发送数据。      end
endngx.thread.spawn(handle_upstream)       --创建线程,后台运行这个函数local datawhile true dolocal reader = sock:receiveuntil("\n", {inclusive = true})data, err, _ = reader()if err thensock:close()upsock:close()returnendupsock:send(data)               --这里是一直接收客户端连接nginx的数据,然后再将收到的数据再发送给客户端。回报文。
end
--实现了一个简单的代理功能,用于在 Nginx 服务器和 Redis 服务器之间转发数据。它使用了 Nginx 的 Lua 模块和 Lua 的套接字库来实现网络通信。

        本文讲解的是Openresty,其中主要是Lua的使用,重新定向,黑名单,负载均衡,对于我本人来说,对于nginx写一些模块并没有使用openresty方便,因为nginx需要使用c语言来写,然后加载动态库等等,它可以直接使用lua进行编程,十分方便。0voice · GitHub

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/60333.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OceanBase JDBC (Java数据库连接)的概念、分类与兼容性

本章将介绍 OceanBase JDBC的 概念与分类,已帮助使用 JDBC 的用户及技术人员更好的 了解JDBC,以及 OceanBase JDBC在与 MySQL 及 Oracle 兼容性方面的相关能力。 一、JDBC 基础 1.1 JDBC 的概念 JDBC 一般指 Java 数据库连接。Java 数据库连接&#xf…

小程序中引入下载到本地的iconfont字体图标加载不出来问题解决

我这个是uniapp项目,字体图标都是一样的,在vue项目中web端、uniapp运行到h5都没问题,但是运行到小程序加载不出来,报错如下: 不让用本地路径,所以我们要转为base64编码,这里给大家提供一个工具,它可以把本地字体文件转为base64:transfonter 进入官网后,第一步: …

如何在 Ubuntu 24.04 上安装和配置 Fail2ban ?

确保你的 Ubuntu 24.04 服务器的安全是至关重要的,特别是如果它暴露在互联网上。一个常见的威胁是未经授权的访问尝试,特别是通过 SSH。Fail2ban 是一个强大的工具,可以通过自动阻止可疑活动来帮助保护您的服务器。 在本指南中,我…

多商户中英双语电商系统设计与开发 PHP+mysql

随着全球电商市场的扩展,多商户平台成为了越来越多商家参与全球贸易的重要方式。为了适应不同语言用户的需求,尤其是中英双语用户的需求,设计一个支持中英双语的电商系统显得尤为重要。本文将重点探讨如何设计一个多商户中英双语电商系统&…

关于 3D Engine Design for Virtual Globes(三维数字地球引擎设计)

《3D Engine Design for Virtual Globes》是一本专注于三维虚拟地球引擎设计的专业书籍。这本书由Patrick Cozzi和Kevin Ring编写,覆盖了设计适用于虚拟球面环境的三维引擎的各个方面。虚拟地球引擎作为地理信息系统(GIS)中的一个核心组件&am…

单元测试、集成测试、系统测试有什么区别

🍅 点击文末小卡片 ,免费获取软件测试全套资料,资料在手,涨薪更快 单元测试、集成测试、系统测试有什么区别 1、粒度不同 集成测试bai粒度居中,单元测试粒度最小,系统du测试粒度最大。 2、测试方式不同…

CE2.【C++ Cont】练习题组2

1.数字反转 https://www.luogu.com.cn/problem/P5705 题目描述 输入一个不小于 100100 且小于 10001000,同时包括小数点后一位的一个浮点数,例如 123.4123.4 ,要求把这个数字翻转过来,变成 4.3214.321 并输出。 输入格式 一行一个…

Golang | Leetcode Golang题解之第557题反转字符串中的单词III

题目&#xff1a; 题解&#xff1a; func reverseWords(s string) string {length : len(s)ret : []byte{}for i : 0; i < length; {start : ifor i < length && s[i] ! {i}for p : start; p < i; p {ret append(ret, s[start i - 1 - p])}for i < le…

适合二开的web组态?

一、web组态的定义和背景 在深入探讨之前&#xff0c;我们先回顾一下“组态”的定义。在工业自动化领域&#xff0c;组态软件是用于创建监控和数据采集&#xff08;SCADA&#xff09;系统的工具&#xff0c;它允许工程师构建图形界面&#xff0c;实现与各种设备和机器的数据交互…

【大数据学习 | HBASE】hbase的读数据流程与hbase读取数据

1. hbase的读数据流程 在解析读取流程之前我们还需要知道两个功能性的组件和HFIle的格式信息 HFILE 存储在hdfs中的hbase文件&#xff0c;这个文件中会存在hbase中的数据以kv类型显示&#xff0c;同时还会存在hbase的元数据信息&#xff0c;包括整个hfile文件的索引大小&…

MySQL 忘记 root 密码,使用跳过密码验证进行登录

MySQL 忘记 root 密码&#xff0c;使用跳过密码验证进行登录 修改 /etc/my.cnf 配置文件&#xff0c;在 [mysqld] 后面任意一行添加 skip-grant-tables vim /etc/my.cnf 重启 MySQL systemctl restart mysqld 登录 MySQL&#xff08;无 -p 选项&#xff0c;无需密码登录&…

[Linux]IO多路转接(上)

1. IO 多路转接之select 1.1 select概述 select 是系统提供的一个多路转接接口&#xff0c;其核心工作在于等待。它能够让程序同时监视多个文件描述符上的事件是否就绪&#xff0c;只有当被监视的多个文件描述符中有一个或多个事件就绪时&#xff0c;select 才会成功返回&…

推荐一款电脑清理和加速工具:Wise Care 365 Pro

Wise Care 365 Pro是一款可以清理注册表和磁盘垃圾文件&#xff0c;保护个人隐私记录&#xff0c;提高电脑使用安全的软件&#xff0c;是优化系统、提高Windows系统运行速度最好的选择!实时保护注册表不被其他程序未经许可地秘密修改。例如阻止程序更改您的浏览器主页&#xff…

Hook小程序

下载&#xff1a; https://github.com/JaveleyQAQ/WeChatOpenDevTools-Python 配置&#xff1a; pip install -r requirements 实现&#xff1a; 开启小程序开发者模式&#xff0c;类似浏览器F12 效果&#xff1a; 使用&#xff1a; 退出微信&#xff0c;进入安装的目录…

Mysql 8迁移到达梦DM8遇到的报错

在实战迁移时&#xff0c;遇到两个报错。 一、列[tag]长度超出定义 在mysql中&#xff0c;tag字段的长度是varchar(20)&#xff0c;在迁移到DM8后&#xff0c;这个长度不够用了。怎么解决&#xff1f; 在迁移过程中&#xff0c;“指定对象”时&#xff0c;选择转换。 在“列映…

PyQt5 在线环境搭建

1.记得升级pip&#xff0c;升级后就会一路畅通的安装&#xff0c;无论是在终端通过命令安装&#xff0c;还是在pycharm中&#xff0c;通过设置&#xff0c; python -m pip install --upgrade pip 升级后安装pyqt5,pyqt5-tools,pyqt5-stubs。 命令安装 pycharm->设置&…

第8章 利用CSS制作导航菜单

8.1 水平顶部导航栏 水平莱单导航栏是网站设计中应用范围最广的导航设计&#xff0c;一般放置在页面的顶部。水平 导航适用性强&#xff0c;几乎所有类型的网站都可以使用&#xff0c;设计难度较低。 如果导航过于普通&#xff0c;无法容纳复杂的信息结构&#xff0c;就需要在…

将数据上传至hdfs的两种方式:java代码上传、将数据放入kafka中,通过flume抽取

目录 1、 生成一条&#xff0c;使用 java 代码将数据放入hdfs上传。 2、 生成一条&#xff0c;编写kafka生产者&#xff0c;将数据放入kafka。kafka source-->flume -->hdfs sink 场景题&#xff1a; 使用 java 代码随机生成学生信息&#xff0c;学生的学号从 0001 开…

【vue】echarts地图添加蒙版图片,多图层地图实现天气信息展示

实现原理&#xff1a;多层图层叠加实现复杂的信息展示。 <template><div class"wrapper"><el-drawertitle"天气信息":modal"iszz":visible.sync"weatherinfo":direction"direction"><drawer:labelnam…

100+SCI科研绘图系列教程(R和python)

科研绘图系列&#xff1a;箱线图加百分比点图展示组间差异-CSDN博客科研绘图系列&#xff1a;箱线图加蜜蜂图展示组间数据分布-CSDN博客科研绘图系列&#xff1a;小提琴图和双侧小提琴图展示组间差异-CSDN博客科研绘图系列&#xff1a;组间差异的STAMP图的ggplot2实现-CSDN博客…