基于openresty的轻量级服务管理集合

简介

tl-ops-manage (tl-openresty-web-manage),基于openresty开发的一款基础服务管理工具,支持服务动态扩展,自定义URL路由,健康检查,服务熔断限流,动态配置检测,日志记录,数据版本控制,后台可视化管理,等等...

做这个项目最开始的想法很简单,只是想在造轮子的过程中了解,学习一些新的知识面。到后面完成了大部分基础功能点后,发现可以整理为一个服务管理工具的项目。于是就有了tl-ops-manage

安装

下载项目 : https://github.com/iamtsm/tl-...

首先需要安装openresty,可以去官网下载对应包, http://openresty.org/cn/downl...

除此外,还需要安装redis,因为在部分模块中有依赖到redis, https://redis.io/download/

进入tl-ops-manage/conf/ 目录下,可以看到tl_ops_manage.conf文件,需要将对应的配置改成自己需要的即可

假设你的项目路径是 /path/to/,那么对应的配置示例如下 :

lua_shared_dict tlopsbalance 100m;
init_worker_by_lua_block {
 -- 启动健康检查
 require("health.tl_ops_health"):init();
 -- 启动限流熔断
 require("limit.fuse.tl_ops_limit_fuse"):init();
 -- 启动路由统计
 require("balance.tl_ops_balance_count"):init();
}
server {
 listen 80;
 server_name _;
 location /tlops/ {
 alias /path/to/tl-ops-manage/web/;
 }
 location / {
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header Host $http_host;
 proxy_set_header X-NginX-Proxy true;
 add_header Access-Control-Allow-Headers *;
 add_header Access-Control-Allow-Methods *;
 set $node '';
 rewrite_by_lua_block {
 require("balance.tl_ops_balance"):init();
 }
 proxy_pass $node;
 }
 location = /tlops/service/list {
 content_by_lua_file "/path/to/tl-ops-manage/api/tl_ops_api_get_service.lua";
 }
 location = /tlops/service/set {
 content_by_lua_file "/path/to/tl-ops-manage/api/tl_ops_api_set_service.lua";
 }
 # ..... more conf detail
}

更改完配置后,需要在openresty配置中引入改配置文件,也就是在对应的openresty/conf/nginx.conf中补充一行

include "/path/to/tl-ops-manage/conf/tl_ops_manage.conf";

最后一步,启动openresty,访问一次 http://localhost/tlops/reset 初始化项目一次即可。

如果正常返回 {"code" : 0, "msg" : "success", "data" : ""},说明初始化成功。

最后访问项目即可 http://localhost/tlops/tl_ops_web_index.html

以下模块,将以两种方式进行说明,一种是在配置文件中填写配置,另外一种是在管理台填写配置。

在文件中设置的节点会在启动时即可生效,在管理后台设置的节点,将会在各模块依赖的时候同步到其中(稍延时,但最终一致)。

服务节点

服务节点是基础配置,添加好的节点会被用于健康检查,节点限流,熔断降级,路由统计,负载均衡等模块。节点的基础数据可以在文件中设置好,或也可以在管理后台中设置。

在文件中配置

配置节点数据对应的文件在 constant/tl_ops_constant_service.lua,在文件中,我提供了demo示例数据,只需要按照对应的格式放入 list 中即可,需要配置多少个,放置多少个即刻。

list = {
 {
 id = snowflake.generate_id( 100 ), -- default snow id
 name = "节点1", -- 当前节点name
 service = "测试服务", -- 当前节点所属service
 protocol = "http://", -- 当前节点协议头
 ip = "127.0.0.1", -- 当前节点ip
 port = 6666, -- 当前节点port
 },
 {
 id = snowflake.generate_id( 100 ),
 name = "节点2",
 service = "测试服务",
 protocol = "http://",
 ip = "127.0.0.1",
 port = 6667,
 }
 ....
}
在管理台配置

健康检查

健康检查依赖设置好的节点,对节点依次进行发包检查节点状态,在健康检查中,如检查间隔,发包超时时间,周期内请求成功多少次算正常服务状态,周期内请求失败多少次才需要转变服务状态,接收服务回包时什么状态才算成功,等等... ,这些需要数据根据每个服务的不同,可以做到配置化,并实时更新。

在文件中配置

