65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 动态软件负载均衡

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (106投票s)

2002年12月10日

71分钟阅读

viewsIcon

521362

downloadIcon

4561

关于 .NET 动态软件负载均衡构想的初步实现

本文内容

  1. 引言
    1. 集群和负载均衡的简短介绍
    2. 我关于动态软件负载均衡的想法
  2. 架构和实现
    1. 架构展望
    2. 程序集和类型
    3. 协作
    4. 一些实现细节
  3. 负载均衡实战 - 平衡 Web 农场
  4. 构建、配置和部署解决方案
    1. 配置
    2. 部署
  5. 关于 MC++ 和 C# 的一些想法
    1. 托管 C++ 到 C# 的转换
    2. C# 的 readonly 字段 vs MC++ 非静态 const 成员
  6. "Bug 糟透了。就是这样。"
  7. 待办事项
  8. 结论
    1. 关于 C# 的(最后)一句话
  9. 免责声明

"成功是屡败屡战而不失热忱的能力。"
温斯顿·丘吉尔

简介

<blog date="2002-12-05"> 太棒了!我今天通过了 70-320 考试,现在是 MCAD.NET。预计下一篇文章将涵盖 XML Web 服务、远程处理或服务组件:) </blog>

本文是关于负载均衡的。既不是“揭秘”,也不是“定义”——而是“实现”:) 我不打算详细讨论什么是负载均衡、它的不同类型或各种负载均衡算法。我也不打算谈论 WLBS、MSCS、COM+ 负载均衡或 Application Center 等专有软件。我将在本文中向您展示的是我不到一周内实现的自定义 .NET 动态软件负载均衡解决方案,以及我为了使其工作而必须解决的问题。尽管源代码只有大约 4 KLOC,但在本文结束时,您将看到该解决方案足以平衡 Web 农场中 Web 服务器的负载。祝您阅读愉快...

每个人都可以阅读这篇文章

...但并非每个人都能理解所有内容。要阅读并理解本文,您需要了解负载均衡的总体概念,但即使您不了解,我也会简要解释一下——所以请继续阅读。要阅读代码,您应该对多线程和网络编程(TCP、UDP 和组播)有一定的经验,并对 .NET Remoting 有基本了解。与 C# 开发者所想的相反,您不需要了解托管 C++ 即可阅读代码。当您编写纯托管代码时,C# 和 MC++ 源代码看起来几乎相同,只有很少的差异,所以我甚至为 C# 开发者提供了一个章节,解释如何将(大部分)MC++ 代码转换为 C#。

在您专注于文章之前,最后一条警告——我不是专业作家,我只是一个开发人员,所以不要对我期望过高(这是我的第三篇文章)。如果您觉得有些地方不理解,那可能是我不是以英语为母语的人(我是保加利亚人),所以我无法表达我的想法。如果您发现语法错误,甚至是错别字,请将其作为 bug 报告给我,我将非常乐意“修复”它。感谢您耐心阅读本段!

集群和负载均衡的简短介绍

对于那些对负载均衡一无所知的人,我将简要解释集群和负载均衡。确实非常简短,因为我没有时间写更多关于它的内容,也因为我不想用枯燥的文字浪费文章的空间。您正在 CodeProject.com 上阅读文章,而不是 ArticleProject.com 上阅读文章:) 了解的人可以跳过下一段,我鼓励其他人阅读。

关键任务应用程序必须 24x7 运行,网络需要能够扩展性能以处理大量客户端请求而不会出现不必要的延迟。“服务器集群”是一组独立的服务器,作为一个系统进行管理,以实现更高的可用性、更易于管理和更强的可伸缩性。它由两台或更多通过网络连接的服务器以及集群管理软件(如 WLBS、MSCS 或 Application Center)组成。该软件提供故障检测、恢复、负载均衡和将服务器作为一个系统进行管理等服务。负载均衡是一种技术,通过将客户端请求分配到计算机集群中的多个服务器上,来扩展基于服务器的程序(如 Web 服务器)的性能。负载均衡用于增强可伸缩性,从而在保持低响应时间的同时提高吞吐量。

我应该警告您,我没有实现完整的集群软件,只实现了其中的负载均衡部分,所以不要期望更多。既然您对负载均衡有了一些了解,我确定您不知道我实现它的想法是什么。所以请继续阅读...

我关于动态软件负载均衡的想法

我们怎么知道一台机器很忙?当我们觉得机器变得非常慢时,我们会启动任务管理器寻找 iexplore.exe 的挂起实例:) 严肃地说,我们看 CPU 利用率。如果它很低,那么内存很低,并且磁盘肯定在交换。如果我们怀疑还有其他原因,我们会运行系统监视器并添加一些性能计数器来查看。好吧,这在你机器旁边并且只有一两台机器要监视的情况下有效。当你有更多的机器时,你将不得不雇佣一个人,给他买一副 20 屈光度的眼镜,让他盯着所有机器的系统监视器控制台,然后大约一周后他就会疯掉:)。但是,即使你能持续监视你的机器,你也无法手动分配它们的工作负载,对吧?你可以使用一些昂贵的软件来平衡它们的负载,但我向你保证你可以自己做到,这就是本文的全部内容。当你能“看到”性能计数器时,你也可以通过编程方式收集它们的值。我认为,如果我们以某种方式组合其中一些值并进行一些计算,它们可以给你一个值,可以用来确定机器的负载。让我们看看这是否可行!

让我们监控 `\\Processor\% Processor Time\_Total` 和 `\\Processor\% User Time\_Total` 性能计数器。您可以通过启动任务管理器并在“性能”选项卡中查看 CPU 利用率来监控它们。(红色曲线显示 % 处理器时间,绿色曲线显示 % 用户时间)。停止或暂停所有 CPU 密集型应用程序(WinAMP、MediaPlayer 等),然后开始监控 CPU 利用率。您已经注意到计数器值几乎保持不变,对吗?现在,关闭任务管理器,等待大约 5 秒钟,然后再次启动它。您应该会注意到 CPU 利用率有一个很大的峰值。几秒钟后,峰值消失了。现在,如果我们立即报告性能计数器值(当我们获得每个计数器样本时),人们可能会认为我们的机器在那一刻非常忙碌(几乎 100%),对吗?这就是为什么我们不报告瞬时值,而是会收集几个计数器值的样本并报告它们的平均值。这样够公平,您不觉得吗?不?!我也不觉得,我只是在测试您:) 那么可用内存、I/O 等呢?因为 CPU 利用率不足以真实地计算机器的工作负载,所以我们应该一次监控多个计数器,对吗?而且,假设当前 ASP.NET 会话数不如 CPU 利用率重要,我们将为每个计数器分配一个权重。现在机器负载将计算为所有被监控性能计数器的加权平均值之和。您现在应该已经猜到我动态软件负载均衡的想法了。然而,一张图片胜过千言万语,而 ASCII 图片胜过两千字:) 这里是一个真实的示例和机器负载计算算法。在下面的示例中,机器负载是通过监控 4 个性能计数器计算的,每个计数器都配置为以相等的时间间隔收集其下一个样本值,并且所有计数器都收集相同数量的样本(这将是您的常见情况)

+-----------+  +-----------+  +-----------+  +-----------+
|% Proc Time|  |% User Time|  |ASP Req.Ex.|  |% Disk Time|
+-----------+  +-----------+  +-----------+  +-----------+
|Weight  0.4|  |Weight  0.3|  |Weight  0.2|  |Weight  0.5|
+-----------+  +-----------+  +-----------+  +-----------+
|         16|  |         55|  |         11|  |         15|
|         22|  |         20|  |          3|  |          7|
|          8|  |         32|  |         44|  |          4|
|         11|  |         15|  |         16|  |         21|
|         18|  |         38|  |         21|  |          3|
+-----+-----+  +-----+-----+  +-----+-----+  +-----+-----+
| Sum |   75|  | Sum |  160|  | Sum |   95|  | Sum |   50|
+-----+-----+  +-----+-----+  +-----+-----+  +-----+-----+
| Avg |   15|  | Avg |   32|  | Avg |   19|  | Avg |   10|
+-----+-----+  +-----+-----+  +-----+-----+  +-----+-----+
| WA  |  6.0|  | WA  |  9.6|  | WA  |  3.8|  | WA  |  5.0|
+-----+-----+  +-----+-----+  +-----+-----+  +-----+-----+

图例

Sum
所有计数器样本的总和
平均值
所有计数器样本的平均值 (总和/计数)
加权平均值
所有计数器样本的加权平均值 (总和/计数 * 权重)
处理器时间百分比
(处理器\% 处理器时间\_总计),处理器执行非空闲线程所花费的百分比。它通过测量空闲线程在采样间隔内处于活动状态的持续时间,然后从间隔持续时间中减去该时间来计算。(每个处理器都有一个空闲线程,当没有其他线程准备运行时,它会消耗周期)。此计数器是处理器活动的主要指标,显示在采样间隔内观察到的平均繁忙时间百分比。它通过监控服务不活动的时间,然后从 100% 中减去该值来计算
用户时间百分比
(处理器\% 用户时间\_总计)是处理器在用户模式下花费的百分比。用户模式是一种受限制的处理模式,专为应用程序、环境子系统和集成子系统设计。另一种特权模式,专为操作系统组件设计,允许直接访问硬件和所有内存。操作系统将应用程序线程切换到特权模式以访问操作系统服务。此计数器显示平均繁忙时间占采样时间的百分比
ASP 请求执行数
(ASP.NET 应用程序\请求执行数\__总计__)是当前正在执行的请求数
磁盘时间百分比
(逻辑磁盘\% 磁盘时间\_总计)是所选磁盘驱动器忙于处理读写请求所花费时间的百分比

总和(处理器时间百分比)= 16 + 22 + 8 + 11 + 18 = 75
平均值(处理器时间百分比)= 75 / 5 = 15
加权平均值(处理器时间百分比)= 15 * 0.4 = 6.0
...
机器负载 = Sum (加权平均值 (每个计数器))
机器负载 = 6.0 + 9.6 + 3.8 + 5.0 = 24.4

架构与实现

我花了半天时间思考如何向您解释这个架构。倒不是它有多复杂,而是因为它会占用文章太多空间,而我想向您展示一些代码,而不是技术规范,甚至不是 DSS。所以我一直在想是采用“自顶向下”还是“自底向上”的方法来解释架构,或者我应该另辟蹊径?最终,正如你们大多数人已经猜到的那样,我决定用我自己的混合方式来解释它:) 首先,您应该了解该解决方案由哪些程序集组成,然后您可以阅读它们的协作、它们包含的类型等等...甚至在此之前,我建议您阅读并理解我在整篇文章(和源代码注释)中使用的两个术语。

机器负载
一台机器的总体工作负载(利用率)——在我们的案例中,这是所有性能计数器(为负载均衡而监控)加权平均值的总和;如果您跳过了“我关于动态软件负载均衡的想法”部分,您可能希望返回并阅读它
最快的机器
负载最小的机器

架构展望

首先,我想为这些“图表”道歉。我只能使用两种软件产品来绘制本文所需的图表。第一种我买不起(我的公司也不愿意为它买单:),第二种把我弄得太恼火了,以至于我放弃了文章中的一个 UML 静态结构图、一个 UML 部署图和几个活动图(它们几乎完成了)。我不会告诉你这个产品的名字,因为我非常喜欢开发它的公司。请接受我的歉意,以及取代原始图表的伪 ASCII 艺术。抱歉:)

