Лабораторная работа №3
Разработка распределенного приложения с использованием фреймворка Spring Boot.
Необходимо:
- Разработать параллельный вариант алгоритма с применением сервис-ориентированного подхода и фреймворка Spring Boot, замерить время его работы.
Вариант задания 1: Разделить элементы матрицы на наибольший элемент.
Как запустить лабораторную работу:
mvn spring-boot:run
Запускаем сервисы (делаем на обоих контейнерах).
curl http://192.168.28.129:8080/matrix/process?size=1000
Команда отправляет запрос на генерацию и обработку матрицы размером 1000x1000 (я запускал с виртуальной машины).
Какие технологии использовали:
- Spring Boot — для создания REST API сервиса.
- RestTemplate — для отправки HTTP-запросов между сервисами.
- CompletableFuture — для асинхронного выполнения задач.
- Docker + Proxmox — для контейнеризации и тестирования распределенной системы.
Как работает программа:
Вот описание работы кода с разбором фрагментов:
Общий обзор
Код реализует распределенную обработку матрицы в два контейнера.
Один контейнер делит матрицу на две части:
- Первую часть он обрабатывает локально.
- Вторую часть отправляет другому контейнеру по HTTP-запросу.
После обработки обе половины объединяются в одну итоговую матрицу.
Контейнер 1 (Основной сервис)
Файл: MatrixProcessorController
Этот сервис принимает запрос от клиента на обработку матрицы.
1. Генерация матрицы
System.out.println("Generating matrix...");
Random rand = new Random();
double[][] matrix = new double[size][size];
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
matrix[i][j] = rand.nextDouble() * 100;
}
}
Создается матрица размера size x size, заполняется случайными числами от 0 до 100.
2. Разбиение матрицы
double[][] firstHalf = new double[size / 2][size];
double[][] secondHalf = new double[size - size / 2][size];
System.arraycopy(matrix, 0, firstHalf, 0, size / 2);
System.arraycopy(matrix, size / 2, secondHalf, 0, size - size / 2);
Матрица делится на две половины:
firstHalf— обрабатывается локально.secondHalf— отправляется во второй контейнер.
3. Обработка двух частей параллельно
CompletableFuture<double[][]> processedFirstHalfFuture = CompletableFuture.supplyAsync(() -> processMatrixLocally(firstHalf));
CompletableFuture<double[][]> processedSecondHalfFuture = CompletableFuture.supplyAsync(() -> restTemplate.postForObject(processHalfUrl, secondHalf, double[][].class));
- Локальная обработка запускается в асинхронном потоке.
- Вторая половина отправляется во второй контейнер по
POST-запросу.
Контейнер 2 обрабатывает её и возвращает результат.
4. Ожидание завершения и объединение
CompletableFuture.allOf(processedFirstHalfFuture, processedSecondHalfFuture).join();
double[][] processedFirstHalf = processedFirstHalfFuture.join();
double[][] processedSecondHalf = processedSecondHalfFuture.join();
double[][] resultMatrix = new double[size][size];
System.arraycopy(processedFirstHalf, 0, resultMatrix, 0, size / 2);
System.arraycopy(processedSecondHalf, 0, resultMatrix, size / 2, size - size / 2);
Обе части матрицы собираются обратно.
Контейнер 2 (Вспомогательный сервис)
Файл: MatrixProcessorController (в другом контейнере)
Этот сервис принимает вторую половину матрицы и выполняет ту же обработку, что и первый контейнер, но отдельно.
1. Прием и обработка
@PostMapping("/process")
public double[][] processMatrix(@RequestBody double[][] matrix) {
System.out.println("Get half matrix for processing");
double maxElement = findMax(matrix);
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
matrix[i][j] /= maxElement;
}
}
System.out.println("Matrix successful proccessed");
return matrix;
}
- Получает массив через POST-запрос.
- Находит максимальный элемент.
- Делит все элементы на этот максимум.
- Отправляет обратно в контейнер 1.
(Размер матрицы 1000х1000)
CT129 (На который поступал запрос от клиента)
Program started...
Generating matrix...
Matrix are generated! Size: 1000
Start processing...
Matrix processed success: 779ms
CT128 (Который обрабатывал половину матрицы)
Get half matrix for processing
Half matrix processed success: 2ms
(Размер матрицы 2000х2000)
CT129 (На который поступал запрос от клиента)
Program started...
Generating matrix...
Matrix are generated! Size: 2000
Start processing...
Matrix processed success: 2966ms
CT128 (Который обрабатывал половину матрицы)
Get half matrix for processing
Half matrix processed success: 5ms
Почему так происходит?
- Рост времени обработки при увеличении матрицы:
- Время обработки на сервере CT129 растет быстрее, чем линейно, что может быть связано с увеличением накладных расходов на передачу данных и синхронизацию потоков.
- Время обработки на CT128 остается минимальным, так как он получает уже готовую половину матрицы и выполняет лишь нормализацию значений.
- Влияние сети:
- Передача больших массивов данных через HTTP вызывает дополнительные задержки, что сказывается на общей производительности.
- Особенности Spring Boot:
- Использование
RestTemplateне является самым эффективным способом передачи данных. Возможен переход на WebFlux для обработки потоков данных без блокировки. CompletableFutureпомогает параллелизировать задачи, но накладные расходы на переключение потоков влияют на общую скорость выполнения.
- Использование
Когда стоит использовать сервисный подход?
Использование сервисного подхода (Spring Boot) оправдано, если:
- Требуется масштабируемость (можно распределять обработку на несколько узлов).
- Важна гибкость (легко добавлять новые сервисы без изменения архитектуры).
- Обрабатываются разнородные задачи (например, обработка изображений, финансовые вычисления и пр.).
- Не критичны задержки при передаче данных по сети.
Если же задача требует высокой производительности и минимальных накладных расходов, лучше рассмотреть MPI или ForkJoin.
Вывод
Spring Boot отлично подходит для разработки распределенных систем, но для вычислительно сложных задач, требующих минимальных задержек, стоит рассмотреть альтернативные парадигмы, такие как MPI (Spring Boot и обработка матрицы 1000x1000 составляет около 779 мс, а для 2000x2000 — 2966 мс. В отличие от этого, MPI разделяет матрицу на части и обрабатывает их параллельно на нескольких узлах, например, CT128 и CT129, где обработка половины матрицы происходит за 2 мс для 1000x1000 и 5 мс для 2000x2000, с общим временем завершения процесса для каждого узла в пределах 170-180 мс для 1000x1000 и 347-359 мс для 2000x2000) или ForkJoin (Для матрицы 1000x1000: ForkJoinPool – 28 мс, Spring Boot – 779 мс. Для матрицы 2000x2000: ForkJoinPool – 51 мс, Spring Boot – 2966 мс.). При увеличении размера данных время обработки растет нелинейно из-за сетевых и потоковых накладных расходов. Оптимизация возможна за счет использования асинхронных технологий, таких как WebFlux, или перехода на другой подход к распределению вычислений.