Всем привет! Это вторая статья по системе мониторинга приложений от компании bitDive. В данной статье мы расскажем, как мы разрабатывали библиотеку, которая интегрируется в клиентские приложения и передаёт события на сервер мониторинга. Основная цель проекта — обработка миллионов сообщений в секунду с минимальным влиянием на производительность приложений клиентов.
1. Постановка задачи
Вводные данные
Перед нашей командой стояла задача:
Обработка 2 миллионов сообщений в секунду.
Минимизация использования CPU.
Минимизация использования оперативной памяти.
Иными словами, мы искали подход, который обеспечивал бы высокую производительность и низкое потребление ресурсов.
2. Попытка использовать Apache Kafka
Почему Kafka?
Apache Kafka — популярный брокер сообщений, известный своей производительностью и надежностью. Мы предположили, что его архитектура идеально подойдёт для нашей задачи.
Проблемы с Kafka
Однако после начала тестирования мы столкнулись с рядом ограничений:
-
Работа через сокеты.
Kafka работает с использованием TCP-сокетов:private PlaintextTransportLayer(SelectionKey key) throws IOException { super(key); SocketChannel socketChannel = (SocketChannel) key.channel(); socketChannel.configureBlocking(false); }
Это создает сложности при интеграции с прокси-серверами. Единственное решение — использование JVM-параметров, что нас не устраивало.
Высокое потребление памяти.
Для гарантии доставки сообщений Kafka использует промежуточное хранение, что значительно увеличивает использование оперативной памяти.Сложности администрирования.
Чтобы обеспечить отказоустойчивость, клиенту необходимо вручную развертывать дополнительные ноды, что требует дополнительных ресурсов.
Вывод: 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
Буферизация данных. Сообщения сохранялись в памяти перед отправкой, что увеличивало потребление ОЗУ.
Высокая нагрузка на CPU. Асинхронная обработка потоков вызывала значительное увеличение нагрузки на процессор.
Вывод: WebFlux улучшил ситуацию с прокси-серверами, но его использование оказалось неэффективным при больших объемах данных.
4. Оптимальное решение: файловая система
После всех экспериментов мы пришли к простому, но эффективному решению: запись сообщений в файлы с последующей отправкой пакетами.
Как это работает?
-
Запись данных в файлы.
Каждое сообщение записывается в файл сразу после его генерации: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(); } } }
-
Пакетная отправка файлов.
Файлы отправляются на сервер каждые 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()); } } } }
Шифрование и подпись.
Перед отправкой файлы шифруются и подписываются для обеспечения безопасности.
Результаты
CPU: 1% загрузки.
Память: 3% использования.
Пропускная способность: 300 000 сообщений/сек.
5. Сравнение подходов: иллюстрации
Использование CPU
Использование памяти
Пропускная способность
6. Заключение
Использование файловой системы для хранения и передачи сообщений оказалось оптимальным решением для нашей задачи. Этот подход минимизировал использование ресурсов клиента и обеспечил высокую производительность.
Ключевые преимущества:
Низкое потребление CPU и памяти.
Простота интеграции.
Высокая безопасность.
Мы надеемся, что наш опыт будет полезен другим командам, которые работают над подобными задачами. Если у вас есть вопросы или идеи, будем рады их обсудить!
Комментарии (3)
dyadyaSerezha
19.01.2025 08:58Не достигнуто 2 млн. сообщений в секунду.
Лень смотреть, но блокирует ли addBinaryBody файл на запись? В любом случае должна получаться фигня. Ну и файл не затирается потом.
Плохо знаю спринг, но не вижу никакой асинхронности, раз получается response. Да и зачем тут она? Ну будет синхронно посылаться в отдельном потоке.
Один пост, это под 3 млн сообщений у вас. Это около 300 МБ. Через один http post. Зачем? (а если требуемые 2 млн в сек, то это 20 ГБ за один post!)
Может, лучше какой-то web socket или pipe? И не раз в 10 сек, а раз в 0.1 - 0.5 сек.
Wolfdp
Не хватает указания что за сообщения (точнее разброс их длины) и как решение скинуть буферизацию на диск собственно этот диск и нагружает. По идеи для сообщений длиной 100 байт будет нагрузка в ~60МБ/с (по 30 на запись и считать).