hbs2/docs/hbs2-git-repo/hbs2-git-problem.tex

628 lines
37 KiB
TeX
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

\documentclass[11pt,a4paper]{article}
\usepackage{polyglossia}
\usepackage{xltxtra}
\usepackage[margin=2cm,a4paper]{geometry}% http://ctan.org/pkg/geometry
\usepackage{pdfpages}
\usepackage{graphicx}
\usepackage[ddmmyyyy]{datetime}
\usepackage{booktabs}
\usepackage{enumitem}
\usepackage{amssymb}
\usepackage{amsmath}
\usepackage{bm}
\usepackage[nomessages]{fp}
\usepackage{caption}
\usepackage{url}
\usepackage{indentfirst}
\usepackage[parfill]{parskip}
\usepackage[ colorlinks=true
, linkcolor=black
, anchorcolor=black
, citecolor=black
, filecolor=black
, menucolor=black
, runcolor=black
, urlcolor=blue]{hyperref}
\usepackage{tikz}
\usetikzlibrary{arrows,snakes,shapes,backgrounds,positioning,calc}
\usepackage{marvosym}
\usepackage{pifont}
\usepackage{fontspec}
\usepackage{fontawesome5}
\setmainlanguage{russian}
\defaultfontfeatures{Ligatures=TeX,Mapping=tex-text}
\setmainfont{Liberation Serif}
\newfontfamily\cyrillicfont{Liberation Serif}[Script=Cyrillic]
\setlist{noitemsep}
\setlength{\intextsep}{2cm}
\renewcommand{\dateseparator}{.}
\renewcommand*\contentsname{Содержание}
\graphicspath{ {img/}}
\title{Проблема репозитория HBS2 GIT}
\begin{document}
\maketitle
\section{Определения}
\begin{description}[itemsep=10pt]
\item[Объект] Любой объект, сохраняемый и передаваемый в системе. Файл,
структура данных и т.п.
\item[Блок] Адресуемые (имеющие криптографический хэш) данные. Данные это последовательность байт.
Может быть любого размера, типичный размер: не более 256K. Зашифрованный блок может
быть больше, так как при шифровании в него добавятся данные алгоритма шифрования.
Блок может быть и меньше 256K, допустим блок это маленький объект. Тогда размер блока
будет равен размеру объекта.
\par \textbf{Note:} при шифровании данные можно предварительно сжимать.
\item[Чанк] Нумерованая часть \textit{блока}. Для каждого блока известного размера можно построить
единственное разбиение на чанки заданного размера. Размер чанка выбирается таким
образом, что бы его можно было поместить в UDP пакет безопасного размера.
Безопасного это такого, что можно не заботиться о фрагментации. Считается безопасным
размер UDP пакета 508 байт, на практике MTU всегда 1500 и либо фрагментацией
занимается сетевой стек и делает это хорошо, либо MTU других размеров уже не
встречаются, в любом случае практических проблем с размером чанка = 1400 не
встретилось. В системе размер чанка принят за 1200 --- 1400 байт. Чанк используется
в протоколе передачи блоков: так как по UDP мы не можем запросить блок целиком, мы
запрашиваем его по частям, каждая такая часть и есть чанк. В протоколе существует
возможность запрашивать не каждый чанк одельно, а последовательностями, именуемыми
burst.
\item[BlockSize] Размер блока. Может быть любой, не должен быть слишком
маленьким, но и слишком большим. Типично в районе 256K.
\item[ChunkSize] Максимальный рамер чанка. Должен иметь размер, не превышающий
максимального безопасного размера для UDP (1200 --- 1400
байт) (с учетом заголовка пакета).
\item[Burst] Последовательность чанков в протоколе. Клиент запрашивает не по каждому чанку
отдельно, а сериями по N чанков, начиная с k-го. Каждая такая последовательность
и есть burst.
\item[Merkle~Tree] Любой крупный объект, данные или последовательность объектов
разбивается на блоки при помощи построения его Merkle дерева
(в нашей интерпретации). Дерево имеет два параметра:
количество элементов в каждом узле, и размер (в каком-то виде) каждого
элемента. Построение дерева происходит в два этапа: сначала мы производим
разбиение, учитывая эти два параметра, затем --- обходим получившееся дерево,
сериализуем каждый узел (пишем блоки в \textit{хранилище}), и из полученных
при сериализации хэшей строим уже дерево Меркля (Merkle~Tree).
Дерево может состоять из узлов типа (Merkle~Tree) либо Leaf
- указание на блок (данных).
\item[Annotated~Merkle~Tree] Merkle~Tree с аннотацией в виде либо короткой
строки, либо ссылкой на блок с метаинформацией.
\item[Peer] Пир. Участник сети, авторизованный своим криптографическим ключом
подписи, и поддерживающий протоколы системы.
\item[RTT] Round trip time, время на посылку запроса и получение
ответа (в сети). Можно рассматривать на уровне прикладного протокола HBS2,
можно на уровне транспортного (TCP). Обычно имеется ввиду уровень протокола
HBS2.
\item[Reflog] Механизм реализации изменяемой ссылки. Каждая ссылка
определяется неким публичным ключом. Для каждой ссылки можно опубликовать
транзакцию, короткий (ChunkSize) блок данных, который может либо содержать
произвольные данные, либо \textit{ссылку} на блок или Merkle~Tree. В случае,
если внутри ссылка -- то данные ссылки будут скачаны пиром автоматически
рекурсивно. Все транзакции Reflog упорядочиваются по их хэшам в виде списка,
который сохраняется в виде Merkle~Tree. Таким образом, значение ссылки типа
Reflog это хэш Merkle~Tree упорядоченного по хэшам списка транзакций.
\end{description}
\section{Проблема}
Медленная передача большого количества маленьких объектов (блоков).
Приводит к очень медленному первоначальному скачиванию репозитория.
Обновления затем происходят с приемлемой скоростью.
Связано с тем, что в текущем протоколе для каждого передаваемого адресуемого
блока нам нужно:
\begin{enumerate}
\item Запросить размер (выяснить его наличие)
\item Запросить его чанки
\end{enumerate}
Таким образом, скачивание одного маленького объекта занимает минимум:
\mbox{$ 2 \times RTT + C + T$}, где C - время обработки запроса на обеих
сторонах, T - время передачи самих данных.
На практике это означает, что например при типичном $ RTT = 50ms $ мы доcтигаем
скорости 6 блоков (чанков) в секунду, что даём нам не более 9Kb/s, реально в
районе 6 --- 8. Каковая скорость и наблюдается экспериментально.
Текущее устройство репозитория hbs2-git:
\begin{figure}[h!]
\centering
\begin{tikzpicture}[every node/.append style={font=\scriptsize}]
\node[ rectangle split
, rectangle split parts=3
, draw
, font=\scriptsize
, text width=3cm
, label={above:Reflog}
] (reflog)
{
Transaction~N
\nodepart{two}...
\nodepart{three}Transaction~1
};
\node[ rectangle split
, rectangle split parts=4
, draw
, font=\scriptsize
, text width=3cm
, right=2cm of reflog.north east, anchor=north west
, label={above:Object~list}
] (objlist)
{
\nodepart{one}HEAD~Object
\nodepart{two}Git Object~1
\nodepart{three}...
\nodepart{four}Git object~N
};
\node[draw,rectangle,below=2cm of objlist,xshift=-3cm,minimum
height=1cm,label={below:Git Object}]
(merkle) {Annotated~Merkle~Tree};
\draw[-latex] (reflog.15) -- (objlist.155);
\draw[-latex] (objlist.205) to [bend right,looseness=1] (merkle.north);
\end{tikzpicture}
\end{figure}
\begin{description}[itemsep=10pt]
\item[Reflog] Это Merkle~Tree списка \textit{транзакций}.
\item[Транзакция] Это небольшой объект (размера не больше ChunkSize),
произвольного содержания, в случае HBS2 Git -- со ссылкой на Merkle~Tree
списка объектов.
\item[Object list] Список всех достижимых объектов репозитория (транзитивное
замыкание всех объектов для коммита) + HEAD (список ссылок в смысле git и их значений)
\item[Git Object] Сохраненный в виде Annotated~Merkle~Tree объект git. В
аннотации указан тип объекта (commit, blob, tree)
\end{description}
Данное устройство было выбрано как
\begin{enumerate}
\item Наиболее простое и очевидное
\item Минимизирующее оверхед по данным на уровне git объектов -- т.е уже
имеющиеся git объекты никогда не будут скачиваться повторно
\item Любая транзакция описывает полный, консистентный репозиторий, т.е можно
не иметь полного набора транзакций, но иметь, тем не менее, полный
репозиторий.
\item Каждый когда-либо созданный git object доступен в сети пиров по хэшу его
Merkle~Tree, что казалось довольно удобно (но практически бесполезно)
\end{enumerate}
Несмотря на стремление к минимизации оверхеда по данным, данное устройство его,
тем не менее, создаёт в другом месте:
Каждое Merkle~Tree это минимум один дополнительный блок -- т.е блок на
Merkle~Tree + блок на блок данных.
Как можно видеть, данное устройство располагает к образованию большого
количества маленьких объектов: сами объекты git как правило маленькие,
транзакции = маленькие объекты (ChunkSize).
Большое число: 10---20 тысяч для репозитория размером порядка 600 коммитов.
Что, если взять скорость 5 блоков (чанков) в секунду (из оценки выше), приводит
к нас с показателю 4000 секунд на 20'000 блоков, что примерно соответствует
наблюдаемой картине (в реальности чуть лучше).
Несмотря на то, что последующие скачивания будут выполняться быстро (объекты не
будут выкачиваться повторно, будут скачаны только недостающие объекты, их
немного), первоначальное развертывание репозитория происходит неприемлемо
медленно.
Это и есть проблема.
\section{Возможные решения}
Что мы в принципе хотим добиться:
\begin{enumerate}
\item Быстрое скачивание всего репозитория;
\item Минимизировать оверхед по данным, т.е что бы одни и те же объекты
(разбитые на блоки), по возможности, скачивались один раз.
\end{enumerate}
\subsection{Большой лог}
Основная идея --- не порождать большое число маленьких объектов, как в текущем
дизайне, вместо этого ввести \textit{лог}, куда будут упорядоченно
писаться объекты git, каждый объект только один раз.
Такой лог представляет собой просто большой файл, который может передаваться
обычными механизмами с максимально высокой скоростью ( десятки Mb/s ).
Данное решение имеет недостатки:
\begin{itemize}
\item[-] Более нет соответствия $ git object \leftrightarrow merkle tree $
\item[-] При отсутствии выравнивания, логи разных форков репозиториев
не сойдутся никогда, следовательно, всегда будет перевыкачиваться
различающаяся часть и эта часть будет расти со временем
\item[-] При наличии <<крупного>> (256K) выравнивания оверхед по данным возрастает
на порядки ( ~ 3Gb для числа объектов $ \approx 12000$ )
\item[-] При наличии <<мелкого>> выравнивания оверхед всё равно
существенный ( $ \approx 50\% $ для блока размером 1K ),
но растёт число мелких объектов и соответствующим падением скорости
передачи.
\end{itemize}
\subsubsection{Большой лог всех объектов}
В случае выровненной записи --- получаем описанные выше недостатки.
В случае невыровненной записи --- получаем расхождение логов у разных писателей
(форков), соответствовать будет лишь начальная часть.
Можно ввести специальную дисциплину работы с логом и мерж лога, тогда можно
будет их переупорядочивать у всех, и логи будут периодически сходиться. Высокая
сложность реализации и много неоднозначностей.
Можно писать лог каждый раз при push, тогда для одинаковых git репозиториев
будут одинаковые логи. Очень медленно и время будет расти с ростом репозитория.
\textbf{Плюсы:}
простая реализация, быстрое первоначальное развертывание.
\textbf{Минусы:}
всё остальное.
\subsubsection{Отсортированный по времени/высоте сегментированный лог}
Как можно видеть, ситуация с большим логом объектов, даже отсортированных по
высоте/времени может приводить к перевыкачиванию значительной части лога в
ситуации, когда кто-то отредактировал историю так, что новые объекты появились в
начале лога, что допустимо в git. В случае больших репозиториев это приведёт к
скачиванию большого количества данных и хранению этих данных. Что бы реже
сталкиваться с подобной проблемой, будем строить стейт следующим образом:
\begin{figure}[h!]
\centering
\begin{tikzpicture}[every node/.append style={font=\scriptsize}]
\node[ rectangle split
, rectangle split parts=3
, draw
, font=\scriptsize
, text width=3cm
, label={above:Reflog}
] (reflog)
{
Transaction~N
\nodepart{two}...
\nodepart{three}Transaction~1
};
\node[ draw
, right = 2cm of reflog.north east, anchor=north west
, rectangle split, rectangle split horizontal, rectangle split parts=4
, label={below:PUSH log}
] (pushlog)
{ \nodepart{one}$S_1$
\nodepart{two}...
\nodepart{three}$S_n$
\nodepart{four}HEAD
};
\draw[-latex] (reflog.12) -- (pushlog) node[midway,above] {merkle~tree};
\end{tikzpicture}
\end{figure}
\begin{itemize}
\item[-] Каждая транзация $T_x$ содержит
отсортированные по времени (высоте) и по хэшу объекты репозитория
\item[-] $S(R_x) = \bigcup$ объектов из всех транзакций Reflog
\item[-] Берем объекты, отсутствующие в $S(R_x)$, сортируем по времени/высоте и затем хэшам
и пишем в лог в сжатом виде можно сжать весь лог)
\item[-] Объект HEAD пишем в тот же лог, начало или конец (TBD)
\item[-] Публикуем транзацию $T_y$, которая содержит ссылку на Merkle~Tree лога из предыдущего
пункта
\end{itemize}
\textbf{Следствия:}
\begin{itemize}
\item[-] Стейт $S(R_x)$ каждой ссылки $R_x$ обновляется инкрементально
\item[-] Каждое обновление (транзакция $T_x$) соответствует push в соответствующий
\textit{git~remote}
\item[-] Каждое обновление содержит объекты, отсутствующие в $S(R_x)$, отсортированные
детерменированным образом, зависимости только состояния репозитория git
\item[-] Каждый сегмент лога, кроме первого, как правило, сравнительно небольшой -- содержит
только новые объекты, созданные пользователем (commit, tree, blob)
\item[-] В рамках одной ссылки логи перевыкачиваться не будут, как логи (содержимое
Merkle~Tree транзаций $T_x$ создаются детерменированным образом в завимости только
содержимого репозитория git
\item[-] Частичные логи могут иногда совпадать, так что они не всегда будут дублироваться между
ссылками. Различные кейсы рассмотрим далее отдельно
\end{itemize}
\textbf{Ситуация 1:}
\begin{itemize}
\item[-] Алиса делает \textit{EXPORT} существующего репозитория в свою ссылку $R_a$
\item[-] Боб делает \textit{git~clone} данного репозитория (ссылки $R_a$)
\item[-] Боб создаёт новую ссылку $R_b$
\item[-] Боб делает \textit{EXPORT} репозитория в эту ссылку
и сообщает ссылку $R_b$ Алисе
\item[-] Боб делает изменения и делает \textit{git~push} в ссылку $R_b$
\end{itemize}
В этом случае:
\begin{enumerate}
\item Создаётся лог, содержащий объекты $S(R_a)$ --- все объекты репозитория,
в порядке создания/высоты и далее в порядке хэшей
\item Публикуется транзация $T_x$ со ссылкой на Merkle~Tree лога объектов
\item Боб принимает данную транзацию, объектов $S(R_a)$ --- данных объектов
у него еще нет, следовательно дублирования нет
\item Объекты $S(R_a)$ распаковываются в репозиторий git
\item Создаётся новая ссылка $R_b$. При \textit{EXPORT} создаётся
первоначальная транзакция, которая ровно такая же, как и $T_x$,
так как содержимое лога определяется только объектами репозитория git,
а они все из $S(R_a)$
\item Боб делает свои изменения и делает \textit{git~push}
\item Создается $T_{b1}$, содержащая объекты $S(R_{b1})$, отсутствующие в
$S(R_a)$
\item Алиса получает $T_{b1}$, содержащая объекты $S(R_{b1})$ и импортирует
эти объекты в свой репозиторий git.
\end{enumerate}
На шаге (5) мог не выполниться \textit{EXPORT}, так как его выполнение зависит
от пользователя. В этом случае на шаге (7) будет создана $T_{b1}$ содержащая все
объекты репозитория, включая те, что были созданы Бобом на шаге (6). В этом
случае, какое-то количество объектов в зависимости от гранулярности разбиения
(BlockSize), попадут в те блоки, что уже есть у Алисы в $S(R_a)$. И чем больше
был размер лога $S(R_a)$, тем больше объектов Алиса не будет скачивать повторно,
так как порядок следования определяется только объектами в репозитории, Боб не
переписывал историю и его объекты будут следовать за объектами Алисы.
Если же Боб переписал историю, что, в целом, возможно, то Алиса не будет
повторно выкачивать только часть лога до изменений Боба.
Таким образом видно, что при создании ссылки $R_b$ нужно форсировать операцию
EXPORT.
Как можно её форсировать, ведь Боб может и не создавать ссылку, или создавать
её в любой момент времени.
Например, при операции PUSH нужно каким-то образом получить все объекты,
которые шли раньше изменений Боба и создать транзакцию $T_{b0}$ в которой
будут эти объекты.
Как система должна понять, что этот \textit{git~push} --- не первоначальная
инициация репозитория, а апдейт? Например, так: при \textit{FETCH}
в случае отсутствия транзакций создаётся (и публикуется) первая транзакция.
Тогда в большей части случаев она будет соответствовать инициации репозитория,
и при последующем \textit{PUSH} или \textit{EXPORT} в создаваемую транзакцию
попадут только те объекты, которые создал владелец новой ссылки (Боб в нашем
случае).
\subsubsection{Merkle~Tree+лог с переменным разбиением}
\begin{figure}[h!]
\centering
\begin{tikzpicture}[every node/.append style={font=\scriptsize}]
\node[ rectangle split
, rectangle split parts=3
, draw
, font=\scriptsize
, text width=3cm
, label={above:Reflog}
] (reflog)
{
Transaction~N
\nodepart{two}...
\nodepart{three}Transaction~1
};
\node[ rectangle split
, rectangle split parts=5
, draw
, font=\scriptsize
, text width=3cm
, right=2cm of reflog.north east, anchor=north west
, label={above:Object~list}
] (objlist)
{
\nodepart{one}HEAD~Object
\nodepart{two}SmallObjectLog
\nodepart{three}...
\nodepart{four}BigGitObject
\nodepart{five}...
};
\node[draw,rectangle,below=1cm of objlist.north east,xshift=2cm,minimum
height=1cm,label={[xshift=1.2cm]above:Small~Object~Log}]
(smallmerkle) {Merkle~Tree};
\node[right=0.1cm of smallmerkle,text width=4.2cm,font=\scriptsize]
(smallmerklenote) {С переменным шагом разбиения:\\один объект=один лист};
\node[ rectangle split
, rectangle split parts=4
, draw
, font=\scriptsize
, text width=3cm
, below=1cm of smallmerkle
, label={below:SmallObjectLog}
] (objlog)
{
\nodepart{one}SmallObject~1
\nodepart{two}...
\nodepart{three}SmallObject~N
\nodepart{four}...
};
\node[draw,rectangle,below=2cm of objlist,xshift=-3cm,minimum
height=1cm,label={below:Git Object}]
(merkle) {Annotated~Merkle~Tree};
\draw[-latex] (reflog.15) -- (objlist.155);
\draw[-latex] (objlist.200) to [bend right,looseness=1] (merkle.north);
\draw[-latex] (objlist.12) -| (smallmerkle.north);
\draw[-latex] (smallmerkle.south) -- (objlog.north);
\end{tikzpicture}
\end{figure}
Несмотря на то, что сейчас дерево разбивается на блоки фиксированного размера,
необходимости в этом нет. Это означает, что мы можем генерировать Merkle~Tree
таким образом, что каждому маленькому объекту будет соответствовать один объект
(лист) Merkle~Tree (т.е они не будут разбиваться: мы сначала генерируем лог
<<маленьких>> объектов, затем разбиваем его, принимая во внимание размер каждой
секции (заголовок+объект), так что каждая секция --- ровно один лист (хэш)
Merkle~Tree.
\textbf{Требование:} Каждый <<маленький>> объект должен быть достаточно
маленьким для того, что бы можно было его безопасно читать в память.
<<Большие>> объекты передаются ровно так, как сейчас, то есть каждый отдельно.
<<Большие>> это, допустим, от 1Mb.
\textbf{Плюсы:}
\begin{itemize}
\item[+] Выполняется требование отсутствия оверхеда по данным
\item[+] Нет множества <<мелких>> объектов: лог <<большой>>,
отдельные объекты тоже <<большие>>.
\end{itemize}
\textbf{Минусы:}
\begin{itemize}
\item[-] Передаваться будет дольше, чем <<большой>> лог
\item[-] Средняя сложность реализации
\item[-] <<Переменное разбиение>> $ => $ объекты git маленькие $ => $ много
маленьких объектов $ => $ \textbf{не работает}
\end{itemize}
\subsection{Ускорение скачивания мелких объектов}
Невредно сделать в любом случае, однако полезность данного механизма может
варьироваться, а сложность может быть велика. Мы рассматриваем в первую очередь
ограничения UDP, так как в настоящий момент по TCP ходят в точности такие же
протоколы, как и по UDP. Это может быть изменено, но потребуется поддержка
механизмов, которые будут динамически разрешать или запрещать протоколы в
зависимости от типов транспорта. Это возможно, сложность --- ниже средней.
\subsubsection{Пакетные запросы}
Поскольку мы можем передать по UDP 1200 --- 1400 байт в одной датаграмме,
мы можем запросить $ 1200 / 32 \approx 32 $ объекта за один раз.
Но если речь идёт о UDP, то ответ, очевидно, не может быть получен в одной
датаграмме, значит, нужен какой-то механизм стриминга/сборки ответа из кусков,
и уже имеющийся механизм не подходит, так как ответ создаётся динамически и
отсутствует в хранилище. Таким образом, его надо или создавать в хранилище (и
увеличивать число данных, причём, мусора), либо как-то создавать временный
ответ и скачивать его текущими механизмами.
Это возможно, но неизящно:
\begin{enumerate}
\item Получить запрос
\item Сформировать ответ (записать данные на /tmp допустим)
\item Посчитать хэш ответа ( как блока(!) )
\item Добавить хэш в специальную таблицу <<временные объекты>>
\item Сообщить хэш и размер запрашивающей стороне
\item Запрашивающая сторона получает блок обычным способом
\item При обработке запроса сервер смотрит, не является ли блок временным
объектом и читает его, а не хранилище
\end{enumerate}
Кроме того, ускорение может получиться не столь значительным, так как
мы можем не знать все объекты заранее, и что бы их узнать, надо сначала
скачать другие объекты.
Из наблюдаемых явлений видно, что в какой-то момент времени очередь запросов
становится весьма большой ( $ \approx 10K $ объектов ), следовательно, можно
запрашивать объекты пачками, следовательно, будет иметь место ускорение в $
\approx 32 $ раза, что приведёт к скорости отдачи мелких объектов в районе
160Kb/s. Что не бог весть что, на самом деле. Текущие скорости отдачи
"нормальных" объектов составляют около ширины канала, десятки мегабайт в
секунду.
\subsubsection{Стриминг Merkle~Tree}
Специальная команда, в ответ на которую передаются все блоки Merkle~Tree, не
дожидаясь запросов.
Проблематичная реализация на UDP, в виду возможного реордеринга и потери
пакетов, а имеющийся текущий механизм, устойчивый к реордерингу и потерям,
неприменим.
Вернее, он применим, если мы создадим механизм <<временного>> хранилища --
аналогичного обычному, но существующему лишь некоторое время. Тогда
любые промежуточные объекты могут быть созданы, существовать в
течение сессии или некоторого времени и впоследствии удалены.
Можно и не создавать временное хранилище, а помечать объекты, как временные
и рекурсивно удалять через некоторое время.
Можно создавать временный объект <<транзитивного замыкания>> для дерева,
сообщать его хэш, и давать клиенту скачивать его обычным образом.
Можно видеть, что любые варианты пакетного скачивания объектов ведут к
работе <<сервера>> и расходованию его ресурсов, т.е при незначительном
усилии со строны клиента (один запрос) сервер вынужден сканировать десятки
тысяч объектов и создавать временные объекты потенциально любых размеров,
что открывает путь для намеренных и ненамеренных атак на пира.
Если у нас есть надёжный транспортный протокол (TCP) то можно стримить данные,
не создавая временных структур, тогда затраты локальных ресурсов будут ниже,
но тем ме менее, возможность DoS остаётся: одна команда со стороны <<клиента>>
приводит к несопоставимо большой вычислительной работе <<сервера>> и передачи
большого объема данных (много больше исходного запроса).
\end{document}