Добрый день, дорогой читатель! Продолжая цикл статей о реализации стека RTB нашей компанией, предлагаю вам ознакомиться с реализацией нашей SSP — VOX Ad Exchange.



Нагрузка здесь не такая большая, как, например, в нашей DSP, а основное время обработки запроса тратится на ожидание ответов от подключенных DSP. Написана она в виде асинхронного ASP.NET MVC-приложения.
Итак, давайте для начала разберёмся, как же происходит обработка запросов.

Запрос на получение рекламы начинает формироваться ещё на стороне клиента. Здесь посредством javascript мы стараемся собрать как можно больше информации о посетителе сайта, на котором установлен наш скрипт. В основном это техническая информация: включен ли в браузере flash player, размеры окна браузера, размеры экрана, получаем адрес страницы и реферер на страницу, с которой пришёл пользователь и прочее. Далее информация собирается в JSON строку, кодируется и отправляется запрос на получение рекламы. Теперь за дело берётся back-end.



Получив запрос на показ рекламы, так же получаем из базы всю известную нам информацию о данном посетителе (историю его интересов, просмотров рекламы и кликов). По user-agent'у мы получаем данные об используемом браузере, его версии и операционной системе посетителя, а по ip-адресу определяем его текущее местоположение. Всю собранную о пользователе информацию мы отправляем в нашу DMP для её дальнейшего анализа.

Собрав данные о пользователе, мы переходим к площадке. Получаем информацию о стоимости рекламного места, CTR площадки, категориях контента, наличию запрещённого контента и т.д., на основе этих данных будет сформирована минимальная пороговая цена аукциона. Закончив с площадками, переходим к самой длительной задаче SSP, это опрос подключенных DSP. На основе полученных выше описанным способом данных формируется запрос к DSP. Далее происходит паралельный опрос DSP, на ответ каждой из них отводиться 120 ms, в которые входит и время на доставку запроса, и получение ответа (ping до серверов, на которых расположена DSP). Так что месторасположение DSP тоже играет важную роль. Получив ответ от всех DSP, остаётся лишь отсортировать их по размеру ставки и взять ответ с самой большой. Ответ в виде JSON'а, для показа рекламы мы возвращаемся на front-end.

Фронтенду остаётся самая малость — вывести код полученного объявления. Так как уследить за качеством кода DSP и его совместимостью с площадками нет никакой возможности, наш код для вывода объявления на страницу создаёт отдельный iframe, загружает в него код, и вот его-то мы и выводим на страницу. На этом и заканчивается обработка запроса на рекламу.

Давайте теперь разберёмся с какими трудностями пришлось столкнуться в ходе реализации SSP, и как мы с ними справились. Одной из основных проблем в ходе реализации SSP стало забивание пула IIS. Причиной этому было ожидание ответа от DSP в течении 100 и более миллисекунд. В то время, когда SSP ждала ответа от DSP, остальные запросы ждали, пока SSP обработает текущий. А решением данной проблемы стало использование асинхронных контроллеров, вот здесь нам и пригодились модификаторы async/await. Также не лишним будет упомянуть, что информация о площадке и DSP кешируется в памяти приложения и ежеминутно обновляется. Следующим основным местом, на которое стоит обратить внимание, является опрос DSP. Первоначально встал вопрос, как распараллелить опрос DSP. В ходе разработки SSP было опробовано несколько вариантов решения этой задачи, таких как: Parallel.ForEach, Task.WaitAll и Task.WhenAll. Особых различий в производительности обнаружено не было, каждый способ исправно выполнял свою задачу, но в связи с переходом на асинхронную модель выбор пал на последний вариант. Сам запрос к DSP осуществляется посредством HttpWebRequest. Самое главное, что следует сделать при работе с HttpWebRequest, это включить поддержку KeepAlive, потому что запросы к DSP идут постоянно друг за другом, и постоянные обрывы и установка соединения начнут заметно тормозить получение ответа. Приведу пример получившегося класса для опроса DSP:

Код
public class DspRequests
{
    private const string _method = "POST";
    private const string _contentType = "application/json";
    private static List<string> _headers = new List<string> { "x-openrtb-version: 2.0" };
    private List<string> _dspUrls { get; set; }

    public List<string> DspUrls {
        get
        {
            returns _dspUrls;
        }
        set
        {
            _dspUrls = value;
        }
    }

    public async Task<List<string>> PollDspAsync(string request)
    {
        List<string> responses = null;
        var tasks = _dspUrls.DspRecords.Select(el => PollAsync(el, request));
        responses = (await Task.WhenAll(tasks)).Where(el => el != null).ToList();
        return responses;
    }

