увеличить использование памяти зверя для массовых запросов

Я запускаю этот boost -beast-client-async-ssl, и это нормально. Но если я создам 10000 сеансов одновременно, использование памяти моей программы вырастет до 400 МБ и никогда не уменьшится. Я бы тестировал без ssl (простой http), да и памяти не росло.

В: Что не так с openssl?

Вот моя функция main.

    //up boost-beast-client-async-ssl session code.   
    struct io_context_runner
    {
        boost::asio::io_context * ioc;
        void operator()()const
        {
            try{
                boost::asio::io_context::work w(*ioc);
                ioc->run();
            }catch(std::exception& e){
                fprintf(stderr, "e: %s\n", e.what());
            }
        }
    };

int main(int argc, char* argv[] ){

    try
    {
        int total_run = 1;
        if (argc > 1) total_run = atoi(argv[1]);

        const char* const host = "104.236.162.70" ;// IP of  isocpp.org
        const char* const port =  "443";  // 
        const char* const target= "/" ; //

        std::string const body = ""; //
        int version =  11;

        // The io_context is required for all I/O
        boost::asio::io_context ioc;

        // The SSL context is required, and holds certificates
        ssl::context ctx{ssl::context::sslv23_client};

        // This holds the root certificate used for verification
        load_root_certificates(ctx);

        typedef std::shared_ptr< async_http_ssl::session > pointer;

        for(int i = 0; i < total_run; ++i){
            pointer s = std::make_shared< async_http_ssl::session >(ioc  , ctx   ) ;
            usleep( 1000000 / total_run ) ;
            s->run( host, port, target, version ) ;
        }
        // Launch the asynchronous operation
        //std::make_shared<session>(ioc, ctx)->run(host, port, target, version);

        // Run the I/O service. The call will return when
        // the get operation is complete.
        std::thread t{ io_context_runner{ &ioc } } ;

        t.join();

        // If we get here then the connection is closed gracefully
    }
    catch(std::exception const& e)
    {
        std::cerr << "Error: " << e.what() << std::endl;
        return EXIT_FAILURE;
    }

       return EXIT_SUCCESS ;
}

Изменить: Ubuntu 14.04, Boost 1.66, g++ 4.9.4. OpenSSL 1.0.1f 6 января 2014 г.

EDIT2: Согласно этот вопрос malloc_trim освобождает (возвращает ОС) много неиспользуемой памяти. Было бы лучше, если бы boost asio сам поддерживал malloc_trim для ssl-соединения в unix-системах !!.


person Khurshid Normuradov    schedule 16.03.2018    source источник
comment
Примечание: Должны ли быть перехвачены исключения, созданные io_service::run. Вероятно, вам следует изменить io_context_runner, чтобы отразить это.   -  person sehe    schedule 17.03.2018


Ответы (1)


Несколько проблем с тем, как вы адаптировали этот пример:

  1. рабочий поток блокирует io_service с экземпляром work, поэтому он никогда не завершится
  2. вы usleep за некоторое время до запуска асинхронных задач, но вы никогда не запускаете ни одну из задач до тех пор, пока цикл не завершится... Это означает, что все задержки выполняются до запуска любая работа.

Вот мое предложение:

  • запустите службу перед запуском асинхронных задач
  • иметь 1 экземпляр work для блокировки службы на случай, если служба будет простаивать перед отправкой следующего HTTP-запроса.
  • не блокируйте work внутри рабочего потока

Прямой эфир на Coliru

#include "example/common/root_certificates.hpp"

#include <boost/beast.hpp>
#include <boost/asio.hpp>

using tcp = boost::asio::ip::tcp;    // from <boost/asio/ip/tcp.hpp>
namespace ssl = boost::asio::ssl;    // from <boost/asio/ssl.hpp>
namespace http = boost::beast::http; // from <boost/beast/http.hpp>

//------------------------------------------------------------------------------

// Report a failure
void
fail(boost::system::error_code ec, char const* what)
{
    std::cerr << what << ": " << ec.message() << "\n";
}

// Performs an HTTP GET and prints the response
class session : public std::enable_shared_from_this<session>
{
    tcp::resolver resolver_;
    ssl::stream<tcp::socket> stream_;
    boost::beast::flat_buffer buffer_; // (Must persist between reads)
    http::request<http::empty_body> req_;
    http::response<http::string_body> res_;

public:
    // Resolver and stream require an io_context
    explicit
    session(boost::asio::io_context& ioc, ssl::context& ctx)
        : resolver_(ioc)
        , stream_(ioc, ctx)
    {
    }

    // Start the asynchronous operation
    void
    run(
        char const* host,
        char const* port,
        char const* target,
        int version)
    {
        // Set SNI Hostname (many hosts need this to handshake successfully)
        if(! SSL_set_tlsext_host_name(stream_.native_handle(), host))
        {
            boost::system::error_code ec{static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category()};
            std::cerr << ec.message() << "\n";
            return;
        }

        // Set up an HTTP GET request message
        req_.version(version);
        req_.method(http::verb::get);
        req_.target(target);
        req_.set(http::field::host, host);
        req_.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);

