ASP.NET Performance Case Study: Web Service calls taking forever
This issue has been pretty frequent since .net came out, and I am bringing it up for two reasons.
- It is something I think every web developer calling web services should be aware of… and
- It is a good example of how the product changes based on feedback from support incidents etc. to better suit our customers
Problem Description
We have an ASP.NET application acting as a GUI to our middle tier (Web Service) and at peak times the application is dead slow. Requests start timing out but some do go through so it doesn’t appear that we are completely deadlocked.
After the peak in volume the application seems to work fine again.
During this time CPU utilization is reasonably low in the GUI process, and it is very low in the Web Service process. So low in fact that it looks like hardly anything is running in the web service application.
Gathering information
In a multi-tier application of any kind it is always hard to determine where to grab memory dumps, and often times the tiers are on different machines so it’s not as easy as just setting the debugger up to grab a dump from all processes at once.
Let’s call the GUI server “Server A”, and the backend server (web services server) “Server B”.
If we take a hang dump of Server A we temporarily stall Server A (even if just for a few seconds) and thus requests to Server B will be drained so we can’t go in and take a dump of Server B and expect all requests to still be alive. If on the other hand we take a hang dump of Server B, stalling it momentarily, Server A’s request might time out because we are debugging Server B. So we have to focus on one of them at a time.
There are a number of possible reasons we could be “hanging” here.
- The web service is slow executing the requests, or possibly even deadlocked.
- The GUI app is slow in parts not related to the web service
- The GUI app is slow getting requests through to the web service
- other
If we focus on Server A (GUI) we can at least see if we are waiting for a response from Server B, in which case, next time we can take dumps from Server B, and find out what we are waiting for. So a hang dump of the asp.net process on Server A, or preferably two, taken about a minute or so apart so we can see if the threads are moving or completely stalled, would be good. Note: if you are taking multiple dumps, make sure that the first dump is ready before you take the next one so you don’t get two identical dumps.
Debugging the issue
Drum roll please!… Introducing a command I haven’t shown before !threadpool
0:065> !threadpool
CPU utilization 7%
Worker Thread: Total: 37 Running: 37 Idle: 1 MaxLimit: 40 MinLimit: 2
Work Request in Queue: 0
--------------------------------------
Number of Timers: 0
--------------------------------------
Completion Port Thread: Total: 2 Free: 2 MaxFree: 4 CurrentLimit: 2 MaxLimit: 40 MinLimit: 2
CPU Utilization is 7%. This is the complete CPU utilization on the box, so not specifically for the Asp.Net process, either way it is pretty low, just like it was mentioned in the problem description.
Currently there are no work requests in the queue and no timers waiting.
We have a maximum of 40 worker threads to execute our requests, and a minimum of 2 alive at any time (this comes from our machine.config settings). And currently we are executing on 37 of these, but it doesn’t appear that the thread pool is exhausted. This jives well with that there are no work requests in the queue.
So what are all these threads doing?
Running ~* kb
it appears that most of the threads are in some kind of wait. More specifically in a CorWaitOneNative…
61 Id: 1164.a44 Suspend: 1 Teb: 7ff3a000 Unfrozen
ChildEBP RetAddr Args to Child
0d58f430 7c822114 77e6711b 00000001 0d58f480 ntdll!KiFastSystemCallRet
0d58f434 77e6711b 00000001 0d58f480 00000000 ntdll!NtWaitForMultipleObjects+0xc
0d58f4dc 791e0b3b 00000001 0d58f5bc 00000001 kernel32!WaitForMultipleObjectsEx+0x11a
0d58f50c 791e0bdd 00000001 0d58f5bc 00000001 mscorsvr!Thread::DoAppropriateWaitWorker+0xc1
0d58f560 791d5ba9 00000001 0d58f5bc 00000001 mscorsvr!Thread::DoAppropriateWait+0x46
0d58f5a8 024840c8 0d58f5b4 00000000 000186a0 mscorsvr!WaitHandleNative::CorWaitOneNative+0x6f
WARNING: Frame IP not in any known module. Following frames may be wrong.
0d58f614 0c9a5cb7 00000000 06c60b10 06c60b10 0x24840c8
0d58f644 0c9a00c2 06c60b10 0677c0c0 00000000 0xc9a5cb7
0d58f688 0c870721 06c60f9c 06c5fe04 06c60b10 0xc9a00c2
0d58f6ac 0c870532 06c60af0 06c5fe04 067592f0 0xc870721
0d58f6d0 0c952ea4 067592f0 06c5fe04 06c5f13c 0xc870532
0d58f800 799a67a2 06c5f1a4 06c5f4a8 799b4168 0xc952ea4
0d58f80c 799b4168 00000002 00000000 024a1710 mscorlib_79990000+0x167a2
0d58f890 791b3208 0d58f9a8 791b3ad7 0d58f8e4 mscorlib_79990000+0x24168
0d58f898 791b3ad7 0d58f8e4 00000000 0d58f8bc mscorsvr!CallDescrWorker+0x30
0d58f9a8 791d3ef0 00bf71bb 79b7c000 00000004 mscorsvr!MethodDesc::CallDescr+0x1b8
0d58fa64 791d3fa4 79bf71bb 79b7c000 79bd46de mscorsvr!MethodDesc::CallDescr+0x4f
0d58fa8c 791ebc5a 0d58faa4 00101ae0 0254c470 mscorsvr!MethodDesc::Call+0x97
0d58fab4 791cc3c8 02554bb0 791b3d64 ffffffff mscorsvr!RegisterWaitForSingleObjectCallback_Worker+0x139
0d58fafc 791ebb31 00101ae0 791ebbcc 02554bb0 mscorsvr!Thread::DoADCallBack+0x5c
0d58fb70 791d4096 02554bb0 02537ae8 793ecec8 mscorsvr!QueueUserWorkItemCallback+0xe3
0d58fb84 791d4055 02537ae8 00000000 791d3fe5 mscorsvr!ThreadpoolMgr::ExecuteWorkRequest+0x19
0d58fba4 791fdacc 00000000 79172262 79172270 mscorsvr!ThreadpoolMgr::WorkerThreadStart+0x129
0d58ffb8 77e66063 0254c108 00000000 00000000 mscorsvr!ThreadpoolMgr::intermediateThreadProc+0x44
0d58ffec 00000000 791fda8b 0254c108 00000000 kernel32!BaseThreadStart+0x34
…this means that someone from managed code used WaitHandle for synchronization, and calls WaitOne().
Checking the managed bit of this thread we can see that we are in WebForm1.Page_Load, calling into a function SlowStuff.seventoo.Service.DoSomethingSlow(), which in turn invokes a web service call, and now sits waiting in a function called GetRequestStream().
0:061> !clrstack
Thread 61
ESP EIP
0x0d58f5d8 0x7c82ed54 [FRAME: ECallMethodFrame] [DEFAULT] Boolean System.Threading.WaitHandle.WaitOneNative(I,UI4,Boolean)
0x0d58f5ec 0x799e4bb1 [DEFAULT] [hasThis] Boolean System.Threading.WaitHandle.WaitOne(I4,Boolean)
0x0d58f620 0x0c9a5cb7 [DEFAULT] [hasThis] Class System.IO.Stream System.Net.HttpWebRequest.GetRequestStream()
0x0d58f64c 0x0c9a00c2 [DEFAULT] [hasThis] SZArray Object System.Web.Services.Protocols.SoapHttpClientProtocol.Invoke(String,SZArray Object)
0x0d58f694 0x0c870721 [DEFAULT] [hasThis] String SlowStuff.seventoo.Service.DoSomethingSlow()
0x0d58f6b4 0x0c870532 [DEFAULT] [hasThis] Void SlowStuff.WebForm1.Page_Load(Object,Class System.EventArgs)
0x0d58f6dc 0x0c952ea4 [DEFAULT] [hasThis] Void System.Web.UI.Control.OnLoad(Class System.EventArgs)
0x0d58f6ec 0x0c952de4 [DEFAULT] [hasThis] Void System.Web.UI.Control.LoadRecursive()
0x0d58f700 0x0c952097 [DEFAULT] [hasThis] Void System.Web.UI.Page.ProcessRequestMain()
0x0d58f744 0x0c950eef [DEFAULT] [hasThis] Void System.Web.UI.Page.ProcessRequest()
0x0d58f780 0x0c95095b [DEFAULT] [hasThis] Void System.Web.UI.Page.ProcessRequest(Class System.Web.HttpContext)
0x0d58f788 0x0c950934 [DEFAULT] [hasThis] Void System.Web.HttpApplication/CallHandlerExecutionStep.System.Web.HttpApplication+IExecutionStep.Execute()
0x0d58f798 0x0c76de50 [DEFAULT] [hasThis] Class System.Exception System.Web.HttpApplication.ExecuteStep(Class IExecutionStep,ByRef Boolean)
0x0d58f7e0 0x0c76d8ba [DEFAULT] [hasThis] Void System.Web.HttpApplication.ResumeSteps(Class System.Exception)
0x0d58f828 0x0c76d783 [DEFAULT] [hasThis] Class System.IAsyncResult System.Web.HttpApplication.System.Web.IHttpAsyncHandler.BeginProcessRequest(Class System.Web.HttpContext,Class System.AsyncCallback,Object)
0x0d58f844 0x02446897 [DEFAULT] [hasThis] Void System.Web.HttpRuntime.ProcessRequestInternal(Class System.Web.HttpWorkerRequest)
0x0d58f880 0x0cb43cf4 [DEFAULT] [hasThis] Void System.Web.RequestQueue.WorkItemCallback(Object)
0x0d58fac0 0x791b3208 [FRAME: ContextTransitionFrame]
If we run ~* e !clrstack
to get the managed stack for all the threads we find that 33 requests are waiting like this in GetRequestStream and 2 requests are waiting in GetResponse.
0:042> kb 2000
ChildEBP RetAddr Args to Child
0d0cebc4 7c822114 77e6711b 00000001 0d0cec14 ntdll!KiFastSystemCallRet
0d0cebc8 77e6711b 00000001 0d0cec14 00000000 ntdll!NtWaitForMultipleObjects+0xc
0d0cec70 791e0b3b 00000001 0d0ced50 00000001 kernel32!WaitForMultipleObjectsEx+0x11a
0d0ceca0 791e0bdd 00000001 0d0ced50 00000001 mscorsvr!Thread::DoAppropriateWaitWorker+0xc1
0d0cecf4 791d5ba9 00000001 0d0ced50 00000001 mscorsvr!Thread::DoAppropriateWait+0x46
0d0ced3c 024840c8 0d0ced48 00000000 000186a0 mscorsvr!WaitHandleNative::CorWaitOneNative+0x6f
WARNING: Frame IP not in any known module. Following frames may be wrong.
0d0ceda8 0ca29fd5 00000000 02970580 02970580 0x24840c8
0d0ceddc 0ca29e49 02970580 02970580 0677c0c0 0xca29fd5
0d0cee0c 0ca29e06 0c9a0110 02970580 0677c0c0 0xca29e49
0d0cee54 0c870721 02970a0c 0296f868 02970580 0xca29e06
0d0cee78 0c870532 02970560 0296f868 067592f0 0xc870721
0d0cee9c 0c952ea4 067592f0 0296f868 0296c800 0xc870532
0d0cefcc 799a67a2 0296c868 0296cb6c 799b4168 0xc952ea4
0d0cefd8 799b4168 00000002 00000000 024a14c4 mscorlib_79990000+0x167a2
0d0cf060 79217188 00000000 00dc9160 0672a508 mscorlib_79990000+0x24168
02442f60 24548bca 2c15ff10 8b020b95 8bcb8bd8 mscorsvr!ComCallMLStubCache::CompileMLStub+0x429
02442f60 00000000 2c15ff10 8b020b95 8bcb8bd8 0x24548bca
Thread 42
ESP EIP
0x0d0ced6c 0x7c82ed54 [FRAME: ECallMethodFrame] [DEFAULT] Boolean System.Threading.WaitHandle.WaitOneNative(I,UI4,Boolean)
0x0d0ced80 0x799e4bb1 [DEFAULT] [hasThis] Boolean System.Threading.WaitHandle.WaitOne(I4,Boolean)
0x0d0cedb4 0x0ca29fd5 [DEFAULT] [hasThis] Class System.Net.WebResponse System.Net.HttpWebRequest.GetResponse()
0x0d0cede4 0x0ca29e49 [DEFAULT] [hasThis] Class System.Net.WebResponse System.Web.Services.Protocols.WebClientProtocol.GetWebResponse(Class System.Net.WebRequest)
0x0d0cee14 0x0ca29e06 [DEFAULT] [hasThis] Class System.Net.WebResponse System.Web.Services.Protocols.HttpWebClientProtocol.GetWebResponse(Class System.Net.WebRequest)
0x0d0cee18 0x0c9a0110 [DEFAULT] [hasThis] SZArray Object System.Web.Services.Protocols.SoapHttpClientProtocol.Invoke(String,SZArray Object)
0x0d0cee60 0x0c870721 [DEFAULT] [hasThis] String SlowStuff.seventoo.Service.DoSomethingSlow()
0x0d0cee80 0x0c870532 [DEFAULT] [hasThis] Void SlowStuff.WebForm1.Page_Load(Object,Class System.EventArgs)
0x0d0ceea8 0x0c952ea4 [DEFAULT] [hasThis] Void System.Web.UI.Control.OnLoad(Class System.EventArgs)
0x0d0ceeb8 0x0c952de4 [DEFAULT] [hasThis] Void System.Web.UI.Control.LoadRecursive()
0x0d0ceecc 0x0c952097 [DEFAULT] [hasThis] Void System.Web.UI.Page.ProcessRequestMain()
0x0d0cef10 0x0c950eef [DEFAULT] [hasThis] Void System.Web.UI.Page.ProcessRequest()
0x0d0cef4c 0x0c95095b [DEFAULT] [hasThis] Void System.Web.UI.Page.ProcessRequest(Class System.Web.HttpContext)
0x0d0cef54 0x0c950934 [DEFAULT] [hasThis] Void System.Web.HttpApplication/CallHandlerExecutionStep.System.Web.HttpApplication+IExecutionStep.Execute()
0x0d0cef64 0x0c76de50 [DEFAULT] [hasThis] Class System.Exception System.Web.HttpApplication.ExecuteStep(Class IExecutionStep,ByRef Boolean)
0x0d0cefac 0x0c76d8ba [DEFAULT] [hasThis] Void System.Web.HttpApplication.ResumeSteps(Class System.Exception)
0x0d0ceff4 0x0c76d783 [DEFAULT] [hasThis] Class System.IAsyncResult System.Web.HttpApplication.System.Web.IHttpAsyncHandler.BeginProcessRequest(Class System.Web.HttpContext,Class System.AsyncCallback,Object)
0x0d0cf010 0x02446897 [DEFAULT] [hasThis] Void System.Web.HttpRuntime.ProcessRequestInternal(Class System.Web.HttpWorkerRequest)
0x0d0cf04c 0x02446448 [DEFAULT] Void System.Web.HttpRuntime.ProcessRequest(Class System.Web.HttpWorkerRequest)
0x0d0cf058 0x02442fc5 [DEFAULT] [hasThis] I4 System.Web.Hosting.ISAPIRuntime.ProcessRequest(I,I4)
0x0d0cf120 0x79217188 [FRAME: ContextTransitionFrame]
0x0d0cf200 0x79217188 [FRAME: ComMethodFrame]
Hmm… so what are these waiting for?
Well… when we are waiting in GetResponse we have sent the request to the web service server and we are waiting for it to return, in which case we can either be blocked/waiting on the server or we could be waiting on a callback thread to process the results on. In our case we have not exhausted the thread pool so we would have had free threads to use as callback threads. So… we are likely waiting on results back from the server on the two threads that are in GetResponse().
Although these two threads are interesting I find it even more interesting that we have 33 threads waiting in GetRequestStream(). This function is just supposed to get us a stream that we can send the request on. OK! So that seems like something that would be rather quick, however in doing so it also needs to get a connection.
So… what would be limiting us from getting a connection?
System.Net.Connections are managed by a class called ServicePoint, that contains information about the URI it services, the maximum number of connections, whether it expects 100-continue etc.
From !dso (!dumpstackobjects)
on thread 61 (which is waiting on GetRequestStream() we can get the HttpWebRequest to the Web Service.
0:061> !dso
Thread 61
ESP/REG Object Name
...
0xd58f5f0 0x6c8b0d0 System.Net.HttpWebRequest
0xd58f5f4 0x6c8f23c System.Net.LazyAsyncResult
0xd58f5f8 0x6c95868 System.Threading.ManualResetEvent
0xd58f610 0x6c8b0d0 System.Net.HttpWebRequest
0xd58f620 0x6c60b10 SlowStuff.seventoo.Service
0xd58f624 0x6c60b10 SlowStuff.seventoo.Service
0xd58f628 0x677c0c0 System.String DoSomethingSlow
0xd58f630 0x6c8b0d0 System.Net.HttpWebRequest
0xd58f634 0x6c8c2e0 System.String http://tempuri.org/DoSomethingSlow
0xd58f638 0x677335c System.String http://tempuri.org/DoSomethingSlow
0xd58f64c 0x6c60b10 SlowStuff.seventoo.Service
...
And if we dump this out we find that it has a member variable called _ServicePoint, so that looks pretty interesting…
0:061> !do 0x6c8b0d0
Name: System.Net.HttpWebRequest
MethodTable 0x0c978b94
EEClass 0x0c96e798
Size 204(0xcc) bytes
GC Generation: 0
mdToken: 0x020001fa (c:\windows\assembly\gac\system\1.0.5000.0__b77a5c561934e089\system.dll)
FieldDesc*: 0x0c977cf4
MT Field Offset Type Attr Value Name
0x79b9788c 0x40000b6 0x4 CLASS instance 0x00000000 __identity
0x0c9774d0 0x4000f0c 0 CLASS shared static s_PrefixList
>> Domain:Value 0x000d5330:NotInit 0x001024b8:0x0274a580 <<
0x0c978b94 0x4000f30 0x10 CLASS instance 0x06c8f23c _WriteAResult
0x0c978b94 0x4000f31 0x14 CLASS instance 0x00000000 _ReadAResult
0x0c978b94 0x4000f32 0x18 CLASS instance 0x00000000 _ContinueDelegate
0x0c978b94 0x4000f33 0x1c CLASS instance 0x0274e5c4 _ServicePoint
0x0c978b94 0x4000f34 0x20 CLASS instance 0x06c91c58 _WriteEvent
0x0c978b94 0x4000f35 0x24 CLASS instance 0x00000000 _WriteInProgressEvent
0x0c978b94 0x4000f36 0xb4 System.Boolean instance 0 _HaveResponse
0x0c978b94 0x4000f37 0xb5 System.Boolean instance 0 _OnceFailed
0x0c978b94 0x4000f38 0xb6 System.Boolean instance 0 _WriteNotifed
0x0c978b94 0x4000f39 0x28 CLASS instance 0x00000000 _HttpResponse
0x0c978b94 0x4000f3a 0x2c CLASS instance 0x00000000 _ResponseException
0x0c978b94 0x4000f3b 0x30 CLASS instance 0x00000000 _CoreResponse
0x0c978b94 0x4000f3e 0x34 CLASS instance 0x027447d0 _Verb
0x0c978b94 0x4000f3f 0x38 CLASS instance 0x027447d0 _OriginVerb
0x0c978b94 0x4000f40 0x3c CLASS instance 0x06c8b6b4 _HttpRequestHeaders
0x0c978b94 0x4000f41 0x40 CLASS instance 0x00000000 _WriteBuffer
0x0c978b94 0x4000f42 0x94 System.Int32 instance 2 _HttpWriteMode
0x0c978b94 0x4000f43 0xb7 System.Boolean instance 0 _AllowAutoRedirect
0x0c978b94 0x4000f44 0x44 CLASS instance 0x06c60b60 _Uri
0x0c978b94 0x4000f45 0x48 CLASS instance 0x06c60b60 _OriginUri
0x0c978b94 0x4000f46 0x4c CLASS instance 0x00000000 _ChallengedUri
0x0c978b94 0x4000f47 0x50 CLASS instance 0x00000000 _ChallengedSpn
0x0c978b94 0x4000f48 0x54 CLASS instance 0x00000000 _MediaType
0x0c978b94 0x4000f49 0x8 System.Int64 instance -1 _ContentLength
0x0c978b94 0x4000f4a 0x58 CLASS instance 0x0274c80c _Version
Btw, we can verify that we have picked the right one by dumping out the Uri
0:061> !do 0x06c60b60
Name: System.Uri
MethodTable 0x020b4eb0
EEClass 0x02472e10
Size 76(0x4c) bytes
GC Generation: 0
mdToken: 0x020001c4 (c:\windows\assembly\gac\system\1.0.5000.0__b77a5c561934e089\system.dll)
FieldDesc*: 0x020b4804
MT Field Offset Type Attr Value Name
0x79b9788c 0x40000b6 0x4 CLASS instance 0x00000000 __identity
0x020b4eb0 0x4000e38 0x8 CLASS instance 0x06c60e44 m_AbsoluteUri
0x020b4eb0 0x4000e39 0x3c System.Boolean instance 0 m_AlreadyEscaped
0x020b4eb0 0x4000e3a 0xc CLASS instance 0x066f0230 m_Fragment
0x020b4eb0 0x4000e3b 0x3d System.Boolean instance 0 m_FragmentEscaped
0x020b4eb0 0x4000e3c 0x2c System.Int32 instance -41085911 m_Hash
...
0:061> !do 0x06c60e44
Name: System.String
MethodTable 0x79b94638
EEClass 0x79b94984
Size 108(0x6c) bytes
GC Generation: 0
mdToken: 0x0200000f (c:\windows\microsoft.net\framework\v1.1.4322\mscorlib.dll)
String: http://seventoo/myslowwebservice/service.asmx
FieldDesc*: 0x79b949e8
MT Field Offset Type Attr Value Name
0x79b94638 0x4000013 0x4 System.Int32 instance 46 m_arrayLength
0x79b94638 0x4000014 0x8 System.Int32 instance 45 m_stringLength
0x79b94638 0x4000015 0xc System.Char instance 0x68 m_firstChar
0x79b94638 0x4000016 0 CLASS shared static Empty
>> Domain:Value 0x000d5330:0x066f0230 0x001024b8:0x066f0230 <<
0x79b94638 0x4000017 0x4 CLASS shared static WhitespaceChars
>> Domain:Value 0x000d5330:0x066f0244 0x001024b8:0x027122b8 <<
Back to the service point
0:061> !do 0x0274e5c4
Name: System.Net.ServicePoint
MethodTable 0x0c9c5d08
EEClass 0x0c9b6840
Size 92(0x5c) bytes
GC Generation: 1
mdToken: 0x02000247 (c:\windows\assembly\gac\system\1.0.5000.0__b77a5c561934e089\system.dll)
FieldDesc*: 0x0c9c5730
MT Field Offset Type Attr Value Name
0x0c9c5d08 0x400112d 0x48 System.Boolean instance 0 m_ProxyServicePoint
0x0c9c5d08 0x400112e 0x49 System.Boolean instance 0 m_UserChangedLimit
0x0c9c5d08 0x400112f 0x2c System.Int32 instance 2 m_ConnectionLimit
0x0c9c5d08 0x4001130 0x30 System.Int32 instance 2 m_ConnectionMode
0x0c9c5d08 0x4001131 0x4a System.Boolean instance 1 m_SupportsPipelining
0x0c9c5d08 0x4001132 0x4 CLASS instance 0x0274e620 m_ConnectionGroupList
0x0c9c5d08 0x4001133 0x8 CLASS instance 0x0677bf00 m_Address
0x0c9c5d08 0x4001134 0x34 System.Int32 instance 900000 m_MaxIdleTime
0x0c9c5d08 0x4001135 0x50 VALUETYPE instance start at 0x0274e614 m_ExpiresAt
0x0c9c5d08 0x4001136 0xc CLASS instance 0x0677bf4c m_ConnectionName
0x0c9c5d08 0x4001137 0x10 CLASS instance 0x0677e2e8 m_IPHostEntryInfos
0x0c9c5d08 0x4001138 0x38 System.Int32 instance 0 m_CurrentAddressInfoIndex
0x0c9c5d08 0x4001139 0x3c System.Int32 instance 2 m_ConnectionsSinceDns
0x0c9c5d08 0x400113a 0x40 System.Int32 instance 2 m_CurrentConnections
0x0c9c5d08 0x400113b 0x14 CLASS instance 0x00000000 m_ServerCertificate
0x0c9c5d08 0x400113c 0x18 CLASS instance 0x00000000 m_ClientCertificate
0x0c9c5d08 0x400113d 0x4b System.Boolean instance 1 m_UseNagleAlgorithm
0x0c9c5d08 0x400113e 0x4c System.Boolean instance 1 m_Expect100Continue
0x0c9c5d08 0x400113f 0x1c CLASS instance 0x0274df08 m_LookupString
0x0c9c5d08 0x4001140 0x20 CLASS instance 0x0677e248 m_CachedRemoteIPAddressInfo
0x0c9c5d08 0x4001141 0x24 CLASS instance 0x027543e4 m_Version
0x0c9c5d08 0x4001142 0x4d System.Boolean instance 1 m_Understands100Continue
0x0c9c5d08 0x4001143 0x28 CLASS instance 0x0274e72c DnsLock
0x0c9c5d08 0x4001144 0x44 System.Int32 instance 140 m_HashCode
0x0c9c5d08 0x4001145 0x4e System.Boolean instance 1 m_ComputedHashCode
Hmm, it has a connection limit of 2. That sorta rings a bell, we have two requests in GetResponse(), i.e. in the middle of some web service requests, and the rest waiting for a connection, so this makes total sense.
We could actually stop right here and conclude that this is the source of the problem. I.e. that we only have two available connections, but let’s talk about that a little bit later and dig a bit further instead…
If we go back to thread 42 (GetResponse()) and run !dso
there, we find a ConnectStream on the stack
0:042> !dso
Thread 42
ESP/REG Object Name
...
0xd0cee2c 0x6cef7b8 System.Net.ConnectStream
...
This connect stream has a connection (one of the two created by the service point)
0:042> !do 0x6cef7b8
Name: System.Net.ConnectStream
MethodTable 0x0c9c44c0
EEClass 0x0c9b6270
Size 104(0x68) bytes
GC Generation: 0
mdToken: 0x02000272 (c:\windows\assembly\gac\system\1.0.5000.0__b77a5c561934e089\system.dll)
FieldDesc*: 0x0c9c3e5c
MT Field Offset Type Attr Value Name
0x79b9788c 0x40000b6 0x4 CLASS instance 0x00000000 __identity
0x79b9c140 0x4000b17 0 CLASS shared static Null
>> Domain:Value 0x000d5330:0x066f1eec 0x001024b8:0x0674a16c <<
0x0c9c44c0 0x400122f 0x3c System.Int32 instance 1 m_CallNesting
0x0c9c44c0 0x4001230 0x20 CLASS instance 0x06cf0c28 m_BufferedData
0x0c9c44c0 0x4001231 0x58 System.Boolean instance 1 m_WriteBufferEnable
0x0c9c44c0 0x4001232 0x59 System.Boolean instance 1 m_BufferOnly
0x0c9c44c0 0x4001233 0x8 System.Int64 instance 293 m_TotalBytesToWrite
0x0c9c44c0 0x4001234 0x10 System.Int64 instance 0 m_BytesLeftToWrite
0x0c9c44c0 0x4001235 0x24 CLASS instance 0x0274f03c m_Connection
0x0c9c44c0 0x4001236 0x5a System.Boolean instance 1 m_WriteStream
0x0c9c44c0 0x4001237 0x28 CLASS instance 0x00000000 m_ReadBuffer
0x0c9c44c0 0x4001238 0x40 System.Int32 instance 0 m_ReadOffset
0x0c9c44c0 0x4001239 0x44 System.Int32 instance 0 m_ReadBufferSize
0:061> !do 0x0274f03c
Name: System.Net.Connection
MethodTable 0x0c9c52f0
EEClass 0x0c9b6454
Size 152(0x98) bytes
GC Generation: 1
mdToken: 0x02000270 (c:\windows\assembly\gac\system\1.0.5000.0__b77a5c561934e089\system.dll)
FieldDesc*: 0x0c9c4bb8
MT Field Offset Type Attr Value Name
0x0c9c52f0 0x40011f3 0x80 System.Boolean instance 1 m_CanPipeline
0x0c9c52f0 0x40011f4 0x81 System.Boolean instance 0 m_Pipelining
0x0c9c52f0 0x40011f5 0x82 System.Boolean instance 1 m_KeepAlive
0x0c9c52f0 0x40011f6 0x58 System.Int32 instance 0 m_Error
0x0c9c52f0 0x40011f7 0x4 CLASS instance 0x027be068 m_ReadBuffer
0x0c9c52f0 0x40011f8 0x5c System.Int32 instance 25 m_BytesRead
0x0c9c52f0 0x40011f9 0x60 System.Int32 instance 25 m_HeadersBytesUnparsed
0x0c9c52f0 0x40011fa 0x64 System.Int32 instance 25 m_BytesScanned
0x0c9c52f0 0x40011fb 0x68 System.Int32 instance 0 m_TotalResponseHeadersLength
0x0c9c52f0 0x40011fc 0x6c System.Int32 instance 65536 m_MaximumResponseHeadersLength
0x0c9c52f0 0x40011fd 0x8 CLASS instance 0x06d0f0bc m_ResponseData
0x0c9c52f0 0x40011fe 0x70 System.Int32 instance 1 m_ReadState
0x0c9c52f0 0x40011ff 0xc CLASS instance 0x027bf074 m_WaitList
0x0c9c52f0 0x4001200 0x10 CLASS instance 0x027bf0dc m_WriteList
0x0c9c52f0 0x4001201 0x74 System.Int32 instance 25 m_CurrentRequestIndex
0x0c9c52f0 0x4001202 0x14 CLASS instance 0x027bf1c0 m_StatusLineInts
0x0c9c52f0 0x4001203 0x18 CLASS instance 0x066f0230 m_StatusDescription
0x0c9c52f0 0x4001204 0x78 System.Int32 instance 0 m_StatusState
The connection in turn has a waitlist that contains all the requests that are waiting for this particular connection
0:061> !do 0x027bf074
Name: System.Collections.ArrayList
MethodTable 0x79ba2ee4
EEClass 0x79ba3020
Size 24(0x18) bytes
GC Generation: 0
mdToken: 0x02000100 (c:\windows\microsoft.net\framework\v1.1.4322\mscorlib.dll)
FieldDesc*: 0x79ba3084
MT Field Offset Type Attr Value Name
0x79ba2ee4 0x4000362 0x4 CLASS instance 0x02a7bd44 _items
0x79ba2ee4 0x4000363 0xc System.Int32 instance 17 _size
0x79ba2ee4 0x4000364 0x10 System.Int32 instance 35 _version
0x79ba2ee4 0x4000365 0x8 CLASS instance 0x00000000 _syncRoot
In this case, 17 requests are waiting for this connection, WOW!!!
And if we would run
0:061> .foreach (req {!do -v 0x02a7bd44 -short}) {!gcroot ${req}}
To find out on what threads all these requests are rooted/used we would see them rooted on thread 24, 26, 28, 30, 32, 34, 36, 37, 38, 44, 46, 48, 50, 52, 54, 56, and 58
The roots look something like this:
Scan Thread 46 (0x1540)
ESP:d1ceb9c:Root:0x29ae580(SlowStuff.seventoo.Service)->0x29aeb18(System.Net.HttpWebRequest)
ESP:d1cebb8:Root:0x29ae580(SlowStuff.seventoo.Service)->0x29aeb18(System.Net.HttpWebRequest)
ESP:d1cebbc:Root:0x29aeb18(System.Net.HttpWebRequest)->0x29aeb18(System.Net.HttpWebRequest)
ESP:d1cebc0:Root:0x68b7fb0(System.Net.LazyAsyncResult)->0x29aeb18(System.Net.HttpWebRequest)
ESP:d1cebdc:Root:0x29aeb18(System.Net.HttpWebRequest)->0x29aeb18(System.Net.HttpWebRequest)
ESP:d1cebec:Root:0x29ae580(SlowStuff.seventoo.Service)->0x29aeb18(System.Net.HttpWebRequest)
ESP:d1cebf0:Root:0x29ae580(SlowStuff.seventoo.Service)->0x29aeb18(System.Net.HttpWebRequest)
ESP:d1cebfc:Root:0x29aeb18(System.Net.HttpWebRequest)->0x29aeb18(System.Net.HttpWebRequest)
ESP:d1cec18:Root:0x29ae580(SlowStuff.seventoo.Service)->0x29aeb18(System.Net.HttpWebRequest)
ESP:d1cec30:Root:0x29afcb0(System.Web.Services.Protocols.SoapClientMessage)->0x29ae580(SlowStuff.seventoo.Service)
ESP:d1cec34:Root:0x29aeb18(System.Net.HttpWebRequest)->0x29ae580(SlowStuff.seventoo.Service)
ESP:d1cec38:Root:0x29ae580(SlowStuff.seventoo.Service)->0x29ae580(SlowStuff.seventoo.Service)
ESP:d1cec50:Root:0x29ae580(SlowStuff.seventoo.Service)->0x29ae580(SlowStuff.seventoo.Service)
ESP:d1cec64:Root:0x29ae580(SlowStuff.seventoo.Service)->0x29ae580(SlowStuff.seventoo.Service)
ESP:d1cec68:Root:0x29ae580(SlowStuff.seventoo.Service)->0x29ae580(SlowStuff.seventoo.Service)
So all these threads are waiting for our thread 42 to finish. In other words, the last thread in the wait list will have to wait for (16*Web Service execution time) + time for currently executing request, before it can even start with its request. Now, that can be a pretty long time:)
When a new request comes in it gets scheduled in on one of the two connections and stands in line awaiting its turn, just like a busy day at the DMV.
A little background
Why 2 connections??? that seems like an awfully small number…
Let’s take a look at RFC 2616, the RFC for Hypertext Transfer Protocol – HTTP/1.1. This is what it says about HTTP connections.
“Clients that use persistent connections SHOULD limit the number of simultaneous connections that they maintain to a given server. A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy. A proxy SHOULD use up to 2N connections to another server or proxy, where N is the number of simultaneously active users. These guidelines are intended to improve HTTP response times and avoid congestion.”*
So now you know where the number 2 likely came from. But as you can see, in our case it presents a bit of a problem.
Solutions please
In the <system.net>
section of machine.config you can set the maxConnection value to indicate how many concurrent connections you want to allow. By default this is set to * and 2, which means that we will have a maximum of 2 simultaneous connections to any given address/port.
Each service point handles a particular URI, so in essence this means that any given service point will have a maximum of 2 connections.
<connectionManagement>
<add address="*" maxconnection="2"/>
</connectionManagement>
You can either change the general maxconnection setting or add one for the particular ipaddress / server name where your web service resides.
Along with setting this up, you should also look over the numbers for maxWorkerThreads/maxIOThreads, minFreeThreads and minLocalRequestFreeThreads when calling web services, per KB821268, but the biggest contention point is definitely maxconnections.
Better yet, in 2.0 a new machine config setting was introduced, and made the default…
<ProcessModel>
<autoconfig="true">
</ProcessModel>
This automatically configures these values for you per the article above
Wrap up
I know that in this post in particular I might have made huge leaps of logic in some places, my college algebra teacher definitely wouldn’t have been proud :) but I want to defend myself by saying that I am trying to show debugging in a step-by-step fashion, when really a lot of the time, when I am debugging, I use the “poke around until I find something interesting” technique that I mentioned earlier…
So far the posts have touched as distant topics as garbage collection, pinvokes, xmlserialization, asp.net intrinsics, and now connection management. It is impossible to know how everything is implemented which is why I am trying to post case studies on a lot of different areas so at least you will know bits and pieces of how things work and build upon that.
Have fun out there!