配置节点数据对应的文件在 constant/tl_ops_constant_health.lua,在文件中,我提供了demo示例数据,只需要按照对应的格式放入 options 中即可,需要配置多少个,放置多少个即刻。

options = {
 {
 check_failed_max_count = 5, -- 自检周期内失败次数
 check_success_max_count = 2, -- 自检周期内成功次数
 check_interval = 10 * 1000, -- 自检服务自检周期 (单位/ms)
 check_timeout = 1000, -- 自检节点心跳包连接超时时间 (单位/ms)
 check_content = "GET / HTTP/1.0", -- 自检心跳包内容
 check_success_status = { -- 自检返回成功状态, 如 201,202(代表成功)
 200
 },
 check_service_name = "测试服务1" -- 该配置所属服务
 },
 {
 check_failed_max_count = 5,
 check_success_max_count = 2,
 check_interval = 10 * 1000
 check_timeout = 1000,
 check_content = "GET / HTTP/1.0",
 check_success_status = {
 200
 },
 check_service_name = "测试服务2"
 }
}
在管理台配置

路由负载

对于负载均衡,分为三个负载策略,URL负载 > 请求参数负载 > 请求COOKIE负载,在负载时,会按照这个顺序进行负载。

而每个策略分为两种模式 指定路由,随机路由。

对于指定路由,一旦请求命中三个模式中的任意一条规则,即会路由到具体节点。

对于随机路由,一旦请求命中三个模式中的任意一条规则,不会路由到具体节点,而是在命中的服务中随机选取一个节点进行路由。

对于模式的不同,每个随机方式也是不相同,对于URL随机负载,随机模式是根据当前请求url的长度设置随机种子,得到随机数进行随机负载。对于请求参数和请求COOKIE的随机负载,是根据当前命中的key的长度设置随机种子,得到随机数进行随机负载。

在文件中配置
-- URL路由
point = { -- 指定路由
 {
 id = snowflake.generate_id( 100 ), -- default snow id
 url = "/test/*", -- 当前url匹配规则
 service = "测试服务1", -- 当前url路由到的service
 node = 0, -- 当前url路由到的service下的node的索引
 host = "tlops1.com", -- 当前url处理的域名范围
 },
 {
 id = snowflake.generate_id( 100 ),
 url = "/api/*",
 service = "测试服务1",
 node = 1,
 host = "tlops2.com",
 },
},
random = { -- 随机路由 (无需指定节点,指定服务即可)
 {
 id = snowflake.generate_id( 100 ),
 url = "/api/*",
 service = "测试服务1",
 host = "tlops2.com",
 },
}
在管理台配置

URL模式

请求参数模式

COOKIE模式

熔断限流

熔断限流,其实是自动化熔断,限流两种配置的组合。对于服务自动化熔断来说,其应该是根据节点 ‘状态’ 来进行一种服务降级的手段。在节点负载过高时,应该对节点减少流量的进入,在服务性能较优时,增加流量的进入,而控制流量的进入就需要用到一些流控手段。所以我将其组合来配置

在文件中配置
-- 熔断配置
options = {
 {
 service_name = "测试服务1", -- 该配置所属服务
 interval = 10 * 1000, -- 检测时间间隔 单位/ms
 node_threshold = 0.3, -- 切换状态阈值 (node失败占比)
 service_threshold = 0.5, -- 切换状态阈值 (service切换阈值,取决于node失败状态占比)
 recover = 15 * 1000, -- 全熔断恢复时间 单位/ms
 depend = depend.token, -- 默认依赖组件 :token_bucket
 level = level.service, -- 默认组件级别,服务层级 [限流熔断针对的层级]
 },
 ....
},
-- 限流配置
options = {
 capacity = 10 * 1024 * 1024, -- 最大容量 10M (按字节为单位,可做字节整型流控)
 rate = 1024, -- 令牌生成速率/秒 (每秒 1KB)
 warm = 100 * 1024, -- 预热令牌数量 (预热100KB)
 half_capacity = 2, -- 限流状态令牌最大容量
 block = 1024, -- 流控以1024为单位
}
-- 依赖限流组件
local depend = {
 token = "token",
 leak = "leak"
}
-- 组件级别
local level = {
 service = "service"
}
在管理台配置

负载均衡

