接下来我们试试从yaml文件中读取配置,当然这不是动态配置分发的好方式

别急,一口吃不成大胖子

这里其实会为大家介绍不少东西:

  • 如何引入第三方库以及配置openresty
  • lua 文件读取
  • yaml 库
  • openresty init_worker
  • openresty timer
  • privileged agent
  • 代码触发 openresty 热重启

创建yaml配置文件

手动创建conf.yaml文件

$ touch conf.yaml

以下内容写入 conf.yaml

upstream:
a8699:
host: '127.0.0.1'
port: 6699

非常简单地一个upstream配置, 如果大家配置都是这样简单,是不是nginx用起来会舒服(不过现实总是那么不简单)

接下来我们来实现如何读取它呢

引入yaml库

我们将使用 luarocks 这个lua包管理工具,其介绍在luarocks介绍,这里不再赘述,如不了解请一定要查阅。

首先我们手动创建luarocks文件

$ touch openresty-dev-1.rockspec

以下内容写入 openresty-dev-1.rockspec

package = "openresty"
version = "dev-1" -- 自己编写第三方库时,如果希望别人远端仓库下载当前源码编译可以配置 source ,比如下git的配置
source = {
url = "git+ssh://git@github.com:fs7744/nature.git",
branch = "main",
}
-- 包描述
description = {
homepage = "https://github.com/fs7744/nature",
maintainer = "Victor.X.Qu"
} -- 依赖包
dependencies = {
"lua-tinyyaml >= 1.0",
} -- 当前包如何编译,这里列举openresty常遇见的一些参数
build = {
type = "make",
build_variables = {
CFLAGS="$(CFLAGS)",
LIBFLAG="$(LIBFLAG)",
LUA_LIBDIR="$(LUA_LIBDIR)",
LUA_BINDIR="$(LUA_BINDIR)",
LUA_INCDIR="$(LUA_INCDIR)",
LUA="$(LUA)",
OPENSSL_INCDIR="$(OPENSSL_INCDIR)",
OPENSSL_LIBDIR="$(OPENSSL_LIBDIR)",
},
install_variables = {
INST_PREFIX="$(PREFIX)",
INST_BINDIR="$(BINDIR)",
INST_LIBDIR="$(LIBDIR)",
INST_LUADIR="$(LUADIR)",
INST_CONFDIR="$(CONFDIR)",
},
}

这里不使用 luarocks init是因为其默认项目结构包含了太多我们不需要的东西,为了不干扰大家,就说明了。

为了方便大家测试,我们将yaml 依赖包安装到当前目录下

$ luarocks install openresty-dev-1.rockspec --tree=deps --only-deps
# 得到以下执行结果
Missing dependencies for openresty dev-1:
lua-tinyyaml >= 1.0 (not installed) openresty dev-1 depends on lua-tinyyaml >= 1.0 (not installed)
Installing https://luarocks.org/lua-tinyyaml-1.0-0.rockspec
Cloning into 'lua-tinyyaml'...
remote: Enumerating objects: 13, done.
remote: Counting objects: 100% (13/13), done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 13 (delta 0), reused 8 (delta 0), pack-reused 0
Unpacking objects: 100% (13/13), 10.32 KiB | 330.00 KiB/s, done.
Note: switching to 'b130c2b375d4560d5b812e1a9c3a60b2a0338d65'. You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch. If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example: git switch -c <new-branch-name> Or undo this operation with: git switch - Turn off this advice by setting config variable advice.detachedHead to false No existing manifest. Attempting to rebuild...
lua-tinyyaml 1.0-0 is now installed in /root/openresty-test/deps (license: MIT License) Stopping after installing dependencies for openresty dev-1 # 查看目录可以看到完整的包结构
$ tree deps/
deps/
├── lib
│ └── luarocks
│ └── rocks-5.1
│ ├── lua-tinyyaml
│ │ └── 1.0-0
│ │ ├── doc
│ │ │ ├── LICENSE
│ │ │ └── README.md
│ │ ├── lua-tinyyaml-1.0-0.rockspec
│ │ └── rock_manifest
│ └── manifest
└── share
└── lua
└── 5.1
└── tinyyaml.lua

