提问人:PanosPlat 提问时间:11/8/2023 最后编辑:Theodor ZouliasPanosPlat 更新时间:11/9/2023 访问量:67
如何构建每 30 秒连接到 80 个 SQL Server 的 Windows 服务?[关闭]
How to build a Windows Service that connects to 80 SQL Servers every 30 seconds? [closed]
问:
这是一个纯粹的理论帖子。
我从事 SQL Server 和 PowerBuilder 已有 20 多年了。 有时,当事情无法以其他方式完成时,我会弄乱 C#。
这次,我被要求使用 Windows 服务 C#、SQL Server 和线程调查以下方案。
我们需要构建一个服务,该服务需要每 30 秒从 80 个不同的 SQL Server 并行获取数据*,创建 XML 并发布它们 - 不间断。 他们建议我构建一个创建 80 个线程的服务,每个线程连接到自己的 SQL Server,每个线程都有自己的计时器(30 秒),检索数据,创建 XML 并发送。
从技术角度来看,这是否可行?是否有任何建议可以更好地实现此方案?
谢谢
*这意味着每 30 秒必须重新访问每个 SQL Server 才能执行操作,而不是等待其他 SQL Server 的操作完成
答:
当我阅读您的问题时,我问自己,您的解决方案方法在未来可以在多大程度上得到支持。
我会自发地想到两个选择:
- 一个 PowerShell 脚本,用于建立与 SQL Server 的连接并查询数据。然后,可以将此脚本存储在 Windows 计时器作业中,并使用参数设置 80 个作业。这样,您还可以扩展/停用/删除单个作业
- 为每个 SQL 数据库开发一个 C# Windows 服务,然后使用参数注册 80 个 Windows 服务。
在这两种情况下,您还可以将负载分配到不同的服务器。如果您已经有使用 Docker 甚至 Kubernetes 的经验,您也可以使用它来实现此用例。
此致敬意
评论
await
这个理论还不错,但 80 是一个任意大的数字,像这样的常见场景涉及每个客户 1 个数据库和一个将指标引入中央存储库的过程。因此,如果 80 代表您当前的客户群,那么我们应该计划将其扩展到 80 以上。
在实践中,此任务不是 CPU 密集型的,SQL 负载在服务器上处理,我们最终要做的就是传输数据,这是您将从中执行的 c# 线程之外的另一个外部协议。因此,在这种情况下,多线程是一个昂贵的选择......但它将简化一些代码,并确保每个循环执行都独立于其他循环中可能发生的中断。
问题在于 HTTP 和 SQL 连接在后台的工作方式,我还没有测试过 SQL,但 HTTP 不允许你同时上传 80 个并行线程,其中一些线程会停滞,其他线程将被迫等待。
SQL 应该可以同时运行所有 80 个服务器,因为您正在连接到不同的服务器。
上次我尝试过这个,在 4 次并行上传后,整体吞吐量性能受到了显着影响,并且在 8 次并行上传后几乎没有任何效果......但那是 .Net 的几个版本,这篇文章与我当时的发现相匹配 最大并发 HttpWebRequest 数......
基于队列或批处理的传输对 HTTP 来说效果要好得多,尤其是在使用 HttpClient 时。
因此,使用单个或有限数量的传输队列更好地对 XML 发送进行建模,以管理读取器的多个线程的可能性(我们接下来将讨论)使用 ConcurrentQueue
XmlMessage
在此示例中,除非 XML 有效负载非常小,否则不应保存 XML 有效负载,否则它只会保留检索 XML 文件以进行传输所需的任何元数据。(纯理论权利;)因为当队列为空时,此循环将重新启动计时器,因此在这种情况下,计时器只是为了在没有样式循环的情况下保持进程运行。
AutoReset = false
while(true)
public static readonly ConcurrentQueue<XmlMessage> TransmissionQueue { get; } = new ();
private static System.Timers.Timer _transmissionTimer = new();
private static HttpClient _httpClient = new();
...
_transmissionTimer.Interval = TimeSpan.FromSeconds(5).TotalMilliseconds;
_transmissionTimer.Elapsed += TransmissionTimer_OnElapsed;
_transmissionTimer.AutoReset = false;
_transmissionTimer.Start();
...
public async void TransmissionTimer_OnElapsed(object sender, ElapsedEventArgs e)
{
// TODO: add Try/Catch error handling...
while(TransmissionQueue.TryDeQueue(out XmlMessage message))
{
var xmlData = ResolveXmlFromMessage(message);
var content = new StringContent(xmlData,
Encoding.UTF8, "application/xml");;
await _httpClient.PostAsync(message.TargetUri, content);
}
// go into idle state, wait for more data
_transmissionTimer.Start();
}
由于 HttpClient 的内部结构(请参阅实例化),如果需要多个传输队列,最好显式声明它们,而不是按需创建客户端。如果您的服务器可以处理批处理请求,或者可以引入多个 XML,或者您可以在客户端上将它们合并到一个文件中,那么这将有助于此解决方案进行扩展。否则,您可以创建 4-8 个 HttpClient 实例以及关联的计时器和队列来对处理进行分区。
当你开始分区时,会有一些乐趣,我会创建一个单一的入口点,要么对队列进行轮询,要么对队列之间的消息进行负载均衡。
- 如果传输顺序很重要,并且您有多个队列,则希望使用始终将数据从同一服务器发送到同一队列的算法
private static readonly ConcurrentQueue<XmlMessage> _transmissionQueue0 = new ();
private static readonly ConcurrentQueue<XmlMessage> _transmissionQueue1 = new ();
private static readonly ConcurrentQueue<XmlMessage> _transmissionQueue2 = new ();
private static readonly ConcurrentQueue<XmlMessage> _transmissionQueue3 = new ();
...
public static void Enqueue(XmlMessage message)
{
// using int to simplify the logic, this might be DB or customer number
int sequenceNumber = message.DatabaseSequenceNumber;
switch(sequenceNumber % 4)
{
case 0: _transmissionQueue0.Enqueue(message); break;
case 1: _transmissionQueue1.Enqueue(message); break;
case 2: _transmissionQueue2.Enqueue(message); break;
case 3: _transmissionQueue3.Enqueue(message); break;
}
}
您可以将每个队列、计时器和 http 客户端封装在一个类定义中,这将减少代码重复,但为了理论起见,我们只想强调基础知识。
所以现在我们已经建立了一个异步数据传输管道,接下来我们需要处理读取数据。
如果您绝对必须确保一个数据库中的延迟不会影响另一个数据库,那么请使用自己的计时器和读取逻辑为每个数据库启动一个线程。您仍然应该能够使用上述方法对消息进行排队以进行传递。Enqueue
但是,如果失败的几率很低,并且影响也很低,那么我们可以有一个 30 秒间隔的中央计时器,并使用并行任务从数据库中提取数据。
- 使用 80,我不会使用 ,如果/当它成为问题时,TPL 将让您控制并行度。
await Task.WhenAll()
- 如何:编写简单的 Parallel.ForEach 循环
List<DataSourceInfo> _sources = new();
private void DataSourceTimer_OnElapsed(object sender, ElapsedEventArgs e)
{
Parallel.ForEach(_sources, source =>
{
XmlMessage xml = GetDataFromSource(source);
Enqueue(xml);
});
}
随着规模的扩展,你可能会演变为使用基于云的计时器和队列,或者至少在 c# 运行时之外使用本地化的消息传递总线队列。概念保持不变,尽可能批量处理,分区以确保吞吐量而不会占用资源。
创建 80 个线程来完成所有操作可能会起作用,但它不能很好地扩展,除非您可以将这些线程拆分到多个服务器上,否则您已经明确表示您不想这样做。
每台服务器 1 个 Db 读取器不能很好地利用资源,但 1 台服务器执行 80 个可能会导致争用条件。您可能需要拆分工作负载,以便每个服务器部署不会达到 20 个工作负载...
另一种解决方案是通过服务器上的 SSIS 或 Jobs 将此逻辑推送到数据库服务器本身。不过,它的管理可能会很快变得繁琐,您可以通过 c# 或 powershell 编写脚本......
评论
await