【编者按】本文系国内 ITOM 管理平台 OneAPM 翻译自 Steven Haines 的文章。Steven Haines 是 Pisksel 技术架构师,目前在奥兰多迪士尼乐园工作。他是在线教育网站 geekcap.com 的创始人,著有上百篇 Java 相关的文章以及三本 Java 著作:《Java 2 From Scratch》《Java 2 Primer Plus》以及《Pro Java EE Performance Management and Optimization》

实现有效 APM 策略所面临的挑战:

  • 代码依赖
  • 过度或不必要的日志
  • 同步与锁
  • 潜在数据库问题
  • 潜在的基础架构问题

1、代码依赖

开发程序是一项具有挑战性的工作。你不仅要为了满足商业需求而建立程序逻辑,还要选择最合适的代码库和工具来帮助你。你能想象自己创建所有的日志管理代码,XML 和 JSON 解析逻辑,或所有的序列化库么?你当然可以编写代码来完成这些事,但是诸多开源开发者团队已经做好了这些事情,你又何必亲力亲为呢?此外,如果你正在与第三方系统集成,你会自己读完专有的通信协议规范,还是购买供应商提供的库帮你完成呢?

我相信你会同意:如果有人已经解决了你的问题,使用他的解决办法会比自己想办法解决效率更高。如果这是一个已经被许多公司采用的开源项目,那么很可能它已经经过完备的测试,文档充足,而且你应该找得到许多使用教程。

然而,使用依赖库是有危险的。你需要回答以下问题:

  • 这个库真的写得很好并且已经充分测试了吗?
  • 你是否用与众多公司一样的方式使用这个库?
  • 你的使用方式是否正确?

请确保在选择外部库之前进行一些调查,如果你对某个库的性能有什么疑问,那就进行一些性能测试。开源项目很好的地方在于你可以访问它们的全部源代码以及测试套件和构建流程。下载它们的源代码,执行编译过程,并查看测试结果。如果你看到很高的测试覆盖率,那么就可以比没有测试案例时信心百倍!

最后,确保正确地使用依赖库。如果正确使用,ORM 工具的确能够大大提高性能。ORM 工具的问题在于,如果你不花时间去学习如何正确地使用它,你就会轻易的砸自己脚,破坏自己的应用性能。关键就在于如果不花时间学习这些工具,本应帮助你的工具反而会伤害你。

2、过度或不必要的日志

日志记录是调试工具库里的强大武器,可以帮助你识别应用执行过程中在特定时间内可能发生的异常。当错误发生时,捕捉错误信息并收集尽可能多的上下文信息是非常重要的。然而,简洁地捕捉错误条件和过度记录之间是有差别的。

最普遍的两个问题就是:

  • 多级别异常日志
  • 错误配置生产日志级别

异常日志能帮助你了解应用程序中发生的问题,因而非常重要。但一个常见的问题是,应用程序所有层级的异常都进行记录。例如,你的某个数据访问对象捕获到一个数据库异常,并将该异常传达到服务层。服务层可能会捕捉该异常,并将其传达到网络层。如果我们在数据层、服务层和网络层上都记录该异常,那么我们对此相同的错误条件就有三条堆栈记录。这会导致写入日志文件的额外负担,还会使日志文件充满冗余信息。但这个问题非常普遍,我敢断言,如果你检查自己的日志文件,你很可能会发现多个这样的例子。

生产应用中常见的另一个大的日志问题与日志级别有关。.NET 日志记录器定义了以下日志记录级别(.NET TraceLevel 与 log4net 中的命名会有所不同,但绝对相似):

  • Off
  • Fatal
  • Error
  • Warning
  • Info
  • Verbose / Debug

在生产应用程序中,你应该只记录 error 或 fetal 级别的日志语句,在更宽松的环境中,捕捉 warning 甚至 info 级别的日志信息也完全可以,但是一旦应用投入生产环境,用户负载将迅速填满日志并使应用程序陷入瘫痪。如果你不经意地将生产环境下的应用日志级别设为 debug,应用的响应时间比正常情况下高两或三倍都不奇怪!

3、同步与锁

有时候,你想确保应用代码中每次只有一个线程执行一段代码子集。 例如,读取单线程规则执行组件之类的共享软件资源,以及文件句柄或网络连接之类的共享基础架构资源。.NET 框架提供了许多不同类型的同步策略,包括锁/监视器、进程间互斥,和读/写锁这类的专用锁。

不管你为什么要同步代码或者选择什么机制实现代码同步,都会导致一个问题:那就是有部分代码一次只能由一个线程执行。 设想去超市,只有一个收银员在工作:许多人进入商店,浏览商品,将商品放进购物车里,但某一时候,他们不得不排队以进行支付。在这个例子中,购物是多线程的,每个人都代表一个线程。然而结账是单线程的,这意味着每个人都要花费排队付款的时间。这个过程如图1所示。

不可忽视的 .NET 应用5大性能问题 技术分享 第1张 图1:线程同步