在openresty中引入当前目录依赖包

大家可以基于上一节中的配置文件修改,(这来不列举完整的内容,避免篇幅过长)

在其中添加如下配置

# 在 stream 下面添加
stream { # $prefix 为 启动openresty 时的参数 -p prefix
# 下面比较长的路径是将各种情况列举处理,基本满足了一般liunx环境下依赖包搜索的大部分可能性
lua_package_path "$prefix/deps/share/lua/5.1/?.lua;$prefix/deps/share/lua/5.1/?/init.lua;$prefix/?.lua;$prefix/?/init.lua;;./?.lua;/usr/local/openresty/luajit/share/luajit-2.1.0-beta3/?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/openresty/luajit/share/lua/5.1/?.lua;/usr/local/openresty/luajit/share/lua/5.1/?/init.lua;";
lua_package_cpath "$prefix/deps/lib64/lua/5.1/?.so;$prefix/deps/lib/lua/5.1/?.so;;./?.so;/usr/local/lib/lua/5.1/?.so;/usr/local/openresty/luajit/lib/lua/5.1/?.so;/usr/local/lib/lua/5.1/loadall.so;";
# 开启 lua code 缓存
lua_code_cache on;
}

在init 时机读取yaml配置

stream {
init_by_lua_block {
-- 文件读取比较长,这里用函数避免影响大家理解
local function read_all(path)
local open = io.open
local close = io.close
local file, data, err
file, err = open(path, "rb")
if not file then
return nil, "open: " .. path .. " with error: " .. err
end data, err = file:read("*all")
if err ~= nil then
file:close()
return nil, "read: " .. path .. " with error: " .. err
end file:close()
return data
end local yaml_path = ngx.config.prefix()..'/conf.yaml'
-- 读取 yaml 文件
local content, err = read_all(yaml_path)
-- 转换yaml数据为lua table结构
local yaml = require("tinyyaml")
local conf = yaml.parse(content)
-- 设置到全局变量中
upstreams = conf.upstream
} server {
preread_by_lua_block {
-- 这里故意加了个a前缀以区分之前的测试代码
local upstream = upstreams['a'..tostring(ngx.var.server_port)]
}
}
}

启动服务并测试,与之前保持一样效果

$ openresty -p ~/openresty-test -c openresty.conf #启动
$ curl http://localhost:8699 -i #测试
HTTP/1.1 200 OK
Date: Fri, 16 Dec 2022 05:19:34 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive HelloWorld
$ curl http://localhost:8689 -i #测试没有upstrem的情况
curl: (56) Recv failure: Connection reset by peer

如何监控yaml文件变动呢?

大家都知道我们不可能像指挥家里小朋友帮我们打酱油一样随意指挥系统在文件变化时通知我们的程序。

不过我们可以安排我们的程序定时检查文件是否被修改了

定时任务api

openresty 里面 可以做定时任务的api 为 ngx.timer.every

我们试一试

