25. 辅助角色服务
25.1 关于辅助角色服务
.NET Core 3.0
新增了 Worker Service
的新项目模板,可以编写长时间运行的后台服务,并且能轻松的部署成 Windows服务
或 Linux 守护程序
。
目前微软提供了两种方式创建长时间运行的后台服务:
- 共宿主方式:中小型项目推荐,无需单独部署
Windows/Linux
服务 - 独立
Worker Service
方式:需独立部署Windows/Linux
服务
25.2 共宿主方式
共宿主方式指的是在现有的 Web
或其他应用程中创建类文件并派生自 BackgroundService
类即可。这种方式的典型特点就是和应用共生存周期,应用启动时启动,应用结束停止运行。
推荐中小型项目使用这种方式。
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace YourPoject.Web.Core;
public class Worker : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Console.WriteLine(DateTime.Now);
// 延迟 1 秒
await Task.Delay(1000, stoppingToken);
}
}
}
之后在 Startup.cs
中注册即可:
services.AddHostedService<Worker>();
25.2.1 最佳实践
最好的实践方式是创建独立的类库项目:YourProject.BackgroundServices
,之后添加 YourPoject.Application
和 YourPoject.Core
层引用,将所有的 Worker
放在该层,同时创建 Startup.cs
类进行 Worker
统一注册,如:
using Microsoft.Extensions.DependencyInjection;
namespace YourProject.BackgroundServices;
public sealed class Startup : AppStartup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddHostedService<Worker>();
services.AddHostedService<Worker2>();
}
}
25.3 独立 Worker Service
方式
独立 Worker Service
方式的主要特点就是它是一个独立的项目,和现有的项目没有直接关联关系,需要分开独立部署。
推荐中大型项目使用这种方式,也就是独立部署成 Windows Service
或者 Linux 守护进程
,具有独立生存周期,即使应用故障了也不会影响它的运行。
25.3.1 如何创建 Worker Service
通过 Visual Studio 2019
提供的 Worker Service
可直接创建。如图:
25.3.2 创建 Worker
当我们创建好 Worker Service
项目时,已经自带了一个 Worker
类并继承自 BackgroundService
基类。
Worker
正是我们辅助角色的主要工作类,在这里我们编写我们所有的业务逻辑。通常 Worker
默认格式为:
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace FurionWorkers
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTime.Now);
await Task.Delay(1000, stoppingToken);
}
}
}
}
当我们创建了 Worker
类之后,需要在 Program.cs
中进行注册,如:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace FurionWorkers
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
});
}
}
如果使用了 Furion
包后可实现自动注册,请同时确保 Worker
声明为 public
级别。
25.3.3 多个 Worker
Worker Service
是支持定义多个 Worker
进行协调工作的,每个 Worker
是完全独立的工作环境,但可共享同一主进程信息。
25.3.4 生命周期
Worker Service
为 Worker
提供了三个执行方法,分别代表三个生命周期:
StartAsync
:负责启动Worker Service
,如果调用StartAsync
方法的线程被一直阻塞了,那么Worker Service
的启动就一直完成不了ExecuteAsync
:Worker Service
真正实现业务逻辑的地方,这里不能调用阻塞代码!!!StopAsync
:负责结束Worker Service
,如果调用StopAsync
方法的线程被一直阻塞了,那么Worker Service
的结束就一直完成不了
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace FurionWorkers
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
// 启动
public override Task StartAsync(CancellationToken cancellationToken)
{
return base.StartAsync(cancellationToken);
}
// 执行逻辑
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTime.Now);
await Task.Delay(1000, stoppingToken);
}
}
// 停止
public override Task StopAsync(CancellationToken cancellationToken)
{
return base.StopAsync(cancellationToken);
}
}
}
25.3.5 集成 Furion
Worker Service
集成 Furion
非常方便,只需要安装 Furion
的包即可,并在 Program.cs
中调用 .Inject()
方法,如:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace FurionWorkers
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.Inject()
.ConfigureServices((hostContext, services) =>
{
// 以下代码可不用编写,Furion 已实现自动注册 Worker;
// services.AddHostedService<Worker>();
});
}
}
默认情况下,Inject()
方法注册了 日志、缓存、依赖注入、加载配置、自定义 Startup
功能。
集成 Furion
后会自动扫描 Worker
类并实现自动注册。
25.3.6 注册服务
Worker Service
注册服务和 Web
略有不同,Web
主要在 Starup.cs
类中注册,Worker Service
在 Program.cs
启动类的 ConfigureServices
方法中注册,如:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace FurionWorkers
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.Inject()
.ConfigureServices((hostContext, services) =>
{
// 注册数据库服务
services.AddDatabaseAccessor(options =>
{
options.AddDb<DefaultDbContext>();
});
// 注册远程请求
services.AddRemoteRequest();
// 等等其他服务注册
});
}
}
25.4 实现简单定时任务
强烈建议使用 【26.1 调度作业】 章节内容实现强大的分布式定时任务。
Furion
框架为 BackgroundService
提供了定时任务的支持。
25.4.1 间隔执行方式
namespace WorkerService;
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private const int delay = 1000;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(delay, stoppingToken);
var taskFactory = new TaskFactory(System.Threading.Tasks.TaskScheduler.Current);
await taskFactory.StartNew(async () =>
{
// 你的业务代码写到这里面
_logger.LogInformation("Worker running at: {time}", DateTime.Now);
await Task.CompletedTask;
}, stoppingToken);
}
}
}
25.4.2 Cron
表达式执行方式
如需了解 Cron
表达式内容,可查阅 【26.2 Cron 表达式】 章节内容。
using Furion.TimeCrontab;
namespace WorkerService;
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly Crontab _crontab;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
_crontab = Crontab.Parse("* * * * * *", CronStringFormat.WithSeconds);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(_crontab.GetSleepTimeSpan(DateTime.Now), stoppingToken);
var taskFactory = new TaskFactory(System.Threading.Tasks.TaskScheduler.Current);
await taskFactory.StartNew(async () =>
{
// 你的业务代码写到这里面
_logger.LogInformation("Worker running at: {time}", DateTime.Now);
await Task.CompletedTask;
}, stoppingToken);
}
}
}
BackgroundService
方式实现定时任务注意事项通过这种方式只是简单的实现定时任务,但~~不能对线程和时间进行精准控制,可能存在一些不执行或者重复执行等问题~~。
所以,强烈建议使用 【26.1 调度作业】 章节内容实现强大的分布式定时任务。
25.4.3 实现 串行
操作
默认情况下,定时任务都是采用 并行
的方式,也就是不会等待上一次任务完成,如果需要等待上一次任务完成,可以修改为 串行
方式:
using Furion.TimeCrontab;
namespace WorkerService;
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly Crontab _crontab;
private bool _isLock = false;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
_crontab = Crontab.Parse("* * * * * *", CronStringFormat.WithSeconds);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (_isLock) goto next;
_isLock = true;
var taskFactory = new TaskFactory(System.Threading.Tasks.TaskScheduler.Current);
var task = await taskFactory.StartNew(async () =>
{
// 模拟耗时操作
await Task.Delay(2000);
_logger.LogInformation("Worker running at: {time}", DateTime.Now);
await Task.CompletedTask;
}, stoppingToken);
// 等待任务完成
await task.ContinueWith(task => _isLock = false);
next:
await Task.Delay(_crontab.GetSleepTimeSpan(DateTime.Now), stoppingToken);
}
}
}
25.5 依赖注入使用
Worker Service
只为 Worker
提供了单例作用域的服务注入,如果需要注入瞬时或作用域对象,需手动创建作用域,如:
public class Worker : BackgroundService
{
// 日志对象
private readonly ILogger<JobService> _logger;
// 服务工厂
private readonly IServiceScopeFactory _scopeFactory;
public Worker(ILogger<Worker> logger
, IServiceScopeFactory scopeFactory)
{
_logger = logger;
_scopeFactory = scopeFactory;
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
// 放在循环外可以避免高频下频繁创建作用域和解析服务
using var scope = _scopeFactory.CreateScope();
var services = scope.ServiceProvider;
while (!stoppingToken.IsCancellationRequested)
{
// 放在循环内针对频率不是很高的操作
// using var scope = _scopeFactory.CreateScope();
// var services = scope.ServiceProvider;
// 获取数据库上下文
var dbContext = Db.GetDbContext(services);
// 获取仓储
var respository = Db.GetRepository<Person>(services);
// 解析其他服务
var otherService = services.GetService<XXX>();
}
return Task.CompletedTask;
}
}
25.6 如何部署
25.6.1 共宿主方式
共宿主方式方式部署非常简单,只需要部署所在的 Web
或其他应用程序项目即可,会自动随着项目启动自动启动。
如果部署到 IIS
中,可能存在 Worker Service
被回收的情况,毕竟是和网站同一个宿主。
25.6.2 独立 Worker Service
方式
Worker Service
支持部署到 Windows Service
中 或 Linux 守护进程中
部署到 Windows Service
-
第一步:安装
Microsoft.Extensions.Hosting.WindowsServices
拓展包 -
第二步:在
Program.cs
中添加.UseWindowsService()
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace FurionWorkers
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseWindowsService()
.Inject()
.ConfigureServices((hostContext, services) =>
{
// 以下代码可不用编写,Furion 已实现自动注册 Worker;
// services.AddHostedService<Worker>();
});
}
}
- 第三步:发布
Worker Service
,可通过dotnet publish -c Release -o C:\FurionWorker
命令发布或通过Visual Studio 2019
发布。
独立发布不依赖 SDK 方式 dotnet publish -c release -r win10-x64 --framework net6.0
- 第四步:通过
sc.exe
工具来管理并创建Windows
服务,通过 管理员模式 并打开控制台,输入:
sc.exe create FurionWorkerServices binPath= C:\FurionWorker\FurionWorker.exe
注意=
后面要有一个空格
创建成功后可通过 sc.exe query FurionWorkerServices
查看服务状态。
- 第五步
启动服务:sc.exe start FurionWorkerServices
,启动之后就可以在 Windows
服务工具中查看了。
停止服务:sc.exe stop NETCoreDemoWorkerService
删除服务:sc.exe delete NETCoreDemoWorkerService
以上所有 sc.exe
命令必须在 管理员 模式下进行。
sc.exe delete NETCoreDemoWorkerService, 执行删除时候, 把Windows
服务工具关闭, 否则, 电脑重启后才会显示删除;
部署到 Linux 守护程序
-
第一步:安装
Microsoft.Extensions.Hosting.Systemd
拓展包 -
第二步:在
Program.cs
中添加.UseSystemd()
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace FurionWorkers
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSystemd()
.Inject()
.ConfigureServices((hostContext, services) =>
{
// 以下代码可不用编写,Furion 已实现自动注册 Worker;
// services.AddHostedService<Worker>();
});
}
}
部署到 Linux 守护进程
就是这么简单。
25.7 关于 Windows
部署日志问题
默认情况下,使用 Windows Services
部署后,日志文件可能会在系统盘的 System32
下。
25.8 反馈与建议
给 Furion 提 Issue。