负载均衡软件分为三部分:一个服务器,报告其运行机器的负载;一个服务器,收集这些负载,无论它们来自哪台机器;以及一个库,询问收集服务器哪台机器负载最低(最快)。报告机器负载的服务器称为“机器负载报告服务器”(MLRS),收集机器负载的服务器称为“机器负载监控服务器”(MLMS)。该库的名称是“负载均衡库”(LBL)。您可以根据需要部署这三部分软件。例如,您可以将它们全部安装在所有机器上。

每台机器上的 MLRS 服务器都会加入一个专门用于负载均衡目的的组播组,并将包含机器负载的消息发送到该组的组播 IP 地址。由于所有 MLMS 服务器在启动时都加入同一个组,它们都会接收到每台机器的负载,因此如果您在所有机器上同时运行 MLRS 和 MLMS 服务器,它们将彼此了解对方的负载。那又怎样?我们有了机器负载,但我们用它们做什么呢?好吧,所有 MLMS 服务器都将机器负载存储在一个特殊的数据结构中,该结构允许它们随时快速检索负载最小的机器。所以现在所有机器都知道哪台机器最快。谁在乎呢?我们还没有真正利用这些信息来平衡任何负载,对吧?我们如何查询 MLMS 服务器哪台机器最快呢?答案是每个 MLMS 都使用 .NET Remoting 运行时注册一个特殊的单例对象,因此 LBL 可以创建(或获取)该对象的一个实例,并向其询问负载最小的机器。问题是 LBL 不能同时向所有机器询问这个问题(目前还不能,但我正在思考这个问题),所以它应该选择一台机器(当然,它可以是它自己正在运行的机器),并将该负载交给需要该信息来执行任何适合的负载均衡活动的客户端应用程序。正如您稍后将看到的,我已在 Web 应用程序中使用 LBL 来分配 Web 农场中所有 Web 服务器之间的工作负载。下面是一个“图表”,它概括地描述了服务器和库之间的协作

     +-----+          ______          +-----+
     |  A  |       __/      \__       |  B  |
     +-----+    __/            \__    +-----+
 +-->| LMS |<--/     Multicast    \-->| LMS |<--+
 |   |     |   /                  \   |     |   |
 |   | LRS |-->\__     Group    __/   |     |   |
 |   |     |      \__        __/      |     |   |
 |<--| LBL |       ^ \______/         | LBL |---+
 |   +-----+       |                  +-----+
 |                 |  +-----+
 |                 |  |  C  |
 |                 |  +-----+
 |                 |  |     |
 |                 |  |     |
 |                 +--| LRS |
 |     Remoting       |     |
 +--------------------| LBL |
                      +-----+

MLMS、MLRS 和 LBL 通信

注意:您应该将机器之间奇怪的图形视为云,也就是说它代表一个局域网 :) 还有一件事——如果您不理解什么是组播,请不要担心,稍后会在协作部分进行解释。

现在再看一遍“图表”。让我提醒您,当一台机器加入一个多播组时,它会收到发送到该组的所有消息,包括该机器自己发送的消息。机器 A 接收自己的负载,以及 C 报告的负载。机器 B 接收 A 和 C 的负载(它不报告自己的负载,因为它上面没有安装 MLRS 服务器)。机器 C 不接收任何东西,因为它没有安装 MLMS 服务器。因为机器 C 的 LBL 应该(通过远程处理)连接到 MLMS 服务器,并且它没有安装这样的服务器,所以它可以连接到机器 A 或 B 并查询远程对象以获取最快的机器。在上面的“图表”中,A 和 C 的 LBL 与机器 A 上的远程对象通信,而 B 的 LBL 与其机器上的远程对象通信。正如您稍后在配置部分将看到的,解决方案源代码中硬编码的东西很少,所以不用担心——您将能够调整几乎所有内容。

程序集与类型

该解决方案由 8 个程序集组成,但目前我们只对其中三个感兴趣:MLMS、MLRS 和 LBL,它们分别位于两个控制台应用程序(MachineLoadMonitoringServer.exeMachineLoadReportingServer.exe)和一个动态链接库(LoadBalancingLibrary.dll)中。令人惊讶的是,MLMS 和 MLRS 不包含任何类型。然而,它们使用多种类型来完成工作。您可能会想知道我为什么这样设计它们。为什么我没有直接在可执行文件中实现这两个服务器。好吧,答案很简单,它反映了我作为开发人员的优势和劣势。如果您有时间阅读,请继续,否则点击此处跳过这个小插曲。

GUI 编程是我讨厌的(尽管我写了一堆 GUI 应用程序)。对我来说,这是一项乏味的工作,更适合设计师而不是开发人员。我喜欢构建复杂的“东西”。服务器端应用程序是我的最爱。多线程、异步编程——那是我喜欢的“东西”。罕见的应用程序,除了少数管理员之外,没有人能“看到”,管理员使用某种管理控制台配置和/或控制它们。如果这些应用程序按预期工作,最终用户几乎永远不会知道他们正在使用它们(例如,在大多数情况下,浏览网站的用户不会意识到 IIS 或 Apache 服务器正在处理他们的请求并提供内容)。现在,我过去写过几个 Windows C++ 服务,最近也写过一些 .NET Windows 服务,所以我可以很容易地将 MLMS 和 MLRS 转换为其中之一。另一方面,我非常喜欢控制台 (CUI) 应用程序,我喜欢在控制台上看到数百条跟踪消息,所以我将 MLMS 和 MLRS 保留为 CUI 形式,原因有二。第一个原因是,当出现问题时(至少会发生一次:)),您可以快速发现问题所在,第二个原因是我没有调试过 .NET Windows 服务(而且由于我调试过 C++ Windows 服务,我可以向您保证这绝非“小菜一碟”)。尽管如此,任何人都可以轻松地在半小时内将这两个 CUI 应用程序转换为 Windows 服务。我没有在可执行文件中实现服务器类,是为了让将它们转换为 Windows 服务的人更容易。他/她只需要在 Window Service 类的代码中编写 4 行代码即可完成工作

  1. 声明服务器成员变量
    LoadXxxServer __gc* server;
    
  2. 在重写的 OnStart 方法中实例化并启动它
        server = new LoadXxxServer ();
        server->Start ();
    
  3. 在重写的 OnStop 方法中停止它
        server->Stop ();
    

XxxMonitoringReporting。我相信您现在明白为什么我将服务器代码实现到单独库中的单独类中,而不是直接在可执行文件中。

我上面提到解决方案由 8 个程序集组成,但你记得其中 2 个(CUI)不包含任何类型,其中一个是 LBL,那么另外 5 个是什么?MLMS 和 MLRS 分别使用库 LoadMonitoringLibrary (LML) 和 LoadReportingLibrary (LRL) 中包含的类型。另一方面,它们和 LBL 使用共享程序集 SharedLibrary (SL) 中的公共类型。所以现在程序集是 MLMS + MLRS + LML + LRL + LBL + SL = 6。第 7 个是我用来测试负载均衡的简单 CUI(不重要)应用程序,所以我将跳过它。最后一个程序集是演示负载均衡实际运行的 Web 应用程序。下面是包含负载均衡解决方案实现所需类型和逻辑的四个最重要程序集的列表。

SharedLibary (SL) - 包含 LML、LRL 和/或 LBL 使用的通用和辅助类型。以下是类型列表(进一步解释)

  • ServerStatus - 枚举,由 LML 和 LRL 的 LoadXxxServer 类使用
  • WorkerDoneEventHandler - 委托,同上
  • Configurator - 实用程序类(稍后讨论),同上
  • CounterInfo - “结构体”类,由 LRL 和 SL 使用
  • ILoadBalancer - 接口,在 LML 中实现并由 LBL 使用
  • IpHelper - 实用程序类,由 LML 和 LRL 使用
  • MachineLoad - “结构体”类(为 Remoting 运行时需要而具有 MarshalByValue 语义),由 LML、LRL 和 LBL 使用
  • Tracer - 实用程序类,LML 和 LRL 中的大多数类都继承它,以便以一致的方式在控制台中进行跟踪

注意: CounterInfo 不完全是 C++ 开发者所称的“结构体”类,因为它在幕后做了很多工作。它的实现并非微不足道,并且包括计时器、同步和性能计数器监控等主题;有关更多信息,请参阅一些实现细节部分。

LoadMonitoringLibrary (LML) - 包含 LoadMonitoringServer (LMS) 类,由 MLMS 直接使用,以及 LMS 类内部使用的所有类。以下是 LML 类型的列表(进一步解释)

  • LoadMonitoringServer - (LMS) 类,MLMS 核心
  • MachineLoadsCollection - 优先队列的模拟,以排序方式存储机器负载,以便能够快速返回负载最小的机器(其实现比其名称更有趣)
  • LoadMapping - “结构体”类,由 MachineLoadsCollection 内部使用
  • CollectorWorker - 实用程序类,其唯一(公共)方法是接受和收集机器负载报告的工作线程
  • ReporterWorker - 实用程序类,其唯一(公共)方法是接受 LBL 请求并报告机器负载的工作线程
  • WorkerTcpState - “结构体”类,由 CollectorWorker 内部使用
  • WorkerUdpState - “结构体”类,由 ReporterWorker 内部使用
  • ServerLoadBalancer - 一个特殊的启用远程处理 (MarshalByRefObject) 类,它被注册为单例用于远程处理,并在服务器端由 LBL 激活以处理其请求

注意: 我曾使用 ReporterWorker 以更快、更简陋的方式实现 LBL 的第一个版本,但后来我放弃了它;现在,LMS 注册了一个单例对象用于 LBL 请求;然而,LMS 仍然使用(功能齐全的)ReporterWorker 类,因此可以构建另一种 LBL,通过简单的 TCP socket 连接到 MLMS 并询问负载最小的机器(我很抱歉我覆盖了旧的 LBL 库)。

LoadReportingLibrary (LRL) - 包含 LoadReportingServer (LRS) 类,由 MLRS 直接使用,以及 LRS 类内部使用的所有类。以下是 LRL 类型的列表(进一步解释)

  • LoadReportingServer - 类,MLRS 核心
  • ReportingWorker - 实用程序类,其唯一(公共)方法是工作线程,该线程启动性能计数器监控,并定期向一个或多个 MLMS 报告本地机器的负载

LoadBalancingLibrary (LBL) - 仅包含一个类 ClientLoadBalancer,由客户端应用程序实例化;该类仅包含一个(公共)方法,其名称“令人惊讶地”为 GetLeastMachineLoad,返回负载最小的机器 :) LBL 通过 Remoting 连接到 LML 的 ServerLoadBalancer 单例对象。有关更多详细信息,请阅读以下部分

协作

为了理解对象如何在程序集内部和程序集之间(以及不同机器上)相互“通信”,您需要理解一些技术术语。因为它们大约占了一页,而且你们大多数人可能都知道它们的意思,所以我将这样做:我将给您一个术语列表,如果您知道它们,请点击这里阅读协作部分,否则,请继续阅读...这些术语是:委托、工作者、TCP、UDP、(IP)组播和远程处理。

