Всем привет! Это вторая статья по системе мониторинга приложений от компании bitDive. В данной статье мы расскажем, как мы разрабатывали библиотеку, которая интегрируется в клиентские приложения и передаёт события на сервер мониторинга. Основная цель проекта — обработка миллионов сообщений в секунду с минимальным влиянием на производительность приложений клиентов.

1. Постановка задачи

Вводные данные

Перед нашей командой стояла задача:

  • Обработка 2 миллионов сообщений в секунду.

  • Минимизация использования CPU.

  • Минимизация использования оперативной памяти.

Иными словами, мы искали подход, который обеспечивал бы высокую производительность и низкое потребление ресурсов.

2. Попытка использовать Apache Kafka

Почему Kafka?

Apache Kafka — популярный брокер сообщений, известный своей производительностью и надежностью. Мы предположили, что его архитектура идеально подойдёт для нашей задачи.

Проблемы с Kafka

Однако после начала тестирования мы столкнулись с рядом ограничений:

  1. Работа через сокеты.
    Kafka работает с использованием TCP-сокетов:

    private PlaintextTransportLayer(SelectionKey key) throws IOException {
        super(key);
        SocketChannel socketChannel = (SocketChannel) key.channel();
        socketChannel.configureBlocking(false);
    }
    

    Это создает сложности при интеграции с прокси-серверами. Единственное решение — использование JVM-параметров, что нас не устраивало.

  2. Высокое потребление памяти.
    Для гарантии доставки сообщений Kafka использует промежуточное хранение, что значительно увеличивает использование оперативной памяти.

  3. Сложности администрирования.
    Чтобы обеспечить отказоустойчивость, клиенту необходимо вручную развертывать дополнительные ноды, что требует дополнительных ресурсов.

Вывод: Kafka не подошла из-за её требований к ресурсам и сложности настройки.

3. Асинхронная отправка через Spring WebFlux

Следующим шагом мы попробовали использовать Spring WebFlux, который позволяет отправлять сообщения асинхронно.

Реализация

Для отправки сообщений мы использовали следующий код:

import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

public class WebFluxSender {
    private final WebClient webClient;

    public WebFluxSender(String serverUrl) {
        this.webClient = WebClient.create(serverUrl);
    }

    public Mono<Void> sendMessage(String message) {
        return webClient.post()
                .uri("/api/messages")
                .bodyValue(message)
                .retrieve()
                .bodyToMono(Void.class);
    }
}

Проблемы с WebFlux

  1. Буферизация данных. Сообщения сохранялись в памяти перед отправкой, что увеличивало потребление ОЗУ.

  2. Высокая нагрузка на CPU. Асинхронная обработка потоков вызывала значительное увеличение нагрузки на процессор.

Вывод: WebFlux улучшил ситуацию с прокси-серверами, но его использование оказалось неэффективным при больших объемах данных.

4. Оптимальное решение: файловая система

После всех экспериментов мы пришли к простому, но эффективному решению: запись сообщений в файлы с последующей отправкой пакетами.

Как это работает?

  1. Запись данных в файлы.
    Каждое сообщение записывается в файл сразу после его генерации:

    import java.io.BufferedWriter;
    import java.io.FileWriter;
    import java.io.IOException;
    
    public class FileMessageWriter {
        private BufferedWriter writer;
    
        public FileMessageWriter(String fileName) throws IOException {
            writer = new BufferedWriter(new FileWriter(fileName, true));
        }
    
        public synchronized void writeMessage(String message) throws IOException {
            writer.write(message);
            writer.newLine();
        }
    
        public void close() throws IOException {
            if (writer != null) {
                writer.close();
            }
        }
    }
    
  2. Пакетная отправка файлов.
    Файлы отправляются на сервер каждые 10 секунд:

    import org.apache.http.client.methods.CloseableHttpResponse;
    import org.apache.http.client.methods.HttpPost;
    import org.apache.http.entity.mime.MultipartEntityBuilder;
    import org.apache.http.impl.client.CloseableHttpClient;
    import org.apache.http.impl.client.HttpClients;
    
    import java.io.File;
    
    public class FileSender {
        public void sendFile(File file, String serverUrl) throws Exception {
            try (CloseableHttpClient client = HttpClients.createDefault()) {
                HttpPost post = new HttpPost(serverUrl + "/upload");
                post.setEntity(MultipartEntityBuilder.create()
                    .addBinaryBody("file", file)
                    .build());
                try (CloseableHttpResponse response = client.execute(post)) {
                    System.out.println("Response: " + response.getStatusLine());
                }
            }
        }
    }
    
  3. Шифрование и подпись.
    Перед отправкой файлы шифруются и подписываются для обеспечения безопасности.

