В статье рассматривается планирование ребаланса кластера Greengage DB (open-source fork Greenplum) после shrink, декомиссии и добавления хостов: формальная модель размещения primary- и mirror-сегментов, минимизация числа перемещений, сравнение точных и эвристических алгоритмов и реализация планировщика в ggrebalance

2.1 Пользовательские сценарии

В предыдущей статье была подробно рассмотрена процедура вывода подмножества сегментов из Greengage-кластера. После перераспределения данных и остановки ненужных инстансов распределение оставшихся сегментов по хостам, вероятно, окажется неравномерным (Рис. 1).

Рис. 1. Состояние кластера после удаления части сегментов
Рис. 1. Состояние кластера после удаления части сегментов

Неравномерное распределение сегментов по хостам, при прочих равных, ведет к возможному замедлению обработки запросов: если хост sdw1 содержит 4 primary-сегмента, а sdw3 — лишь 1, то запросы на больших данных стабильно будут работать медленнее на sdw1 из-за кратно большей нагрузки I/O, CPU и кратного потребления памяти — классическая проблема MPP-хранилищ. Распределение зеркал усугубляет проблему аналогично. Какая бы стратегия зеркалирования ни была выбрана изначально, перекос в распределении primary-сегментов после shrink влечет перекос в зеркалах, и при падении одного из хостов promoted-реплики еще больше ухудшают производительность всей системы. В свою очередь, равномерное расположение сегментов делает планирование ресурсов кластера более удобным и детерминированным. Благодаря единообразной структуре каждому хосту требуется одинаковый объем памяти, одинаковый диапазон портов и одинаковая емкость диска. Упрощается работа с системами мониторинга и автоматизации.

Помимо сокращения сегментов через shrink существует пара сценариев, связанных непосредственно с менеджментом хостов системы. Инженерам может потребоваться либо вывести из эксплуатации определенные хосты (декомиссия) ввиду необходимости обслуживания, замены, экономии и перераспределения ресурсов, либо добавить дополнительные, чтобы масштабировать нагрузку (Рис. 2, 3). В таких случаях Greengage не перераспределяет сегменты автоматически: ни с выводимого хоста, ни на добавляемый.

Рис. 2. Декомиссия хоста
Рис. 2. Декомиссия хоста
Рис. 3. Добавление хоста
Рис. 3. Добавление хоста

Во всех описанных сценариях конечная цель идентична: достичь такого распределения сегментов, при котором каждый активный или вновь добавленный хост будет содержать одинаковое количество primary-сегментов и одинаковое количество реплик, сохраняя при этом выбранную топологию зеркалирования — Grouped или Spread.

2.2 Достижение равномерного распределения. Формальная модель кластера

Пусть в кластере m хостов, n primary-сегментов и n зеркал.

Обозначим множество хостов черезH = \{h_1, h_2, \ldots, h_m\}, множества сегментов и зеркал как P = \{p_1, p_2, \ldots, p_n\} и M = \{r_1, r_2, \ldots, r_n\}соответственно.

Для упрощения описания введем соответствующие множества индексов: \hat{S} = \{1, \ldots, n\} \subseteq \mathbb{N} — индексы и сегментов, и зеркал; \hat{H} = \{1, \ldots, m\} \subseteq \mathbb{N} — набор индексов хостов.

В терминологии, связанной с Greengage, существуют два типа стратегии зеркалирования: Grouped и Spread. Стратегия Grouped подразумевает, что для набора сегментов, функционирующих на одном хосте, набор их зеркал также лежит целиком на одном хосте (очевидно, другом). Набор сегментов, расположенных в одном месте, будем называть primary-группой. Стратегия Spread, в свою очередь, означает, что для любой primary-группы размера k соответствующие зеркала расположены на k различных хостах.

Введем обозначение конфигурации кластера. Пусть следующая совокупность отображений называется конфигурацией кластера:

\sigma = (F, R) \in C,F: \hat{S} \to \hat{H}, \quad R: \hat{S} \to \hat{H},C = \hat{H}^n \times \hat{H}^n

Отображения однозначно определяют, какой primary или mirror на каком хосте располагается. Так, например, начальное состояние кластера с Рис. 1 можно описать следующим образом:

F = \left( \begin{array}{cccccccccccc} 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 & 10 & 11 & 12 \\ 1 & 1 & 1 & 1 & 2 & 2 & 2 & 2 & 3 & 3 & 3 & 3 \end{array} \right)R = \left( \begin{array}{cccccccccccc} 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 & 10 & 11 & 12 \\ 2 & 2 & 2 & 2 & 3 & 3 & 3 & 3 & 1 & 1 & 1 & 1 \end{array} \right)

Под заполнением или нагрузкой хоста h будем понимать величину:

\lambda_{\text{total}}^{h}(\sigma) = \lambda_{\text{primary}}^{h}(\sigma) + \lambda_{\text{mirror}}^{h}(\sigma)\lambda_{\text{primary}}^{h}(\sigma) = \left| \{ i \in \hat{S} : F(i) = h \} \right|\lambda_{\text{mirror}}^{h}(\sigma) = \left| \{ i \in \hat{S} : R(i) = h \} \right|

Сбалансированной конфигурацией кластера называется такое состояние \sigma_{\text{bal}}, что \forall h \in \hat{H}\;\; \lambda_{\text{primary}}^{h}(\sigma) = \lambda_{\text{mirror}}^{h}(\sigma) = k,\quad k > 0.

Primary-группой хоста h \in \hat{H} называется множество G_{h}^{p}(\sigma) = \{ i \in \hat{S} : F(i) = h \}. Таким образом, можно ввести понятие допустимой (feasible) конфигурации кластера. Состояние \sigma(F, R)— допустимо, если выполняются следующие условия:

(1)\ \text{Non-colocation:}\ \forall i \in \{1,\ldots,n\}\; F(i) \neq R(i)(2)\ \text{Load balance:}\ \forall h \in \{1,\ldots,m\}\;\; \lambda_{\text{primary}}^{h} = \lambda_{\text{mirror}}^{h} = \frac{n}{m}(3)\ \text{Mirroring strategy:}\text{Grouped:}\ \forall h \in \{1,\ldots,m\}\;\forall i,j \in G_{h}^{p}(\sigma):\; R(i) = R(j)\text{Spread:}\ \forall h \in \{1,\ldots,m\}\;\forall i,j \in G_{h}^{p}(\sigma),\ i \neq j:\; R(i) \neq R(j)