委托
一种安全、类型安全的方法,通过对方法的“引用”间接调用类的某个方法;与 C/C++ 函数指针(回调)非常相似,同时又有所不同;
工作者
实用程序类,通常只有一个方法,作为单独的线程启动;该类保存线程完成其工作所需的数据(即状态);
TCP
一种基于连接、面向流的传输协议,具有端到端错误检测和纠正功能。基于连接意味着在交换数据之前,主机之间会建立通信会话。主机是 TCP/IP 网络上由逻辑 IP 地址标识的任何设备。TCP 提供可靠的数据传输和易用性。具体而言,TCP 会通知发送方数据包的传输情况,保证数据包以发送时的相同顺序传输,重传丢失的数据包,并确保数据包不会重复;
UDP
一种无连接、不可靠的传输协议。无连接意味着主机之间在交换数据之前不建立通信会话。UDP 通常用于使用广播或多播 IP 数据报的一对多通信。UDP 无连接数据报传输服务不可靠,因为它不保证数据包传输,并且如果数据包未传输则不发送通知。此外,UDP 不保证数据包以发送时的相同顺序传输。由于不保证 UDP 数据报的传输,因此使用 UDP 的应用程序如果需要可靠性,则必须提供自己的机制。尽管 UDP 似乎有一些限制,但在某些情况下它很有用。例如,Winsock IP 多播是使用 UDP 数据报类型套接字实现的。UDP 由于开销低而效率很高。Microsoft 网络使用 UDP 进行登录、浏览和名称解析;
多播 (Multicasting)
一种技术,允许数据从一个主机发送,然后复制到许多其他主机,而不会造成网络流量噩梦。这项技术是作为广播的替代方案开发的,如果大量使用广播可能会对网络带宽产生负面影响。多播数据仅在网络中工作站上运行的进程对该数据感兴趣时才复制到网络。并非所有协议都支持多播概念——在 Win32 平台上,只有两种协议能够支持多播流量:IP 和 ATM;
IP 组播
IP 组播依赖于一组特殊的地址,称为组播地址。正是这个组地址命名了一个给定的组。例如,如果五台机器都想通过 IP 组播相互通信,它们都加入同一个组地址。一旦它们加入,一台机器发送的任何数据都会复制到该组的每个成员,包括发送数据的机器。组播 IP 地址是 224.0.0.0 到 239.255.255.255 范围内的 D 类 IP 地址
Remoting
无论它们是否在同一台计算机上,在不同操作系统进程之间进行通信的过程。.NET 远程处理系统是一种架构,旨在简化位于不同应用程序域(无论是否在同一台计算机上)以及不同上下文(无论是否在同一应用程序域中)中的对象之间的通信。

我将从内到外开始,即我将首先解释各个类如何在程序集内部相互通信,然后我将解释程序集之间如何协作。

程序集内协作(线程同步操作指南:)

LoadReportingServerLoadMonitoringServer 类被实例化并调用其 Start 方法时,它们会分别启动一个或两个线程以异步完成其工作(当然,也能够响应“停止”命令)。启动线程非常容易,但控制它却不那么容易。例如,当服务器需要停止时,它们应该通知线程它们即将停止,以便线程可以完成其工作并适当退出。另一方面,当服务器启动线程时,它们应该在线程即将进入其线程循环并执行其初始化代码时得到通知。在接下来的几段中,我将解释我是如何解决这些同步问题的,如果您知道更酷的方法,请通过下面的留言板告诉我。在下面的段落中,我将把 LoadReportingServerLoadMonitoringServer 类的实例称为“(该)服务器”。

Start 方法执行时,LMS 对象创建一个 worker 类实例,将其自身 (this) 的引用、一个委托的引用以及本节不感兴趣的其他一些有用变量传递给它。然后,服务器对象创建一个处于未触发状态的 AutoResetEvent 对象。接着,LMS 对象启动一个新线程,将 worker 类中方法的地址作为 ThreadStart 委托传递。(我将作为线程启动的 worker 类方法称为 worker 线程。)线程启动后,服务器对象阻塞,(无限期地)等待事件对象被触发。现在,当线程的初始化代码完成时,它通过服务器提供的委托回调服务器,传递一个布尔参数,指示其初始化代码是成功执行还是出现问题。委托在服务器类中的目标方法设置(置为触发状态)AutoResetEvent 对象,并在一个私有布尔成员中记录线程初始化的结果。设置事件对象会解除服务器的阻塞:它现在知道线程的启动代码已完成,并且还知道线程初始化的结果。如果线程未能成功初始化,它就已经退出,服务器只是停止。如果线程成功初始化,它就会进入其线程循环,等待服务器通知它何时应该退出循环(即服务器正在停止)。有人可能会争辩说这种“worker 线程到主线程”的同步看起来过于复杂,他可能是对的。如果我们只需要知道线程已经完成了初始化代码(并且不关心它是否成功初始化),我们可以直接将 AutoResetEvent 对象的引用传递给 worker,然后线程会将其设置为触发状态,但是您看到我们需要知道线程是否成功初始化。

现在这是更复杂的部分。我们现在唯一需要解决的问题是如何停止线程,即让它退出其线程循环。好吧,这就是我所说的小菜一碟。如果您还记得,服务器已经将自己的引用 (this) 传递给了 worker。服务器有一个 Status 属性,它是一个枚举,描述了服务器的状态(启动中、已启动、停止中、已停止)。由于线程拥有服务器的引用,在其线程循环中,它会检查(通过调用 Status 属性)服务器是否即将停止(Status == ServerStatus::Stopping)。如果服务器正在停止,那么线程也会停止,即线程安静地退出,一切正常。因此,当服务器被请求停止时,它会将其私有成员变量 status 修改为 Stopping,并在配置的时间间隔内 Join 线程(等待线程退出)。如果线程在指定时间内退出,服务器会将其状态更改为 Stopped,然后我们完成。但是,线程在处理请求时可能会超时,因此服务器会通过调用线程的 Abort 方法来中止线程。我将线程循环编写在 try...catch...finally 块中,并在其 catch 子句中,线程会检查它们是否死于非命:),即服务器引发了 ThreadAbortException。然后线程会执行其清理代码并退出。(我还以为这更容易解释呢:)

这就是服务器类如何与工作类(主线程到工作线程)通信。程序集中其余的对象通过相互引用或通过委托进行通信。现在是解释程序集如何“相互通信”的部分,即 MLRS 如何将其机器负载发送到 MLMS,以及 LBL 如何从 MLMS 获取最小机器负载。

跨机器程序集协作

我将首先“谈谈”MLRS 如何向 MLMS 报告机器负载。为了节省文章的一些空间(您的一些带宽,以及我的一些打字工作:),我将把 LoadReportingServer 类称为 LRS,将 LoadMonitoringServer 类称为 LMS。请勿将它们与带有“M”前缀的服务器应用程序混淆。

LMS 启动两个工作线程。一个用于收集机器负载,另一个用于向感兴趣的客户端报告最小负载。前者名为 CollectorWorker,后者名为 ReporterWorker。我之前在某处提到 ReporterWorker 不那么有趣,所以我只谈谈 CollectorWorker。在下面的段落中,我将简单地称它为收集器。当收集器线程启动时,它会创建一个 UDP 套接字,在本地绑定它并将其添加到多播组。这就是收集器的初始化代码。然后它进入一个线程循环,定期轮询套接字以获取到达的请求。当请求到来时,收集器读取传入数据,解析它,验证它,如果它是一个有效的机器负载报告,它将负载输入到 LMS 类的机器负载集合中。这就是您需要了解的关于 MLMS 如何接受 MLRS 的机器负载的全部内容。

LRS 启动一个线程来报告当前机器负载。该工作者的名称是 ReportingWorker,我将其称为报告器。该线程的初始化代码是开始监控性能计数器,创建一个 UDP 套接字,并使其成为与 MLMS 的收集器对象加入的同一多播组的成员。在其线程循环中,报告器等待预定义的时间,然后获取当前机器负载并将其发送到多播端点。一个名为“交换机”的网络设备然后将负载分发到所有已加入多播组的机器,即所有 MLMS 收集器都将接收到负载,包括在 MLRS 机器上运行的 MLMS(如果那里已安装并运行 MLMS)。

现在来到最有趣的部分——LBL 如何查询哪个是负载最小的机器(最快的机器)。好吧,这很简单,只需要对 .NET Remoting 有基本了解。如果您不理解 Remoting,但您理解 DCOM,那么假设 .NET Remoting 与 DCOM 的关系就像 C++ 与 C 的关系。您会非常接近,同时又与 Remoting 的真实面貌相去甚远,但您会明白它的意思。(事实上,我读了几本关于 DCOM 的书,其中一些将其称为“COM Remoting 基础设施”)。当 MLMS 启动时,它会将一个名为 ServerLoadBalancer 的类作为单例(一个只实例化一次,后续创建请求都会获取对同一个先前实例化对象的引用)注册到 Remoting 运行时。当获取最快机器的请求到来时(调用 GetLeastMachineLoad 方法),单例会要求 MachineLoadsCollection 对象返回其最小负载,然后将其交给发起远程调用的客户端对象。

下面是一个你可能想听的关于需要无参数构造函数的远程对象的故事。如果你想跳过这个故事,点击这里,否则请欣赏...

现在你们都知道一个对象可以被注册进行远程处理,但可能没有多少人知道你对对象的实例化没有简单的控制权。这意味着你不会创建单例对象的一个实例并将其注册到远程处理运行时,而是远程处理运行时在收到第一个对象创建请求时创建该对象。现在,所有服务器激活的对象都必须有一个无参数构造函数,单例也不例外。但是我们想将我们的 ServerLoadBalancer 类传递给机器负载集合的引用。我只有两种方法可以做到这一点——第一种是将对象注册到远程处理运行时,通过远程处理创建它的一个实例并调用一个“内部”方法 Initialize,将机器负载集合传递给它。最初这听起来是个好主意,我就这样做了。然后我先启动了客户端测试应用程序,然后启动了服务器。你能猜到发生了什么吗?客户端首先创建了单例,但它没有被初始化——轰!!!这不是我们期望的,对吧?所以我思考了一下如何找到一个解决方案。幸运的是,我想到了如何解决这个问题。我决定将 LoadMonitoringServer 类的一个静态成员,它将保存机器负载集合。最初它将是一个 null 引用,然后当服务器启动时,我将它设置为服务器的机器负载集合。现在当我们的“无参数构造”单例对象第一次被远程处理运行时实例化时,它将通过 LoadMonitoringServer::StaticMachineLoads 成员变量获取机器负载,整个问题就消失了。我只需要将静态成员变量标记为 (private public),这样它只在程序集内部可见。我知道我的方法是一种hack,如果您知道更好的模式可以解决我的问题,我很乐意学习它。

还有一个有趣的问题。客户端 (LBL) 如何针对远程 ServerLoadBalancer 类进行编译?它应该引用 (#using "...dll") LML 还是其他什么?嗯,这个问题有一个解决方案,而且我没有发明它,尽管我很想:) 我之前提到,SharedLibrary 包含 LBL、LMS 和 LRS 使用的一些共享类型。不,这不是您想的那样!我没有将 ServerLoadBalancer 类放在那里,即使我想放也因为需要 MachineLoadsCollection 类,而后者位于 LML 中。我所认为的优雅解决方案(以及我所做的)是在 SharedLibrary 中定义一个接口,并在 LML 的 ServerLoadBalancer 类中实现了它。LBL 尝试通过 Remoting 创建 ServerLoadBalancer,但它没有明确尝试创建 ServerLoadBalancer 实例,而是创建了一个实现 ILoadBalancer 接口的实例。这就是它的工作原理。LBL 通过 Remoting 在 LMS 端创建/激活单例,并调用其 GetLeastMachineLoad 方法来确定最快的机器。

一些实现细节

下面列出了一些很酷、可重用或值得一提的辅助类。我将尝试解释它们的酷之处,但您绝对应该查看源代码才能看到它们:)

配置器

我非常喜欢 .NET 配置类,我讨厌重复造轮子,但这个类是该解决方案的特定配置类,并且至少在某一点上比 .NET 配置类更酷。这个类更酷的地方在于,当配置发生变化时,它可以通知某些对象,即底层配置文件已被某个文本编辑器修改。所以我构建了自己的配置器类,它使用 FileSystemWatcher 类来嗅探配置文件中的写入,当文件发生变化时,配置器对象会重新加载文件,并向所有需要了解该变化的订阅者引发事件。这些订阅者只有两个,它们是负载监控和报告服务器。当它们收到事件时,它们会重新启动自己,以便立即反映最新的更改。

