Proxygen — HTTP-фреймворк для С++ от Facebook

https://code.facebook.com/posts/1503205539947302/introducing-proxygen-facebook-s-c-http-framework/
  • Перевод
  • Tutorial
Proxygen — это коллекция библиотек для использования протокола HTTP на С++, включающая в числе прочего очень простой в использовании HTTP-сервер. Кроме классического HTTP/1.1 фреймворк Proxygen поддерживает SPDY/3 и SPDY/3.1. Вскоре также будет полностью поддерживаться HTTP/2.

Proxygen не задумывался как замена Apache или nginx — эти проекты сфокусированы на создании достаточно гибких и конфигурируемых веб-серверов, позволяющих благодаря тонкой настройке добиться максимальной производительности. Задачей Proxygen является работать достаточно хорошо на дефолтных настройках, давая программисту простые в использовании веб-сервер и веб-клиент, легко интегрирующиеся в уже существующие проекты. Мы хотим помочь людям строить веб-сервисы на С++ с меньшими затратами и мы верим, что Proxygen — отличный фреймворк для этого. Вы можете почитать документацию по нему и подключиться к разработке на Github.


Предыстория


Proxygen начинался как проект по созданию настраиваемого высокопроизводительного обратного прокси с функцией балансировки нагрузки около четырёх лет назад. Мы планировали, что Proxygen станет библиотекой для генерации прокси-серверов (вы небось уже догадались об этом из самого названия Proxygen). Но с тех пор он серьёзно эволюционировал. Мы осознаём, что уже существует приличное количество программ, решающих подобные задачи (Apache, nginx, HAProxy, Varnish, etc), тем ни менее, мы решили пойти своим путём.

Почему мы создали свой собственный HTTP-стек?

Интеграция

Возможность быстро и легко интегрироватьcя в существующую инфраструктуру Facebook была критически важной. Например, возможность администрировать нашу HTTP-инфраструктуру с такими инструментами как Thrift упрощает интеграцию с существующими системами. Возможность легко отслеживать и измерять производительность Proxygen с помощью таких систем как ODS (наше внутреннее средство мониторинга) даёт возможность быстро реагировать на новые данные и модифицировать продукт. Создание собстенного HTTP-стека дало нам возможность более тесно взаимодействовать с нужными нам системами и компонентами.

Переиспользоавние кода

Мы хотели создать фундамент для построения сетевых компонентов для всех наших проектов. В данный момент более дюжины наших внутренних систем построены с использованием Proxygen, включая части таких систем как Haystack, HHVM, наши балансировщики HTTP-трафика, кое-какие части нашей мобильной инфраструктуры. Proxygen представляет собой платформу, где мы можем, например, работать над поддержкой протокола HTTP/2 и как только он будет полностью готов — получить его поддержку во всех наших продуктах.

Масштабируемость

Мы честно пытались брать уже существующие продукты и масштабировать их на всю нашу инфраструктуру. С некоторыми это даже получалось, кое-какие варианты проработали достаточно долго. Но в какой-то момент оказывалось, что используемый продукт больше не в состоянии угнаться за ростом наших мощностей.

Функционал

Некоторое количество фич на момент начала написания Proxygen отсутствовало в других аналогичных проектах (а в некоторых отсутствует и сейчас). Часть из этих возможностей для нас очень полезны: SPDY, WebSockets, HTTP/1.1 (keep-alive), TLS-фальстарт, кое-какие особенности распределения нагрузки. Построение собственного HTTP-стека развязало нам руки в плане реализации этого функционала.

Изначально запущенный в 2011 году несколькими нашими инженерами, стремившимися сделать использование HTTP-протокола более эффективным, Proxygen разрабатывался командой из 3-4 основных разработчиков и дюжиной внутренних контрибьютеров. Основные вехи проекта:

  • 2011 — Начало разработки Proxygen. В том же году проект начал обрабатывать часть реального траффика Facebook.
  • 2012 — Добавление поддержки SPDY/2.
  • 2013 — Выход в продакшн SPDY/3, начало работы над SPDY/3.1
  • 2014 — Выход в продакшн SPDY/3.1, начало работы над HTTP/2


Есть ещё некоторое количество важных моментов в разработке, но мы думаем, что код расскажет эту историю лучше нас.