负载均衡目前支持三种策略,分别是,API最长前缀匹配负载,请求参数精准匹配负载,请求cookie精准匹配负载。每种策略支持两种模式,分别是,指定节点负载,随机节点负载,且每种模式的配置相互独立,互不影响。

API策略在文件中的配置
point = {
 {
 id = snowflake.generate_id( 100 ), -- default snow id
 url = "/*", -- 当前url匹配规则
 service = "测试服务1", -- 当前url路由到的service
 node = 0, -- 当前url路由到的service下的node的索引
 host = "tlops1.com", -- 当前url处理的域名范围
 }
},
random = {
 {
 id = snowflake.generate_id( 100 ), -- default snow id
 url = "/*", -- 当前url匹配规则
 service = "测试服务1", -- 当前url路由到的service
 node = 0, -- 当前url路由到的service下的node的索引
 host = "tlops1.com", -- 当前url处理的域名范围
 }
},
cookie策略在文件中的配置
point = {
 {
 id = snowflake.generate_id( 100 ), -- default snow id
 key = "_tl_session_id", -- 当前cookie匹配名称
 value = "session_iamtsm", -- 当前cookie名称对应值
 service = "测试服务1", -- 当前cookie路由到的service
 node = 0, -- 当前cookie路由到的service下的node的索引
 host = "tlops1.com", -- 当前cookie处理的域名范围
 }
},
random = {
 {
 id = snowflake.generate_id( 100 ), -- default snow id
 key = "_tl_token_id", -- 当前cookie匹配名称
 value = "token_iamtsm", -- 当前cookie名称对应值
 service = "测试服务1", -- 当前cookie路由到的service
 node = 0, -- 当前cookie路由到的service下的node的索引
 host = "tlops1.com", -- 当前cookie处理的域名范围
 }
},
参数策略在文件中的配置
point = {
 {
 id = snowflake.generate_id( 100 ), -- default snow id
 key = "_tl_id", -- 当前请求参数匹配名称
 value = "0", -- 当前请求参数名称对应值
 service = "测试服务1", -- 当前请求参数路由到的service
 node = 0, -- 当前请求参数路由到的service下的node的索引
 host = "tlops1.com", -- 当前请求参数处理的域名范围
 }
},
random = {
 {
 id = snowflake.generate_id( 100 ), -- default snow id
 key = "_tl_name", -- 当前请求参数匹配名称
 value = "iamtsm", -- 当前请求参数名称对应值
 service = "tlops-demo", -- 当前请求参数路由到的service
 node = 0, -- 当前请求参数路由到的service下的node的索引
 host = "tlops1.com", -- 当前请求参数处理的域名范围
 }
},

路由统计

路由统计,是在负载均衡模块上扩充的一种功能,在服务进行负载逻辑时,可能存在负载成功,或失败,有些情况下可能需要统计负载情况进行展示。其对应的配置在 constant/tl_ops_constant_balance.lua中。

在文件中配置
-- 路由统计时间间隔
count = {
 interval = 5 * 60 -- 统计周期 单位/s, 默认:5min
}

# 实现源码说明

服务节点

我将服务划分为服务节点,节点隶属于服务,是一个上下级关系。用如下配置可以展示他们之间的关系

product = { ---- 服务
 { ---- 节点1
 id = snowflake.generate_id( 100 ), ---- default snow id
 name = "product-node-1", ---- 当前节点name
 service = "product", ---- 当前节点所属service
 protocol = "http://", ---- 当前节点协议头
 ip = "127.0.0.1", ---- 当前节点ip
 port = 6666, ---- 当前节点port
 },
 {---- 节点2
 id = snowflake.generate_id( 100 ), ---- default snow id
 name = "product-node-2", ---- 当前节点name
 service = "product", ---- 当前节点所属service
 protocol = "http://", ---- 当前节点协议头
 ip = "127.0.0.1", ---- 当前节点ip
 port = 6667, ---- 当前节点port
 }
}

例如在业务场景中会有产品功能,对于产品功能来说,运行他的应该是有一个或多个机器实例,产品业务对应的就是产品服务,这些机器实例对应的就是产品节点。

对于tl-ops-manage来说,服务-节点是基础,其他的路由,检查等模块都需要依赖服务-节点。所以服务-节点可以理解为公共全局配置,仅有一份,其对应的项目文件在 constant/tl_ops_constant_service.lua