计数器信息

我曾经把这个类称为“结构体”类。我对它不公平:),因为它是解决方案中最重要的类之一。它封装了一个 PerformanceCounter 对象,检索一些样本值,并将它们存储在一个循环队列中。什么是循环队列?嗯,我想没有这样的东西:),但既然我“发明”了它,那就让我来解释一下它是什么。它是一个简单的队列,允许添加的元素数量有限。当队列“溢出”时,它会弹出第一个元素,并将新元素推入队列。以下是一个将数字 1 到 7 存储在 5 元素循环队列中的示例

Pass    Queue            Running Total (Sum)
----    -----            -------------------
        []               =               0
1       [1]              = 0 + 1      =  1
2       [2 1]            = 1 + 2      =  3
3       [3 2 1]          = 3 + 3      =  6
4       [4 3 2 1]        = 6 + 4      = 10
5       [5 4 3 2 1]      = 10 + 5     = 15
6       [6 5 4 3 2]      = 15 - 1 + 6 = 20
7       [7 6 5 4 3]      = 20 - 2 + 7 = 25

我为什么需要循环队列?当然是为了限制每个被监控性能计数器的状态。如果第 5 次传递是 3 秒前计数器的状态,则其平均值为 15/5 = 3;如果现在是第 7 次传递,则计数器的平均值为 20/5 = 4。听起来很真实,不是吗?因此,我们使用循环队列来存储暂时的计数器样本,并了解过去 N 个样本在过去 M 秒内测量的平均值。您可以看到运行总和的计算是多么容易。现在计数器唯一需要做的就是将其运行总和除以它收集的样本值数量,即可得知其平均值。您知道机器负载是给定机器所有受监控性能计数器的加权平均值之和。但您可能会问,在以下情况下会发生什么?

我们有两台机器:A 和 B。它们都只测量一个计数器,即它们的 CPU 利用率。一台机器负载监控服务器正在第三台机器 C 上运行,而负载均衡客户端在第四台机器 D 上。A 和 B 的负载报告服务器刚刚启动。它们的 CounterInfo 类分别记录了 50 和 100(因为机器 B 的管理员刚刚启动了 IE:))。A 和 B 配置为每秒报告一次,但它们应该报告 5 个样本值的加权平均值。1 秒过去了,但 A 和 B 都只收集了 1 个样本值。现在 D 询问 C 哪台机器负载最小。应该报告哪一台?A 还是 B?答案很简单:都不报告。除非机器收集了所有性能计数器所需的样本值数量,否则不允许它报告其负载。这意味着,除非 A 和 B 首次填满了它们的循环队列,否则它们将阻塞并且不向调用者(LRS 的报告工作器)返回它们的加权平均值。

机器负载集合

这个类可能比有趣更棘手。通常,它用于存储一个或多个 LRS 向 LMS 报告的负载。这是该类的笨拙之处。该类的一个优点是它以 3 种不同的数据结构存储负载,以模拟 .NET BCL 中缺少的一种数据结构——一个可以存储多个具有相同键的元素的优先队列,或者用 STL 术语来说,类似于

std::priority_queue <std::vector <X *>, ... >

我知道 C++ 铁杆粉丝对此了如指掌,但对于其他听众来说:`std::priority_queue` 是一个模板容器适配器类,它限制了底层容器类型的顶层元素的访问功能,顶层元素始终是最大或优先级最高的元素。新元素可以添加到 `priority_queue` 中,并且可以检查或删除 `priority_queue` 的顶层元素。我从 MSDN 中引用了定义,但我想稍微修正一下:您应该将“始终是最大或优先级最高的”理解为“始终是 `less` 函数对象返回的最大或优先级最高的”。最初,我考虑使用 `priority_queue` 模板类,并在其中放入 `gcroot` 引用,但后来我认为这会比帮助我,以及您这位读者,更令人困惑和困难。您知道 `gcroot` 模板的作用吗?不知道?那没关系:) 在 .NET BCL 类中,我们有一个与优先队列非常相似的东西——那就是 `System::Collections` 中的 `SortedList` 类。因为它几乎可以存储任何基于 `Object` 的实例,所以我们可以将 `ArrayList` 引用放入其中,以模拟一个可以存储多个具有相同键的元素的优先队列。还有一个 `Hashtable` 可以帮助我们解决某些问题,但我们稍后会讲到。同时,请继续阅读以了解我为什么首先需要这些数据结构。

机器负载不会按名称进入机器负载集合,即它们以机器负载作为键添加到负载集合中。这就是为什么在每台机器报告其负载之前,它会将其转换为无符号长整型,然后通过网络将其传输到 LMS。这有助于限制存储的负载数量,例如,如果机器 A 的负载为 20.1,机器 B 的负载为 20.2,则集合认为这些负载相等。当 LMS“获取”负载时,它会将其添加到 SortedList 中,即如果我们有三台机器——“A”、“B”和“C”,负载分别为 40、20 和 30,那么 SortedList 看起来像这样

[B:20][C:30][A:40]

如果有人询问最快的机器,我们总是返回排序列表中的第一个位置元素(因为它按升序排序)。

嗯,我希望它能那么简单,但事实并非如此。当第四台机器“D”报告负载 20 时会发生什么?您现在应该已经猜到我为什么需要为每个负载存储一个 ArrayList 了,所以它现在起作用了——它存储了机器 B 和 D 的负载

[D:20]
[B:20][C:30][A:40]

现在,如果有人询问最快的机器,我们将返回存储在 SortedList 第一个元素中的 ArrayList 的第一个元素,对吗?它是机器“B”。

但是,如果机器“B”报告另一个负载,等于 40 呢?我们应该保留第一个报告的负载吗?当然不!否则,我们会将“B”作为最快的机器返回,而“D”将是负载最小的机器。所以我们应该从第一个 ArrayList 中移除机器“B”的旧负载,并在适当的位置插入其新负载。数据结构如下所示

            [B:40]
[D:20][C:30][A:40]

现在,你是如何找到机器“B”的旧负载以便移除它的呢?嗯?我想是用你的眼睛。这就是我们上面提到的 Hashtable 的用武之地。它是机器的旧负载与其当前所在列表之间的映射。所以当我们添加机器负载时,我们首先检查机器之前是否报告过负载,如果报告过,我们找到旧负载所在的 ArrayList,将其从列表中移除,然后将新负载添加到新列表中,对吗?不对。我们还有一件事要做,但首先让我向你展示问题,然后你就会猜到我还做了什么来让集合按预期工作。

想象一下机器“D”报告了新负载——45。现在你会说数据看起来像下面这样

      [B:40]
[C:30][A:40][D:45]

你希望它看起来像这样!但这只是因为我在尝试可视化第一次加载时犯了一个错误。实际上,之前的加载集合看起来像这样

^
|
M
A
C       .   .   .   .
H       .   .   .   .
I       .   .   .   .
N       .   .   .   .
E       .   .   B   .
S       D   C   A   .

LOAD    20  30  40  .   .   .   -->

所以你现在会同意,集合实际上看起来像这样

^
|
M
A
C       .   .   .   .
H       .   .   .   .
I       .   .   .   .
N       .   .   .   .
E       .   .   B   .
S       .   C   A   D

LOAD    20  30  40  45  .   .   -->

是的,第一个列表是空的,当请求查找负载最小的机器时,如果您尝试弹出负载为 20 的 ArrayList 的第一个元素(这是最小负载),您会得到 IndexOutOfRangeException,就像我在调试以了解发生了什么之前几次遇到的一样。因此,当我们从 ArrayList 中移除旧负载时,我们应该检查它是否已成为孤儿(现在为空),如果是这种情况,我们也应该从 SortedList 中移除 ArrayList

这是 Add 方法的代码

void MachineLoadsCollection::Add (MachineLoad __gc* machineLoad)
{
    DEBUG_ASSERT (0 != machineLoad);
    if (0 == machineLoad)
        return;

    String __gc* name = machineLoad->Name;
    double load = machineLoad->Load;
    Object __gc* boxedLoad = __box (load);

    rwLock->AcquireWriterLock (Timeout::Infinite);

    // a list of all machines that have reported this particular
    // load value
    //
    ArrayList __gc* loadList = 0;

    // check whether any machine has reported such a load
    //
    if (!loads->ContainsKey (boxedLoad))
    {
        // no, this is the first load with this value - create new list
        // and add the list to the main loads (sorted) list
        //
        loadList = new ArrayList ();
        loads->Add (boxedLoad, loadList);
    }
    else
    {
        // yes, one or more machines reported the same load already
        //
        loadList = static_cast<ArrayList __gc*> (loads->get_Item (boxedLoad));
    }

    // check if this machine has already reported a load previously
    //
    if (!mappings->ContainsKey (name))
    {
        // no, the machine is reporting for the first time
        // insert the element and add the machine to the mappings
        //
        loadList->Add (machineLoad);
        mappings->Add (name, new LoadMapping (machineLoad, loadList));
    }
    else
    {
        // yes, the machine has reported its load before
        // we should remove the old load; get its mapping
        //
        LoadMapping __gc* mappedLoad =
            static_cast<LoadMapping __gc*> (mappings->get_Item (name));

        // get the old load, and the list we should remove it from
        //
        MachineLoad __gc* oldLoad = mappedLoad->Load;
        ArrayList __gc* oldList = mappedLoad->LoadList;

        // remove the old mapping
        //
        mappings->Remove (name);

        // remove the old load from the old list
        //
        int index = oldList->IndexOf (oldLoad);
        oldList->RemoveAt (index);

        // insert the new load into the new list
        //
        loadList->Add (machineLoad);

        // update the mappings
        //
        mappings->Add (name, new LoadMapping (machineLoad, loadList));

        // finally, check if the old load list is totally empty
        // and if so, remove it from the main (sorted) list
        //
        if (oldList->Count == 0)
            loads->Remove (__box (oldLoad->Load));
    }

    rwLock->ReleaseWriterLock ();
}

现在,对于好奇的人,这里是 get_MinimumLoad 属性的代码

MachineLoad __gc* MachineLoadsCollection::get_MinimumLoad ()
{
    MachineLoad __gc* load = 0;

    rwLock->AcquireReaderLock (Timeout::Infinite);

    // if the collection is empty, no machine has reported
    // its machineLoad, so we return "null"
    //
    if (loads->Count > 0)
    {
        // the 1st element should contain one of the least
        // loaded machines -- they all have the same load
        // in this list
        //
        ArrayList __gc* minLoadedMachines =
            static_cast<ArrayList __gc*> (loads->GetByIndex (0));
        load = static_cast<MachineLoad __gc*> (minLoadedMachines->get_Item (0));
    }

    rwLock->ReleaseReaderLock ();

    return (load);
}