В данный момент у нас есть уже несколько лет опыта использования Proxygen. Библиотека обработала уже триллионы HTTP(S) и SPDY-запросов. Мы считаем, что уже достигли того этапа, когда этим проектом не стыдно поделиться с сообществом.

Архитектура


Ядро HTTP-слоя разделено на четыре абстракции: сессия, кодек, транзакция и обработчик. Для каждого соединения создаётся сессия. Каждая сессия имеет кодек, отвечающий за сериализацию и десериализацию фреймов на HTTP-сообщения. Сессия отвечает за направление каждого сообщения от кодека в определённую транзакцию. Пользователь библиотеки отвечает за написание обработчиков приходящих в транзакцию сообщений. Этот дизайн позволяет нам поддерживать новые мультиплексируемые протоколы, такие как SPDY и HTTP/2.

image

Proxygen активно использует возможности последнего стандарта С++ и зависит от Thrift и Folly. Мы использовали семантику перемещения для избежания затрат на копирование больших объектов вроде буферов тела и заголовков запросов и ответов. Кроме того, используя под капотом неблокируемый ввод-вывод и линуксовый epoll мы создали сервер эффективный как по использованию памяти, так и по затратам процессорного времени.

HTTP-сервер


Пример сервера, который мы включили в релиз — хорошая стартовая точка, если вы хотите начать с простого, работающиего из коробки, асинхронного костяка сервера.

EchoServer.cpp
/*
 *  Copyright (c) 2014, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 *
 */
#include "EchoHandler.h"
#include "EchoStats.h"
#include "proxygen/httpserver/HTTPServer.h"
#include "proxygen/httpserver/RequestHandlerFactory.h"

#include <folly/Portability.h>
#include <folly/Memory.h>
#include <folly/io/async/EventBaseManager.h>
#include <unistd.h>

using namespace EchoService;
using namespace proxygen;

using folly::EventBase;
using folly::EventBaseManager;
using folly::SocketAddress;

using Protocol = HTTPServer::Protocol;

DEFINE_int32(http_port, 11000, "Port to listen on with HTTP protocol");
DEFINE_int32(spdy_port, 11001, "Port to listen on with SPDY protocol");
DEFINE_int32(thrift_port, 10000, "Port to listen on for thrift");
DEFINE_string(ip, "localhost", "IP/Hostname to bind to");
DEFINE_int32(threads, 0, "Number of threads to listen on. Numbers <= 0 "
             "will use the number of cores on this machine.");

class EchoHandlerFactory : public RequestHandlerFactory {
 public:
  void onServerStart() noexcept override {
    stats_.reset(new EchoStats);
  }

  void onServerStop() noexcept override {
    stats_.reset();
  }

  RequestHandler* onRequest(RequestHandler*, HTTPMessage*) noexcept override {
    return new EchoHandler(stats_.get());
  }

 private:
  folly::ThreadLocalPtr<EchoStats> stats_;
};

int main(int argc, char* argv[]) {
  gflags::ParseCommandLineFlags(&argc, &argv, true);
  google::InitGoogleLogging(argv[0]);
  google::InstallFailureSignalHandler();

  std::vector<HTTPServer::IPConfig> IPs = {
    {SocketAddress(FLAGS_ip, FLAGS_http_port, true), Protocol::HTTP},
    {SocketAddress(FLAGS_ip, FLAGS_spdy_port, true), Protocol::SPDY},
  };

  if (FLAGS_threads <= 0) {
    FLAGS_threads = sysconf(_SC_NPROCESSORS_ONLN);
    CHECK(FLAGS_threads > 0);
  }

  HTTPServerOptions options;
  options.threads = static_cast<size_t>(FLAGS_threads);
  options.idleTimeout = std::chrono::milliseconds(60000);
  options.shutdownOn = {SIGINT, SIGTERM};
  options.handlerFactories = RequestHandlerChain()
      .addThen<EchoHandlerFactory>()
      .build();

  HTTPServer server(std::move(options));
  server.bind(IPs);

  // Start HTTPServer mainloop in a separate thread
  std::thread t([&] () {
    server.start();
  });

  t.join();
  return 0;
}


EchoHandler.cpp
/*
 *  Copyright (c) 2014, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 *
 */
#include "EchoHandler.h"

#include "EchoStats.h"
#include "proxygen/httpserver/RequestHandler.h"
#include "proxygen/httpserver/ResponseBuilder.h"