        // Look up the domain name
        resolver_.async_resolve(
            host,
            port,
            std::bind(
                &session::on_resolve,
                shared_from_this(),
                std::placeholders::_1,
                std::placeholders::_2));
    }

    void
    on_resolve(
        boost::system::error_code ec,
        tcp::resolver::results_type results)
    {
        if(ec)
            return fail(ec, "resolve");

        // Make the connection on the IP address we get from a lookup
        boost::asio::async_connect(
            stream_.next_layer(),
            results.begin(),
            results.end(),
            std::bind(
                &session::on_connect,
                shared_from_this(),
                std::placeholders::_1));
    }

    void
    on_connect(boost::system::error_code ec)
    {
        if(ec)
            return fail(ec, "connect");

        // Perform the SSL handshake
        stream_.async_handshake(
            ssl::stream_base::client,
            std::bind(
                &session::on_handshake,
                shared_from_this(),
                std::placeholders::_1));
    }

    void
    on_handshake(boost::system::error_code ec)
    {
        if(ec)
            return fail(ec, "handshake");

        // Send the HTTP request to the remote host
        http::async_write(stream_, req_,
            std::bind(
                &session::on_write,
                shared_from_this(),
                std::placeholders::_1,
                std::placeholders::_2));
    }

    void
    on_write(
        boost::system::error_code ec,
        std::size_t bytes_transferred)
    {
        boost::ignore_unused(bytes_transferred);

        if(ec)
            return fail(ec, "write");

        // Receive the HTTP response
        http::async_read(stream_, buffer_, res_,
            std::bind(
                &session::on_read,
                shared_from_this(),
                std::placeholders::_1,
                std::placeholders::_2));
    }

    void
    on_read(
        boost::system::error_code ec,
        std::size_t bytes_transferred)
    {
        boost::ignore_unused(bytes_transferred);

        if(ec)
            return fail(ec, "read");

        // Write the message to standard out
        //std::cout << res_ << std::endl;

        // Gracefully close the stream
        stream_.async_shutdown(
            std::bind(
                &session::on_shutdown,
                shared_from_this(),
                std::placeholders::_1));
    }

    void
    on_shutdown(boost::system::error_code ec)
    {
        if(ec == boost::asio::error::eof)
        {
            // Rationale:
            // http://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error
            ec.assign(0, ec.category());
        }
        if(ec)
            return fail(ec, "shutdown");

        // If we get here then the connection is closed gracefully
    }
};

//up boost-beast-client-async-ssl session code.   
struct io_context_runner
{
    boost::asio::io_context& ioc;
    void operator()()const
    {
        try{
            ioc.run();
        }catch(std::exception& e){
            fprintf(stderr, "e: %s\n", e.what());
        }
    }
};

namespace async_http_ssl {
    using ::session;
}

#include <thread>