我们有七个线程,都需要访问一段同步代码块,所以它们依次获得权限访问该代码块,执行其功能,然后继续。

在图2中总结了线程同步的过程。 不可忽视的 .NET 应用5大性能问题 技术分享 第2张

图2 线程同步过程

首先,为特定的对象(System.Object 派生)创建锁,意味着当一个线程试图进入同步代码块时必须获取该同步对象的锁。如果该锁可用,则该线程被授予执行同步代码的权限。在图2中的例子中,当第二个线程到达时,第一个线程已经占有了该锁,所以第二个线程被强制等待,直到第一个线程执行完毕。当第一个线程执行结束时,会释放该锁,然后第二个线程被授予访问权限。

正如你可能猜测到的,线程同步将给 .NET 应用带来一个极大的挑战。我们设计应用程序时,希望其能支持数十个甚至数百个同步请求,但线程同步会把所有处理这些请求的线程串行化,导致性能瓶颈!

解决的办法有两种:

  • 仔细检查同步的代码,以确定是否存在其他可行办法
  • 限制同步代码块的范围

有时候,你要访问必须同步的共享资源,但很多时候,你可以用完全避免同步的方法重新解决该问题。例如,我们之前使用的规则过程引擎有单线程的要求,因此拖慢了程序中所有请求的执行速度。这显然是一个设计上的缺陷,我们可以用一个可以并行工作的库取代之。你需要问自己是否有更好的选择:如果你在往一个本地文件系统写入信息,你是否可以把信息发送给某项服务,再由该服务将信息存储到数据库中?你是否可以将对象设为不可变,从而无论是否有多线程访问它都没关系?等等,等等…

对于那些必须要同步的代码段,请合理地选择锁。你的目标是将同步代码块隔离以满足最低限度的同步要求。通常最好是定义一个特定的对象进行同步,而不是对包含同步代码的对象进行同步,因为你可能会在不经意间拖慢该对象的其他交互。最后,考虑使用读/写锁,而不是标准的锁,这样,你可以在资源只进行同步变化时,允许读操作。

4.潜在的数据库问题

几乎所有的内容应用最终都会涉及到向/从数据库或文档存储储存/检索数据。因此,数据库、数据库查询,以及存储过程调优对应用程序的性能来说是最重要的。

程序架构师/开发人员和数据库架构师/开发人员之间有一个哲学性的划分。应用程序架构师倾向于认为所有的业务逻辑都应该驻留在应用程序中,数据库应该只提供访问数据的通道。另一方面,数据库架构师更倾向于认为将业务逻辑推到数据库中更有益提高性能。这个划分的答案很可能就是介于两者之间。

作为一个应用程序架构师,我倾向于将更多的业务逻辑应用在程序当中,但我完全承认数据库架构师能更好的理解数据和与数据交互的最佳方式。我认为,这两个群体之间的协同合作才能产生最佳的解决方案。但是,不管你倾向于哪一方,请确保你的数据库架构师检查你的数据模型,所有的查询语句和存储过程,他们都有丰富的知识帮助你以最佳的方式来调整和配置数据库,他们有大量的工具可以为你调整查询语句。例如,有一些工具可用于 SQL 调优,遵循以下这些步骤:

  • 分析 SQL 语句
  • 确定查询的执行计划
  • 利用人工智能生成备选的 SQL 语句
  • 确定所有备选方案的执行计划
  • 提出最佳的查询方式来完成目标

当我在写数据库查询代码时,我使用了这类工具,并在高负载情况下量化了结果,一些细微的调整和优化,都能导致极大的性能提升。

5、潜在的基础架构问题

之前提过,.NET 应用运行在分层的环境中,其层级结构如图3所示:

不可忽视的 .NET 应用5大性能问题 技术分享 第3张 图3.NET分层执行模型

你的应用程序运行在 ASP.NET 或是 Windows Forms 容器中,使用 ADO 库与运行在 CLR 内部的数据库交互,而 CLR 运行在操作系统中,操作系统又运行在硬件里。而该硬件又与其他包含不同技术堆栈的硬件通过网络相连。在你的应用与外部环境之间,以及在应用的组件之间,通常有多个负载平衡器。我们还有 API 管理服务以及多级缓存。所有这一切,都是为了说明,基础构造数量庞杂,都可能影响应用程序的性能!

因此,你必须细致地调整基础架构。检查你的应用组件和数据库所运行的操作系统和硬件设备,以确保它们的最佳表现。测量服务器之间的网络延迟,并确保你有足够的带宽来满足应用程序之间的交互。检查缓存,确保较高的缓存命中率。分析负载平衡器的行为以确保请求很快地分发到所有可用的服务器。总之,你需要全面检查应用程序的性能,既包括应用业务交易也包括支持它们的基础架构。

OneAPM 助您轻松锁定 .NET 应用性能瓶颈,通过强大的 Trace 记录逐层分析,直至锁定行级问题代码。以用户角度展示系统响应速度,以地域和浏览器维度统计用户使用情况。想阅读更多技术文章,请访问 OneAPM 官方博客