嗯,关于 MachineLoadsCollection 类如何存储机器负载并返回负载最小的机器,差不多就是这些了。现在我们将看看这个类还有什么很酷的地方。我称之为“死神”,它确实如此——一个名为 GrimReaper (GR) 的方法,它异步运行(使用 Timer 类)并杀死死机!:) 说真的,GR 知道每台机器一旦报告负载,应该多长时间再次报告。如果一台机器未能及时报告其负载,它就会从 MachineLoadsCollection 容器中移除。通过这种方式,我们保证一台已经死亡(或与网络断开连接)的机器不会作为最快的机器返回,至少在它再次报告之前不会(那时它会被重新纳入负载均衡)。然而,在短短约 30 行代码中,我在 GR 代码中犯了两个错误。第一个非常笨拙——我试图在遍历哈希表的元素时从中删除一个元素,但第二个真的是个麻烦!然而,我很快就发现了它,因为我喜欢控制台应用程序:) 我在 GR 执行时输出一个星号 (*),在它杀死一台机器时输出一个插入符号 (^)。然后我观察到,即使(唯一)一台机器定期报告其负载,在某个时候,GR 正在杀死它!我盯着控制台至少 3 分钟。GR 代码很简单,我以为不可能在那里犯错误。我错了。我突然想到我没有考虑到 GR 代码执行需要一些时间的事实。它运行得足够快,但它确实需要一些时间间隔。好吧,在这段时间内,GR 正在锁定机器负载集合。当集合被锁定时,收集器工作器被阻塞,等待集合被解锁,以便它可以将新接收的负载输入到其中。因此,当 GR 代码结束时集合最终被解锁时,收集器输入了机器的负载。你可以猜到,当 GR 配置为在更短的时间间隔内运行,而机器在更长的时间间隔内报告时会发生什么。GR 锁定,锁定,再锁定,而收集器阻塞,阻塞,再阻塞,直到一台机器被 GR 本身延迟。然而,由于 GR 对外界一无所知,它认为这台机器已经死了,所以它将这台机器从负载均衡中移除,直到下次它报告全新的负载。我解决这个问题的方案是什么?我心里有数,但我会在文章的下一个版本中实现它,因为我真的时间不够了。(我无法在 11 月的比赛中发布文章,因为我无法及时完成这篇文章。看起来用普通英语写作比编写托管 C++ 代码更困难,而且我也不想错过 12 月的比赛:)

如果有人感兴趣,这是死神的代码

void MachineLoadsCollection::GrimReaper (Object __gc* state)
{
    // get the state we need to continue
    //
    MachineLoadsCollection __gc* mlc = static_cast<MachineLoadsCollection __gc*> (state);
    // temporarily suspend the timer
    //
    mlc->grimReaper->Change (Timeout::Infinite, Timeout::Infinite);
    // check if we are forced to stop
    //
    if (!mlc->keepGrimReaperAlive)
        return;
    // get the rest of the fields to do our work
    //
    ReaderWriterLock __gc* rwLock = mlc->rwLock;
    SortedList __gc* loads = mlc->loads;
    Hashtable __gc* mappings = mlc->mappings;
    int reportTimeout = mlc->reportTimeout;

    rwLock->AcquireWriterLock (Timeout::Infinite);

    // Bring out the dead :)
    //

    // enumerating via an IDictionaryEnumerator, we can't delete
    // elements from the hashtable mappings; so we create a temporary
    // list of machines for deletion, and delete them after we have
    // finished with the enumeration
    //
    StringCollection __gc* deadMachines = new StringCollection ();

    // walk the mappings to get all machines
    //
    DateTime dtNow = DateTime::Now;
    IDictionaryEnumerator __gc* dic = mappings->GetEnumerator ();
    while (dic->MoveNext ())
    {
        LoadMapping __gc* map = static_cast<LoadMapping __gc*> (dic->Value);
        // check whether the dead timeout has expired for this machine
        //
        TimeSpan tsDifference = dtNow.Subtract (map->LastReport);
        double difference = tsDifference.TotalMilliseconds;
        if (difference > (double) reportTimeout)
        {
            // remove the machine from the data structures; it is
            // now considered dead and does not participate anymore
            // in the load balancing, unless it reports its load
            // at some later time
            //
            String __gc* name = map->Load->Name;

            // get the old load, and the list we should remove it from
            //
            MachineLoad __gc* oldLoad = map->Load;
            ArrayList __gc* oldList = map->LoadList;

            // remove the old mapping (only add it to the deletion list)
            //
            deadMachines->Add (name);

            // remove the old load from the old list
            //
            int index = oldList->IndexOf (oldLoad);
            oldList->RemoveAt (index);

            // finally, check if the old load list is totally empty
            // and if so, remove it from the main list
            //
            if (oldList->Count == 0)
                loads->Remove (__box (oldLoad->Load));
        }
    }

    // actually remove the dead machines from the mappings
    //
    for (int i=0; i<deadMachines->Count; i++)
        mappings->Remove (deadMachines->get_Item (i));

    // cleanup
    //
    deadMachines->Clear ();

    rwLock->ReleaseWriterLock ();

    // resume the timer
    //
    mlc->grimReaper->Change (reportTimeout, reportTimeout);
}

负载均衡实战 - 平衡 Web 农场

我构建了一个超级简单的 .NET Web 应用程序(用 C# 编写),它使用 LBL 在 Web 农场中执行负载均衡。尽管这个应用程序非常小,但它很有趣,值得在本文中占有一席之地,所以我们开始吧。首先,我编写了一个类,它封装了 LBL 中的负载均衡类 ClientLoadBalancer,将其命名为 Helper,并将其实现为单例,以便 Web 应用程序的 Global 类和网页类可以看到它的一个实例。然后,我在 Global 类的 Session_OnStart 方法中使用了它,将每个新会话的第一个 HTTP 请求重定向到最可用的机器。此外,在示例网页中,我再次使用它来动态构建用于进一步处理的 URL,再次将本地主机替换为最快的机器。现在有人可能会争辩(而且他可能是对的)说用户可能会花很多时间阅读那个页面,所以当他最终点击“更快”链接时,之前更快的机器那时可能不是最快的机器。只是不要忘记,访问另一台机器的 Web 应用程序将再次触发其 Session_OnStart,所以无论如何,用户都将被重定向到最快的机器。现在,如果您不明白我在说什么,那是因为我还没有展示任何代码。所以它在这里

protected void Session_Start (object sender, EventArgs e)
{
    // get the fastest machine from the load balancer
    //
    string fastestMachineName = Helper.Instance.GetFastestMachineName ();

    // we should check whether the fastest machine is not the machine,
    // this web application is running on, as then there'll be no sence
    // to redirect the request
    //
    string thisMachineName = Environment.MachineName;
    if (String.Compare (thisMachineName, fastestMachineName, false) != 0)
    {
        // it is another machine and we should redirect the request
        //
        string fasterUrl = Helper.Instance.ReplaceHostInUrl (
            Request.Url.ToString (),
            fastestMachineName);
        Response.Redirect (fasterUrl);
    }
}

这是示例网页中的代码

private void OnPageLoad (object sender, EventArgs e)
{
    // get the fastest machine and generate the new links with it
    //
    string fastestMachineName = Helper.Instance.GetFastestMachineName ();
    link.Text = String.Format (
        "Next request will be processed by machine '{0}'",
        fastestMachineName);
    // navigate to the same URL, but the host being the fastest machine
    //
    link.NavigateUrl = Helper.Instance.ReplaceHostInUrl (
        Request.Url.ToString (),
        fastestMachineName);
}

如果您认为我在 Helper 类中硬编码了设置,那您就错了。首先,我讨厌在代码中硬编码或使用魔术值(尽管您可能会在这样的文章中看到一些)。其次,我是在同事的电脑上测试解决方案的,所以提前编写几行代码,帮助我避免了本来不可避免的重新编译。我只是在那里部署了 Web 应用程序。这是 Helper 类(注意我已将键硬编码到 Web.config 文件中;-))的简单 C# 代码

class Helper
{
    private Helper ()
    {
        // assume failure(s)
        //
        loadBalancer = null;
        try
        {
            NameValueCollection settings = ConfigurationSettings.AppSettings;

            // assume that MLMS is running on our machine and the web app
            // is configured to create its remoted object using the defaults;
            // if the user has configured another machine in the Web.config
            // running MLMS, try to get its settings and create the remoted
            // object on it
            //
            string machine = Environment.MachineName;
            int port = 14000;
            RemotingProtocol protocol = RemotingProtocol.TCP;

            string machineName = settings ["LoadBalancingMachine"];
            if (machineName != null)
                machine = machineName;

            string machinePort = settings ["LoadBalancingPort"];
            if (machinePort != null)
            {
                try
                {
                    port = int.Parse (machinePort);
                }
                catch (FormatException)
                {
                }
            }

            string machineProto = settings ["LoadBalancingProtocol"];
            if (machineProto != null)
            {
                try
                {
                    protocol = (RemotingProtocol) Enum.Parse (
                        typeof (RemotingProtocol),
                        machineProto,
                        true);
                }
                catch (ArgumentException)
                {
                }
            }

            // create a proxy to the remoted object
            //
            loadBalancer = new ClientLoadBalancer (
                machine,
                protocol,
                port);
        }
        catch (Exception e)
        {
            if (e is OutOfMemoryException || e is ExecutionEngineException)
                throw;
        }
    }

    public string GetFastestMachineName ()
    {
        // assume that the load balancing could not be created or it will fail
        //
        string fastestMachineName = Environment.MachineName;
        if (loadBalancer != null)
        {
            MachineLoad load = loadBalancer.GetLeastMachineLoad ();
            if (load != null)
                fastestMachineName = load.Name;
        }
        return (fastestMachineName);
    }

    public string ReplaceHostInUrl (string url, string newHost)
    {
        Uri uri = new Uri (url);
        bool hasUserInfo = uri.UserInfo.Length > 0;
        string credentials = hasUserInfo ? uri.UserInfo : "";
        string newUrl = String.Format (
            "{0}{1}{2}{3}:{4}{5}",
            uri.Scheme,
            Uri.SchemeDelimiter,
            credentials,
            newHost,
            uri.Port,
            uri.PathAndQuery);
        return (newUrl);
    }

    public static Helper Instance
    {
        get { return (instance); }
    }

    private ClientLoadBalancer loadBalancer;
    private static Helper instance = new Helper ();
} // Helper

如果您想知道服务器运行时是什么样子,以及我为 Web 应用程序设计了多么棒的外观和感觉,这里有一张截图让您失望:)

构建、配置和部署解决方案

要加载解决方案文件,您需要做一点小技巧。打开您的 IIS 管理控制台(开始/运行...键入 inetmgr),然后创建一个新的虚拟目录 LoadBalancingWebTest。当您被询问文件夹时,选择 X:\Path\To\SolutionFolder\LoadBalancingWebTest。现在您可以毫无问题地打开解决方案文件(SoftwareLoadBalancing.sln)。在 Visual Studio .NET 中加载它,首先构建 SharedLibrary 项目,因为其他项目依赖于它,然后构建 LML 和 LRS,最后构建整个解决方案。请注意,安装项目不会自动构建,因此您应该手动选择并构建它们。

注意: 编译解决方案时,您会收到 15 个警告。所有警告都声明:warning C4935: assembly access specifier modified from 'xxx',其中 xxx 可能是 privatepublic。我不知道如何让编译器停止抱怨这个。没有其他 4 级警告。如果这些让您感到尴尬,我很抱歉。

如果您有 VS.NET,就是这样。如果没有,您只能编译 Web 应用程序,因为它使用 C# 编写,并且可以使用 .NET 框架附带的免费 C# 编译器进行编译。否则,请购买一份 VS.NET,成为 CodeProject(和 Microsoft)的支持者 :) 顺便说一句,我现在意识到我应该用 C# 编写我的下一篇文章,这样像我一样的“穷”家伙也能玩得开心。对不起,伙计们!我保证在我尝试编写的下一篇文章中,大部分都会使用 C#。

配置

如果您查看解决方案中 SharedFiles 文件夹下的 Common.h 头文件,您会注意到我从该文件中复制并粘贴了所有配置键的含义。然而,我知道在您喜欢这篇文章之前您不会去查看它(而且现在也差不多到文章结尾了:),所以这里是对 XML 配置文件和 Common.h 中各种宏的解释。

Common.h 有什么共同之处?

这个头文件几乎被解决方案中的所有项目使用。它有几个(有用的)宏,我将要讨论,所以如果你想阅读它们,请继续。否则,点击这里只阅读 XML 配置文件。

首先,我将讨论 .NET 成员访问修饰符。它们有 5 个,尽管您可能只使用其中 4 个,除非您正在编写 IL 代码。现有语言以不同的方式引用它们,所以我将为您提供它们的名称和一些解释的比较表。

.NET 术语 C# 关键字 MC++ 关键字 解释
私有的 私有的 私有 private 该成员仅在其定义的类中可见,并且其他程序集不可见;请注意 MC++ 中 private 关键字的双重使用——第一个(在此表中)指定成员是否对其他程序集可见,另一个指定成员是否对同一程序集中的其他类可见。
public public 公共 public 所有程序集和类均可见
受保护的 公共 protected 所有程序集均可见,但只能由派生类使用
族与程序集 internal 私有公共 在程序集内部的所有类中可见,但对外部程序集不可见

因为我最喜欢 C# 关键字,所以我 #define 了并在代码中使用了四个宏,以避免在 MC++ 中键入双重关键字

#define PUBLIC      public  public
#define PRIVATE     private private
#define PROTECTED   public  protected
#define INTERNAL    private public

接下来是更有趣的“内容”。您有三种在负载报告和监控服务器之间进行通信的选项:通过 UDP + 多播、仅 UDP 或 TCP。顺便说一下,如果我用 C# 编写这篇文章,您就不会拥有这些选项。真的!C# 在预处理方面太差劲了,编译器作者犯了一个大错,他们没有在编译器中包含一些真正的预处理功能,我无话可说!尽管如此,我用 MC++ 编写了这篇文章,所以我拥有了在开始编写类通信代码时急需的酷炫 #define 指令。您可以使用两个宏来使解决方案使用一种通信协议或另一种,以及/或禁用/启用多播。以下是它们的定义

#define USING_UDP           1
#define USING_MULTICASTS    1

现在,C# 大师:) 会争辩说我仍然可以用几对 #ifdef#endif 指令编写与协议无关的代码。说实话,我不是这种编码风格的拥护者。我宁愿在这样的 #if 块中定义一个通用宏,并在我需要它的任何地方使用它。所以我就是这样做的。我编写了宏来创建 TCP 或 UDP 套接字,连接到远程端点,并通过 UDP 和 TCP 发送和接收数据。然后我编写了几个遵循以下模式的通用宏

#if defined(USING_UDP)
#   define SOCKET_CREATE(sock) SOCKET_CREATE_UDP(sock)
#else
#   define SOCKET_CREATE(sock) SOCKET_CREATE_TCP(sock)
#endif

你明白我的意思,对吧?实际代码中没有 #ifdef。我只是写 SOCKET_CREATE (socket);,预处理器就会生成创建相应套接字的代码。这是另一个很好的宏,我用于异常处理,但在此之前,我将为您提供一些关于 .NET 异常处理的规则(您可能知道)

  • 只捕获你能处理的异常,不要多。这意味着如果你期望你正在调用的方法抛出 ArgumentNullException 和/或 ArgumentOutOfRangeException,你应该编写两个 catch 子句,并且只捕获这些异常。
  • 另一条规则是绝不“吞噬”你捕获但无法处理的异常。你必须重新抛出它,这样你的方法的调用者就知道它失败的原因了。
  • 这与第二条规则相关:有两种异常你无能为力,只能向用户报告并终止:它们是 OutOfMemoryExceptionExecutionEngineException。我不知道哪个更糟糕——可能是后者,尽管如果你内存不足,你几乎无能为力。

因为我在这里不是在编写生产代码,所以我允许自己在(大部分源代码中)捕获所有可能的异常,当我不需要处理它们,只需知道出了问题时。所以我捕获了基类 Exception。这违反了我上面写的所有规则,但我编写了一些代码以符合第二条和第三条规则——如果我捕获到 OutOfMemoryExceptionExecutionEngineException,我立即重新抛出它。这是我捕获通用 Exception 类后调用的宏

#define TRACE_EXCEPTION_AND_RETHROW_IF_NEEDED(e)        \
    System::Type __gc* exType = e->GetType ();          \
    if (exType == __typeof (OutOfMemoryException) ||    \
        exType == __typeof (ExecutionEngineException))  \
        throw;                                          \
    Console::WriteLine (                                \
        S"\n{0}\n{1} ({2}/{3}): {4}\n{0}",              \
        new String (L'-', 79),                          \
        new String ((char *) __FUNCTION__),             \
        new String ((char *) __FILE__),                 \
        __box (__LINE__),                               \
        e->Message);

最后,关于断言。C 语言有 assert 宏,VB 有 Debug.Assert 方法,.NET 在 Debug 类中也有一个静态方法 Assert。该方法的一个重载接受一个布尔表达式和一个描述测试的字符串。C 语言的 assert 更智能。它只需要一个表达式,并通过将表达式字符串化自动构建包含表达式的字符串。现在,我确实讨厌 C# 缺乏一些真正的预处理功能这一事实。然而,MC++(感谢上帝!)并没有被编译器编写者屠杀(遗留代码支持万岁),所以这是 C 语言 assert 宏的 .NET 版本

#define DEBUG_ASSERT(x) Debug::Assert (x, S#x)

如果我用 C# 编写这篇文章的代码,我应该输入

Debug.Assert (null != objRef, "null != objRef");
在我需要断言的任何地方。在 MC++ 中,我只需编写
DEBUG_ASSERT (0 != objRef);
它会自动展开成
Debug::Assert (0 != objRef, S"0 != objRef");
更不用说我可以在 DEBUG_ASSERT 宏中使用的 __LINE____FILE____FUNCTION__ 宏了!现在,大家和我一起大声尖叫:“C# 太烂了!”:)

 

调整配置文件

我知道你们都很聪明(否则你们在 CodeProject 上干什么呢?:)),聪明人不需要冗长的解释,他们只需要看一个例子。所以这里就是——机器负载监控和报告服务器都使用的 XML 配置文件。所有元素的解释都在文件下方给出

<?xml version="1.0" encoding="utf-8"?>

<configuration>

    <LoadReportingServer>

        <IpAddress>127.0.0.1</IpAddress>
        <Port>12000</Port>
        <ReportingInterval>2000</ReportingInterval>

    </LoadReportingServer>

    <LoadMonitoringServer>

        <IpAddress>127.0.0.1</IpAddress>
        <CollectorPort>12000</CollectorPort>
        <CollectorBacklog>40</CollectorBacklog>
        <ReporterPort>13000</ReporterPort>
        <ReporterBacklog>40</ReporterBacklog>
        <MachineReportTimeout>4000</MachineReportTimeout>
        <RemotingProtocol>tcp</RemotingProtocol>
        <RemotingChannelPort>14000</RemotingChannelPort>

        <PerformanceCounters>
            <counter alias="cpu"
                    category="Processor"
                    name="% Processor Time"
                    instance="_Total"
                    load-weight="0.3"
                    interval="500"
                    maximum-measures="5" />
            <-- ... -->
        </PerformanceCounters>

    </LoadMonitoringServer>

</configuration>

即使你很聪明,我知道你们中有些人还是有些问题,我将一一解答。首先,我将解释所有元素及其属性的用途,并涵盖一些奇怪的设置,所以请继续阅读...(为节省空间,我将 LoadReportingServer 元素称为 LRS,将 LoadMonitoringServer 称为 LMS)。

元素/属性 含义/用法
LRS/IpAddress 当您使用 UDP + 组播(默认)时,IpAddress 是 MLMS 和 MLRS 加入以进行通信的组播组的 IP 地址。如果您不使用组播,但仍使用 UDP 或 TCP,此元素指定 MLMS 服务器的 IP 地址(或主机名),MLRS 会向其报告。请注意,因为您不使用组播,MLRS 服务器无法将它们的机器负载“组播”到所有 MLMS 服务器。毫无疑问,在任何情况下,此元素的文本都应与 LMS/IpAddress 相等。
LRS/Port 使用 UDP + 组播、仅 UDP 或 TCP,这是 MLRS 服务器发送机器负载和 MLMS 服务器接收机器负载的端口。
LRS/ReportingInterval MLRS 服务器将其机器负载报告给 MLMS 服务器。ReportingInterval 指定了 MLRS 服务器向一个或多个 MLMS 服务器报告其负载的时间间隔(以毫秒为单位)。如果您留意了一些实现细节部分,我曾说过,即使时间间隔已过,机器也可能不会报告其负载,因为它尚未收集计算负载所需的原始数据。有关更多信息,请参阅 counter 元素的 interval 属性。
LMS/IpAddress 在 UDP + 组播场景中,这是组播组的 IP 地址,就像 LRS/IpAddress 元素中一样。当您只使用 UDP 或 TCP 时,此地址将被忽略。
LMS/CollectorPort MLMS 服务器接受 TCP 连接或在使用 UDP 时接收数据的端口。
LMS/CollectorBacklog 此元素指定了 MLMS 服务器在配置为 TCP 通信时将使用的最大套接字数量。
LMS/ReporterPort 如果您没有仔细阅读这篇文章,您可能会想知道这个元素指定了什么。嗯,在我的第一个设计中,我没有想到 Remoting 会如此好地帮助我构建负载平衡库 (LBL)。我编写了一个迷你 TCP 服务器,它接受 TCP 请求并返回负载最轻的机器。因为 LBL 必须连接到 MLMS 服务器并询问哪台机器最快,您可以想象我编写了 GetLeastLoadedMachine 方法的几个重载,接受超时和默认机器,如果根本没有可用机器的话。在我完成 LBL 客户端时,我决定这个设计太逊了,所以我使用 Remoting 从头重写了 LBL 库(是的,事情就是这样:)。现在,我很遗憾地告诉您,我覆盖了原始库的源文件。但是,我将 TCP 服务器完全保留了下来——它以 ReporterWorker 类的形式存在,并保留在 LoadMonitoringLibrary 项目的 ReporterWorker.h/.cpp 文件中。如果您想编写一个替代的 LBL 库,请随意——只需编写一些代码连接到 LMS 报告器工作程序,它将立即报告最快机器的负载。请注意,该工作程序接受 TCP 套接字,因此您应该始终使用 TCP 连接到它。
LMS/ReporterBacklog 不难看出,这就是我上面谈到的 TCP 服务器的积压。
LMS/MachineReportTimeout 现在这是一个有趣的设置。MachineReportTimeout 是机器报告其连续负载以保持负载平衡的最大时间间隔(以毫秒为单位)。这意味着,如果机器在 5 秒前报告过,并且超时时间间隔设置为 3 秒,则该机器将从负载平衡中移除。如果它稍后报告,它将重新回到服务中。我认为这有点逊,因为人们可能希望配置每台机器以不同的时间间隔报告,但我(现在)没有时间修复这个问题,所以您应该学会适应这个“特性”。解决我“逊色”的一种方法是给这个设置一个足够大的值。但请注意,如果一台机器宕机,在时间间隔结束之前,您将无法将其从负载平衡中移除——所以不要给它太大的值。
LMS/RemotingProtocol 最初,我只想通过 TCP 使用 Remoting。我认为 HTTP 会太慢(它在 OSI 堆栈中比 TCP 高一个级别)。然后,当我回想起 Remoting 有多复杂时,我意识到 HTTP 协议比 Remoting 本身快得多。所以我决定给您一个选择要使用哪个协议。目前,该解决方案只支持 TCP 和 HTTP 协议,但您可以轻松扩展它以使用您想要的任何协议。此设置接受一个字符串,它可以是“tcp”或“http”(当然,不带引号)。
LMS/RemotingChannelPort 这是 MLMS 用于向 Remoting 运行时注册和激活负载平衡对象的端口。
LMS/PerformanceCounters 此元素包含用于计算机器负载的性能计数器集合。下面是 counter XML 元素的属性,用于描述我上面某个地方写过的 CounterInfo 对象。
counter/alias 尽管目前未使用,此属性指定了否则过长的性能计数器路径的别名。有关我添加此属性的原因,请参阅待办事项部分。
counter/category 计数器的总类别,例如 ProcessorMemory 等。
counter/name 类别中的特定计数器,例如 % Processor TimePage reads/sec 等。
counter/instance 如果计数器有两个或更多实例,instance 属性指定计数器的确切实例。例如,如果您有两个 CPU,则第一个 CPU 的实例是“0”,第二个是“1”,并且两者都命名为“_Total”
counter/load-weight 平衡计数器值的权重。例如,您可以给 Processor\% Processor Time\_Total 的值比 Processor\% User Time\_Total 的值更大的权重。您懂的。
counter/interval 请求性能计数器返回其下一个样本值的时间间隔(以毫秒为单位)。
counter/maximum-measures 循环队列(我上面提到过)的大小,它存储性能计数器的瞬态。换句话说,该元素指定应该收集多少个计数器值才能获得一个像样的加权平均值 (WA)。在收集到至少 maximum-measures 个样本值之前,计数器不会报告其 WA。如果 CounterInfo 类在收集到所需数量的样本值之前被要求返回其 WA,它将阻塞并等待,直到收集到它们。

