OpenResty中的upstream healthcheck功能沉思录

2024-03-18 15:08

本文主要是介绍OpenResty中的upstream healthcheck功能沉思录,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

综述

healthcheck功能本质上还是个定时器,去定期检查指定upstream组的状态,它发送指定的http请求并解析响应码,去探测upstream中每个peer的存活状态,再结合历史请求记录来判断并标记其状态,如果有状态改变,就在共享内存中更新版本记录,下次执行时,所有的worker进程都要更新到最新的peer状态。

下面的表述都假定我们要监控的upstream组名是ats_node_backend, 也就是对应nginx.conf中的这个代码块

upstream ats_node_backend {
    server 127.0.0.2;
    server 127.0.0.3 backup;
    server 127.0.0.4 fail_timeout=23 weight=7 max_fails=200 backup;

}

配置参数解释
hc.spawn_checker(options)
options中包含如下选项,在调用该接口时作为参数传递进来
type        必须存在并且是http,目前只支持http
http_req  必须存在,健康探测的http请求raw字符串
timeout    默认1000,单位ms
interval    健康探测的时间间隔,单位ms, 默认1000,推荐2000
valid_status   合法响应码的表,比如{200, 302}
concurrency   并发数,默认1
fall            默认5,对UP的设备,连续fall次失败,认定为DOWN
rise           默认2,对DOWN的设备,连续rise次成功,认定为UP
shm       必须配置,用于健康检查的共享内存名称,通过ngx.shared[shm]得到共享内存
upstream   指定要做健康检查的upstream组名,必须存在
version  默认0
primary_peers  主组
backup_peers  备组
statuses  存放合法响应码的数组,来自ipairs()得到的valid_status配置项
根据options会构造一个ctx表来存放所有的配置数据,并会作为定时器ngx.timer.at()中的第三个参数
ctx的内容如下
upstream   指定的upstream组名
primary_peers 主组
backup_peers  备组
http_req  健康检查的raw http请求
timeout  超时时间,单位s,注意不是ms
interval  健康检查的间隔,单位s,注意不是ms
dict  存放统计数据的共享内存
fall   认为DOWN之前的连续失败次数,默认5
rise  认为UP之前的连续成功次数,默认2
statuses  认为正常的http状态码的表{200,302}
version    0 每次执行定时任务时的版本号,有peer状态改变,版本号加1
concurrency    创建该数目的轻量线程来并发发送健康检测请求的个数

上面的这些配置项,将作为一个上下文保存下来,在不同的阶段被反复调用

使用方法

在init_worker_by_lua_block阶段,在nginx.conf中放入下面的代码

