如何构建每 30 秒连接到 80 个 SQL Server 的 Windows 服务?[关闭]

How to build a Windows Service that connects to 80 SQL Servers every 30 seconds? [closed]

提问人:PanosPlat 提问时间:11/8/2023 最后编辑:Theodor ZouliasPanosPlat 更新时间:11/9/2023 访问量:67

问:


想改进这个问题吗?更新问题,以便可以通过编辑这篇文章用事实和引文来回答。

11天前关闭。

这是一个纯粹的理论帖子。

我从事 SQL Server 和 PowerBuilder 已有 20 多年了。 有时,当事情无法以其他方式完成时,我会弄乱 C#。

这次,我被要求使用 Windows 服务 C#、SQL Server 和线程调查以下方案。

我们需要构建一个服务,该服务需要每 30 秒从 80 个不同的 SQL Server 并行获取数据*,创建 XML 并发布它们 - 不间断。 他们建议我构建一个创建 80 个线程的服务,每个线程连接到自己的 SQL Server,每个线程都有自己的计时器(30 秒),检索数据,创建 XML 并发送。

从技术角度来看,这是否可行?是否有任何建议可以更好地实现此方案?

谢谢

*这意味着每 30 秒必须重新访问每个 SQL Server 才能执行操作,而不是等待其他 SQL Server 的操作完成

C# SQL-Server 多线程并 行处理 Windows-services

评论

1赞 Zohar Peled 11/8/2023
无论如何,我都不是多线程专家,但 80 对于线程来说听起来是一个非常大的数字。我还猜测某些数据可能被缓存,因此我建议探索更改跟踪。此外,是否需要为每个数据库(或 SQL Server 实例)创建一个 XML,还是需要从从所有数据库收集的数据创建一个 XML?
0赞 PanosPlat 11/8/2023
谢谢。XML 是每个 SQL Server 的,并发送给不同的供应商。
0赞 Zohar Peled 11/8/2023
所以基本上你可以把它想象成 80 个不同的服务,每个服务都从自己的数据库中读取自己的数据,创建自己的 XML 并将其发送给自己的客户,对吧?
0赞 PanosPlat 11/8/2023
是的。就是这个想法。我们想要一个服务来统治它们,哈哈哈。因此,一台物理计算机,一个代码库,80 个 SQL Server 提供服务。
1赞 Charlieface 11/8/2023
你不需要线程。使用任务和流式传输所有结果,这将使内存使用率和上下文切换保持在较低水平。这在高性能服务器上可能是可行的。await

答:

0赞 AK94 11/8/2023 #1

当我阅读您的问题时,我问自己,您的解决方案方法在未来可以在多大程度上得到支持。

我会自发地想到两个选择:

  1. 一个 PowerShell 脚本,用于建立与 SQL Server 的连接并查询数据。然后,可以将此脚本存储在 Windows 计时器作业中,并使用参数设置 80 个作业。这样,您还可以扩展/停用/删除单个作业
  2. 为每个 SQL 数据库开发一个 C# Windows 服务,然后使用参数注册 80 个 Windows 服务。

在这两种情况下,您还可以将负载分配到不同的服务器。如果您已经有使用 Docker 甚至 Kubernetes 的经验,您也可以使用它来实现此用例。

此致敬意

评论

1赞 PanosPlat 11/8/2023
谢谢。不过有一个问题。是否可以在一台 PC 中安装 Windows 服务 80 次(每个服务具有不同的参数)?我们试图解决的问题是,到目前为止,我们为每个 SQL Server 提供了一个 Windows 服务,因此我们需要 80 台 PC 来托管它们。
0赞 AK94 11/8/2023
可以安装多个执行相同操作的 Windows 服务。您只需要注意以不同的方式命名它们。
0赞 PanosPlat 11/8/2023
这就是问题所在。我们需要一个代码库、一个安装程序、一台 PC 来服务于一切。我们=我的雇主:)
2赞 AK94 11/8/2023
但这不是问题。您可以有一个接受输入参数的代码库。一个安装程序也没有问题。在 Windows 或 LInux 中,您可以轻松地从 cli 中安装服务并传递名称。使用不同的名称,如果 PC 有足够的通信等,您还可以在 1 台 PC 上运行所有这些服务。对于 Windows:stackoverflow.com/questions/8164859/...Ubuntu 示例:medium.com/@benmorel/...
0赞 Charlieface 11/8/2023
Powershell 速度较慢,不能多线程或轻松使用。80 个服务意味着 80 个进程,这意味着大量的上下文切换。await
-1赞 Chris Schaller 11/8/2023 #2

这个理论还不错,但 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 = falsewhile(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 秒间隔的中央计时器,并使用并行任务从数据库中提取数据。

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 编写脚本......