作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
胡安·巴勃罗·西达的头像

胡安·巴勃罗·西达

Juan是一名拥有10多年经验的软件架构师. 他是一名合格的 .. 网。和Java开发人员,并且热爱Node.js和Erlang.

以前在

Globant
分享

软件开发可能是一个非常复杂的过程. 作为开发者,我们需要考虑许多不同的变量. 有些是我们无法控制的, 有些是我们在实际执行代码时不知道的, 有些是由我们直接控制的. 和 .网络开发人员 没有例外吗.

考虑到这一现实,当我们在受控环境中工作时,事情通常会按计划进行. 一个例子是我们的开发机器,或者我们可以完全访问的集成环境. 在这些情况下, 我们可以使用工具来分析影响代码和软件的不同变量. 在这些情况下, 我们也不需要处理服务器的繁重负载, 或者并发用户试图在同一时间做同样的事情.

在描述和安全的情况下,我们的代码将工作得很好, 但在生产中受到重负荷或其他一些外部因素的影响, 可能会出现意想不到的问题. 生产中的软件性能很难分析. 大多数时候,我们必须在理论场景中处理潜在的问题:我们知道问题可能会发生, 但是我们不能测试它. 这就是为什么我们需要基于我们正在使用的语言的最佳实践和文档来进行开发, ,避免 常见的错误.

正如前面提到的, 当软件上线时, 事情可能会出错, 代码可能会以我们没有计划的方式开始执行. 我们可能会遇到这样的情况:我们不得不处理问题,却没有能力调试或确定发生了什么. 在这种情况下我们能做什么?

高CPU使用率是指一个进程在很长一段时间内使用超过90%的CPU——我们就有麻烦了

如果一个进程在很长一段时间内使用超过90%的CPU,我们就有麻烦了

在本文中,我们将分析一个高CPU使用率的真实场景 .. 网。 web应用程序 在窗户。服务器上, 识别问题所涉及的过程, 更重要的是, 为什么这个问题首先会发生,我们如何解决它.

CPU使用和内存消耗是被广泛讨论的话题. 通常很难确切地知道什么是合适的资源(CPU), 内存, 一个特定进程应该使用的I/O), 在什么时间. 尽管有一件事是肯定的——如果一个进程在很长一段时间内使用了超过90%的CPU, 我们遇到麻烦是因为服务器在这种情况下无法处理任何其他请求.

这是否意味着流程本身存在问题? 不一定. 这可能是因为该进程需要更多的处理能力,或者它正在处理大量数据. 首先,我们唯一能做的就是试着找出发生这种情况的原因.

所有的操作系统都有几个不同的工具来监视服务器上正在发生的事情. 窗户。服务器有专门的任务管理器, 性能监视器,在我们的例子中我们用 新遗物服务器 哪个是监视服务器的好工具.

第一症状和问题分析

在部署应用程序之后, 在前两周的时间流逝中,我们开始看到服务器有CPU使用高峰, 导致服务器无响应. 为了让它再次可用,我们不得不重新启动它, 这个事件在那段时间内发生了三次. 正如我之前提到的,我们使用新遗物服务器作为服务器监视器,它显示 w3wp.exe 进程在服务器崩溃时使用了94%的CPU.

Internet信息服务(IIS)工作进程是一个windows进程(w3wp.exe),它运行Web应用程序, 并负责处理发送到特定应用程序池的Web服务器的请求. IIS服务器可以有几个应用程序池(和几个不同的应用程序池) w3wp.exe 进程),这可能会产生问题. 基于进程所拥有的用户(这在New Relic报告中显示), 我们认识到问题出在我们的 .网。 c# web表单遗留应用程序.

的 .. 网。框架与windows调试工具紧密集成, 因此,我们尝试做的第一件事是查看事件查看器和应用程序日志文件,以查找有关正在发生的事情的一些有用信息. 无论我们是否在事件查看器中记录了一些异常,它们都没有提供足够的数据来分析. 这就是我们决定进一步收集更多数据的原因, 所以当事件再次发生时,我们会做好准备.

