当我们谈论某样东西具有可靠性时,我们是指它值得信赖,而且可以预测。但是就软件而言,还必须具备其他重要属性,才可以说代码具有可靠性。
软件必须具有复原性,意思是说在出现内部和外部中断情况时,它仍然可以继续正常运行。它必须是可恢复的,以便它知道如何将自己恢复到先前已知的一致状态。软件必须可预测,这样它会提供及时的预期服务。它必须不可中断,意思是更改和升级都不会影响它的服务。最后,软件必须是生产就绪的,意思是它包含最少的 bug,并且只需要进行数量有限的更新。如果满足了这些条件,那么软件就真正称得上可靠了。
可靠代码的这些关键属性取决于不同的因素 — 有些取决于软件的整体体系结构,有些取决于将运行软件的操作系统,还有一些则取决于用来开发应用程序的工具和构建应用程序所基于的框架。复原能力是一种依赖于每一层的属性,应用程序的复原能力取决于其最薄弱的一环。
现在,请设想一下基于 Microsoft .NET Framework 的应用程序。这些应用程序委托运行时进行某些操作,这些操作在本机环境中不存在(例如 IL 代码的实时编译),或者已处于开发人员的直接控制之下(例如内存管理)。就可靠性而言,平台自身可以引入自己的故障点,这些故障点会影响在其上运行的应用程序的可靠性。了解这些故障可能在哪里发生以及可以使用什么样的技术来创建更可靠的基于 .NET 的应用程序非常重要。
了解运行时故障
某些异常事件在任何时候、任何代码段中都有可能发生。这些事件我们统称为异步异常,包括资源耗尽(内存不足和堆栈溢出)、线程终止和访问冲突。(在执行托管代码时,访问冲突会在运行时中发生。)
最后这个情形不是很有意义 — 如果确实发生了这样的事件,就意味着公共语言运行时 (CLR) 实现中发现了严重的 bug,应予以修复。但是对前两种情形,有必要进行进一步的分析。
理论上,我们会认为资源耗尽会得到运行时的妥善管理,并且它们绝不会影响应用程序代码继续运行的能力。可这只是理论,实际情况要复杂得多。
为了说明这个问题,我们首先来看一下某些常见的服务器应用程序如何处理内存不足 (OOM) 事件。对可用性要求很高的服务器应用程序(例如 ASP.NET 和 Exchange Server 2007)已通过 AppDomain 和进程回收达到了此目的。操作系统提供了非常强大的机制来清理内存和进程使用的大多数其他资源 — 所有这一切都在进程终止后完成。
就客户端而言,当内存压力达到即使很小的分配也会出现故障这种程度时,由于严重的超负荷和分页,会使整体系统进入一定程度上的无响应状态,导致用户宁愿去按重置按钮或者寻求任务管理器的帮助,也不愿意等待任何恢复代码的执行。从某种意义上说,用户的第一反应是手动执行 ASP.NET 或 Exchange 2007 会自动执行的同一个操作。
某些 OOM 甚至并不是由运行代码的任何特殊问题所引起的。运行在计算机上的其他进程或运行在该进程中的其他 AppDomain 可能会占用可用的资源池,并导致分配失败。从这种意义上说,应认为资源耗尽是异步的,因为它们在执行代码的任何时候都可能发生,并且它们可能依赖于运行代码外部和独立于运行代码的各种环境因素。
由于运行时可能会分配内存以执行与其自身运行相关的操作,因此该问题会变得更加严重。下面是几个发生在运行时的分配示例,它们在资源有限的环境中可能会发生故障:
装箱和取消装箱
延迟的类加载,直到第一次使用类为止
对 MarshalByRef 对象的远程操作
对字符串的某些操作
安全检查
JITing 方法