using namespace proxygen;

namespace EchoService {

EchoHandler::EchoHandler(EchoStats* stats): stats_(stats) {
}

void EchoHandler::onRequest(std::unique_ptr<HTTPMessage> headers) noexcept {
  stats_->recordRequest();
}

void EchoHandler::onBody(std::unique_ptr<folly::IOBuf> body) noexcept {
  if (body_) {
    body_->prependChain(std::move(body));
  } else {
    body_ = std::move(body);
  }
}

void EchoHandler::onEOM() noexcept {
  ResponseBuilder(downstream_)
    .status(200, "OK")
    .header("Request-Number",
            folly::to<std::string>(stats_->getRequestCount()))
    .body(std::move(body_))
    .sendWithEOM();
}

void EchoHandler::onUpgrade(UpgradeProtocol protocol) noexcept {
  // handler doesn't support upgrades
}

void EchoHandler::requestComplete() noexcept {
  delete this;
}

void EchoHandler::onError(ProxygenError err) noexcept {
  delete this;
}

}


Мы провели бенчмарк нашего эхо-сервера на компьютере с 32 логическими ядрами Intel® Xeon® CPU E5-2670 @ 2.60GHz и 16 GiB памяти, варируя количество рабочих потоков от одного до восьми. Мы запускали клиент на той же машине, дабы избежать сетевых задержек и вот что мы получили:

# Параметры настройки клиента:
# На каждый рабочий поток сервера по 2 клиента
# 400 одновременно открытых соединений
# 100 запросов на соединение
# 60 секунд тестирования
# В результатах указано среднее значение по результатам 10 тестов
# простой GET, 245 байт заголовков запроса, 600 байт ответа (без сохранения на диск)
# SPDY/3.1 позволяет до 10 параллельных запросов на соединение

image

Хотя сам по себе эхо-сервер достаточно примитивная штука по сравнению с настоящим веб-сервером, этот бенчмарк всё же показывает насколько эффективно Proxygen работает со SPDY и HTTP/2. HTTP-сервер из комплекта Proxygen легко использовать и он сразу работает достаточно производительно, хотя мы больше фокусировались на простоте применения, чем на максимально возможной скорости работы. К примеру, модель фильтров в сервере даёт вам возможность обрабатывать некоторые общие блоки данных по определённых для них алгоритмам, причём таким образом, что каждый отдельный блок кода алгоритма легко поддаётся юнит тестерованию. С другой стороны, необходимость аллокация памяти, связанная с этой моделью фильтров не идеальна для высокопроизводительных приложений.

Влияние


Proxygen позволяет нам быстро реализовать нужный функционал, выпустить его в продакшн и сразу получить результат. К примеру, нам было интересно оценить формат сжатия заголовков запросов HPACK, но к сожалению у нас не было ни клиентов HTTP/2, ни серверов, да и вообще спецификация HTTP/2 сама по себе всё ещё в процессе разработки. Proxygen позволил нам реализовать HPACK, попробовать использовать его поверх SPDY и выкатить релиз одновременно на наши сервера и мобильные клиенты. Возможность оперативно экспериментировать с реальным трафиком в формате HPACK дало нам возможность понять его реальную производительность и оценить выгоды от его использования.

Открытый код


Кодовая база Proxygen находится в состоянии активной разработки и будет продолжать эволюционировать. Если вам нравится протокол HTTP, высокопроизводительный сетевой код, современный С++, мы будем рады поработать с вами! Пожалуйста, не стесняйтесь присылать пулреквесты на Github.

Мы активно участвуем в разработке открытых проектов и всегда ищем возможность поделиться своим кодом с сообществом. Команда разработки сетевой инфраструктуры уже выложила в опенсор Thrift и Proxygen — два важных сетевых компонента Facebook. Мы надеемся, что они найдут своё применения и в других проектах.

