从源码角度分析ScheduleMaster的节点管理流程
ScheduleMaster是一个开源的分布式任务调度系统,它基于.NET Core 3.1平台构建,支持跨平台多节点部署运行。
项目主页:https://github.com/hey-hoho/ScheduleMasterCore
本篇从源码角度分析一下节点控制的核心流程。
生命周期事件
生命周期事件增强了整个应用进程的控制能力,由于节点状态与之关系密切,所以必须要首先了解下生命周期事件具体干了什么活。
借助于ASP.NET Core框架的HostedService模型,我们把生命周期管理器封装在一个后台托管服务AppLifetimeHostedService
中,在它的StartAsync
方法中注册了我们需要的事件:
public Task StartAsync(CancellationToken cancellationToken)
{
_appLifetime.ApplicationStarted.Register(OnStarted);
_appLifetime.ApplicationStopping.Register(OnStopping);
_appLifetime.ApplicationStopped.Register(OnStopped);
return Task.CompletedTask;
}
这里主要涉及的事件就是应用启动和停止时所需要处理的逻辑,分别对应节点状态的变更,下面重点说一下启动事件。
ScheduleMaster采用了典型的中心化结构搭建,基于1个master节点和N和worker节点提供服务,其中master扮演了整个系统资源调度的角色,worker则是实际执行任务的角色。这样的话,master就必须要感知到它所能调度的资源清单,所以系统引入了节点注册概念。
根据注册发起者的不同,可以分为如下两种模式:
自动注册模式
手动注册模式
自动注册模式
接触过微服务架构的朋友应该会对服务注册发现这一过程比较熟悉,借鉴了相似的设计,节点自动注册就类似服务注册的样子,在节点启动时自动把自身的配置信息注册到控制中心,默认的方式就是从配置文件读取节点信息,同时也支持使用命令行参数覆盖配置文件中的字段:
private void OnStarted()
{
// ....
//判断是否要自动根据配置文件注册节点信息
if (AppCommandResolver.IsAutoRegister())
{
_logger.LogInformation("enabled auto register...");
// 设置节点信息
ConfigurationCache.SetNode(_configuration);
// ....
}
}
public static void SetNode(IConfiguration configuration)
{
NodeSetting = configuration.GetSection("NodeSetting").Get<NodeSetting>();
string identity = AppCommandResolver.GetCommandLineArgsValue("identity");
if (!string.IsNullOrEmpty(identity))
{
NodeSetting.IdentityName = identity;
}
string protocol = AppCommandResolver.GetCommandLineArgsValue("protocol");
if (!string.IsNullOrEmpty(protocol))
{
NodeSetting.Protocol = protocol;
}
string ip = AppCommandResolver.GetCommandLineArgsValue("ip");
if (!string.IsNullOrEmpty(ip))
{
NodeSetting.IP = ip;
}
string port = AppCommandResolver.GetCommandLineArgsValue("port");
if (!string.IsNullOrEmpty(port))
{
NodeSetting.Port = Convert.ToInt32(port);
}
string priority = AppCommandResolver.GetCommandLineArgsValue("priority");
if (!string.IsNullOrEmpty(priority))
{
NodeSetting.Priority = Convert.ToInt32(priority);
}
NodeSetting.MachineName = Environment.MachineName;
}
再看一下如何判断节点是否开启了自动注册模式:
public static bool IsAutoRegister()
{
//优先读取环境参数
string option = Environment.GetEnvironmentVariable("SMCORE_AUTOR");
//再看命令行参数中是否也有设置
string cmdArg = GetCommandLineArgsValue("autor");
if (!string.IsNullOrEmpty(cmdArg))
{
option = cmdArg;
}
return option != "false";
}
很明显,在节点启动时如果指定了特定的环境变量SMCORE_AUTOR
或命令行参数autor
并且值为false即表示关闭自动注册模式,否则默认开启。
要注意的是,master节点只提供了自动注册模式。
手动注册模式
自动注册模式虽然流程简单,但是需要提前配置好节点信息,这对于节点弹性部署并不友好,因此为了增加系统灵活性,系统也提供了手动注册节点的模式,这时候对worker注册的主动权转移到master手里,需要先在master控制台中创建好要注册的节点,然后执行连接操作,最后启动服务即可。
这个过程中比较核心的是连接验证过程,设计这个流程的原因是为了保障创建连接的双方是可信状态,实现数据匹配,其核心过程为:
worker节点在启动时通过环境变量
SMCORE_WORKEROF
或者命令行参数workerof
指定归属的master名称在控制台中对节点执行[连接]操作,master携带验证信息对worker发起连接请求
如果验证通过,则使用指定的节点名称去数据库查询完整的节点配置信息,并为worker节点缓存配置数据,worker生成一个新的访问秘钥返回
标记节点状态为空闲中,此时worker并不运行任何调度服务,处于空跑状态
对节点执行[启用]操作,开启调度功能
验证连接过程的核心代码为:
public async Task<(bool success, string content)> Connect()
{
HttpClient client = CreateClient();
client.DefaultRequestHeaders.Add("sm_connection", SecurityHelper.MD5(ConfigurationCache.NodeSetting.IdentityName));
client.DefaultRequestHeaders.Add("sm_nameto", _server.NodeName);
var response = await client.PostAsync("/api/server/connect", null);
return (response.IsSuccessStatusCode, await response.Content.ReadAsStringAsync());
}
[HttpPost, AllowAnonymous]
public IActionResult Connect()
{
string workerof = AppCommandResolver.GetTargetMasterName();
string encodeKey = Request.Headers["sm_connection"].FirstOrDefault();
if (string.IsNullOrEmpty(workerof) || string.IsNullOrEmpty(encodeKey))
{
_logger.LogWarning("connect failed! workerof or encodekey is null...");
return BadRequest("Unauthorized Connection.");
}
if (!Core.Common.SecurityHelper.MD5(workerof).Equals(encodeKey))
{
_logger.LogWarning("connect failed! encodekey is unvalid, wokerof:{0}, encodekey:{1}", workerof, encodeKey);
return BadRequest("Unauthorized Connection.");
}
string workerName = Request.Headers["sm_nameto"].FirstOrDefault();
var node = _db.ServerNodes.FirstOrDefault(x => x.NodeName == workerName);
if (node == null)
{
_logger.LogWarning("connect failed! unkown worker name:{0}...", workerName);
return BadRequest("Unkown Worker Name.");
}
Core.ConfigurationCache.SetNode(node);
string secret = Guid.NewGuid().ToString("n");
QuartzManager.AccessSecret = secret;
_logger.LogInformation("successfully connected to {0}!", workerof);
LogHelper.Info($"与{workerof}连接成功~");
return Ok(secret);
}
健康检查
健康检查是为了保障不可用的worker节点及时被发现并剔除调度,其验证方式使用了ASP.NET Core框架自带的健康检查机制中间件,通过访问一个指定的路由地址获取节点的健康情况,如果连续N次检查失败就把该节点强制剔除下线,多次检查目的是为了避免因短暂的网络抖动导致出现误判情况,这个次数N可以根据实际情况进行配置,默认是3次。
首先master启动的时候会注册一个每分钟执行一次的后台定时任务,这个任务会拉取所有状态是非[下线]的worker节点,然后对其发起健康检查请求:
public class SystemSchedulerRegistry : Registry
{
public SystemSchedulerRegistry()
{
NonReentrantAsDefault();
//对运行节点每分钟一次心跳监测
Schedule<WorkerCheckJob>().ToRunEvery(1).Minutes();
}
}
internal class WorkerCheckJob : IJob
{
/// <summary>
/// 执行计划
/// </summary>
public void Execute()
{
using (var scope = ConfigurationCache.RootServiceProvider.CreateScope())
{
Core.Interface.INodeService service = scope.ServiceProvider.GetService<Core.Interface.INodeService>();
AutowiredServiceProvider provider = new AutowiredServiceProvider();
provider.PropertyActivate(service, scope.ServiceProvider);
service.WorkerHealthCheck();
}
}
}
具体判断节点无效的流程为:
读取系统配置的最大允许无响应次数
给节点维护一个失败计数器,本质是一个字典,key是节点名称,value是连续失败的次数
对节点发起健康检查请求,如果请求成功就更新节点的最后刷新时间,并把计数器归0
如果请求失败但没有达到最大失败次数,把计数器加1,等待下次检查
如果已经达到最大失败次数,则把节点标记下线,释放该节点占据的锁,同时把计数器归0
worker的中间件注册过程为:
public void ConfigureServices(IServiceCollection services)
{
// ....
services.AddHealthChecks();
// ....
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ....
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHealthChecks("/health");
});
// ....
}
访问控制
为了保证worker接口访问的安全性,系统加入了动态秘钥验证机制,每次节点启动或者被连接的时候都会生成一个新的秘钥,持有合法秘钥的请求才会被节点正常处理,否则直接返回401 Unauthorized。
public void OnActionExecuting(ActionExecutingContext context)
{
var anonymous = (context.ActionDescriptor as ControllerActionDescriptor).MethodInfo.GetCustomAttributes(typeof(AllowAnonymousAttribute), false);
if (anonymous.Any())
{
return;
}
var secret = context.HttpContext.Request.Headers["sm_secret"].FirstOrDefault();
if (string.Compare(Common.QuartzManager.AccessSecret, secret, StringComparison.CurrentCultureIgnoreCase) != 0)
{
context.Result = new UnauthorizedObjectResult($"w:{Common.QuartzManager.AccessSecret} m:{secret}");
}
}
节点访问
在master控制台中对任务的操作最终都被分发到关联的worker节点上,通过worker提供的webapi接口实现远程调用。以启动任务为例,我们看一下具体分发和远程调用过程:
private async Task<bool> DispatcherHandler(Guid sid, RequestDelegate func)
{
var nodeList = _nodeService.GetAvaliableWorkerForSchedule(sid);
if (nodeList.Any())
{
foreach (var item in nodeList)
{
if (!await func(item))
{
return false;
}
}
return true;
}
throw new InvalidOperationException("running worker not found.");
}
public async Task<bool> ScheduleStart(Guid sid)
{
return await DispatcherHandler(sid, async (ServerNodeEntity node) =>
{
_scheduleClient.Server = node;
return await _scheduleClient.Start(sid);
});
}
可以看到,启动操作会首先查询任务的执行节点,然后依次遍历执行远程调用,只要其中一个节点执行命令失败那么整个操作就会失败。
最终的httpclient请求被封装在Hos.ScheduleMaster.Core.Services.RemoteCaller.ServerClient
类中,它的CreateClient
方法从IHttpClientFactory
获取了一个客户端实例,并把节点的访问秘钥放入请求头中,以此完成安全性验证:
protected HttpClient CreateClient()
{
if (_server == null)
{
throw new ArgumentException("no target worker that can send the request.");
}
HttpClient client = _httpClientFactory.CreateClient("workercaller");
client.DefaultRequestHeaders.Add("sm_secret", _server.AccessSecret);
client.BaseAddress = new Uri($"{_server.AccessProtocol}://{_server.Host}");
return client;
}
写在最后
到这里基本把节点的核心操作都分析完毕了,希望能对关注这个项目的朋友带来帮助~
从源码角度分析ScheduleMaster的节点管理流程的更多相关文章
- 源码角度分析-newFixedThreadPool线程池导致的内存飙升问题
前言 使用无界队列的线程池会导致内存飙升吗?面试官经常会问这个问题,本文将基于源码,去分析newFixedThreadPool线程池导致的内存飙升问题,希望能加深大家的理解. (想自学习编程的小伙伴请 ...
- 从源码角度看finish()方法的执行流程
1. finish()方法概览 首先我们来看一下finish方法的无参版本的定义: /** * Call this when your activity is done and should be c ...
- 从源码角度分析 MyBatis 工作原理
一.MyBatis 完整示例 这里,我将以一个入门级的示例来演示 MyBatis 是如何工作的. 注:本文后面章节中的原理.源码部分也将基于这个示例来进行讲解.完整示例源码地址 1.1. 数据库准备 ...
- Android的Message Pool是什么——源码角度分析
原文地址: http://blog.csdn.net/xplee0576/article/details/46875555 Android中,我们在线程之间通信传递通常采用Android的消息机制,而 ...
- 【原创】源码角度分析Android的消息机制系列(三)——ThreadLocal的工作原理
ι 版权声明:本文为博主原创文章,未经博主允许不得转载. 先看Android源码(API24)中对ThreadLocal的定义: public class ThreadLocal<T> 即 ...
- 【原创】源码角度分析Android的消息机制系列(五)——Looper的工作原理
ι 版权声明:本文为博主原创文章,未经博主允许不得转载. Looper在Android的消息机制中就是用来进行消息循环的.它会不停地循环,去MessageQueue中查看是否有新消息,如果有消息就立刻 ...
- 从源码角度分析 Kotlin by lazy 的实现
by lazy 的作用 延迟属性(lazy properties) 是 Kotlin 标准库中的标准委托之一,可以通过 by lazy 来实现. 其中,lazy() 是一个函数,可以接受一个 Lamb ...
- Java面试题 从源码角度分析HashSet实现原理?
面试官:请问HashSet有哪些特点? 应聘者:HashSet实现自set接口,set集合中元素无序且不能重复: 面试官:那么HashSet 如何保证元素不重复? 应聘者:因为HashSet底层是基于 ...
- Adroid学习之 从源码角度分析-禁止使用回退按钮方案
有时候,不能让用户进行回退操作,如何处理? 查看返回键触发了哪些方法.在打开程序后把这个方法禁止了. 问题:程序在后台驻留,这样就会出现,其他时候也不能使用回退按钮.如何处理,在onpase()时方法 ...
- 【原创】源码角度分析Android的消息机制系列(六)——Handler的工作原理
ι 版权声明:本文为博主原创文章,未经博主允许不得转载. 先看Handler的定义: /** * A Handler allows you to send and process {@link Mes ...
随机推荐
- Calendar 获取当前月份最后一周
import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; public class Ca ...
- 【原创】GmSSL Android库编译
相关内容: GmSSL Linux编译 环境搭建 重要 用编译方法2编译出的库,集成到工程之后,发现报 incompatible target错误,各种找不到定义.32位和64位都不行. 如果你也遇到 ...
- Python操作数据库读书笔记
SQLite 简介 什么是 SQLite? SQLite是一个进程内的库,实现了自给自足的.无服务器的.零配置的.事务性的 SQL 数据库引擎.它是一个零配置的数据库,这意味着与其他数据库一样,您不需 ...
- linux端口映射,telnet通信失败
linux端口映射 1.第一种方法, 使用firewalld # 开启伪装IP firewall-cmd --permanent --add-masquerade # 配置端口转发,将到达本机的123 ...
- java的死锁与解决方法
一.什么是死锁? 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无限等待. 二.产生死锁的原因与四个条件 2.1 死锁原因 竞争资 ...
- 后端006_登录之后返回Token
现在开始我们就可以写登录相关的东西了.首先登录相关的流程是这样的,前端输入用户和密码传给后端,后端判断用户名和密码是否正确,若正确,则生成JWT令牌,若不正确,则需要让前端重新输入,前端如果拿到了JW ...
- USB TTL CMOS 电平
USB转TTL模块的作用就是把电平转换到双方都能识别进行通信. TTL电平信号规定,+5V等价于逻辑"1",0V等价于逻辑"0"(采用二进制来表示数据时).这样 ...
- 手把手教你基于luatos的4G(LTE Cat.1)模组接入华为云物联网平台
摘要:本期文章采用了4G LTE Cat.1模块,编程语言用的是lua,实现对华为云物联网平台的设备通信与控制 本文分享自华为云社区<基于luatos的4G(LTE Cat.1)模组接入华为云物 ...
- RTC 科普视频丨聊聊空间音频的原理与其背后的声学原理
在现在很多的线上实时互动场景中,我们重视的不仅仅是互动体验,还要提升沉浸感.而在很多场景中,仅凭空间音频技术,就可以带来如临其境的体验.空间音频技术的原理是怎样的呢? 看过我们新一期的 RTC 科普视 ...
- java 环境变量配置详细教程(2023 年全网最详细)
前言: 在上一篇文章中,壹哥给大家重点讲解了 Java 实现跨平台的原理,不知道你现在有没有弄清楚呢?如果你还有疑问,可以在评论区留言- 之前的三篇文章,主要是理论性的内容,其实你暂时跳过不看也是可以 ...