int main(int argc, char *argv[]) {
    // The io_context is required for all I/O
    boost::asio::io_context ioc;
    std::thread t;

    try {
        // Run the I/O service. The call will return when all work is complete
        boost::asio::io_context::work w(ioc);
        t = std::thread { io_context_runner{ioc} };

        int total_run = 1;
        if (argc > 1)
            total_run = atoi(argv[1]);

#if 0
        auto host = "104.236.162.70";                   // IP of  isocpp.org
        auto port = "443";                              //
        auto target = "/";                              //
#else
        auto host = "127.0.0.1";
        auto port = "443";
        auto target = "/BBB/http_client_async_ssl.cpp";
#endif

        std::string const body = ""; //
        int version = 11;

        // The SSL context is required, and holds certificates
        ssl::context ctx{ssl::context::sslv23_client};

        // This holds the root certificate used for verification
        load_root_certificates(ctx);

        typedef std::shared_ptr<async_http_ssl::session> pointer;

        for (int i = 0; i < total_run; ++i) {
            pointer s = std::make_shared<async_http_ssl::session>(ioc, ctx);
            usleep(1000000 / total_run);
            s->run(host, port, target, version);
        }
    } catch (std::exception const &e) {
        std::cerr << "Error: " << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    if (t.joinable())
        t.join();

    // If we get here then the connections have been closed gracefully
}

В моей системе профилирование памяти с 1 соединением:

введите здесь описание изображения

При 100 соединениях:

введите здесь описание изображения

С 1000 подключений:

введите здесь описание изображения

Анализ

Что это значит? Все еще кажется, что Beast использует все больше памяти при отправке большего количества запросов, верно?

Ну нет. Проблема в том, что вы начинаете запросы быстрее, чем они могут быть выполнены. Таким образом, нагрузка на память увеличивается в основном из-за того, что в данный момент существует много экземпляров session. После завершения они автоматически освобождают ресурсы (из-за использования shared_ptr<session>).

Выполнение запросов последовательно

Чтобы донести мысль, вот модифицированная версия, которая принимает обработчик on_completion_ с сеансом:

std::function<void()> on_complete_;

// Resolver and stream require an io_context
template <typename Handler>
explicit
session(boost::asio::io_context& ioc, ssl::context& ctx, Handler&& handler)
    : resolver_(ioc)
    , stream_(ioc, ctx)
    , on_complete_(std::forward<Handler>(handler))
{
}

~session() {
    if (on_complete_) on_complete_();
}

Теперь вы можете переписать основную логику программы в виде цепочки асинхронных операций:

struct Tester {
    boost::asio::io_context ioc;
    boost::optional<boost::asio::io_context::work> work{ioc};
    std::thread t { io_context_runner{ioc} };

    ssl::context ctx{ssl::context::sslv23_client};

    Tester() {
        load_root_certificates(ctx);
    }

    void run(int remaining = 1) {
        if (remaining <= 0)
            return;

        auto s = std::make_shared<session>(ioc, ctx, [=] { run(remaining - 1); });
        s->run("127.0.0.1", "443", "/BBB/http_client_async_ssl.cpp", 11);
    }

    ~Tester() {
        work.reset();
        if (t.joinable()) t.join();
    }
};

int main(int argc, char *argv[]) {
    Tester tester;
    tester.run(argc>1? atoi(argv[1]):1);
}

С помощью этой программы (Полный код на Coliru) мы получаем гораздо более стабильные результаты:

  • 1 запрос:

    введите здесь описание изображения

  • 100 запросов:

    введите здесь описание изображения

  • 1000 запросов:

    введите здесь описание изображения

Восстановление пропускной способности

Ну, это слишком консервативно, отправка многих запросов может стать очень медленной. Как насчет некоторого параллелизма? Легкий:

int main(int argc, char *argv[]) {
    int const total      = argc>1? atoi(argv[1]) : 1;
    int const concurrent = argc>2? atoi(argv[2]) : 1;

    {
        std::vector<Tester> chains(concurrent);

        for (auto& chain : chains)
            chain.run(total / concurrent);
    }

    std::cout << "All done\n";
}

Это все! Теперь у нас может быть concurrent отдельных цепочек выполнения, обслуживающих ~общее количество запросов. Посмотрите разницу во времени выполнения:

$ time ./sotest 1000
All done

real    0m53.295s
user    0m13.124s
sys 0m0.232s
$ time ./sotest 1000 10
All done

real    0m8.808s
user    0m8.884s
sys 0m1.096s

Использование памяти продолжает выглядеть здоровым:

введите здесь описание изображения

person sehe    schedule 16.03.2018
comment
Добавлены три главы, объясняющие результаты профилирования и изменение ограничения одновременных сеансов (и избавление от usleeps). Теперь выполнение 10 000 запросов в 20 одновременных цепочках выполняется без ошибок и по-прежнему стабильно использует память в моей системе: i.imgur .com/Vh9pFEz.png - person sehe; 17.03.2018
comment
1) Я не понимаю, почему ‹‹ не блокируют работу внутри рабочего потока ›› ?? 2) В реальном проекте io_context никогда не должен заканчиваться, потому что он ожидает запросов от другой части системы (клиентов). Итак, я не могу реализовать, как вы предложили. 3) Извините, но это не ответ на мой вопрос, ЧТО НЕ ТАК С OPENSSL?? т. е. ПОЧЕМУ ЭТО СТОЛЬКО ПАМЯТИ СЪЕДЕТ, И НИКОГДА НЕ ОСВОБОДИТ? - person Khurshid Normuradov; 17.03.2018
comment
Вот это да. Не кричи. С OpenSsl все в порядке. Мои тесты ясно показывают это. (Перечитайте разделы «Анализ» и «Последовательность»). Вы можете переместить рабочий замок в другое место. Просто не сбрасывайте его, если вы не хотите чистого завершения работы. Конечно, вас не волнует чистое завершение работы, вы можете оставить его в рабочем потоке, но это затрудняет проверку на наличие утечек памяти. (По иронии судьбы libcrypto.so пропускает фиксированное количество байтов). - person sehe; 17.03.2018
comment
@sehe ну, это довольно удивительный ответ! Раз уж мы затронули эту тему... Я так и не смог заставить полностью работать пример http-crawl, кажется, он зависает в конце. Это похоже на то, что делается здесь, оно пытается одновременно посетить 10 000 самых популярных веб-сайтов, ранжированных Alexa, и выполнить GET. В основном это стресс-тест/тест производительности, который может быть полезен для профилирования. Возможно, вы могли бы выяснить, почему он зависает на последних нескольких запросах? Вот он: https://github.com/boostorg/beast/tree/develop/example/http/client/crawl - person Vinnie Falco; 25.03.2018
comment
@VinnieFalco было бы полезно, если бы была четкая проблема с реальными проблемами с ним. Это также было бы хорошим местом для связанных дискуссий - person sehe; 25.03.2018