Спасибо всем инженерам, поучаствовашим в разработке данного проекта: Ajit Banerjee, David Gadling, Claudiu Gheorghe, Rajat Goel, Peter Griess, Martin Lau, Adam Lazur, Noam Lerner, Xu Ning, Brian Pane, Praveen Kumar Ramakrishnan, Adam Simpkins, Viswanath Sivakumar и Woo Xie.
Инфопульс Украина 260,16
Creating Value, Delivering Excellence
Поделиться публикацией
Комментарии 19
  • –3
    интересная штука, хоть и от фейсбука
    • +4
      Мне, напротив, нравится поход Фэйсбука к собственной инфраструктуре и разработке вообще.
      А чем вас смущает, что фреймворк от «от фейсбука»?
      • +2
        У меня весь этот маркетинг вызывает ассоциацию с показом мод: «фреймворк от фейсбука» и «библиотека от твиттера» звучат примерно так же, как «сумка от версаче» и «сорочка от армани». Что же это значит по сути?
        — Этот код написан инженерами (чаще всего одним-двумя) данной компании. Ок.
        — Код используется внутри компании в 0-10 местах (скорее всего около 2), значит, он в принципе как-то работает в 0-5 конкретных сценариях использования. Тоже не впечатляет, потому что странно публиковать вообще неработающий код.
        — Есть шанс чуть выше среднего, что код кто-то ревьюил, потому что это больше распространено в крупных конторах. (Как минимум, со слов спикеров от этих контор на конференциях. У меня лично очень маленькая выборка, и она скорее опровергает этот тезис.)
    • –1
      В результатах указано среднее значение по результатам 10 тестов

      Читал описание и на английском, и всё равно не понял, что это за значение. Печаль-беда.
      • +2
        Запустили на сервере определенное количество потоков. Запустили на каждый поток по 2 клиента, которые в течении 60 секунд дёргали сервер GET-запросами. Получили какое-то количество обработанных запросов. Повторили всё вышеуказанное 10 раз и усреднили результат.
        • –1
          Если это количество запросов за минуту — то очень мало. Серьезно, на одном потоке менее 1000 rps?
        • +1
          Судя по всему — это количество запросов в секунду, которое может обработать сервер при заданном количестве потоков.
          • 0
            Похоже на правду. К сожалению, из текста это не очень ясно. Между прочим, с 8 потоками это почти 2 гигабита пропускной способности.
            • 0
              Это всё ведь локально на одной машине гонялось.
              • 0
                Да, для таких программ на простых задачах легко упереться в сеть. Интересно, как ведут себя балансировка и прочие вкусности — но про них как-то не очень много деталей.
        • 0
          Тоже присматриваюсь к этому фреймворку. Интересно было бы «пощупать» его в работе.
          Автор, вы пробовали что-нибудь на нем делать? Поделитесь своими впечатлениями.
          • 0
            Нет пока, я смотрел на него поскольку мне в своём продукте нужен HTTP/2, но он у них пока не закончен. Но я буду за ним следить.
          • 0
            И все же — значения в минуту или в секунду? Еще, насколько я понял, асинхр. движек у вас folly, построен на libevent. С чем связан такой выбор?
            • +1
              Учитывая то, что как минимум работал (а может и по сей день работает) с Facebook такой гуру С++ как Andrei Alexandrescu, то мне был бы этот код реально интересен. А учитывая то, что он очень интенсивно работает над новыми стандартами С++, то этот код может оказаться прекрасным примером применения всего самого «вкусного» из С++. Спасибо за публикацию!
              • 0
                Ну Алексадреску давно уже подался в D, и в развитии C++ участвует уже довольно слабо.
              • +1
                Меня одного немного пугают вот такие вещи?
                • +1
                  Выглядит странно по началу. Но если подумать, смысл есть. Они получают параметр по ссылке, а не по указателю. Был бы указатель, после удаления записали бы nullptr в него и дальше с nullptr сравнивали бы. А так если что-то сломалось вроде как надо помнить что объект уже удалили. Ну и чтобы около каждого места с деструктором не ставить установку дополнительного флага «объект уничтожен», можно это автоматизировать через такого вот заботливого родителя. Который сообщит куда следует, что умер уже.
                  Хотя мне кажется лучше указатели копировать на объекты, чем так извращаться. Наверняка у них была веская причина или какой-то вид багов, от которых они таким образом спасались.
                  • +1
                    Нет, я понимаю зачем это можно было бы использовать, но они в примере, вроде, предлагают его использовать с асинхронными вызовами. Может я неправильно их понял и там всего лишь обработка асинхронных событий, но если там действительно асинхронные вызовы то от возможных гонок такие проверки не спасут.
                    • 0
                      Да, действительно странно. Т.к. для асинхронного кода эти классы явно еще не готовы. Ни синхронизации, ни атомарного флага не видно.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое