.NET客户端-服务器应用程序卡死,需要想法来解决!

5

大家好,我是 Stack Overflow 的助手。

我有一个 .NET 的客户端-服务器应用程序,运行着几百个客户端。该项目是从 VB6 迁移到 .NET 大约一年前,并且是一款卡牌/棋盘游戏平台。

尽管下面我将尽可能提供详细信息,但问题在于当通道内有 40-70 个玩家时会出现频繁的卡顿。

架构

1. 服务器 (.NET 4.0)

  • 分为三个项目:ServerNET、Listener 和 Channel。
  • Listener 充当登录服务器,客户端首先连接到此服务器。它负责检查版本和帐户信息等内容。同时让客户端选择要连接的频道。它基本上是一个 TCPListener 在 do-while 中不断监听正在尝试连接的任何人。这不是导致双方冻结的原因。
  • Channel 表示单个端口,客户端在完成 Listener 后连接到 Channels。就像航天飞机一样,这是主要部分。类似于 MIRC 频道,它将所有用户绑定在一起,大多数数据都被发送给频道内的人,例如聊天和可以加入其他玩家创建并由服务器托管的游戏。这是一个控制台应用程序,作为玩家的中心。玩家信息存储在“Client”类中,其中包括 TCPClient 和其他一些属性。每个客户端都运行在一个线程上,发出异步调用,由服务器处理。此外,“Client”对象存储在名为“ClientCollection”的集合类中。当频道内有大约 40-70 名玩家时,频道会被冻结。每个频道允许最多 100 名玩家。
  • ServerNET 是整个系统相关的主体,并且执行所有其他通道无关的一般性任务。这是一个表单应用程序,运行服务器选项等内容。

2. 客户端 (.NET 2.0)

  • 使用 TCPClient 运行,大多数情况下是单线程,而服务器则是多线程。
  • 必须使用 .NET 2.0。
  • 主要由视觉效果和其他不重要的内容组成。

当有 40 个以上的客户端连接到单个频道时,它开始随机完全冻结(或者说我们现在还没有证据或足够的数据来指出问题所在)。我们真的不认为网络流量是问题(还不确定),因为我们已经在不同的服务器上使用了各种设置进行了尝试。我们使用的所有服务器机器在硬件方面都能够处理那么多进程。因此,这是关于方法和代码方面发生了什么。

我们无法解决问题的原因在于我们不确定导致问题的原因。请看以下示例:
系统A有55个人在线,他们的频道#1没有任何冻结。系统A使用A1 IP,频道位于16xxx端口。
系统B有25个人在线,他们的频道#4会随机冻结一两分钟。系统B使用B1 IP和18xxx频道端口。它与没有冻结的系统A在同一台机器上。
总之,这似乎与在线人数无关,但是当数字上升时,它发生得更频繁。
我们尝试在Channel项目中滚动Application.DoEvents()以进行无限循环,认为某些X进程会导致频道进入冻结状态几分钟,从而导致频道暂停。然后,在几秒钟内执行排队的每个操作,而它被冻结时。每个频道的CPU使用率平均在7%-20%之间,看起来情况正在好转。但是这并不是永久有效的解决方案。
我们怀疑以下事项:
- ClientCollection保存玩家和TCPClients是从CollectionBase继承的。也许这会在同步期间造成混乱。这曾经是一个数组,我们遇到了较少的这些问题。也许它不应该从CollectionBase继承,而应该从其他地方继承? - 我们使用SyncLock(C#中的锁)同步ClientCollection。(尽管我们在开始使用锁之前就有这个问题)
服务器信息 Intel Xeon X3460 2.80GHz 16 GB RAM 64位Windows Server 2008 Enterprise 我知道没有看到整个代码是不可能解决问题的,但我很遗憾无法发布代码。相反,我正在寻找一个思路来指导我。但是,我们很乐意分享任何其他信息以解决此问题。
感谢所有帮助!

听起来像是一种无法在没有代码的情况下解决的竞态条件。 - ntziolis
3个回答

2

我们在一个非常类似的应用程序上遇到了非常相似的问题(我们向大约1300个用户推送统计数据)。

我最好的猜测是,在您的TCPClient上,您设置了无限超时。不幸的是,这是默认行为。因此,当TCPClient在读取时阻塞时,它有时会完全冻结。

将超时设置为30秒(或适合您情况的其他时间)。

TcpClient newClient = incoming.AcceptTcpClient();
newClient.NoDelay = true;  // Send & receive immediately, even when the buffers aren't full
newClient.ReceiveTimeout = 30000;
newClient.SendTimeout = 30000;

将使用这些属性运行测试,并让您知道(希望今天)。感谢您的建议。 - Mithgroth
我们已经发布了一个测试,其中NoDelay设置为true,Timeouts设置合适(如15-20秒)。同时需要更正的是,Timeout属性是以毫秒为单位设置的,而不是秒。目前测试已经进行了约5个小时,但尚未出现冻结情况。然而,我们需要等待更多用户连接(已测试约45个用户,以前会因此而冻结,这是一个好兆头)。 - Mithgroth
我们已经测试了几天,没有关于它冻结的报告。虽然还不确定你的解决方案是否合理,但我仍然有困难理解如何通过设置三个属性就能使其变得更好 :) 我还想请教您对TCPListener和TCPClient对象或其他任何优化的进一步意见。我们成功地在单个通道中达到了70左右的结果,非常满意。谢谢!(附言:当我获得15点声望时,我会投赞成票,目前这个新账户有些懒惰 :p) - Mithgroth

1
对于我而言,服务器应用程序中使用同步套接字是大忌。不要为每个连接的客户端使用一个线程。不要使用 TcpClient.Read/TcpClient.Send
阅读有关 BeginRead/EndRead+BeginSend/EndSend 方法的内容。它们比使用线程和同步方法更具可扩展性。

更新

异步读取并不意味着您不能同步处理读取命令。异步读取的原因在于能够获取完整的命令,而无需为每个客户端使用自己的线程。

像这样进行阅读:

  1. BeginRead
  2. 在 OnRead(BeginRead 回调)中调用 EndRead
  3. 0 字节 = 断开连接
  4. 将接收到的数据附加到缓冲区(如果您的命令是字符串,请勿将字符串用作缓冲区,而应使用 StringBuilder)
  5. 检查缓冲区是否包含完整的数据包。
  6. 调用处理完整数据包的方法/事件/委托
  7. 调用 BeginRead

正如您所看到的,处理仍然可以是同步的,而且您不必为每个客户端创建新线程。据我所知,.Net使用IO完成端口来进行套接字IO操作,这可以很好地扩展。

当使用套接字时,使用BeginSend/EndSend并不是必需的,因为通常在发送时只需要“火而忘之”。真正影响性能的是每个客户端的读取线程。


1

系统死机时是否可能进行完整进程挂起转储?

这样你就可以看到每个线程正在做什么,以更好地了解原因。

  • 要进行挂起转储,需要下载Windows调试工具,该工具随 .net 4.0 SDK 一起提供。
  • 然后使用 -Hang 标志和进程ID 运行 AdPlus.vbs。
  • 最后在 WinDbg 中运行 ~ *e !clrstack 命令以获取所有的调用堆栈。

下载中,将发布结果。 - Mithgroth
由于这是一个挂起的情况,连续几秒钟内进行2到3次转储,将为我们提供哪个线程仍然停留在同一位置的更好视图。 - Menahem
@Menahem http://i.imgur.com/JiQEp.png 和 http://hotfile.com/dl/108098662/e4a2350/DumpText.rar.html 看起来好像我缺少了某些东西,因为出现了一个巨大的错误。 - Mithgroth
看起来你运行WinDbg的机器与运行应用程序的机器版本不同(x86 / x64,不同的.NET版本)。你能否在类似的操作系统和.NET上尝试一下?我稍后会尝试访问你发送的文件。 - Menahem
@Menahem 你好,我在类似的操作系统和.NET上成功运行了它,并成功获取了每个线程的调用堆栈。虽然对于这个问题来说并没有像我想象的那样有帮助,但是从现在开始,它肯定会对我在其他主题上有所帮助。除此之外,你还能推荐其他windbg在这种情况下的用途吗? - Mithgroth
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接