stream {
init_by_lua_block {
ngx.timer.every(1, function()
ngx.log(ngx.ERR, 'hello ngx.timer.every')
end)
}

启动服务并测试

$ openresty -p ~/openresty-test -c openresty.conf -g 'daemon off;' #启动
nginx: [error] init_by_lua error: init_by_lua:31: no request
stack traceback:
[C]: in function 'every'
init_by_lua:31: in main chunk
# 很不幸,报错了

为什么呢?

其实大家在查阅 openresty api 文档时,总是会看到有很长一个 context 列表,里面全是一些执行阶段

ngx.timer.every


syntax: hdl, err = ngx.timer.every(delay, callback, user_arg1, user_arg2, …)

context: init_worker_by_lua*, set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*, ssl_client_hello_by_lua*

Similar to the ngx.timer.at API function, but

…..

这是openresty以及nginx设计带来的限制,只允许在对应执行阶段运行相应的api

简单来说:

为了高性能,openresty 利用nginx的事件通知机制 + 协程 避免了自身执行的阻塞,

但是大家写代码总是喜欢同步阻塞的思路,为了减少你等我,我等你等死锁情况以及nginx在一些宝贵的执行阶段对资源、时间非常“小气”,所以限制了很多api的使用

比如 init 阶段, nginx的事件通知机制并未完全建立,所以很多api无法使用

总之这些限制出于性能与安全的考虑。(详细可以参考openresty)

我们换到 init_worker

stream {
init_worker_by_lua_block {
ngx.timer.every(1, function()
ngx.log(ngx.ERR, 'hello ngx.timer.every')
end)
}

启动服务

$ openresty -p ~/openresty-test -c openresty.conf #启动
$ cat logs/error.log
# 可以看到每秒都有两个执行log,这由于我们启动了2个worker,所以init_worker_by_lua_block被执行了两次
2022/12/17 15:55:45 [error] 2784#2784: *19 stream [lua] init_worker_by_lua:3: hello ngx.timer.every, context: ngx.timer
2022/12/17 15:55:45 [error] 2785#2785: *20 stream [lua] init_worker_by_lua:3: hello ngx.timer.every, context: ngx.timer
2022/12/17 15:55:46 [error] 2784#2784: *21 stream [lua] init_worker_by_lua:3: hello ngx.timer.every, context: ngx.timer
2022/12/17 15:55:46 [error] 2785#2785: *22 stream [lua] init_worker_by_lua:3: hello ngx.timer.every, context: ngx.timer

如果我们想只有一个 worker 处理,我们也可以通过 判断ngx.worker.id()实现,

不过更加推荐以下的特权进程方式

特权进程

特权进程 是为了不影响 worker 处理用户真实请求,而大家总有额外处理其他东西的需求,比如我们这里的监听文件变化,不需要每个worker都监听

特权进程很特别:

它不监听任何端口,这就意味着不会对外提供任何服务;

它拥有和 master 进程一样的权限,一般来说是 root 用户的权限,这就让它可以做很多 worker 进程不可能完成的任务;

特权进程只能在 init_by_lua 上下文中开启;

我们改动一下代码

stream {
init_by_lua_block {
-- 开启特权进程
local process = require("ngx.process")
local ok, err = process.enable_privileged_agent()
if not ok then
log.error("failed to enable privileged_agent: ", err)
end
} init_worker_by_lua_block {
-- 限制只允许特权进程执行 hello ngx.timer.every
if require("ngx.process").type() ~= "privileged agent" then
return
end
ngx.timer.every(1, function()
ngx.log(ngx.ERR, 'hello ngx.timer.every')
end)
}
}

启动服务

$ openresty -p ~/openresty-test -c openresty.conf #启动
$ cat logs/error.log
# 可以看到只有每秒一个执行log
2022/12/17 16:13:50 [error] 3094#3094: *7 stream [lua] init_worker_by_lua:7: hello ngx.timer.every, context: ngx.timer
2022/12/17 16:13:51 [error] 3094#3094: *8 stream [lua] init_worker_by_lua:7: hello ngx.timer.every, context: ngx.timer
2022/12/17 16:13:52 [error] 3094#3094: *9 stream [lua] init_worker_by_lua:7: hello ngx.timer.every, context: ngx.timer

监听文件变化

实现如下

stream {
init_worker_by_lua_block {
-- 限制只允许特权进程执行 文件监听
if require("ngx.process").type() ~= "privileged agent" then
return
end
-- 对比变化占位变量
local yaml_change_time = nil
ngx.timer.every(1, function()
-- 获取文件属性
local lfs = require("lfs")
local yaml_path = ngx.config.prefix()..'/conf.yaml'
local attributes, err = lfs.attributes(yaml_path)
if not attributes then
ngx.log(ngx.ERR, "failed to fetch ", yaml_path, " attributes: ", err)
return
end
-- 对比变化时间
local last_change_time = attributes.change
if yaml_change_time == last_change_time then
return
end
yaml_change_time = last_change_time
ngx.log(ngx.ERR, yaml_path,' change at ', yaml_change_time)
end)
}
}

启动服务

$ openresty -p ~/openresty-test -c openresty.conf #启动
# 我们修改几次conf.yaml文件后
$ cat logs/error.log
# 可以看到每次变化时都有log输出
2022/12/17 16:21:11 [error] 3211#3211: *4 stream [lua] init_worker_by_lua:23: /root/openresty-test//conf.yaml change at 1671247838, context: ngx.timer
2022/12/17 16:21:21 [error] 3211#3211: *14 stream [lua] init_worker_by_lua:23: /root/openresty-test//conf.yaml change at 1671265280, context: ngx.timer

如何通知配置变化呢?

由于我们监听yaml文件是在特权进程,无法直接更新worker进程内的upstream数据

当然我们可以通过一些进程间通信机制实现,

但这里篇幅关系,我们先直接最简单办法,让 openresty 热重启,重新读取配置

实现如下:

stream {
init_worker_by_lua_block {
-- 对比变化占位变量
local yaml_change_time = nil
ngx.timer.every(1, function()
-- 省略代码
yaml_change_time = last_change_time -- 发送热重启信号
local signal = require('resty.signal')
signal.kill(process.get_master_pid(), signal.signum("HUP"))
end)
}
}

在启动服务后,

直接测试,可以得到与之前相反的结果

$ curl http://localhost:8699 -i  #测试, yaml配置还有upstrem的情况
HTTP/1.1 200 OK
Date: Sat, 17 Dec 2022 08:39:35 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive HelloWorld
$ curl http://localhost:8689 -i #测试 yaml配置没有upstrem的情况
curl: (56) Recv failure: Connection reset by peer

我们修改conf.yaml

upstream:
a8689:
host: '127.0.0.1'
port: 6699

直接测试,可以得到与之前相反的结果

$ curl http://localhost:8699 -i  #测试, yaml配置已经没有upstrem的情况
curl: (56) Recv failure: Connection reset by peer
$ curl http://localhost:8689 -i #测试 yaml配置已经有upstrem的情况
HTTP/1.1 200 OK
Date: Sat, 17 Dec 2022 08:39:59 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive HelloWorld

完整的例子在: https://github.com/fs7744/nature/blob/main/docs/demo/yaml

小结

我们完成一个基本的tcp代理动态配置的实现,

也同时也简单了解了

  • 如何引入第三方库以及配置openresty
  • lua 文件读取
  • yaml 库
  • openresty init_worker
  • openresty timer
  • privileged agent
  • 代码触发 openresty 热重启

以下针对一些特别介绍,大家要想搞懂openresty,一定要阅读和理解

目录

监听yaml配置的更多相关文章

  1. 【oracle常见错误】oracle监听程序配置/“ORA-12541: TNS: 无监听程序”

    问题描述 在用PL/SQL Developer连接Oracle 11g时报错“ORA-12541: TNS: 无监听程序”,如下图所示.可以按照如下的步骤进行解决. 解决方案 监听程序配置 从开始菜单 ...

  2. ORA-12541:TNS:无监听程序 配置Oracle Myeclipse无法连接上 花费一天时间解决掉的

    背景:自己机子做oracle服务器,其他机子可以ping得通我的机子,但是jdbc就是连不上,后来用plsql连出现无监听程序.... 我昨天重新安装Oracle后,用PL/SQL Developer ...

  3. Oracle Net Manager 的使用方法(监听的配置方法)

    一,在服务端配置oracle端口 win+R  输入netca 弹出如下窗口后 选择监听程序配置,点击下一步 二.配置端口后使用Telnet工具调试端口是否联通 在命令行输入telnet 服务器ip ...

  4. Spring整合ActiveMQ及多个Queue消息监听的配置

        消息队列(MQ)越来越火,在java开发的项目也属于比较常见的技术,MQ的相关使用也成java开发人员必备的技能.笔者公司采用的MQ是ActiveMQ,且消息都是用的点对点的模式.本文记录了实 ...

  5. .net core Kestrel宿主服务器自定义监听端口配置

    在.net core的web程序中,除了可以在项目中硬编码服务器的监听端口外,还可以在外部通过json文件配置. 方法如下: 第一步:在项目中新建一个名为Hosting.json的文件.当然,文件名可 ...

  6. 多线程消息监听容器配置[ 消费者spring-kafka配置文件]

    <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.sp ...

  7. oracle 监听服务配置

    最近在red hat 6.6虚拟机上安装了Oracle 11gR2数据库,安装完毕,使用没有问题,通过主机也可以访问到虚拟机上的数据库.然而,在重新启动虚拟机后,主机无法访问到数据库,提示错误: PS ...

  8. oracle-11g-R2监听文件配置

    客户端连接oracle数据库时出现如下错误: Listener refused the connection with the following error: ORA-, TNS:listener ...

  9. 免Oracle客户端程序监听程序配置

    Oracle默认安装时,监听程序和tnsnames程序中的监听方式都是默认的localhost,但免客户端的程序是连接不上的.这时需要: 1.将listener中的(HOST = localhost) ...

  10. Windows8安装Oracle11.2.0.1-0624,附带 DBCA建库、netca创建监听、配置PLSQL、定义客户端的环境变量 NLS_LANG、定义客户端的环境变量 TNS_ADMIN01

    Windows8安装Oracle11.2.0.1                                         操作系统:Windows 8 企业版 64bit Oracle:11. ...

随机推荐

  1. 接口自动化测试复习巩固第二天,管理员后端验证和接口抓包+requests实现

    接口自动化测试第二天,需要用到的第三方库有os,openpyxl,json,pytest,requests 首选我们今天的目标是写出一个测试登录用例的脚本,这里我用的是分层设计,整个框架暂时被分为工具 ...

  2. C++ ——vector数组笔记

    vector 是 C++ 标准库中的一个动态数组容器(Sequence Container),它可以自动管理内存大小,可以在运行时根据需要动态增长或缩小.它是一个非常常用且强大的容器,用于存储一系列元 ...

  3. Java-Enum常量特定方法

    OnJava8-Enum-常量特定方法 用枚举实现责任链模式 责任链(Chain Of Responsibility)设计模式先创建了一批用于解决目标问题的不同方法,然后将它们连成一条"链& ...

  4. Springboot开发的应用为什么这么占用内存

    Springboot开发的应用为什么这么占用内存 Java的原罪 Java 程序员比 c或者是c++程序员相比轻松了很多. 不要管理繁杂的内存申请与释放,也不用担心因为忘记释放内存导致很严重的内存泄漏 ...

  5. [转帖]ssh_exporter

    https://github.com/treydock/ssh_exporter SSH exporter The SSH exporter attempts to make an SSH conne ...

  6. [转帖]linux系统主机双网卡实现路由转发问题与解决

    1. 环境 拓扑: 网卡配置: host1: 192.168.1.1/24 host2: 左eth0: 192.168.1.2/24 右eth1: 192.168.2.2/24 host3: 192. ...

  7. [转帖]webpagetest 私有化部署

    https://www.jianshu.com/p/83bd6b3473ae 介绍 webpagetest 是一款开源的 web 性能测试工具,开源地址,每个人都可在其公共实例上对自己的 web 应用 ...

  8. [转帖]20个常用的Linux工具命令

    https://www.cnblogs.com/codelogs/p/16060113.html 简介# 网上有很多辅助开发的小工具,如base64,md5之类的,但这些小工具其实基本都可以用Linu ...

  9. uni-app三目运算 uni-app监听属性

    三目运算 <text>{{mag>10 ? '优秀' : ""}}</text> 三目运算的高级用法 大于1000用kg表示 小于1000,用g表示 ...

  10. package.json中^,~的详细说明

    场景描述 在package.json这个文件中,我们经常可以看见这样的信息 但是我们很少注意的是 版本前面的 ^ 到底是什么意思 今天我们就来讲一下(端好小板凳) "dependencies ...