4

We are troubleshooting the following performance issues on a .NET Core API endpoint:

  1. The endpoint consistently returns in less than 500MS under minor load.
  2. When we hit the endpoint from 3 browsers, with one request a second, it gets progressively slower (within a minute of adding a third browser making calls, response times drops to 50,000MS or worse.
  3. Each additional browser adds threads used by the API, e.g. 40 threads base, 2nd browser hitting endpoint leads to 52 threads, third spikes to 70, and so on.
  4. When one endpoint is loaded, the entire API returns slowly (all endpoints). This is my main reason for thinking "thread exhaustion", along with point #3.

The code currently looks like this:

    public IActionResult GetPresentationByEvent(int eventid)
    {
      return Authorized(authDto =>
      {
        var eventList = _eventService.GetPresentationByEvent(eventid);
        return Ok(eventList)
      })
    }

My theory is that return Authorized(authDto => holds a thread until it returns, leading to thread exhaustion.

    public async Task<IActionResult> GetPresentationByEvent(int eventid)
    {
      return Authorized(async authDto =>
      {
        Task<List<whatever>> eventList = _eventService.GetPresentationByEvent(eventid);
        return Ok(eventList)
      }
    }

Authorized is part of a third-party library, so I can't test this easily. Would like to know if this looks like a likely problem/solution.

VSO
  • 11,546
  • 25
  • 99
  • 187
  • 4
    Well this would only work, if the calls are async all the way down, e.g. right until the database/file access. See https://blog.stephencleary.com/2013/11/there-is-no-thread.html – keuleJ Aug 22 '19 at 14:25
  • 3
    "third spikes to 70" is very odd. What are those threads *doing*? You should have a base number of threads after warmup, and each simultaneous request after that should only increase by one thread. – Stephen Cleary Aug 22 '19 at 14:39
  • I am looking into it now. I changed the call to return a new list without going to db / doing any auth. Each browser establishes a SignalR connection as well, so maybe I am spinning up threads there and the endpoint slowing down is just a side effect. Will test now. Ty for replies, they are really helping me think through this. – VSO Aug 22 '19 at 14:52
  • 1
    @StephenCleary Let me correct myself - each browser is running a load test function, which makes one request per second to the API. So, from that perspective, I think the number of threads makes sense. P.S. - it's not signalR, made SignalR connections without hitting this endpoint, and the endpoint/whole API responds fine. Now even more lost, since making the endpoint return an empty list still leaves it unable to handle 3 requests a second (from 3 browsers). – VSO Aug 22 '19 at 14:55

2 Answers2

6

Yes async await can reduce thread exhaustion. In a few words thread exhaustion arise when you generate more tasks than your ThreadPool can handle.

There are subtle specifities that you can check here : Thread starvation and queuing

The only thing that you have to keep in mind on your side is that you should never block inside a task. This implies calling asynchronous code with async await (and never using .Wait or .Result on a non finished task).

If you use some blocking code wich is not using the async await pattern you have to spawn it on a dedicated thread (not the task thread queue).

Bruno Belmondo
  • 2,299
  • 8
  • 18
4

My theory is that return Authorized(authDto => holds a thread until it returns, leading to thread exhaustion.

Yes. You can easily tell whether a method is synchronous by looking at its return value. IActionResult is not an awaitable type, so this method will run synchronously.

Authorized is part of a third-party library, so I can't test this easily. Would like to know if this looks like a likely problem/solution.

Possibly. It all depends on whether Authorized can handle asynchronous delegates. If it can, then something like this would work:

public async Task<IActionResult> GetPresentationByEvent(int eventid)
{
  return Authorized(async authDto =>
  {
    Task<List<whatever>> eventList = _eventService.GetPresentationByEventAsync(eventid);
    return Ok(await eventList);
  });
}

Note:

  1. Tasks should be awaited before being passed to Ok or other helpers.
  2. This introduces GetPresentationByEventAsync, assuming that your data-accessing code can be made asynchronous.

Since making GetPresentationByEvent asynchronous may take some work, it's worthwhile to investigate whether Authorized can take asynchronous delegates before attempting this.

Does Using Async Await Avoid Thread Exhaustion?

Yes and no. Asynchronous code (including async/await) does use fewer threads, since it avoids blocking threads. However, there is still a limit. Thread exhaustion is still possible since asynchronous code needs a free thread to complete. With asynchronous code, usually you can achieve an order of magnitude or two greater scalability before you run into scalability problems like thread exhaustion.

For more conceptual information on async ASP.NET, see this MSDN article.

Fildor
  • 14,510
  • 4
  • 35
  • 67
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks for the reply. Looks like that's part of the issue , but there is something else going on outside of the scope of my question that I need to dig into. Accepting the first answer since he was first, but really appreciate your help. – VSO Aug 22 '19 at 15:03