云计算之路-阿里云上:两个重要突破
我们发布博文云计算之路:用阿里云 vs Azure的对比测试揭开乌云的面纱之后,阿里云产品、技术相关负责人赶到了上海,和我们一起寻找问题的真正原因。不管是哪个层面的原因,找出问题的真正原因并解决问题才是最重要的。
经过一天的努力,针对“CPU波动”与“请求执行时间长”这两个问题有了重要突破。
CPU问题
对于“阿里云 vs Azure的对比测试”中发现的CPU占用100%问题(对应于访问高峰期CPU大幅波动问题),发现竟然与磁盘IO有关,这个磁盘IO是因为内存页面交换文件。
阿里云虚拟机中的页面交换文件设置是Windows系统的默认设置——在系统盘上,见下图:
Azure虚拟机中的页面交换文件放在了速度非常快的Temporary Storage上。
Azure的Temporary Storage磁盘IO性能要比阿里云的系统盘快好几倍。
虽然虚拟机的可用内存绰绰有余,但在高并发的情况下,会产生大量内存分配的操作,有可能造成Windows进行了页面文件交换的操作。
于是,我们在阿里云虚拟机上禁用了页面交换文件,如下图:
压测显示CPU竟然有了完全不一样表现,CPU占用率不再是一条100%的直线,波动也很稳健。
我们现在已经把所有Web服务器的页面交换文件禁用,看今天访问高峰期的CPU表现。
请求执行时间长(Request Execution Time)问题
在故障期间大家访问速度网站奇慢的时候,对应的服务器端的表现就是超长的请求执行时间。
昨天,我们在故障期间用DebugDiag(Debug Diagnostic Tool)抓取了w3wp进程的dump文件。
通过DebugDiag的分析报告发现很多处理请求的线程发生了阻塞:
然后通过windbg进一步分析,找到阻塞发生的具体地点。
使用的windbg命令是:
.symfix c:\websymbols
.reload
loadby sos clr
~* e !clrstack
得到的结果是:
System.Threading.ReaderWriterLock.AcquireReaderLockInternal(Int32)
System.Configuration.Internal.InternalConfigRoot.AcquireHierarchyLockForRead()
System.Configuration.Internal.InternalConfigRoot.GetConfigRecord(System.String)
System.Configuration.Internal.InternalConfigRoot.GetUniqueConfigRecord(System.String)
System.Web.CachedPathData.Init(System.Web.CachedPathData)
System.Web.CachedPathData.GetConfigPathData(System.String)
System.Web.CachedPathData.GetConfigPathData(System.String)
System.Web.CachedPathData.GetConfigPathData(System.String)
System.Web.HttpContext.GetFilePathData()
System.Web.HttpContext.SetImpersonationEnabled()
System.Web.HttpRuntime.ProcessRequestNotificationPrivate(System.Web.Hosting.IIS7WorkerRequest, System.Web.HttpContext)
System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr, IntPtr, IntPtr, Int32)
System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr, IntPtr, IntPtr, Int32)
DomainNeutralILStubClass.IL_STUB_ReversePInvoke(Int64, Int64, Int64, Int32)
[ContextTransitionFrame: 0000000031ddef58]
线程阻塞是发生在ASP.NET Runtime读取web.config相关配置信息,申请读操作锁的时候,而且申请超时时间是-1。也就是说如果申请不到读操作锁,线程就一直在这儿等待。
从这里可以看出,在故障期间请求根本还没有到达应用程序的处理代码。也不应该是ASP.NET的bug,如果ASP.NET出这么大的问题,微软就没法在IT界混了。
我们猜测某种特殊情况造成了线程在这个地方申请读操作锁时发生了死锁(deadlock)。
根据我们的进一步分析,死锁可能发生在System.Configuration.Internal.InternalConfigRoot.GetConfigRecord()中,请看精简后的IL反编译出来的代码:
// System.Configuration.Internal.InternalConfigRoot public IInternalConfigRecord GetConfigRecord(string configPath) { try { this.AcquireHierarchyLockForRead(); //... this.hlFindConfigRecord(parts, out num, out baseConfigurationRecord); //... } finally { this.ReleaseHierarchyLockForRead(); } try { this.AcquireHierarchyLockForWrite(); //... this.hlFindConfigRecord(parts, out num2, out baseConfigurationRecord2); //... baseConfigurationRecord3 = (BaseConfigurationRecord)RuntimeConfigurationRecord.Create(this, baseConfigurationRecord2, text); baseConfigurationRecord2.hlAddChild(text2, baseConfigurationRecord3); //.. } } finally { this.ReleaseHierarchyLockForWrite(); } return result; }
如果某个线程在AcquireHierarchyLockForWrite()之后,一直不ReleaseHierarchyLockForWrite(),其他线程都会在AcquireHierarchyLockForRead()时阻塞。
而在AcquireHierarchyLockForWrite()之后,代码中有读取配置文件的磁盘IO操作,在System.Configuration.BaseConfigurationRecord中的InitConfigFromFile()中可以看到。
看到有磁盘IO操作,就让人想到了阿里云。连CPU波动问题都可能是磁盘IO引起的,这个地方直接涉及文件操作,怎么可能不让人怀疑是磁盘IO问题引起的。
于是,我们大胆假设了一下,可能还是磁盘IO问题引起的。
然后想办法避开这个问题进行验证。我们想到了一个办法,用内存虚拟出一块硬盘,将网站文件放在这块RamDisk上,这样就不会涉及磁盘IO操作。如果这样做了之后,故障消失,就能锁定问题的原因。
现在网站文件已经在RamDisk上,今天访问高峰就是验证的时刻。
参考资料:
ASP.NET Case Study: Deadlock waiting in GetToSTA
Debugging a classic ReaderWriterLock deadlock with SOSex.dll