数据收集

收集用户模式进程转储的最简单方法是使用 调试诊断工具v2.0 或者简单地调试. DebugDiag有一组收集数据(DebugDiag Collection)和分析数据(DebugDiag Analysis)的工具。.

那么,让我们开始定义使用调试诊断工具收集数据的规则:

  1. 打开DebugDiag Collection并选择 表演。.

    调试诊断工具

  2. Select 性能计数器 并点击 下一个.
  3. 点击 添加Perf触发器.
  4. 扩大 处理器 (不是 过程)对象并选择 %处理器时间. 注意,如果您使用的是窗户。 Server 2008 R2,并且处理器数量超过64个, 请选择 处理器的信息 对象,而不是 处理器 object.
  5. 在实例列表中,选择 _Total.
  6. 点击 添加 然后点击 OK.
  7. 选择新添加的触发器并单击 编辑阈值.

    性能计数器

  8. Select 以上 在下拉菜单中.
  9. 将阈值更改为 80.
  10. 输入 20 表示秒数. 如果需要,您可以调整此值, 但是要注意不要指定小的秒数,以防止错误触发.

    性能监视器触发器属性

  11. 点击 OK.
  12. 点击 下一个.
  13. 点击 添加转储目标.
  14. Select Web应用程序池 从下拉菜单中.
  15. 从应用程序池列表中选择您的应用程序池.
  16. 点击 OK.
  17. 点击 下一个.
  18. 点击 下一个 再一次。.
  19. 如果您愿意,请为您的规则输入一个名称,并记下转储文件保存的位置. 如果需要,您可以更改此位置.
  20. 点击 下一个.
  21. Select 立即激活规则 并点击 完成.

所描述的规则将创建一组迷你文件,这些文件的大小相当小. 最后的转储将是一个具有完整内存的转储,并且该转储将大得多. 现在,我们只需要等待高CPU事件再次发生.

一旦我们有转储文件在选定的文件夹中,我们将使用DebugDiag分析工具来分析收集的数据:

  1. 选择性能分析器.

    调试分析工具

  2. 添加转储文件.

    调试分析话费转储文件

  3. 开始分析.

DebugDiag将花费几分钟(或几分钟)来解析转储并提供分析. 当它完成分析时, 你会看到一个网页,上面有一个总结和很多关于线程的信息, 类似于下面这个:

分析Sumary

正如您在摘要中看到的那样, 有一个警告说“在一个或多个线程上检测到转储文件之间的高CPU使用率。.“如果我们点击推荐, 我们将开始理解应用程序的问题在哪里. 我们的示例报告是这样的:

按平均CPU计算的前10个线程

正如我们在报告中看到的,有一个关于CPU使用的模式. 所有具有高CPU使用率的线程都与同一个类相关. 在跳转到代码之前,让我们先看一下第一个.

.网。调用栈

这是我们的问题的第一个线程的细节. 我们感兴趣的部分如下:

.. 网。调用堆栈详细信息

这里有一个对代码的调用 GameHub.OnDisconnected () 是什么触发了有问题的操作, 但在这个调用之前,我们有两个Dictionary调用, 这可能会让你对正在发生的事情有所了解. 让我们看看 .. 网。代码查看该方法正在做什么:

public override Task OnDisconnected () {
    	试一试
    	{
        	var userId = GetUserId();
        	字符串connId;
        	如果(onlineSessions.TryGetValue(userId, out connId)
            	onlineSessions.删除(userId);
    	}
    	抓住(异常)
    	{
        	/ /忽略
    	}

    	返回基地.OnDisconnected ();
    	}

我们显然有麻烦了. 报告的调用堆栈显示问题出在字典上, 在这段代码中,我们访问的是一个字典, 具体来说,引起这个问题的是这一行:

如果(onlineSessions.TryGetValue(userId, out connId)

这是字典声明:

静态 Dictionary onlineSessions = new Dictionary();

这有什么问题 .网。代码?

每个有面向对象编程经验的人都知道静态变量将被这个类的所有实例共享. 中的静态是什么意思,让我们深入了解一下 .网络世界.

根据 .. 网。 c#规范:

使用 静态 修饰符来声明静态成员, 哪个属于类型本身而不是特定对象.

这就是 .. 网。 c#语言规范中关于 静态类和成员:

与所有类类型一样,静态类的类型信息由 .. 网。 Framework公共语言运行时(CLR),当装入引用该类的程序时. 程序不能准确指定加载类的时间. 然而, 保证在程序中第一次引用类之前,它被加载并初始化其字段和调用其静态构造函数. 静态构造函数只被调用一次, 并且静态类在程序所在的应用程序域的生命周期内一直保留在内存中.

非静态类可以包含静态方法、字段、属性或事件. 即使没有创建类的实例,静态成员也可以在类上调用. 静态成员总是通过类名访问,而不是实例名. 无论创建了多少个类实例,静态成员只存在一个副本. 静态方法和属性不能访问其包含类型中的非静态字段和事件, 它们不能访问任何对象的实例变量,除非在方法参数中显式传递.

这意味着静态成员属于类型本身,而不是对象. 它们也由CLR加载到应用程序域中, 因此,静态成员属于承载应用程序的进程,而不是特定的线程.

因为web环境是一个多线程环境, 因为每个请求都是一个新线程,由 w3wp.exe process; and given that the 静态 members are part of the process, 我们可能有这样一个场景:几个不同的线程试图访问静态(由几个线程共享)变量的数据, 这可能最终导致多线程问题.

字典 文档 在线程安全下声明如下:

A Dictionary 是否可以同时支持多个读取器,只要不修改集合. 即便如此,在集合中枚举本质上也不是线程安全的过程. 在枚举与写访问竞争的罕见情况下, 在整个枚举过程中必须锁定集合. 允许多个线程对集合进行读写访问, 您必须实现自己的同步.

这句话解释了为什么我们会有这个问题. 根据转储信息,问题出在字典FindEn试一试方法上:

.. 网。调用堆栈详细信息

如果我们看一下字典FindEn试一试 实现 我们可以看到,该方法遍历内部结构(bucket)来查找值.

下面是 .. 网。代码正在枚举集合,这不是线程安全的操作.

public override Task OnDisconnected () {
    	试一试
    	{
        	var userId = GetUserId();
        	字符串connId;
        	如果(onlineSessions.TryGetValue(userId, out connId)
            	onlineSessions.删除(userId);
    	}
    	抓住(异常)
    	{
        	/ /忽略
    	}

    	返回基地.OnDisconnected ();
	}

结论

正如我们在垃圾场看到的那样, 有几个线程试图同时迭代和修改共享资源(静态字典), 最终导致迭代进入无限循环, 导致线程消耗超过90%的CPU.

对于这个问题有几种可能的解决方案. 我们首先实现的是锁定和同步对字典的访问,代价是损失性能. 当时服务器每天都会崩溃,所以我们需要尽快修复这个问题. 即使这不是最优解决方案,它也解决了问题.

解决这个问题的下一步是分析代码并找到最优的解决方案. 重构代码是一种选择:new ConcurrentDictionary最后 类可以解决这个问题,因为它只在bucket级别锁定,这将提高整体性能. 尽管这是一个很大的进步,还需要进一步的分析.

聘请Toptal这方面的专家.
现在雇佣
胡安·巴勃罗·西达的头像
胡安·巴勃罗·西达

位于 阿根廷科尔多瓦

成员自 2014年8月7日

作者简介

Juan是一名拥有10多年经验的软件架构师. 他是一名合格的 .. 网。和Java开发人员,并且热爱Node.js和Erlang.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

以前在

Globant

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.