Статьи

7000 одновременных соединений с асинхронным WCF


Редко когда веб-сервис выполняет интенсивные вычисления с привязкой к процессору.
Гораздо более распространенным для бизнес-приложений является веб-сервис, который выполняет одну или несколько интенсивных операций ввода-вывода. Обычно наш веб-сервис обращается к базе данных по сети, читает и записывает файлы или, возможно, вызывает другой веб-сервис. Если мы выполняем эти операции синхронно, поток, который обрабатывает запрос веб-службы, будет проводить большую часть своего времени в ожидании ввода-вывода. Выполняя операции ввода-вывода асинхронно, мы можем освободить поток, обрабатывающий запрос, для обработки других запросов, ожидая завершения операции ввода-вывода.

В моих экспериментах с простой автономной службой WCF я смог продемонстрировать до 7000 одновременных соединений, обрабатываемых всего 12 потоками.

Прежде чем я покажу вам, как написать асинхронную службу WCF, я хочу прояснить распространенное заблуждение (да, я тоже до года или около того назад), что асинхронные операции ввода-вывода порождают потоки. Многие из API в .NET BCL (библиотека базовых классов) предоставляют асинхронные версии своих методов. Так, например, HttpWebRequest имеет пару методов BeginGetResponse / EndGetResponse наряду с синхронным методом GetResponse. Этот шаблон называется моделью асинхронного программирования (APM). Когда APM поддерживает операции ввода-вывода, они реализуются с использованием службы операционной системы, называемой портами завершения ввода-вывода(IOCP). IOCP предоставляет очередь, где операции ввода-вывода могут быть припаркованы, пока ОС ожидает их завершения, и предоставляет пул потоков для обработки завершенных операций. Это означает, что выполняемые операции ввода-вывода не используют потоки.

Инфраструктура WCF позволяет вам определять ваши рабочие контракты, используя APM. Вот контракт для операции GetCustomer:

[ServiceContract(SessionMode = SessionMode.NotAllowed)]
public interface ICustomerService
{
    [OperationContract(AsyncPattern = true)]
    IAsyncResult BeginGetCustomerDetails(int customerId, AsyncCallback callback, object state);
    Customer EndGetCustomerDetails(IAsyncResult asyncResult);
}

По сути, GetCustomerDetails принимает идентификатор клиента и возвращает клиента. Чтобы создать асинхронную версию контракта, я просто следовал шаблону APM и создал BeginGetCustomerDetails и EndGetCustomerDetails. Вы сообщаете WCF, что внедряете APM, устанавливая для AsyncPattern значение true в операционном контракте.

IAsyncResult, который возвращается из метода ‘begin’ и передается в качестве аргумента методу ‘end’, связывает их вместе. Вот простая реализация IAsyncResult, которую я использовал для этих экспериментов, вы должны иметь возможность использовать ее для любой асинхронной службы WCF:

public class SimpleAsyncResult<T> : IAsyncResult
{
    private readonly object accessLock = new object();
    private bool isCompleted = false;
    private T result;

    public SimpleAsyncResult(object asyncState)
    {
        AsyncState = asyncState;
    }

    public T Result
    {
        get
        {
            lock (accessLock)
            {
                return result;
            }
        }
        set
        {
            lock (accessLock)
            {
                result = value;
            }
        }
    }

    public bool IsCompleted
    {
        get
        {
            lock (accessLock)
            {
                return isCompleted;
            }
        }
        set
        {
            lock (accessLock)
            {
                isCompleted = value;
            }
        }
    }

    // WCF seems to use the async callback rather than checking the wait handle
    // so we can safely return null here.
    public WaitHandle AsyncWaitHandle { get { return null; } }

    // We will always be doing an async operation so completed synchronously should always
    // be false.
    public bool CompletedSynchronously { get { return false; } }

    public object AsyncState { get; private set; }
}

Теперь у нас есть AsyncResult, мы можем реализовать наш сервис на основе APM, внедрив ICustomerService:

[ServiceBehavior(
    InstanceContextMode = InstanceContextMode.PerCall,
    ConcurrencyMode = ConcurrencyMode.Multiple)]
public class CustomerService : ICustomerService
{
    public const int DelayMilliseconds = 10000;

    public IAsyncResult BeginGetCustomerDetails(int customerId, AsyncCallback callback, object state)
    {
        var asyncResult = new SimpleAsyncResult<Customer>(state);

        // mimic a long running operation
        var timer = new System.Timers.Timer(DelayMilliseconds);
        timer.Elapsed += (_, args) =>
        {
            asyncResult.Result = GetCustomer(customerId);
            asyncResult.IsCompleted = true;
            callback(asyncResult);
            timer.Enabled = false;
            timer.Close();
        };
        timer.Enabled = true;
        return asyncResult;
    }

    public Customer EndGetCustomerDetails(IAsyncResult asyncResult)
    {
        return ((SimpleAsyncResult<Customer>) asyncResult).Result;
    }

    private static Customer GetCustomer(int customerId)
    {
        return new Customer(customerId, "Mike_" + customerId);
    }
}

Мы имитируем длительную операцию ввода-вывода с помощью таймера. Я считаю, что Таймер также использует порт завершения ввода-вывода, но не цитируйте меня по этому поводу. Когда WCF вызывает BeginGetCustomerDetails, мы сначала создаем новый SimpleAsyncResult с объектом состояния WCF. WCF передаст AsyncResult методу EndGetCustomerDetails после завершения таймера, поэтому мы можем использовать его для передачи любого состояния ответа. В нашем случае это экземпляр Customer.

Затем мы устанавливаем таймер и прикрепляем закрытие к событию Elapsed. Когда происходит событие Elapsed таймера, мы создаем экземпляр клиента, передаем его нашему AsyncResult, а затем передаем AsyncResult в функцию обратного вызова WCF.

После завершения метода BeginGetCustomerDetails WCF возвращает свой поток обратно в пул потоков WCF, чтобы он мог обслуживать другие запросы. Десять секунд спустя операционная система отправляет элемент в очередь IOCP, поток из пула выбирает элемент и выполняет продолжение. Это в свою очередь вызывает обратный вызов WCF, который в свою очередь выполняет EndGetCustomerDetails. Затем WCF упаковывает клиента в ответ SOAP и возвращает его клиенту.

Только небольшая часть этих 10 секунд фактически заняла поток, поэтому мы должны иметь возможность совершать тысячи одновременных вызовов этой службы.

В моих тестах этот сервис смог обработать чуть более 7000 одновременных соединений.

Код здесь: https://github.com/mikehadlow/Mike.AsyncWcf

Просто следуйте инструкциям README для запуска тестов и попробуйте сами.