健康检查

健康检查的主要逻辑是 定时检查器 的实现。

定时检查器根据配置启动相应定时器执行检查逻辑。配置加载器根据管理端新增或修改的配置动态同步到定时检查器中。实现方式都是通过ngx.timer

首先,我们先了解下健康检查对应的配置,还是以product服务为例

{---- product服务自检配置
 check_failed_max_count = 5, #自检时心跳包最大失败次数,达到这个次数会将在线节点置为下线。
 check_success_max_count = 2, #自检时心跳包最大成功次数,达到这个次数会将下线节点置为上线。
 check_interval = 5 * 1000, #自检周期, 默认单位/ms
 check_timeout = 1000, #自检心跳包接收超时时间,默认单位/ms
 check_content = "GET / HTTP/1.0", #自检心跳包内容,可自定义,但是需要被检方处理兼容。
 check_service_name = "product" #自检服务名称
}

在项目启动时,会首先读取 constant/tl_ops_constant_health.lua中的服务配置,并根据配置个数启动对应的timer。

进入 require("health.tl_ops_health"):init(); 方法后,我们可以看到一系列的启动器,如根据配置启动相应的health-check-timer, 还有配置加载器的启动,以及服务的版本初始化,以及服务健康检查配置版本的初始化

我们先从健康检查主逻辑开始,可以看到先执行了一段配置初始化逻辑, 初始化配置中,会检查配置的合法性,以及对服务状态,节点状态的初始值进行定义,随后有一段timer_list的逻辑(先忽略,配置加载器相关)。

可以看到如下代码,是根据confs的数量,用ngx.timer.at去启动相应的定时器去执行 tl_ops_health_check 逻辑,而 tl_ops_health_check 也就是执行相应的 tl_ops_health_check_main逻辑, conf 就是每个定时器所需的健康检查配置

for index, conf in ipairs(confs) do
 local ok, _ = ngx.timer.at(0, tl_ops_health_check, conf)
 if not ok then
 tlog:err("tl_ops_health_check_start failed to run check , create timer failed " , _)
 return nil
 end
end

接下来进入核心逻辑tl_ops_health_check_main,可以看到核心逻辑主要由两部分组成,也就是 dynamic_conf_change_start(同步配置),tl_ops_health_check_nodes(心跳包),接下来将对这两个方法进行详细解析

心跳包

由于nginx在启动时是多woker进程来处理请求,而openresty将nginx处理请求分为七个阶段。

健康检查的启动应与请求阶段无关,所以应该放在 init_by_lua阶段或者init_worker_by_lua阶段较为合适,但是定时器的生命周期只能在init_worker_by_lua阶段(如下图),所以健康检查的启动器就应在init_worker_by_lua阶段来做。

由于init_worker_by_lua阶段是初始化worker进程阶段,所以在此阶段是存在多个worker进程,也就是可能存在抢占执行定时器的情况。而每个定时器有其依赖的配置,多个worker之间数据不共享,就会导致健康检查数据统计、配置不一致的情况。

所以才会有这样一段逻辑,tl_ops_health_check_get_lock对应的就是加锁逻辑,主要方式是通过 ngx.shared 共享内存来实现,有兴趣可以查看下具体实现代码。

---- 心跳包
if tl_ops_health_check_get_lock(conf) then
 tl_ops_health_check_nodes(conf)
end

我们继续看回代码,在抢占锁后,只会有一个worker进程进入锁内,并执行 tl_ops_health_check_nodes,进行发送心跳包。

对于发送心跳包,我们可以看到是对 服务-节点 依次进行遍历发送socket包(心跳包内容自定义),如心跳周期正常结束,进入 tl_ops_health_check_node_ok 成功逻辑,否则进入tl_ops_health_check_node_failed 失败逻辑

心跳成功
心跳失败

路由负载

API路由负载
COOKIE路由负载
请求参数路由负载

熔断限流

自动化熔断
令牌桶限流
令牌桶扩容
令牌桶缩容

路由统计


动态配置

增量配置同步
修改配置同步

后台管理

概况统计展示
服务节点管理
路由策略管理
健康配置管理
熔断配置管理
作者:TohSuM原文地址:https://segmentfault.com/a/1190000041939897

%s 个评论

要回复文章请先登录注册