Целевое количество сегментов на хост обозначим параметром q. Множество возможных состояний задается как A(\text{strategy}, q) \subseteq C.

При балансировке кластера происходит переход из начального состояния (в общем случае произвольного) в сбалансированное конечное. Из-за значительного времени переноса отдельного сегмента (подробнее перемещение будет описано в третьей части статьи, часто это связка backup/restore) время выполнения данной операции становится существенным на промышленных объемах данных. Поэтому привести систему в сбалансированное состояние желательно за как можно меньшее число переносов сегментов. Исходя из описанных предпосылок в контексте операции ребаланса возникает оптимизационная задача: перевести кластер из начального состояния в сбалансированное за наименьшее число перемещений.

Если формально: Пусть\sigma_{0}(F_{0}, R_{0}) \text{ - начальное состояние}. Необходимо найти \sigma^{}(F^{}, R^{*}) \in A(\text{strat}, q) такое, что:

d(\sigma^{*}, \sigma_{0}) = \sum_{i=1}^{n} \mathbf{1}[F^{*}(i) \neq F_{0}(i)] + \sum_{i=1}^{n} \mathbf{1}[R^{*}(i) \neq R_{0}(i)] \;\to\; \min

Данная задача относится к области комбинаторной оптимизации и может быть сведена к задаче смешанного целочисленного линейного программирования (СЦЛП, MILP). Доказано, что такие задачи принадлежат классу NP-полных задач. Практическое значение данного понятия заключается в сложности решения с вычислительной точки зрения, так как время вычисления растет экспоненциально с увеличением размерности задачи. Говоря о размерностях, в нашем контексте можно допустить, что небольшие кластеры содержат до 60 сегментов, средние от 60 до 300, крупные промышленные аналитические системы — больше 300 (обычно не превышает 1000). Но даже с такими, казалось бы, разумными ограничениями, пространство возможных решений огромно. Из этого можно сделать вывод, что нецелесообразно опираться только на точные алгоритмы. В связи с этим часто прибегают к использованию приближенных эвристических и метаэвристических методов.

2.3 Анализ методов поиска сбалансированного состояния

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

Точный MILP-решатель (PuLP + CBC)

Первый подход заключается в использовании готового точного решателя для MILP-формулировки задачи. В качестве решателя был выбран Python-пакет PuLP, который решает задачи MILP на базе алгоритма CBC, сочетающего в себе метод ветвей и границ и секущих плоскостей. Это точный алгоритм экспоненциальной сложности, хоть и содержит некоторые эвристики. MILP-формулировка нашей задачи основана на линеаризации в лоб:

X^{n\times m},\quad x_{ij} = \left\{ \begin{array}{ll} 1, & \text{если } F(i)=j,\ i \in \hat{S},\ j \in \hat{H} \\ 0, & \text{иначе} \end{array} \right.Y^{n\times m},\quad y_{ij} = \left\{ \begin{array}{ll} 1, & \text{если } R(i)=j,\ i \in \hat{S},\ j \in \hat{H} \\ 0, & \text{иначе} \end{array} \right.d = \sum_{i=1}^{n}\left(1 - X[i][F_0(i)]\right) + \sum_{i=1}^{n}\left(1 - Y[i][R_0(i)]\right) \rightarrow \min\forall i \in \hat{S}:\quad \sum_{h=1}^{m} X[i][h] = 1\forall i \in \hat{S}:\quad \sum_{h=1}^{m} Y[i][h] = 1X[i][j] + Y[i][j] \le 1,\quad \forall i \in \hat{S},\ j \in \hat{H}\left\{ \begin{array}{ll} \sum_{i=1}^{n} X[i][h] &= q \\ \sum_{i=1}^{n} Y[i][h] &= q \end{array} \right. \quad \forall h \in \hat{H}+ \text{ ограничения на тип зеркалирования}

Точные методы используются в анализе для получения точных значений минимального количества перемещений и демонстрации потери качества более быстрых эвристических методов.

При линеаризации модель обрастает O(m^2) различных ограничений, из-за чего алгоритм начинает неудовлетворительно справляться уже на средних значениях n.

Точный CP-SAT-решатель (Google OR-Tools)

Второй точный подход использует CP-SAT-решатель из инструментария Google OR-Tools и опирается на парадигму Constraint Programming. CP-SAT лучше работает с большим количеством ограничений, которые вдобавок удобнее описывать в этом фреймворке.

Жадный алгоритм (GreedyRebalance)

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

\begin{array}{l} \textbf{Algorithm 1 } \text{GreedyRebalance}(\sigma_0, H_T, M) \\ \hline \textbf{Require:} \\ \quad \text{Initial configuration } \sigma_0 = (P_0, R_0); \\ \quad \text{Target host set } H_T \subseteq H, |H_T| = m; \\ \quad \text{Mirroring strategy } M \in \{\text{Grouped}, \text{Spread}\} \\ \textbf{Ensure:} \\ \quad \text{Feasible } \sigma = (P, R) \in F(M, \delta) \text{ with small } d(\sigma, \sigma_0) \\ \hline 1:\ P \leftarrow \text{BalancePrimaries}(P_0, H_T) \\ 2:\ R \leftarrow \text{AssignMirrors}(P, R_0, H_T, M) \\ 3:\ \textbf{return } (P, R) \end{array}

Алгоритм выполняется в два шага. В строке 1 псевдокода вызывается BalancePrimaries, которая перераспределяет primary-сегменты по хостам так, чтобы на каждом хосте оказалось ровно n/m primary. Делает это жадно: итерирует по хостам с избытком сегментов и переносит сегменты на хосты с дефицитом, пока нагрузка не выровняется. Никакого перебора вариантов — один линейный проход (при этом учитывается декомиссия или добавление хостов).

После балансировки primary-сегментов вызывается AssignMirrors, которая расставляет зеркала с учетом уже зафиксированного размещения primary P_0 и исходного размещения зеркал R_0: алгоритм старается оставить зеркало на прежнем хосте, если это не нарушает стратегию зеркалирования, тем самым минимизируя число перемещений зеркал. Результат -- конфигурация (P, R), гарантированно допустимая по всем ограничениям из раздела 2.2. Общая сложность алгоритма O(n\log n + nm)при m \ll n.

Многократный запуск жадного алгоритма со случайной инициализацией

Этот вариант многократно запускает жадный алгоритм из случайных начальных конфигураций. После k запусков выбирается наилучшая конечная конфигурация. Гипотетически такой подход может дать небольшое улучшение за счет случайности, но системного поиска в нем нет.

Алгоритм имитации отжига (Simulated Annealing)

Метод имитации отжига (Simulated Annealing) относится к метаэвристическим подходам к решению оптимизационных задач. Идея алгоритма имитации отжига заимствована из исследований поведения атомов в металле в процессе его отжига (Алгоритм 2).

\begin{array}{l} \textbf{Algorithm 2 SimulatedAnnealing}(\sigma_0,\mathcal{M}) \\[4pt] \hline \textbf{Require: } \text{Initial configuration } \sigma_0 \text{; strategy } \mathcal{M} \text{; parameters } T_0, \rho, T_{\min}, I_{\max} \\ \textbf{Ensure: } \text{Near-optimal } \sigma^* \in \mathcal{F}(\mathcal{M}, \delta) \\ \hline 1:\ \ \sigma \leftarrow \text{GreedyRebalance}(\sigma_0, H_T, \mathcal{M}); \quad \sigma^* \leftarrow \sigma \\ 2:\ \ T \leftarrow T_0 \\ 3:\ \ \textbf{for } k = 1, \ldots, I_{\max} \textbf{ do} \\ 4:\ \ \quad \sigma' \leftarrow \text{Move}(\sigma, \mathcal{M}) \\ 5:\ \ \quad \textbf{if } \sigma' = \emptyset \textbf{ then continue} \\ 6:\ \ \quad \textbf{end if} \\ 7:\ \ \quad \Delta \leftarrow d(\sigma', \sigma_0) - d(\sigma, \sigma_0) \\ 8:\ \ \quad \textbf{if } \Delta < 0 \textbf{ or } \mathrm{Uniform}(0,1) < \exp(-\Delta / (T \cdot n)) \textbf{ then} \\ 9:\ \ \quad\quad \sigma \leftarrow \sigma' \\ 10:\ \ \quad\quad \textbf{if } d(\sigma, \sigma_0) < d(\sigma^*, \sigma_0) \textbf{ then} \\ 11:\ \ \quad\quad\quad \sigma^* \leftarrow \sigma \\ 12:\ \ \quad\quad \textbf{end if} \\ 13:\ \ \quad \textbf{end if} \\ 14:\ \ \quad T \leftarrow \max(T_{\min}, \rho\, T) \\ 15:\ \ \textbf{end for} \\ 16:\ \ \textbf{return } \sigma^* \end{array}

Наивное (жадное) решение является теплым стартом для алгоритма, оно дает разумную начальную точку, что существенно сокращает время выхода в хорошую область пространства поиска.

T_0 — начальная температура, определяющая готовность принимать ухудшения в начале работы, чтобы "выпрыгнуть" из локальных экстремумов (является гиперпараметром). На каждой итерации метод Move случайно перемещает primary или mirror, валидирует (не всегда) условия допустимого решения и возвращает какую-то новую конфигурацию кластера.

d(\sigma', \sigma_0) — целевая функция, число сегментов, хост которых в конфигурации \sigma' отличается от исходного в \sigma_0. В строке 8 указано ключевое правило принятия. Улучшение принимается всегда. Ухудшение принимается с вероятностью e^{-\Delta/Tn} —чем больше ухудшение \Delta и чем ниже температура T, тем меньше вероятность принятия.

Делитель n нормирует на размерность задачи, чтобы параметры T_0 и T_{min} не зависели от числа сегментов.

max(T_{min}, \rho T) -- геометрическое охлаждение с коэффициентом \rho < 1. На каждой итерации температура снижается, алгоритм становится все более "жадным" и перестает принимать ухудшения. Алгоритм продолжает искать улучшения до последней итерации I_{max}.

Поиск с запретами (Tabu Search)

Поиск с запретами (Tabu Search) — это метаэвристический метод математической оптимизации, основанный на локальном поиске. В нашей реализации он частично заимствует идеи алгоритма имитации отжига.

\begin{array}{l} \textbf{Algorithm 3 TabuSearch}(\sigma_0,\mathcal{M}) \\[4pt] \hline \textbf{Require: } \text{Initial configuration } \sigma_0 \text{; strategy } \mathcal{M} \text{; parameters } \tau \text{ (tenure), } K \text{ (neighborhood size), } I_{\max} \\ \textbf{Ensure: } \text{Near-optimal } \sigma^* \in \mathcal{F}(\mathcal{M}, \delta) \\ \hline 1:\ \ \sigma \leftarrow \text{GreedyRebalance}(\sigma_0, H_T, \mathcal{M}); \quad \sigma^* \leftarrow \sigma \\ 2:\ \ \mathcal{T} \leftarrow \emptyset \qquad \triangleright \text{ tabu list: move key} \mapsto \text{expiry iteration} \\ 3:\ \ \textbf{for } k = 1, \ldots, I_{\max} \textbf{ do} \\ 4:\ \ \quad \mathcal{N} \leftarrow \text{GenerateCandidates}(\sigma, \mathcal{M}, K) \qquad \triangleright |\mathcal{N}| \le K, \text{ see Algorithm 4} \\ 5:\ \ \quad \sigma' \leftarrow \arg\min_{\hat{\sigma} \in \mathcal{N}} \Big\{ d(\hat{\sigma}, \sigma_0) \;\Big|\; \underbrace{\mathcal{T}(\hat{\sigma}) \le k}_{\text{not tabu}} \;\lor\; \underbrace{d(\hat{\sigma}, \sigma_0) < d(\sigma^*, \sigma_0)}_{\text{aspiration}} \Big\} \\ 6:\ \ \quad \textbf{if } \sigma' = \emptyset \textbf{ then continue} \\ 7:\ \ \quad \textbf{end if} \\ 8:\ \ \quad \mathcal{T}(\text{key}(\sigma')) \leftarrow k + \tau \\ 9:\ \ \quad \sigma \leftarrow \sigma' \\ 10:\ \ \quad \textbf{if } d(\sigma, \sigma_0) < d(\sigma^*, \sigma_0) \textbf{ then} \\ 11:\ \ \quad\quad \sigma^* \leftarrow \sigma \\ 12:\ \ \quad \textbf{end if} \\ 13:\ \ \textbf{end for} \\ 14:\ \ \textbf{return } \sigma^* \end{array}\begin{array}{l} \textbf{Algorithm 4 GenerateCandidates}(\sigma,\mathcal{M},K) \\[4pt] \hline \textbf{Require: } \text{Current solution } \sigma = (P, R) \text{; strategy } \mathcal{M} \text{; budget } K \text{; seen-key set} \\ \quad \mathcal{V} \leftarrow \emptyset \\ \textbf{Ensure: } \text{Candidate set } \mathcal{N} \\ \hline 1:\ \ \mathcal{N} \leftarrow \emptyset \\ 2:\ \ \text{// Half the budget: primary moves} \\ 3:\ \ \textbf{while } |\mathcal{N}| < \lfloor K/2 \rfloor \textbf{ do} \\ 4:\ \ \quad \text{Pick a segment } s \text{ uniformly at random from } P \\ 5:\ \ \quad \text{Pick a target host } h' \text{ uniformly at random from } H_T \setminus \{P(s)\} \\ 6:\ \ \quad \text{Let } \sigma' = (P', R) \text{ where } P' \text{ reassigns } s \text{ to } h' \text{ (if } \mathcal{M} = \text{Grouped, move the} \\ \quad\quad \text{entire primary group of } s \text{ together)} \\ 7:\ \ \quad \text{key} \leftarrow (s, P(s), h') \\ 8:\ \ \quad \textbf{if } \sigma' \in \mathcal{F}(\mathcal{M}, \delta) \textbf{ and } \text{key} \notin \mathcal{V} \textbf{ then} \\ 9:\ \ \quad\quad \mathcal{N} \leftarrow \mathcal{N} \cup \{(\text{key}, \sigma')\}; \quad \mathcal{V} \leftarrow \mathcal{V} \cup \{\text{key}\} \\ 10:\ \ \quad \textbf{end if} \\ 11:\ \ \textbf{end while} \\ 12:\ \ \text{// Remaining budget: mirror moves} \\ 13:\ \ \textbf{while } |\mathcal{N}| < K \textbf{ do} \\ 14:\ \ \quad \text{Pick a segment } s \text{ uniformly at random from } R \\ 15:\ \ \quad \textbf{if } \mathcal{M} = \text{Grouped} \textbf{ then} \\ 16:\ \ \quad\quad \text{Pick a target host } h' \text{ from } H_T \text{ such that } h' \ne R(s) \text{ and all mirrors} \\ \quad\quad\quad \text{of the group of } s \text{ can be moved to the same host } h' \text{ without violating co-} \\ \quad\quad\quad \text{location constraints} \\ 17:\ \ \quad \textbf{else } (\mathcal{M} = \text{Spread}) \\ 18:\ \ \quad\quad \text{Pick a target host } h' \text{ from } H_T \text{ such that } h' \ne R(s), h' \ne P(s), \text{ and} \\ \quad\quad\quad h' \text{ holds no other mirror of the same content} \\ 19:\ \ \quad \textbf{end if} \\ 20:\ \ \quad \text{Let } \sigma' = (P, R') \text{ where } R' \text{ reassigns } s \text{ to } h' \\ 21:\ \ \quad \text{key} \leftarrow (s, R(s), h') \\ 22:\ \ \quad \textbf{if } \sigma' \in \mathcal{F}(\mathcal{M}, \delta) \textbf{ and } \text{key} \notin \mathcal{V} \textbf{ then} \\ 23:\ \ \quad\quad \mathcal{N} \leftarrow \mathcal{N} \cup \{(\text{key}, \sigma')\}; \quad \mathcal{V} \leftarrow \mathcal{V} \cup \{\text{key}\} \\ 24:\ \ \quad \textbf{end if} \\ 25:\ \ \textbf{end while} \\ 26:\ \ \textbf{return } \mathcal{N} \end{array}

В строке 1, аналогично SA, теплый старт из жадного решения. В строке 2 инициализируется список запретных конфигураций \mathcal{T}— словарь вида {ключ конфигурации → номер итерации до которой ее появление запрещено}. Запись означает: "этот вариант состояния кластера запрещен до итерации k + \tau".

На каждой итерации цикла в строке 4 вызывается GenerateCandidates(\sigma,\mathcal{M},K) (Алгоритм 4), формирующий множество из K кандидатов-соседей конфигурации \sigma. В строке 5 среди кандидатов выбирается лучший по целевой функции, удовлетворяющий хотя бы одному из двух условий: ход не является запрещенным, либо ход дает наилучшее решение. Это не дает алгоритму упустить наилучшее решение из-за логики запретов. Если ни одного допустимого кандидата нет, итерация пропускается (строка 6). В строке 8 выбранный ход вносится в запретный список на \tau]итераций вперед — это исключает немедленный откат к предыдущей конфигурации и вынуждает алгоритм исследовать новые области пространства поиска. В строках 9-12 обновляется текущее и лучшее найденное решение.

GenerateCandidates формирует окрестность следующим образом: первая половина множества (\lfloor K/2 \rfloor кандидатов, строки 3-11) — перемещения primary-сегментов. Случайно выбирается сегмент s, который переносится на выбранный целевой хост h' из множества хостов H_T \setminus \{P(s)\}. Для стратегии Grouped перемещается вся primary-группа целиком.

Вторая половина множества (строки 13-24) — ходы с зеркалами, логика которых зависит от стратегии. Для Grouped выбирается хост, на который можно переместить всю группу зеркал без нарушения критерия совместного размещения primary/mirror. Для Spread целевой хост не должен совпадать ни с primary-хостом сегмента, ни с хостами других зеркал того же сегмента. Множество уже просмотренных ключей \mathcal{V} исключает дублирование шагов в рамках одной итерации. Каждый кандидат проходит проверку допустимости \sigma' \in \mathcal{F}(\mathcal{M}, \delta) — недопустимые конфигурации отбрасываются немедленно.

Поиск в большой окрестности (Large Neighborhood Search, LNS)

В качестве базового LNS-метода (Large Neighborhood Search) мы реализуем алгоритм поиска в большой окрестности (PlainLNS, Алгоритм 5), теплый старт которого выполняется из жадного решения. На каждой итерации оператор разрушения удаляет случайную долю \rho \sim \mathrm{Uniform}(\rho_{\min}, \rho_{\max}) сегментов из текущего решения, после чего оператор восстановления строит полную допустимую конфигурацию на освобожденных позициях. Оба оператора выбираются равномерно случайно из фиксированного набора (Таблица 1). Критерий принятия решения является жадным: улучшающее решение принимается всегда, а равное по стоимости — с вероятностью \frac{1}{2}.

\begin{array}{l} \textbf{Algorithm 5 PlainLNS}(\sigma_0,\mathcal{M}) \\[4pt] \hline \textbf{Require: } \text{Initial configuration } \sigma_0 \text{; strategy } \mathcal{M} \text{; parameters } \rho_{\min}, \rho_{\max}, I_{\max} \\ \textbf{Ensure: } \text{Near-optimal } \sigma^* \in \mathcal{F}(\mathcal{M}, \delta) \\ \hline 1:\ \ \sigma \leftarrow \text{GreedyRebalance}(\sigma_0, H_T, \mathcal{M}); \quad \sigma^* \leftarrow \sigma \\ 2:\ \ \textbf{for } k = 1, \ldots, I_{\max} \textbf{ do} \\ 3:\ \ \quad \rho \leftarrow \text{Uniform}(\rho_{\min}, \rho_{\max}) \\ 4:\ \ \quad d \leftarrow \text{Uniform}(\mathcal{D}_{\mathcal{M}}); \quad r \leftarrow \text{Uniform}(\mathcal{R}) \qquad \triangleright \text{ draw operators uniformly} \\ 5:\ \ \quad \mathcal{B} \leftarrow \text{Destroy}(\sigma, d, \rho) \qquad \triangleright |\mathcal{B}| = \lfloor \rho n \rfloor \text{ freed segments} \\ 6:\ \ \quad \sigma' \leftarrow \text{Repair}(\sigma, r, \mathcal{B}) \\ 7:\ \ \quad \textbf{if } \sigma' \text{ infeasible } \textbf{then continue} \\ 8:\ \ \quad \textbf{end if} \\ 9:\ \ \quad \textbf{if } d(\sigma', \sigma_0) < d(\sigma, \sigma_0) \text{ or } \Big[d(\sigma', \sigma_0) = d(\sigma, \sigma_0) \text{ and } \text{Uniform}(0,1) < \frac{1}{2}\Big] \\ \quad\quad \textbf{then} \\ 10:\ \ \quad\quad \sigma \leftarrow \sigma' \\ 11:\ \ \quad\quad \textbf{if } d(\sigma, \sigma_0) < d(\sigma^*, \sigma_0) \textbf{ then} \\ 12:\ \ \quad\quad\quad \sigma^* \leftarrow \sigma \\ 13:\ \ \quad\quad \textbf{end if} \\ 14:\ \ \quad \textbf{end if} \\ 15:\ \ \textbf{end for} \\ 16:\ \ \textbf{return } \sigma^* \end{array}

Таблица 1. Операторы разрушения и восстановления

Тип

Оператор

Применимая стратегия

Destroy \mathcal{D}

Разрушение групп

Grouped

Плохие сегменты

Обе

Удаление по Шоу

Обе

Случайные сегменты

Обе

Repair \mathcal{R}

Жадное восстановление

Обе

Восстановление наиболее ограниченных

Обе

Восстановление на основе сожаления

Обе

Операторы разрушения работают следующим образом:

  • Оператор удаления "Случайные сегменты" удаляет \lfloor \rho \times n \rfloor равномерно выбранных сегментов, получается конфигурация B.

  • "Плохие сегменты" нацелены на сегменты, чье текущее размещение максимально отклоняется от начального (расстояние дискретно: 0 — ни primary, ни зеркало не были перемещены относительно исходной конфигурации; 1 — один из двух хостов (либо primary, либо зеркало) отличается от исходного; 2 — и primary, и зеркало отличаются от исходного).

  • "Удаление по Шоу" удаляет кластер взаимно схожих сегментов (разделяющих основной хост или хост-зеркало).

  • "Разрушение групп" (только для стратегии Grouped) удаляет все сегменты, принадлежащие случайно выбранному подмножеству первичных групп.

  • Все операторы восстановления переназначают основные хосты и хосты-зеркала для каждого сегмента из B, восстанавливая выполнимость ограничений (1)-(3) из раздела 2.2:

  • "Жадное восстановление" назначает хосты в порядке убывания дефицита нагрузки.

  • "Восстановление наиболее ограниченных" обрабатывает сегменты с наименьшим числом допустимых вариантов размещения в первую очередь.

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

Адаптивный поиск в большой окрестности (Adaptive LNS, ALNS)

ALNS (Adaptive Large Neighborhood Search) развивает идею LNS за счет адаптивного выбора операторов (Алгоритм 6). Алгоритм использует решение жадного метода в качестве начальной точки и итеративно улучшает его, чередуя операторы разрушения и восстановления. Ключевой особенностью метода является адаптивный выбор операторов: каждому оператору сопоставлен вес, обновляемый каждые \Omega итераций на основе накопленных оценок по правилу экспоненциального сглаживания с коэффициентом реакции \rho. Оператор, систематически приносящий улучшения, получает больший вес и выбирается с большей вероятностью. Оператор, систематически приносящий улучшения, получает больший вес и выбирается с большей вероятностью.

Критерий принятия решения основан на методе имитации отжига: ухудшающий переход с разницей стоимости \Delta принимается с вероятностью e^{-\Delta / T}, где температура T убывает логарифмически от T_0 = 1 до T_{\min}. Размер разрушения \rho_k адаптируется к фазе поиска: в начале итераций используются большие окрестности для исследования пространства решений, в конце — узкие для интенсификации. При стагнации текущее решение перезапускается из наилучшего найденного. По завершении основного цикла к лучшему решению применяется финальный локальный поиск (обмен зеркальными хостами между группами, только для стратегии Grouped).

\begin{array}{l} \textbf{Algorithm 6 ALNS}(\sigma_0,\mathcal{M}) \\[4pt] \hline \textbf{Require: } \text{Initial configuration } \sigma_0 \text{; strategy } \mathcal{M} \text{; parameters } T_{\min}, I_{\max}, \rho, \Omega \quad \text{score values } s^{+} > s^{= } > s^{-} \\ \textbf{Ensure: } \text{Near-optimal } \sigma^* \in \mathcal{F}(\mathcal{M}, \delta) \\ \hline 1:\ \ \sigma \leftarrow \text{GreedyRebalance}(\sigma_0, H_T, \mathcal{M}); \quad \sigma^* \leftarrow \sigma \\ 2:\ \ \text{Initialize operator weights } \mathbf{w}^d \in \mathbf{R}^{|\mathcal{D}|}, \mathbf{w}^r \in \mathbf{R}^{|\mathcal{R}|}; \quad T \leftarrow 1.0 \\ 3:\ \ \textbf{for } k = 1, \ldots, I_{\max} \textbf{ do} \\ 4:\ \ \quad \rho_k \leftarrow \text{AdaptiveDestroySize}(k, k_{\text{last improv}}) \triangleright \text{larger near stagnation, smaller late in search} \\ 5:\ \ \quad d \sim \text{Categorical}(\mathbf{w}^d); \quad r \sim \text{Categorical}(\mathbf{w}^r) \\ 6:\ \ \quad \mathcal{B} \leftarrow \text{Destroy}(\sigma, d, \rho_k) \\ 7:\ \ \quad \sigma' \leftarrow \text{Repair}(\sigma, r, \mathcal{B}) \\ 8:\ \ \quad \textbf{if } \sigma' \text{ infeasible } \textbf{then } \mathbf{w}^d_d \mathrel{+}= s^-; \textbf{ continue} \\ 9:\ \ \quad \textbf{end if} \\ 10:\ \ \quad \Delta \leftarrow d(\sigma', \sigma_0) - d(\sigma, \sigma_0) \\ 11:\ \ \quad \textbf{if } \Delta < 0 \text{ or } \text{Uniform}(0,1) < \exp(-\Delta/T) \textbf{ then} \\ 12:\ \ \quad\quad \sigma \leftarrow \sigma' \\ 13:\ \ \quad\quad \textbf{if } d(\sigma, \sigma_0) < d(\sigma^*, \sigma_0) \textbf{ then} \\ 14:\ \ \quad\quad\quad \sigma^* \leftarrow \sigma; \quad \text{score}(d,r) \mathrel{+}= s^+ \\ 15:\ \ \quad\quad \textbf{else} \\ 16:\ \ \quad\quad\quad \text{score}(d,r) \mathrel{+}= s^= \\ 17:\ \ \quad\quad \textbf{end if} \\ 18:\ \ \quad \textbf{else} \\ 19:\ \ \quad\quad \text{score}(d,r) \mathrel{+}= s^- \\ 20:\ \ \quad \textbf{end if} \\ 21:\ \ \quad \textbf{if } k \bmod \Omega = 0 \textbf{ then} \\ 22:\ \ \quad\quad \textbf{for all operator } o \textbf{ do} \\ 23:\ \ \quad\quad\quad w_o \leftarrow \max(w_{\min}, (1-\rho)\, w_o + \rho \cdot \overline{\text{score}}_o) \\ 24:\ \ \quad\quad \textbf{end for} \\ 25:\ \ \quad \textbf{end if} \\ 26:\ \ \quad T \leftarrow \exp\Big( (1 - k/I_{\max}) \cdot \ln 1 + (k/I_{\max}) \cdot \ln T_{\min} \Big) \\ 27:\ \ \quad \textbf{if } \text{Stagnating}(\sigma, \sigma^*) \textbf{ then} \\ 28:\ \ \quad\quad \sigma \leftarrow \sigma^* \\ 29:\ \ \quad \textbf{end if} \\ 30:\ \ \textbf{end for} \\ 31:\ \ \sigma^* \leftarrow \text{LocalSearch}(\sigma^*, \mathcal{M}) \\ 32:\ \ \textbf{return } \sigma^* \end{array}

2.4 Сравнение выбранных подходов

В итоге рассматриваются восемь алгоритмов: два точных решателя (MILP на PuLP+CBC и CP-SAT) — используются на малых размерностях для оценки улучшений остальных; жадный алгоритм; случайный перезапуск жадного; имитация отжига; поиск с запретами; обычный LNS без адаптации; ALNS. Все алгоритмы запускаются на одной и той же выборке экземпляров конфигураций разных размеров с одними и теми же seed-значениями.

Задачи делятся по трем размерам — малые, средние и крупные — и по двум стратегиям зеркалирования (Grouped и Spread). Дополнительно выделены три операционных сценария: балансировка без изменения числа хостов, декомиссия хостов и добавление новых. На малых экземплярах, где точные методы успевают найти оптимум, используется зазор до оптимума — на сколько процентов решение хуже точного. На средних и крупных, где точные методы слишком медленны и превышают установленный тайм-аут (взято 300 с), используется коэффициент улучшения относительно жадного решения — на сколько процентов алгоритм смог сократить число перемещений сверх того, что дает жадный. Также фиксируется время достижения лучшего найденного решения.

В таблице ниже приведено описание наборов, на которых проводились сравнительные эксперименты.

Таблица 2. Количество конфигураций в экспериментах. Всего 170 различных начальных состояний

Набор

Кол-во конфигураций

Кол-во seeds

Всего конфигураций

Кол-во primary

small_grouped

4

5

20

12—​48

small_spread

4

5

20

12—​48

medium_grouped

4

5

20

100—​320

medium_spread

4

5

20

80—​240

large_grouped

4

5

20

256—​1024

large_spread

4

5

20

224—​930

decommission_grouped

3

5

15

120—​500

decommission_spread

2

5

10

120—​200

expansion_grouped

3

5

15

120—​540

expansion_spread

2

5

10

120—​200

Рассмотрим результаты сравнения на экземплярах малой размерности. Здесь участвовали точные методы как контроль оптимальности. Использовались наборы small_grouped и small_spread — всего 40 экземпляров и 360 запусков для сбора статистики. Метрика улучшения (improvement rate) рассчитывается как:

IR = \frac{d_{alg} - d_{opt}}{d_{opt}} \cdot 100\%

Здесь d_{alg} — количество перемещений оцениваемого алгоритма d_{opt} — число перемещений точного алгоритма (CBC или CP-SAT).

Таблица 3. Зазор до оптимума, % (по сравнению с точными методами) на малых экземплярах

Алгоритм

IR, %

Mdn IR, %

IRmax, %

ALNS

0.00

0.00

0.00

PlainLNS

0.00

0.00

0.00

SA

0.00

0.00

0.00

RandomRestart

5.43

0.00

38.10

Tabu

7.00

0.00

50.00

Greedy

10.00

0.00

50.00

Из приведенной таблицы можно увидеть: три алгоритма — ALNS, PlainLNS, SA — достигают нулевого зазора на всех малых конфигурациях. То есть они находят оптимальное решение каждый раз. Жадный алгоритм в среднем отстает от оптимума на 10%, RandomRestart — на 5.4%, Tabu — на 7%. Вывод: на маленьких задачах уже простой LNS и SA достаточны для оптимальности, никакой сложный адаптивный механизм не нужен.

Далее рассмотрим эксперименты на среднем и крупном n. Ввиду критического замедления точных алгоритмов (пространство поиска увеличивается экспоненциально) будем сравнивать улучшение методов относительно жадного подхода. Наборы: medium_grouped/spreadlarge_grouped/spread. Всего 80 экземпляров и 560 запусков.

Таблица 4. Зазор до стоимости наивного алгоритма. Средние и крупные конфигурации

Алгоритм

Стратегия

IR, %

Стандартное отклонение

ALNS

spread

21.47

5.24

PlainLNS

spread

21.83

4.78

SA

spread

6.00

9.13

RandomRestart

spread

0.22

0.83

Tabu

spread

5.43

9.19

ALNS

grouped

0.25

1.26

PlainLNS

grouped

0.22

1.23

SA

grouped

0.49

1.54

RandomRestart

grouped

0.22

1.23

Tabu

grouped

0.80

1.68

Здесь проявляется ключевой результат всего исследования: стратегии Grouped и Spread демонстрируют принципиально различное поведение. Для стратегии Grouped жадный алгоритм уже дает практически оптимальное решение — ни один из методов не улучшает его более чем на 0.8%, и медианное улучшение равно нулю у всех. Это объясняется структурой задачи: в Grouped-варианте зеркала определяются одним перестановочным отображением, и после фазы балансировки первичных сегментов пространство маневра практически исчерпано. Для стратегии Spread картина противоположная. Сам жадный алгоритм оставляет зазор порядка 22—​23% от оптимума, и PlainLNS (21.83%), ALNS (21.47%) почти полностью закрывают этот зазор — оба статистически значимо лучше жадного. SA дает лишь 6%, RandomRestart — менее 0.3%, то есть практически ничего.

Таким образом, жадный алгоритм дает наилучшее значение для Grouped-стратегии на средних и крупных конфигурациях. Как начальное решение его результат более чем подходит для планирования перемещений утилитой ggrebalance. Но все-таки выиграть пару-тройку перемещений не будет лишним, тем более для стратегии Spread есть куда расти. На данный момент видно, что наилучшие результаты дают PlainLNS и ALNS. Осталось разобраться, так ли нужна адаптивность LNS в нашей задаче. Здесь поможет критерий Уилкоксона. Из 40 парных наблюдений на наборах medium_groupedmedium_spread ALNS выигрывает лишь 1 раз, PlainLNS — 3 раза, в 36 случаях результат идентичен. Критерий Уилкоксона дает p = 0.705 — никакой статистической значимости. Это означает, что весь механизм адаптивного выбора операторов, обновление весов, экспоненциальное сглаживание и т.д. не приносит никакой практической пользы на данном классе задач. Вероятная причина может заключаться в том, что пространство поиска достаточно однородно, и все операторы одинаково полезны, поэтому адаптации нечему учиться.

В завершение приведем график зависимости времени поиска решения всех методов от размерности задачи.

Рис. 4. Зависимость времени выполнения от размерности задачи
Рис. 4. Зависимость времени выполнения от размерности задачи

В итоге сравнительного анализа неадаптивный метод поиска PlainLNS кандидат №1 для использования в задаче балансировки Greengage-кластера.

2.5 Реализация в рамках утилиты ggrebalance

Модуль планирования (planner.py) реализует логику построения плана перебалансировки сегментов кластера Greengage. Краткая диаграмма компонентов, описывающая планирование операций, изображена на Рис. 5. Центральным классом является Planner, который принимает текущее состояние кластера из gp_segment_configuration, набор параметров запуска и строит объект Plan — список логических перемещений (LogicalMove), которые необходимо выполнить для достижения сбалансированного состояния. Построение плана происходит в методе plan(), который последовательно выполняет несколько этапов. Сначала проверяется статус всех сегментов — наличие сегментов в состоянии down немедленно останавливает планирование, поскольку перебалансировка на частично недоступном кластере небезопасна. Затем, при необходимости, планируется уменьшение числа сегментов (shrink). После этого, если не указан флаг --skip-rebalance, планируются собственно перемещения сегментов.

Рис. 5. Компоненты планировщика ggrebalance
Рис. 5. Компоненты планировщика ggrebalance

Прежде чем решать задачу балансировки, планировщик формирует полное множество хостов, участвующих в операции. Каждый хост представлен объектом со статусом: active (существующий хост кластера), new (добавляемый хост) или decommissioned (выводимый из кластера хост).

Входные параметры командной строки — --target-hosts--add-hosts--remove-hosts — транслируются в эти статусы. Если указан --target-hosts, все существующие хосты, не вошедшие в этот список, помечаются как decommissioned, а хосты из списка, отсутствующие в кластере — как new. Параметр --add-hosts добавляет новые хосты со статусом new, а --remove-hosts помечает существующие как decommissioned.

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

Далее представление кластера на основе gp_segment_configuration преобразуется в абстрактное числовое представление, аналогичное \sigma = (F, R), и передается решателю PlainLNS, содержащему GreedyBalancer, результат которого улучшается через LNS. Конечная сбалансированная конфигурация в итоге становится целевой для построения плана. Результат работы решателя сравнивается с исходным размещением. Для каждого сегмента, у которого изменился хост, создается объект LogicalMove.

2.6 Оценка ресурсов. Порты. Объем данных

аждое перемещение требует назначения порта для сегмента на целевом хосте. Этим занимается отдельный класс, который при инициализации собирает все занятые порты из gp_segment_configuration и отдельно отслеживает порты первичных и зеркальных сегментов. При выделении порта для существующего хоста планировщик сначала пытается использовать текущий порт сегмента — если он свободен на целевом хосте, он и будет назначен. Такой подход минимизирует изменения конфигурации. Если порт уже занят, ищется следующий свободный порт, начиная с базового порта для данной роли (primary/mirror) на этом хосте. Для новых хостов первый сегмент каждой роли устанавливает базовый порт — именно на основе него строится нумерация последующих сегментов. Такой подход обеспечивает консистентность схемы портов и отсутствие конфликтов на уровне планируемой конфигурации. Также в процессе выделения порта выполняется опрос целевого хоста для проверки доступности данного порта. Следовательно, для каждого перемещения выполняется сетевой запрос, что увеличивает накладные расходы при планировании. Это взаимодействие можно исключить, передав опцию --skip-resource-estimation.

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

При планировании также немаловажно знать, какой объем данных с учетом всех табличных пространств нужно переместить, хватит ли на целевых хостах места для дополнительных сегментов (даже если с этого хоста будет перенесен другой сегмент, его данные останутся на месте). Поэтому такую первоначальную проверку планировщик утилиты ggrebalance не может не произвести. Оценка выполняется в два шага. На первом шаге отправляются параллельные SSH-запросы (с du) на исходные хосты для получения размеров директорий данных всех перемещаемых сегментов. Дополнительно выполняется SQL-запрос к кластеру для получения расположения табличных пространств (tablespaces) перемещаемых сегментов — и для них также запрашиваются размеры. На втором шаге запрашивается информация о свободном пространстве на целевых хостах. Все перемещения агрегируются по файловым системам целевых хостов. Это важно, потому что несколько директорий данных могут находиться на одной файловой системе — и суммарное требование к пространству нужно сравнивать именно с объемом свободного места на этой файловой системе, а не по директориям независимо.

К оцененному размеру применяется коэффициент безопасности DISK_SPACE_SAFETY_MARGIN (10%), увеличивающий требуемое пространство. Это компенсирует погрешности оценки и накладные расходы файловой системы. Если на каком-либо хосте не хватает места, генерируется подробное сообщение об ошибке с разбивкой по файловым системам, директориям и затронутым сегментам. После успешной валидации объем запланированных к перемещению данных выводится в лог. Оценку занимаемого места также можно пропустить опцией --skip-resource-estimation.

Таким образом, взяв за исходную конфигурацию пример с Рис. 1 (после shrink):

select hostname,role, array_agg(content) from gp_segment_configuration group by hostname, role order by hostname;
hostname | role |  array_agg
----------+------+-------------
cdw      | p    | {-1}
sdw1     | p    | {0,1,2}
sdw1     | m    | {8,3}
sdw2     | m    | {0,1,2}
sdw2     | p    | {5,6,4,7,3}
sdw3     | m    | {5,6,4,7}
sdw3     | p    | {8}
(7 rows)

ggrebalance при планировании выведет следующий план:

20260412:20:09:41:014374 ggrebalance:cdw:gpadmin-[INFO]:-Final plan:

------------------------BALANCE MOVES--------------------------------
Total moves planned: 4

  [1] Move Segment(content=3, dbid=5, role=p) [834.81 MB]
      From: sdw1:7005:/home/gpadmin/.data/primary/gpseg3
      To:   sdw3:7005:/home/gpadmin/.data/primary/gpseg3

  [2] Move Segment(content=3, dbid=14, role=m) [834.26 MB]
      From: sdw2:7053:/home/gpadmin/.data/mirror/gpseg3
      To:   sdw1:7053:/home/gpadmin/.data/mirror/gpseg3

  [3] Move Segment(content=7, dbid=9, role=p) [834.26 MB]
      From: sdw2:7009:/home/gpadmin/.data/primary/gpseg7
      To:   sdw3:7009:/home/gpadmin/.data/primary/gpseg7

  [4] Move Segment(content=7, dbid=18, role=m) [833.71 MB]
      From: sdw3:7057:/home/gpadmin/.data/mirror/gpseg7
      To:   sdw1:7057:/home/gpadmin/.data/mirror/gpseg7

В нем отображаются шаги, выполнение которых приведет кластер в сбалансированное состояние, с указанием целевых директорий расположения данных и оценкой перемещаемого объема.

Выводы по второй части

В работе была рассмотрена задача перебалансировки сегментов Greengage-кластера после shrink-операций, а также в сценариях декомиссии и добавления хостов. В отличие от простого вывода сегментов из эксплуатации, такая операция требует не только физического переноса данных, но и построения корректной целевой конфигурации, в которой сохраняется допустимость размещения primary и mirror сегментов, соблюдается выбранная стратегия зеркалирования и достигается равномерная нагрузка на все активные хосты.

Для формализации задачи была введена модель конфигурации кластера через отображения размещения primary- и mirror-сегментов по множеству хостов. На этой основе задача ребаланса была сведена к комбинаторной оптимизации с целевой функцией минимизации числа перемещений. Анализ точных методов показал, что они пригодны как эталон для задач малой размерности, однако при росте числа сегментов становятся вычислительно непрактичными. Это делает оправданным переход к эвристическим и метаэвристическим подходам.

Практическая ценность ggrebalance состоит в том, что утилита закрывает важный пробел в управлении Greengage-кластером: после масштабирования кластера (шринк) она позволяет привести систему к предсказуемому, эксплуатационно удобному состоянию. Равномерное распределение primary- и mirror-сегментов уменьшает риск деградации производительности, упрощает планирование ресурсов, делает конфигурацию кластера более однородной и упрощает сопровождение со стороны администраторов, мониторинга и средств автоматизации.

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

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