...以及另一个配置文件:)

“另一个配置文件”是什么?嗯,它是示例负载平衡 Web 应用程序中的 Web.config 文件。它在 appSettings 部分定义了 3 个关键键。它们是运行 MLMS 的机器,以及机器注册其远程对象的 Remoting 端口和协议。

<appSettings>
    <add key="LoadBalancingMachine" value="..." />
    <add key="LoadBalancingPort" value="..." />
    <add key="LoadBalancingProtocol" value="..." />
</appSettings>

您已经看过 Web 应用程序的 Helper 类中的代码,所以您可以弄清楚这些键的含义。最后一个键接受一个字符串,它可以是“TCP”或“HTTP”,没有其他选项。

部署

有 7 种方法可以将解决方案部署到一台机器上。没错——七种。为了缩短文章并延长我的寿命,我将机器负载监控服务器称为 LMS,机器负载报告服务器称为 LRS,负载平衡库称为 LBL。以下是变体:

  1. LMS、LRS、LBL
  2. LMS、LRS
  3. LMS、LBL
  4. LMS
  5. LRS、LBL
  6. LRS
  7. LBL

由您决定安装什么以及在哪里安装。但我开发了安装项目,所以您必须注意我将要告诉您的内容。有 4 个安装程序。第一个是用于示例负载平衡 Web 应用程序。第二个是用于解决方案的服务器部分,即机器负载监控和报告服务器。它们捆绑在一个安装程序中,但安装后由您决定运行哪一个。第三个安装程序只包含负载平衡库,第四个安装程序包含解决方案的完整源代码,包括安装程序的源代码。

以下是一个简单的场景来测试代码是否有效:(您应该在局域网中设置一个组播组或请管理员执行此操作)。我们将使用两台机器——A 和 B。在机器 A 上,首先构建 SharedLibrary 项目,然后构建整个解决方案(您可以跳过安装项目)。部署 Web 应用程序。修改 MLMS 和 MRLS 的 XML 配置文件。运行服务器。部署 Web 应用程序,修改其 Web.config 文件并启动它。点击网页链接。它应该可以工作,并且负载平衡应该将您重定向到同一台机器 (A)。现在,仅将 MLRS 和 Web 应用程序部署到机器 B。修改配置文件,但这次,在 Web.config 中,将 LoadBalancingMachine 键设置为“A”。您刚刚向 B 的 LBL 解释了使用机器 A 的远程负载平衡对象。在机器 B 上运行 MLRS。它应该开始向 A 的 MLMS 报告 B 的负载。现在在机器 A 上执行一些 CPU 密集型操作(如果 < WinXP,右键单击桌面并将鼠标拖到任务栏后面;这应该会给您大约 100% 的 CPU 利用率)。其 Web 应用程序应该将您重定向到机器 B 上的 Web 应用程序。现在停止 B 的 MLRS 服务器。启动 B 的 Web 应用程序。它应该将您重定向到 A 的 Web 应用程序。我想就是这样了。尽情玩弄所有可能的部署场景吧:)

关于 MC++ 和 C# 的一些想法

Managed C++ 到 C# 的翻译

将纯托管 C++ 代码转换为 C# 没有比这更容易的了。只需在脑海中按 Ctrl-H 并替换以下序列(这只适用于我的源文件,因为其他开发人员可能不会以我使用的方式使用空格)。

MC++                C#
----                ----
::                  .
->                  .
__gc*
__gc
__sealed __value
using namespace     using
: public            :
S"                  "
__box (x)           x

虽然上面的替换将转换 85% 的代码,但您仍然需要手动执行以下几项操作:

  • 您必须翻译所有预处理器指令,例如,删除头文件保护 (#if !defined (...) ... #define ... #endif),并手动将宏替换为它们应该生成的代码。
  • 您必须将所有 C++ 强制类型转换转换为 C# 强制类型转换,即
    static_cast<SomeType __gc*> (expression) to
    
    ((SomeType) expression) or (expression as SomeType)
    
  • 您必须为类中的所有成员添加适当的访问修饰符关键字,即您应该更改
    PUBLIC:
        ... Method1 (...) {...}
        ... Variable1;
    PRIVATE:
        ... Method3 (...) {...}
    
    to
    public  ... Method1 (...) {...}
    public  ... Variable1;
    private ... Method3 (...) {...}
    
  • 您必须将头文件和实现文件合并为一个 C# 源文件。

C# 的 readonly 字段与 MC++ 的非静态 const 成员

令人沮丧的是,MC++ 没有 C# 的只读字段(不是属性)的等价物。在 C# 中,可以编写以下类:

public class PerfCounter
{
    public PerfCounter (String fullPath, int sampleInterval)
    {
        // validate parameters
        //
        Debug.Assert (null != fullPath);
        if (null == fullPath)
            throw (new ArgumentNullException ("fullPath"));
        Debug.Assert (sampleInterval > 0);
        if (sampleInterval <= 0)
            throw (new ArgumentOutOfRangeException ("sampleInterval"));

        // assign the values to the readonly fields
        //
        FullPath = fullPath;
        SampleInterval = sampleInterval;
    }

    // these are marked public, and make a great replacement of
    // read-only (getter) properties
    //
    public readonly String  FullPath;
    public readonly int     SampleInterval;
}

您可以看到 C# 程序员不必实现只读属性,因为只读字段已经足够好用。在 Managed C++ 中,您可以通过编写以下类来模拟只读字段:

public __gc class PerfCounter
{
public:
    PerfCounter (String __gc* fullPath, int sampleInterval) :
        FullPath (fullPath),
        SampleInterval (sampleInterval)
    {
        // validate parameters
        //
        Debug::Assert (0 != fullPath);
        if (0 == fullPath)
            throw (new ArgumentNullException (S"fullPath"));
        Debug::Assert (sampleInterval > 0);
        if (sampleInterval <= 0)
            throw (new ArgumentOutOfRangeException (S"sampleInterval"));

        // the values have been assigned in the initialization list
        // of the constructor, we have nothing more to do -- COOL!>
        //
    }

public:
    const String __gc*  FullPath;
    const int           SampleInterval;
};

到目前为止,一切顺利。您可能想知道我为什么要抱怨 MC++。看起来 MC++ 版本甚至比 C# 版本更酷。好吧,示例类太简单了。现在,想象一下当您发现一个无效参数时,您应该将其更改为默认值,就像下面的 C# 类中一样:

public class PerfCounter
{
    public PerfCounter (String fullPath, int sampleInterval)
    {
        // validate parameters
        //
        Debug.Assert (null != fullPath);
        if (null == fullPath)
            throw (new ArgumentNullException ("fullPath"));
        Debug.Assert (sampleInterval > 0);
        // change to a reasonable default value
        //
        if (sampleInterval <= 0)
            sampleInterval = DefaultSampleInterval;

        // you can STILL assign the values to the readonly fields
        //
        FullPath = fullPath;
        SampleInterval = sampleInterval;
    }

    public readonly String  FullPath;
    public readonly int     SampleInterval;
    private const int       DefaultSampleInterval = 1000;
}

现在,相应的 MC++ 代码将无法编译,您将在下面看到原因:

public __gc class CrashingPerfCounter
{
public:
    CrashingPerfCounter (String __gc* fullPath, int sampleInterval) :
        FullPath (fullPath),
        SampleInterval (sampleInterval)
    {
        // validate parameters
        //
        Debug::Assert (0 != fullPath);
        if (0 == fullPath)
            throw (new ArgumentNullException (S"fullPath"));
        Debug::Assert (sampleInterval > 0);
        // the second line below will cause the compiler to
        // report "error C2166: l-value specifies const object"
        //
        if (sampleInterval <= 0)
            SampleInterval = DefaultSampleInterval;

        // the values have been assigned in the initialization list
        // of the constructor, and that's the only place we can
        // initialize non-static const members -- NOT COOL!
        //
    }

public:
    const String __gc*  FullPath;
    const int           SampleInterval;

private:
    static const int    DefaultSampleInterval = 1000;
};

现在,有人可能会争辩说,我们可以在构造函数的初始化列表中初始化 const 成员 SampleInterval,像这样:

SampleInterval (sampleInterval > 0 ? sampleInterval : DefaultSampleInteval)

他会是对的。然而,如果我们需要首先连接到数据库来执行检查,或者我们需要对参数执行多次检查,我不知道如何在初始化列表中完成。你知道吗?这就是为什么在只读字段方面 MC++ 不如 C#。现在,程序员被迫将 const 字段设为非 const 和 private,并编写代码来实现只读属性,像这样:

public __gc class LamePerfCounter
{
public:
    LamePerfCounter (String __gc* fullPath, int sampleInterval)
    {
        // validate parameters
        //
        Debug::Assert (0 != fullPath);
        if (0 == fullPath)
            throw (new ArgumentNullException (S"fullPath"));
        Debug::Assert (sampleInterval > 0);
        if (sampleInterval <= 0)
            sampleInterval = DefaultSampleInterval;

        // assign the values to the member variables
        //
        this->fullPath = fullPath;
        this->sampleInterval = sampleInterval;
    }

    __property String __gc* get_FullPath ()
    {
        return (fullPath);
    }

    __property int get_SampleInterval ()
    {
        return (sampleInterval);
    }

private:
    String __gc*        fullPath;
    int                 sampleInterval;
    static const int    DefaultSampleInterval = 1000;
};

“Bug 很糟糕。句号。”

约翰·罗宾斯

“我相信我和我的同事会正确使用我的代码。然而,为了避免错误,我会验证一切。我验证别人传递给我代码的数据,我验证我代码的内部操作,我验证我在代码中做的每一个假设,我验证我的代码传递给别人的数据,以及我验证我的代码调用返回的数据。如果有需要验证的东西,我就会验证它。这种强迫性的验证对我同事来说没有任何个人恩怨,我也没有任何心理问题(可以说)。我只是知道错误从何而来;我也知道如果你想尽早发现错误,就不能让任何东西未经检查地通过。”

约翰·罗宾斯

我照约翰在他的书中所说的那样做,您也应该这样做。相信我,但请验证我的代码。我认为我已彻底调试了我的代码,自前天(我开始写这篇文章的时候)以来,我再也没有遇到过其中的错误。但是,如果您看到这些讨厌的家伙之一,请告诉我。我的电子邮件是 stoyan_damov[at]hotmail.com。

虽然我认为我没有错误(从统计学上讲,我应该有 8 个 KLOC 中的错误),但我很乐意与您分享一个惊人的微软错误。我花了相当长的时间才找到它,但不幸的是,当它后来消失时(是的!它消失了),我无法重现它。我将我所有的类都包装在 SoftwareLoadBalancing 命名空间中。到目前为止一切顺利。现在我在 SharedLibrary 程序集中有几个共享类。负载监控库使用这些类中的一个来完成其工作,因此它 #using 了 SharedLibrary。我能够多次构建 LML,然后突然,链接器抱怨它找不到我在 SoftwareLoadBalancing 命名空间中使用的共享类。我将该类命名为 X 以节省一些打字。我关闭了解决方案,进入了共享库的 Debug 文件夹,删除了所有内容,删除了公共 Bin 文件夹中的所有文件,然后再次尝试。结果相同!我让链接器又抱怨了三次,然后启动了 ILDAsm 工具。当我查看 SharedLibrary.dll 时,我发现类 X 被“包装”了两次在 SoftwareLoadBalancing 命名空间中,即它现在是 SoftwareLoadBalancing::SoftwareLoadBalancing::X。因为我想做一些测试,没有时间处理这个错误,我尝试像这样在 LML 中给命名空间起别名:

using namespace SLB = SoftwareLoadBalancing;

然后,我尝试使用以下构造访问 X 类:

SLB::SLB::X __gc* x = new SLB::SLB::X ();

也许我对 C++ 命名空间别名不太了解,或者文档没有解释,但这次发生的是链接器再次抱怨找不到 SoftwareLoadBalancing::SLB::X 类!!!编译器只“替换”了一次 SLB 为 SoftwareLoadBalancing。不用说,我非常尴尬。编译器不仅将我的类包装在两个命名空间中,而且没有帮助我解决问题!:) 你知道我当时做了什么吗?我以链接器或编译器应该理解的方式给命名空间起了别名:

using namespace SLB = SoftwareLoadBalancing::SoftwareLoadBalancing;

然后,我尝试像这样实例化 X 类:

SLB::X __gc* x = new SLB::X ();

我相信您不知道发生了什么,因为我向您隐瞒了一个简单的事实。我每次都在重建。现在您能猜到发生了什么吗?链接器再次抱怨它无法在 SoftwareLoadBalancing::SoftwareLoadBalancing 命名空间中找到类 X。搞什么鬼?!我怒了!我疯了!我再次启动 ILDasm,并查看了 SharedLibrary。该类在 SoftwareLoadBalancing 命名空间中被正确包装了一次。现在,我不知道这是编译器、链接器还是我的头脑中的一个错误。我所知道的是,下次我遇到这样的问题时,我不会去追逐源文件中不存在的错误,而是会启动我心爱的 ILDasm,看看是我做错了什么,还是微软想把我逼疯:)

待办事项

(那到底你做了什么,竟然有这么多待办事项?!)

  • 将 MLMS 和 MLRS 可执行文件重写为 .NET Windows 服务?
  • 没有管理控制台——没时间做,但谁知道呢,也许在文章的下一版本中,我会构建一个配置 GUI,并编写一些代码来实现远程服务器管理。
  • 在此版本中,机器负载几乎是静态计算的,即计数器及其权重是可配置的,但计算机器负载的算法是相同的。如果我有一些时间(例如,我可爱的妻子回老家度假一段时间:),我将实现一个表达式解释器,这样你们就可以输入算术和布尔表达式(公式)来按您自己的意愿计算机器负载,例如,可以输入这样的表达式:
    cpu * 0.2 + ((sessions < 10) * 0.2 + (sessions >= 10) * 0.5) * sessions
    
  • 发现一种更好的存储机器负载的方法(例如,实现一个真正的优先级队列),尽管目前的对我来说很好用:)
  • 你还记得我之前在文章中写到的关于返回负载最小的机器吗?“现在,如果有人询问最快的机器,我们将返回 ArrayList 的第一个元素,它存储在 SortedList 的第一个元素中,对吗?”。虽然我可能看起来想对了,但实际上我有点错了。想象一下我们有 5 台机器,报告了最小负载 10。现在想象有 100 个查询请求 MLMS 查找负载最小的机器。如果我们能在机器 B 再次报告其负载之前回答这些查询,我们将把机器 B 作为最快的机器发送给所有 100 个查询。一旦客户端收到答案,它们都会蜂拥而至使机器 B 过载,所以下次,它甚至可能无法报告其负载 :) 我们必须做的是,要么以某种循环方式报告最快的机器,要么从最快机器列表中返回一台随机机器。但这将在文章的下一版本中实现。
  • 修改死神的代码,使其不会在机器未按时报告时(可能是由于 UDP 数据包丢失,而不是因为机器已死)将其移除。相反,有一个可配置的计数器,每次机器未能按时报告其负载时,该计数器递减,当计数器达到零时,该机器将从负载平衡中移除。
  • 安全性——除了检查方法中的参数和 LMS 接收的 TCP/UDP 请求的有效性之外,没有安全性代码。如果 Michael Howard 或 David LeBlanc(必读书籍“编写安全代码”的作者)阅读了这篇文章,我敢肯定他们会给它打零分。我很抱歉!如果我实现了安全性,这个月就不会有这篇文章了,我真的想赢得 CodeProject 的十二月比赛:)
  • “清理”一些类。它们直接使用其他类的内部成员,这不是非常酷的面向对象编程,您不觉得吗?
  • 代码覆盖率——我知道这不应该出现在待办事项列表中,但尽管我可以发誓我已经检查了所有代码,请不要相信我,并尝试将代码置于一些非常奇怪的情况中。
  • 构建一个由 NDoc 生成的 .CHM 文档?真的,我没时间。我夜以继日地工作以赶上一些“不可能完成的任务”的截止日期,我从我微不足道的睡眠时间中抽出时间来写这样的文章。也许有一天,也许。
  • 在下面的留言板中写下您的功能请求,我将考虑在文章的下一版本中实现它。

结论

感谢您阅读本文!这是我一生中写过的最长的一篇文章(我几个月前才开始写文章:) 我对您的耐心印象深刻!现在我感谢了您,我也应该向微软说声“谢谢!”,是它为我们带来了出色的 .NET 技术。如果 .NET 不存在,我怀疑我是否会写这样一篇文章,即使写了,也不会包含 C++ 源代码。.NET 框架让编程变得如此简单!它只是迫使您整天编写代码:) 我感觉我不是在编程,而是在原型设计。它比以前的 VB 更容易。真的!

现在让我们看看您从文章中学到了(或只是阅读了)什么:

  • 什么是负载平衡(一般而言)
  • 我关于动态软件负载均衡的理念、架构和一些实现细节
  • 一些多线程问题以及如何解决它们
  • 网络编程基础,包括 TCP、UDP 和组播
  • 一些(我希望)有用的技巧和变通方法
  • 如果 COM 是爱,那么 .NET 就是激情
  • 我是一个亲微软的人:)

啊啊啊啊啊啊啊我忘了告诉你们!请不要在下面的留言板里发消息教我不要使用 __gc*,而我可以直接输入 *。我就是喜欢 __gc 关键字,就是这样:)

下面有两本书,我曾读过,它们对编写本文的源代码很有帮助。您会惊讶地发现它们不是 .NET 书籍。我不是开玩笑——可能有超过 250 本 .NET 书籍,我读过大约 10 本,这就是为什么我真的无法向您推荐任何 .NET 书籍。如果我说“X 书是 Y 主题上最好的”,那是不公平的,因为我至少没有读过一半的 .NET 书籍,无法给出(权威的)建议。下面的书不仅仅是“必备”和“必读”:)。它们对于 Windows 开发人员来说是无价之宝。停止阅读这篇文章,现在就去买它们吧!:)

Jeffrey Richter、Jason D. Clark 的《Programming Server-Side Applications for Microsoft Windows 2000》(ISBN 0-7356-0753-2)

“我们开发者知道编写容错代码是我们应该做的,但我们常常认为所需的细节关注是繁琐的,因此就省略了。我们变得自满,认为操作系统会‘照顾好我们’。许多开发者甚至相信内存是无限的,泄漏各种资源是可以的,因为他们知道进程死亡时操作系统会自动清理一切。当然,许多应用程序都是这样实现的,结果并不具有破坏性,因为应用程序往往运行时间短,然后重新启动。然而,服务是永久运行的,省略适当的错误恢复和资源清理代码是灾难性的!”

John Robins 的《Debugging Applications》(ISBN 0-7356-0886-5)

“Bug 真令人讨厌。句号。Bug 是导致你忍受死亡行军项目、错过截止日期、熬夜和脾气暴躁的同事的原因。Bug 确实能让你的生活变得悲惨,因为如果你的软件中潜入了足够多的 Bug,客户就会停止使用你的产品,你可能会失业。Bug 是一个严肃的问题……当我写这本书的时候,NASA 就因为一个在需求和设计阶段潜入的 Bug 失去了一艘火星探测器。随着计算机控制着越来越多任务关键系统、医疗设备和超级昂贵的硬件,Bug 不再是被人嘲笑或被视为开发过程中自然发生的事情。”

关于 C++ .NET 托管扩展的最佳文本(除了规范和迁移指南之外),它不是一本书,而是一个微软官方课程 (MOC):“使用 Microsoft Visual C++ .NET 托管扩展编程”(2558)。如果您计划使用托管 C++ 进行任何 .NET 开发,您绝对应该参加此课程。

关于 C# 的(最后)一句话

你们中的许多人可能想知道我为什么用 Managed C++ 实现这个解决方案,而不是 C#,因为我只编写托管代码。我知道大多数 .NET 开发人员都在追随 C# 的潮流。我也是。我整天都在用 C# 编程。那是我的工作。然而,我太爱 C++ 了,所以我更喜欢用 Managed C++ 编写。我喜欢 MC++ 而不是 C# 的原因可能有一百个,而 IJW(以及一般的非托管代码)可能是列表中的最后一个。C# 与 C 或 C++ 没有任何关系,除了语法上的一些细微相似之处,无论微软如何试图说服我们相反。它更接近 Java,而不是 C/C++。我听起来很极端吗?嗯,我的一位 C/C++ 死忠同事(Boby -- http://606u.dir.bg/)强迫自己学习和使用 VB.NET,以便在开发 .NET 应用程序时不会忘记 C++。现在谁是极端分子呢?:) 微软正在非常努力地推动我们忘记 C++,所以他们和一些开源 C++ 死忠是唯一使用它的人:) 你们没注意到吗?截至今天,ATL 列表每天只会生成几个独特的帖子,而几个月前,在微软突然决定放弃它,然后在一周内又在 ATL 社区的强烈反对下恢复它之前,每天至少有 10-20 个帖子。微软对此说了什么,嗯?COM 没有死!COM 将继续存在!ATL 活在(某个时空中)。废话,废话 :) 我崇拜 .NET,但我不希望在三四年后,当它被更酷的技术取代时,去尘土飞扬的书架上寻找“C 语言编程”和“C++ 编程语言”书籍。C++ 万岁!:) 无论如何,我曾想用 C# 实现这个解决方案,因为那样每个月的奖品会更好看:)

免责声明

本软件“按原样”提供,存在所有缺陷,不附带任何保证。如果您发现源代码或文章有用,祝我圣诞快乐:) 

祝您圣诞最快乐,期待明年再见:) 

史托扬

© . All rights reserved.