local hc = require "resty.upstream.healthcheck"local ok, err = hc.spawn_checker{shm = "healthcheck",  -- defined by "lua_shared_dict"upstream = "foo.com", -- defined by "upstream"type = "http",http_req = "GET /status HTTP/1.0\r\nHost: foo.com\r\n\r\n",-- raw HTTP request for checkinginterval = 2000,  -- run the check cycle every 2 sectimeout = 1000,   -- 1 sec is the timeout for network operationsfall = 3,  -- # of successive failures before turning a peer downrise = 2,  -- # of successive successes before turning a peer upvalid_statuses = {200, 302},  -- a list valid HTTP status codeconcurrency = 10,  -- concurrency level for test requests}if not ok thenngx.log(ngx.ERR, "failed to spawn health checker: ", err)returnend

这里Host可以是127.0.0.1或是其它ip。如果你需要一个查看探测的upstream组的状态,可以再在nginx.conf中配置

server {...# status page for all the peers:location = /status {access_log off;allow 127.0.0.1;deny all;default_type text/plain;content_by_lua_block {local hc = require "resty.upstream.healthcheck"ngx.say("Nginx Worker PID: ", ngx.worker.pid())ngx.print(hc.status_page())}}}

然后在外面直接访问

wget 'http://127.0.0.1/status'

就可以看到所有upstream的peer状态,特别是我们检测的ats_node_backend这组,没有配置检测的upstream组前面会有"(NO checkers)"字样。


代码架构

本身是个定时器,该模块对外提供了两个接口


该定时器内干的活儿如下:



自问自答

1.如何对一个peer做健康探测?
通俗的健康探测可以是尝试与peer连接,或者发送http请求。后者是一种比较精确地检查服务是否正常的情况,因为只是做端口探测的话,如果服务僵死了,端口还是探测得通的。
healthcheck模块这里采用lua轻量级线程去专门向要检查的服务发送指定的http请求,并接收服务的响应状态码,根据状态码的情况来判断服务是否是正常的,断定UP或是DOWN


2.如何保存对peer健康探测的每次结果?
探测的结果无非两种情况:ok或是fail。因此,分为两种情况来对应存放,比如以key为ok:ats_node_backend:p9的记录来primary peers中peer id为9的设备的累计成功次数,同理nok:ats_node_backend:b1表示backup peers中peer id为1的设备的累计失败次数。
如果当前探测结果是成功,会首先去共享内存中查询累计的成功次数,并在原来的次数基础上增加1并更新到共享内存中。修改之后如果次数为0,就需要将对应的失败记录从共享内存中清除。


3.如何判断是否要改变当前的peer状态?
需要考虑3个因素,参见上面配置的标注(1)(2)(3):
(1)当前peer的状态peer.down, 从upstream模块的接口upstream.get_primary_servers()中获取peer.down的值, 另外也要谨慎处理当前worker中的这个peer.down值
(2)目前成功或是失败的次数
(3)healthcheck模块配置中的判断成功或失败的阈值,从定时器的ctx参数中可以得到, ctx.ok或ctx.fail
下面以判断下线状态为例来说明:
该peer当前是上线状态,也就是peer.down=nil或false,fails次数超过设定失败阈值,认定为下线(DOWN)
进一步的操作为调用set_peer_down_globally()函数,同时更改peer.down=true


4.set_peer_down和peer.down有啥关系?

关系是一样的


5.节点内多台设备的健康探测是怎样执行的?
采用nginx轻量级线程去并发执行多个检查任务,异步执行完之后,回调处理函数,处理完后该线程死掉。


6.当一个worker在healthcheck过程中发现某个peer掉线了,它是如何处理的?它如何将该peer的状态传递到所有其它worker知道?

在该脚本中是分为三步来完成的,参考上面的图示:
(1)在set_peer_down_globally()函数中,对探测做出的结果,来设置一次set_peer_down,同时给出下面的两步来诱发后面的处理
说明ctx.new_version=true;
同时在共享内存中存入对应的记录,标记以d:ats_node_backend:p9为key的记录来表明该peer是UP还是DOWN状态
(2)在do_check()函数中,如果有新版本的话,先查询共享内存中的v:ats_node_backend的记录的值,加1,更新ctx.version,同时置空ctx.new_version。
这里的代码处理非常巧妙:
dict:add(key, 0)
local new_ver, err = dict:incr(key, 1)
使用dict:add()是为了避免插入重复的键值,如果该键已经存在,直接返回nil和和err="exists",如果不存在,直接置0
下一步是在原来的基础上加1,这一步妥妥能执行。
(3)在下一次定时任务执行时,所有worker进程首先去共享内存中查找key为v:ats_node_backend的记录的值,也就是获取ctx.version的精确值。比较当前值与ctx.version,如果ctx.version的值小于共享内存中的值,说明需要更新peer版本。



注意每条日志的worker进程号不一样,说明它们都在更新peer版本号。


7.其它的worker如何更新peer版本呢?

就是去共享内存中检查key为d:ats_node_backend:p9的记录,如果存在该记录,说明peer是DOWN,否则peer是UP。只有peer.down和查到的值不同,就需要set_peer_down,同时设置peer.down=down


8.为啥不使用ctx.version来传递版本号,而要专门在共享内存中记录专门的记录来传递呢?
这主要是涉及到nginx中worker进程的执行和配置同步问题。一般来说,通常每份代码都会被所有的worker执行到,但是对peers的健康检查,我们只需要一个worker去执行就够了,一个worker执行完后,其它的worker来同步它的状态就够了。在do_check中我们看到除了下面的get_lock保护的代码是某个worker执行的,其它的代码,所有的worker都会去执行。定时任务采用进程抢占方式,每次执行的worker进程并不固定,这样的话,ctx.version一般是不连续的,通过共享内存方式,可以保证每个worker进程每次都可以得到最新的peer信息,而且peer version是逐渐递增的。
但是在一个worker执行过程中,ctx中的所有的字段都是可以在函数中传递的。


9.为啥要所有的worker进程都执行set_peer_down这个函数?
https://github.com/openresty/lua-upstream-nginx-module#set_peer_down

上面的官方文档强调了,该函数的执行必须所有的worker都执行,才能在server级别上生效。只在一个worker中执行,只能在该worker内部生效。


10.在nginx.conf中调用时,能否采用单进程调用方式,比如使用ngx.worker.id()==0的if条件语句?
这也是我曾经犯过的错误,答案当然是否定的。原因有两个:一个是为了让upstream.set_peer_down接口在server级别生效,必须所有的worker进程都要调用该函数;另一个,就是healthcheck模块内部已经实现了使用单个worker去进行实际的健康检测功能。


11.外部如何获取upstream中各peer之间的状态?

该模块对外提供了一个状态查询接口_M.status_page()
下面说下它的处理细节:
在status_page()中会创建一个局部数组类型的表,来获取所有的统计信息,最后将这些数组中的元素拼接成字符串。
local bits = new_tab(n * 20, 0)
n = #us   us是upstream的缩写,指upstream的组数,每组upstream会占用20个数组元素
table.concat  将table中的元素按照指定分隔符连接起来
bits数组内容如下
bits[0]="Upstream "
bits[1]="ats_node_backend"
bits[2]="   (NO checkers)"
bits[3]="\n    Primary Peers\n"
bits[4]="        "
bits[5]="10.10.101.10:18980"
bits[6]=" DOWN\n"
……
bits[n]="   Backup Peers\n"
bits[n+1]="        "
bits[n+2]="10.10.101.12:18980"
bits[n+3]=" up\n"
……

bits[n+m]="\n"


参考文献
[1].https://github.com/openresty/lua-resty-upstream-healthcheck
[2].https://github.com/openresty/lua-upstream-nginx-module#set_peer_down


这篇关于OpenResty中的upstream healthcheck功能沉思录的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/822738

相关文章

Spring Boot整合Redis注解实现增删改查功能(Redis注解使用)

《SpringBoot整合Redis注解实现增删改查功能(Redis注解使用)》文章介绍了如何使用SpringBoot整合Redis注解实现增删改查功能,包括配置、实体类、Repository、Se... 目录配置Redis连接定义实体类创建Repository接口增删改查操作示例插入数据查询数据删除数据更

使用EasyPoi快速导出Word文档功能的实现步骤

《使用EasyPoi快速导出Word文档功能的实现步骤》EasyPoi是一个基于ApachePOI的开源Java工具库,旨在简化Excel和Word文档的操作,本文将详细介绍如何使用EasyPoi快速... 目录一、准备工作1、引入依赖二、准备好一个word模版文件三、编写导出方法的工具类四、在Export

JS纯前端实现浏览器语音播报、朗读功能的完整代码

《JS纯前端实现浏览器语音播报、朗读功能的完整代码》在现代互联网的发展中,语音技术正逐渐成为改变用户体验的重要一环,下面:本文主要介绍JS纯前端实现浏览器语音播报、朗读功能的相关资料,文中通过代码... 目录一、朗读单条文本:① 语音自选参数,按钮控制语音:② 效果图:二、朗读多条文本:① 语音有默认值:②

C#实现高性能拍照与水印添加功能完整方案

《C#实现高性能拍照与水印添加功能完整方案》在工业检测、质量追溯等应用场景中,经常需要对产品进行拍照并添加相关信息水印,本文将详细介绍如何使用C#实现一个高性能的拍照和水印添加功能,包含完整的代码实现... 目录1. 概述2. 功能架构设计3. 核心代码实现python3.1 主拍照方法3.2 安全HBIT

录音功能在哪里? 电脑手机等设备打开录音功能的技巧

《录音功能在哪里?电脑手机等设备打开录音功能的技巧》很多时候我们需要使用录音功能,电脑和手机这些常用设备怎么使用录音功能呢?下面我们就来看看详细的教程... 我们在会议讨论、采访记录、课堂学习、灵感创作、法律取证、重要对话时,都可能有录音需求,便于留存关键信息。下面分享一下如何在电脑端和手机端上找到录音功能

Android实现图片浏览功能的示例详解(附带源码)

《Android实现图片浏览功能的示例详解(附带源码)》在许多应用中,都需要展示图片并支持用户进行浏览,本文主要为大家介绍了如何通过Android实现图片浏览功能,感兴趣的小伙伴可以跟随小编一起学习一... 目录一、项目背景详细介绍二、项目需求详细介绍三、相关技术详细介绍四、实现思路详细介绍五、完整实现代码

Python使用FastAPI实现大文件分片上传与断点续传功能

《Python使用FastAPI实现大文件分片上传与断点续传功能》大文件直传常遇到超时、网络抖动失败、失败后只能重传的问题,分片上传+断点续传可以把大文件拆成若干小块逐个上传,并在中断后从已完成分片继... 目录一、接口设计二、服务端实现(FastAPI)2.1 运行环境2.2 目录结构建议2.3 serv

Debian 13升级后网络转发等功能异常怎么办? 并非错误而是管理机制变更

《Debian13升级后网络转发等功能异常怎么办?并非错误而是管理机制变更》很多朋友反馈,更新到Debian13后网络转发等功能异常,这并非BUG而是Debian13Trixie调整... 日前 Debian 13 Trixie 发布后已经有众多网友升级到新版本,只不过升级后发现某些功能存在异常,例如网络转

基于Java和FFmpeg实现视频压缩和剪辑功能

《基于Java和FFmpeg实现视频压缩和剪辑功能》在视频处理开发中,压缩和剪辑是常见的需求,本文将介绍如何使用Java结合FFmpeg实现视频压缩和剪辑功能,同时去除数据库操作,仅专注于视频处理,需... 目录引言1. 环境准备1.1 项目依赖1.2 安装 FFmpeg2. 视频压缩功能实现2.1 主要功

使用Python实现无损放大图片功能

《使用Python实现无损放大图片功能》本文介绍了如何使用Python的Pillow库进行无损图片放大,区分了JPEG和PNG格式在放大过程中的特点,并给出了示例代码,JPEG格式可能受压缩影响,需先... 目录一、什么是无损放大?二、实现方法步骤1:读取图片步骤2:无损放大图片步骤3:保存图片三、示php