Результаты

  • CPU: 1% загрузки.

  • Память: 3% использования.

  • Пропускная способность: 300 000 сообщений/сек.

5. Сравнение подходов: иллюстрации

Использование CPU

График использования CPU
График использования CPU

Использование памяти

График использования памяти
График использования памяти

Пропускная способность

График пропускной способности
График пропускной способности

6. Заключение

Использование файловой системы для хранения и передачи сообщений оказалось оптимальным решением для нашей задачи. Этот подход минимизировал использование ресурсов клиента и обеспечил высокую производительность.

Ключевые преимущества:

  1. Низкое потребление CPU и памяти.

  2. Простота интеграции.

  3. Высокая безопасность.

Мы надеемся, что наш опыт будет полезен другим командам, которые работают над подобными задачами. Если у вас есть вопросы или идеи, будем рады их обсудить!

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


  1. Wolfdp
    19.01.2025 08:58

    Не хватает указания что за сообщения (точнее разброс их длины) и как решение скинуть буферизацию на диск собственно этот диск и нагружает. По идеи для сообщений длиной 100 байт будет нагрузка в ~60МБ/с (по 30 на запись и считать).


    1. FrolikovEA Автор
      19.01.2025 08:58

      согласен . Добавим графики в статью


  1. dyadyaSerezha
    19.01.2025 08:58

    1. Не достигнуто 2 млн. сообщений в секунду.

    2. Лень смотреть, но блокирует ли addBinaryBody файл на запись? В любом случае должна получаться фигня. Ну и файл не затирается потом.

    3. Плохо знаю спринг, но не вижу никакой асинхронности, раз получается response. Да и зачем тут она? Ну будет синхронно посылаться в отдельном потоке.

    4. Один пост, это под 3 млн сообщений у вас. Это около 300 МБ. Через один http post. Зачем? (а если требуемые 2 млн в сек, то это 20 ГБ за один post!)

    5. Может, лучше какой-то web socket или pipe? И не раз в 10 сек, а раз в 0.1 - 0.5 сек.


    1. FrolikovEA Автор
      19.01.2025 08:58

      данные отправляются в файле который архивируется и шифруется и подписывается

      так что бы мы не отправляем через http post миллион сообщений


      1. dyadyaSerezha
        19.01.2025 08:58

        Ок, а что насчёт 2 млн в сек? Не шмогла?


        1. FrolikovEA Автор
          19.01.2025 08:58

          шмогла


          1. dyadyaSerezha
            19.01.2025 08:58

            Извините:

            • Пропускная способность: 300 000 сообщений/сек.


  1. Antharas
    19.01.2025 08:58

    Мы надеемся, что наш опыт будет полезен другим командам, которые работают над подобными задачами

    Да, спасибо, полезный опыт(с).

    Kafka обязательно нужно выкинуть, поскольку она использует tcp. WebClient тоже нафиг, не нужен. Возьмем просто apache httpcomponents и будем делать тоже самое…

    Какой-то, мягко говоря, сюр.


    1. FrolikovEA Автор
      19.01.2025 08:58

      Ну если вы не желаете читать ВНИМАТЕЛЬНО статью давайте я вам вынесу тезисно
      Kafka
      Это создает сложности при интеграции с прокси-серверами. Единственное решение — использование JVM-параметров, что нас не устраивало.


      Наше решение интегрируется в приложение клиента и если будет потребность использовать Proxy сервер то для kafka это только JVM параметры или использовать дополнительные прослойки , такие как Kafka-Proxy

      И если компании нужно будет увеличить пропускную способность то ей придётся администрировать сервера kafka

      WebClient  - хорошее решение но как и написано в статье имеет свои недостатки такие как
      1) если при большом потоке сообщений не успевают отправляется сообщения то копится очередь что ведёт за собой "выедание" памяти

      2) если нет связи с сервером куда отправляются сообщения то сообщения копятся в памяти (смотрим пункт 1) или пропадают

      Наше решение мы складываем в файлики на диске и формируем каждые 10 секунд ( настраиваемый параметр) новый файл для отправки . Данное решение позволяет убрать нагрузку на CPU и на ОЗУ. Да задействован диск и на него идёт нагрузка но вспомните у вас есть такие микро сервисы которые зависят от скорости диска