    private async Task<string> PollAsync(string url, string bidRequest)
    {
        string response = null;

        HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
        httpWebRequest.Method = _method;
        httpWebRequest.ContentType = _contentType;
        httpWebRequest.KeepAlive = true;
        foreach (string header in _headers)
            httpWebRequest.Headers.Add(header);
        httpWebRequest.Timeout = 120;

        using (StreamWriter streamWriter = new StreamWriter(await httpWebRequest.GetRequestStreamAsync()))
        {
            streamWriter.Write(bidRequest);
        }
        
        using (HttpWebResponse webResponse = (HttpWebResponse) await httpWebRequest.GetResponseAsync())
        {
            if (webResponse.StatusCode == HttpStatusCode.OK)
            {
                using (StreamReader streamReader = new StreamReader(webResponse.GetResponseStream()))
                {
                    response = streamReader.ReadToEnd();
                }
            }
        }

        return response;
    }
}


Клиентская часть нашей SSP. Это личный кабинет веб-издателя (площадки), который содержит статистику по всем его рекламным местам. VOX — это self-service платформа, позволяющая создавать как баннерные рекламные места (5 самых популярных в RTB размеров), так и Rich Media баннеры (fullscreen). На каждое рекламное место можно повесить заглушку (HTML или Javascript), а также добавить счетчик показов сторонней системы.



Вот на этом, пожалуй, мы и закончим обзор работы нашей SSP.

Комментарии (6)


  1. vlsergey
    03.08.2015 15:39

    На второй картинке, похоже, ошибка — вместо второго «опрос DSP» должен быть «ответ DSP».

    Не ясно, зачем на графике две отправки данных в DMP и ни одного получения. Если мы рассматриваем «свою» DSP, то там должно быть получение. Если же «чужую» — то там снова и отправка, и получение данных (если SSP не скрывает информацию о посещении).

    И хочется цифр: сколько запросов в секунду обрабатывает один SSP-сервер, сколько запросов в секунду с него уходит на DSP, и сколько максимум времени обрабатывается запрос от браузера.


    1. mdorokhin
      03.08.2015 16:18

      Благодарим за внимательность, схему мы поправили.

      По поводу производительности. В среднем входящий трафик составляет 150 — 200 запросов в секунду, бывают пики до 1000 запросов в секунду, всё это обрабатывают два сервера с 6 экземплярами приложения в пуле каждый. Таким образом нагрузка на один экземпляр приложения 12 — 16 запросов в секунду. На DSP уходит весь этот трафик. Максимальное время обработки запроса складывается из времени ответа DSP (максимум мы ждём 120 миллисекунд) плюс 10 — 20 миллисекунд время работы самой SSP, в итоге максимум 140 миллисекунд.


      1. vlsergey
        03.08.2015 16:38

        «в итоге максимум 140 миллисекунд» — то есть никакого жёсткого лимита со стороны front proxy вы не задаёте, так?

        «нагрузка на один экземпляр приложения 12 — 16 запросов в секунду» — возможно, я чего-то не понимаю в .NET, но это какие-то очень маленькие цифры. Один эксземпляр обрабатывает один запрос одновременно? Что будет с сервером, когда придёт нормальная нагрузка в 50к запросов в секунду?

        SSP на netty (Pure Java NIO), обрабатывает по 10к запросов / секунду на инстанс, по инстансу на сервер, загружая пару CPU.


        1. mdorokhin
          03.08.2015 17:31

          Совершенно верно, жёсткого лимита нет.

          Да, действительно нагрузки здесь очень маленькие. Параллельность обработки запросов обеспечивается за счёт асинхронной работы приложения. Само приложение работает на IIS и особых чудес в производительности от него ждать не приходиться, но с текущей нагрузкой он справляется. Нагрузка растёт не мгновенно, а в случае нехватки мощностей конечно будем разбираться, например можно посмотреть в сторону проекта Katana, тем более что у нас уже есть опыт работы с ним.


  1. vlsergey
    03.08.2015 15:45

    Понятно, что существенная часть кода опущена ( «dspUrls.DspRecords» на это намекает ). Но ещё интересно, куда и как вы статистику пишете.


    1. mdorokhin
      03.08.2015 16:34

      В сыром виде статистика пишется в Aerospike. Далее, каждые 5 минут, она агрегируется и перекочёвывает в PostgreSQL. С ним уже работает клиентская часть.