Архитектура буферов

И вот тут на сцену выходит TM - Traffic Manager, который реализует функции QoS (и некоторые другие). Он может быть частью чипа коммутации, а может быть отдельной микросхемой - для нас сейчас важно то, что он заправляет буферами.

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

Любой сетевой ASIC или NP обладает некоторым объёмом встроенной (on-chip) памяти (порядка десятков МБ). Так называемые Deep-Buffer свитчи имеют ещё внешнюю (off-chip) память, исчисляемую уже гигабайтами. И той и другой управляет модуль чипа - MMU.

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

И тут практикуют:


Crossbar

Идея в том, чтобы для каждой пары (входной интерфейс - выходной интерфейс) выделить аппаратный буфер.

https://fs.linkmeup.ru/images/articles/buffers/crossbar.png

Это, скорее, умозрительный эксперимент, потому что в плане сложности, стоимости реализации и эффективности это проигрышный вариант.


Shared Buffer

По числу существующих в мире коробок этот вариант, однозначно, на первом месте.

https://fs.linkmeup.ru/images/articles/buffers/shared_buffer.png

Используется Shared Buffer на немодульных устройствах без фабрики коммутации, в которых установлен один чип (обычно, но может быть больше).

Аппаратно - это память (обычно SRAM), встроенная прямо в чип - она так и называется on-chip (OCB). Много туда не засунешь, поэтому объём до 100 МБ.
Зачастую это единственная память, которая в одночиповых устройствах используется для буферизации.
Пусть, однако, эта кажущаяся простота не вводит вас в заблуждение - для того, чтобы в десятки мегабайтов поместить трафик сотни портов 100Гб/с, да ещё и обеспечить отсутствие потерь, за ними должны скрываться годы разработок и нетривиальная архитектура.
А так оно и есть - я чуть ниже неглубоко вас окуну.

Итак, есть соблазн эту память взять и просто равномерно разделить между всеми портами. Такой статический дизайн имеет право на жизнь, но сводит на нет возможность динамически абсорбировать всплески трафика.

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

Dedicated + Shared

Из доступной памяти каждому порту выделяется определённая небольшая часть - это Dedicated Buffer. За каждым портом кусочек памяти законодательно закреплён и не может быть использован другими портами. То есть при любых обстоятельствах у порта будет свой защищённый кусочек. Минимальный размер Dedicated Buffer где-то настраивается, где-то нет. Но лучше без основательного понимания в дефолты не лезть.
Доля каждого порта в абсолютных цифрах очень маленькая - порядка единиц кБ.
Гарантируемый минимум выделяется для хранения как входящих пакетов, так и выходящих.
Остальная часть памяти как раз общая - Shared Buffer - может быть использована любым портом по мере необходимости. Из неё динамически выделяются куски для тех интерфейсов, которые испытывают перегрузку.
Например, если чип пытается на один из интерфейсов передать больше трафика, чем тот способен отправлять в единицу времени, то эти пакеты сначала заполняют выделенный для этого порта буфер, а когда он заканчивается, автоматически начинают складываться в динамически выделенный буфер из общей памяти. Как только все пакеты обработаны, память освобождается.
Под общий буфер может быть отдано 100% той памяти, что осталась после вычитания из неё выделенных для портов кусочков (Dedicated). Но она так же может быть перераспределена - за счёт общего буфера можно увеличить выделенные. Так, если выделить 80% под Shared, то оставшиеся 20% равномерно распределятся по Dedicated.

Наличие Shared Buffer’а решает огромную проблему, позволяя сглаживать всплески трафика, когда перегрузку испытывает один или несколько интерфейсов.

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

Поэтому зачастую, помимо Dedicated и Shared буферов, резервируют ещё Headroom buffers.

Headroom buffers

Это последний способ сохранить пакеты, когда даже общий буфер уже забит. Естественно, он тоже отрезается от общей памяти, поэтому на первый взгляд выглядит не очень логичным откусить от общей памяти кусок, назвать его по-другому и сказать, мол, мы всё оптимизировали.
На самом деле Headroom буферы решают довольно специфическую задачу - помочь lossless приложениям с PFC - Priority-based Flow Control.
PFC - это механизм Ethernet Pause, который умеет притормаживать не всю отправку, а только по конкретным приоритетам Ethernet CoS.
Например, два приложения на отправителе: RoCE и репликация БД. Первое - чувствительная к задержкам и потерям вещь, второе - массивные данные.
Коммутатор, заметив заполнение общего буфера, отправляет Pause для no-drop трафика, например, RoCE, чтобы тот не пришёл к закрытым воротам и не был отброшен.
Задача буфера Headroom здесь в том, чтобы сохранить in-flight пакеты приоритетной очереди (те, что сейчас в кабеле), пока Pause летит к отправителю с просьбой притормозить.
То есть пакеты репликации начнут дропаться, когда заполнится общий буфер, а пакеты RoCE будут складываться в Headroom.

Помимо lossless headroom бывает и headroom для обычного трафика, чтобы помочь сохранить более приоритетный. Но это на домашнее задание.

https://fs.linkmeup.ru/images/articles/buffers/buffer_types.png
При наступлении перегрузки буферы будут задействованы в следующем порядке.
Для входящего best-effort трафика:
  1. Dedicated buffers
  2. Shared buffers

Для входящего lossless трафика:

  1. Dedicated buffers
  2. Shared buffers
  3. Lossless headroom buffers

Для всего исходящего трафика:

  1. Dedicated buffers
  2. Shared buffers

Разумеется, описанное выше лишь частный пример, и от вендора к вендору ситуация может различаться (разительно).

Например бродкомовские чипы (как минимум Trident и Tomahawk) имеют внутреннее разделение памяти по группам портов. Общая память делится на порт-группы по 4-8 портов, которые имеют свой собственный кусочек общего буфера. Порты из одной группы, соответственно буферизируют пакеты только в своём кусочке памяти и не могут занимать другие. Это тоже один из способов снизить влияние перегруженных портов друг на друга. Такой подход иногда называют Segregated Buffer.

Admission Control

Admission Control - входной контроль - механизм, который следит за тем, можно ли пакет записывать в буфер. Он не является специфичным для Shared-буферов, просто в рамках статьи - это лучшее место, чтобы о нём рассказать.

Формально Admission Control делится на Ingress и Egress.
Задача Ingress Admission Control - во-первых, вообще убедиться, что в буфере есть место, а, во-вторых, обеспечить справедливое использование памяти.
Это означает, что у каждого порта и очереди всегда должен быть гарантированный минимальный буфер. А ещё несколько входных портов не оккупируют целиком весь буфер, записывая в него всё новые и новые пакеты.

Задача Egress Admission Control - помочь чипу абсорбировать всплески, не допустив того, чтобы один или несколько выходных портов забили целиком весь буфер, получая всё новые и новые пакеты с кучи входных портов.

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

Как именно понять, сколько буфера занято конкретным портом/очередью и главное, сколько ещё можно ему выдать?
Это может быть статический порог, одинаковый для всех портов, а может быть и динамически меняющийся, регулируемый параметром Alpha.

Alpha

Итак, почти во всех современных чипах память распределяется динамически на основе информации о том, сколько общей памяти вообще свободно и сколько ещё можно выделить для данного порта/очереди.

На самом деле минимальной единицей аккаунтинга является не порт/очередь, а регион (в терминологии Мелланокс). Регион - это кортеж: (входной порт, Priority Group на входном порту, выходной порт, Traffic Class на выходном порту).

Каждому региону назначается динамический порог, сколько памяти он может под себя подмять. При его превышении, очевидно, пакеты начинают дропаться, чтобы не влиять на другие регионы.
Этот порог вычисляется по формуле, множителями которой являются объём свободной на данный момент памяти и параметр alpha, специфичный для региона и настраиваемый:
Threshold [Bytes] = alpha * free_buffer [Bytes]
Его значение варьируется от 1/128 до примерно 8 с шагом х2. Чем больше эта цифра, тем больший объём свободной памяти доступен региону.
Например, если на коммутаторе 32 региона, то:
при alpha=1/64 каждому региону будет доступна 1/64 часть свободной памяти, и даже при максимальной утилизации они все смогут использовать только половину буфера.
при alpha=1/32 вся память равномерно распределится между регионами, ни один из них не сможет влиять на другие, а при полной утилизации 100% памяти будет занято.
при alpha=1/16 каждый регион может претендовать на больший объём памяти. И если все регионы разом начнут потреблять место, то им всем не хватит, потому что памяти потребовалось бы 200%. То есть это своего рода переподписка, позволяющая сглаживать всплески.

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

При получении каждого пакета, механизм Admission Control вычисляет актуальный порог для региона, которому принадлежит пакет. Если порог меньше размера пакета, тот отбрасывается.
Если же больше, то он помещается в буфер и уже не будет отброшен никогда, даже если регион исчерпал все лимиты. Объём свободной памяти уменьшается на размер пакета.
Это происходит для каждого приходящего на чип пакета.

Написанное выше об Admission Control и Alpha может быть справедливо не только для Shared Buffers, но и для других архитектур, например, VoQ.

Дальнейшее чтиво:

Crossbar и Shared Buffer - это архитектуры, которые могут использоваться для устройств фиксированной конфигурации (возможно, даже multi-chip), но не подходят для модульных.
Взглянем же теперь на них.
Дело в том, что они состоят из нескольких линейных карт, каждая из которых несёт как минимум один самостоятельный чип коммутации.
И этот чип, будь то ASIC, NP или даже CPU не может в своей внутренней памяти динамически выделять буферы для тысяч очередей выходных интерфейсов - кишка тонка.
https://fs.linkmeup.ru/images/articles/buffers/modular_chassis.png

Далее поговорим про архитектуры памяти для модульных шасси.


Output Queueing

Наиболее логичным кажется буферизировать пакеты как можно ближе к месту возможного затора - около выходных интерфейсов.
Кому как не выходному чипу знать о здоровье своих подопечных интерфейсов, обслуживать по несколько QoS очередей для каждого и бороться с перегрузками?
https://fs.linkmeup.ru/images/articles/buffers/oq.png
И это правда так.
Но есть одна фундаментальная проблема - в случае перегрузок пакеты будут приходить на Egress PFE, чтобы умирать. Они проделают весь огромный путь от входного интерфейса через фабрику коммутации до выходного буфера через фабрику для того, чтобы узнать, что мест нет и быть печально дропнутыми.
Это бессмысленная и бесполезная утилизация полосы пропускания фабрики.
https://fs.linkmeup.ru/images/articles/buffers/drop.png
И вот уже вырисовывается следующая логичная мысль - выбросить пакет нужно как можно раньше.
Как было бы здорово, если бы мы могли это сделать на входной плате.

Input Queuing

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

https://fs.linkmeup.ru/images/articles/buffers/iq.png

Постойте! Как же входной чип узнает, что выходной интерфейс не занят?

С точки зрения Data Plane никакой обратной связи, от выходного чипа входному, очевидно, нет. Распространение между ними информации, необходимой для лукапа (некстхопы, интерфейсы, заголовки) производится средствами медленного Control Plane - тоже не подойдёт.
Так вот для сигнализации такой информации между линейными платами появляется арбитр. У разных вендоров он может быть реализован по-разному, но суть его в следующем - входной чип регулярно запрашивает у выходного разрешение на отправку нового блока данных. И пока он его не получит - держит пакеты в своём буфере, не отправляя их в фабрику.
Соответственно выходной чип, получив такой запрос, смотрит на утилизацию выходного интерфейса и решает, готов ли он принять пакет. Если да - отправляет разрешение (Grant).
Это на первый взгляд контринтуитивное поведение - каковы же накладные расходы на такой арбитраж, насколько это увеличивает задержки, если на отправку пакета данных нужно дождаться RTT в пределах коробки - пока запрос улетит на выходной чип, пока тот обработает, пока ответ вернётся назад.
Тут некоторые платформы оптимизируют: request/grant’ы присобачиваются к data-пакетам piggyback’ом.
Итога вместо data1 → request2 → data2 → request3 получается data1+request2 → data2+request3.
В любом случае тут для меня начинается область магического искусства, но вендоры эту революцию успели совершить и есть масса платформ, на которых арбитр прекрасно со своей задачей справляется.
Хотя обычно он применяется не для Input Queueing в описанном виде.
Дело в том, что эффективность Input Queueing не очень высокая - очень часто придётся ждать, пока интерфейс освободится. Эх, прям вспоминается старый добрый Ethernet CSMA/CD.

Combined Input and Output Queueing

Гораздо выгоднее в этом плане разрешить буферизацию и на выходе. Тогда арбитр будет проверять не занятость интерфейса, а степень заполненности выходного буфера - вероятность, что в нём есть место, гораздо выше.

https://fs.linkmeup.ru/images/articles/buffers/cioq.png

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

Кроме того, для обеспечения QoS придётся хоть какой-то минимум его функций реализовывать в двух местах, что опять же скажется на цене продукта

Но у CIOQ (как и у IQ) есть фундаментальный недостаток, заставивший в своё время немало поломать голову лучшим умам - Head of Line Blocking.

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

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

Трагическая история, как в реальной жизни, так и на сетевом оборудовании.


Virtual Output Queueing

Как можно исправить эту дорожную ситуацию? Например, сделав три полосы - одна налево, другая прямо, третья направо.

Ровно то же самое сделали разработчики сетевого оборудования.
Они взяли входной буфер побольше и подробили его на множество очередей.
Для каждого выходного интерфейса они создали по 8 очередей на каждом чипе коммутации. То есть перенесли все задачи по обеспечению QoS на входной чип. На выходном же при этом остаётся самая базовая FIFO очередь, в которой никогда не будет заторов, потому что их контроль взял себя входной чип.
https://fs.linkmeup.ru/images/articles/buffers/voq.png
Если взять грубо коробку со 100 интерфейсами, то на каждой плате в буферах нужно будет выделить 800 очередей.
Если в коробке всего 10 линейных карт, то общее число очередей на ней будет 100*8*10 = 8000.
Однако V в VOQ означает виртуальный, не потому, что они как бы выходные, но на самом деле находятся на входных платах, а потому что Output Queue для каждого выходного интерфейса распределён между всеми линейными картами. То есть сумма 10и физических очередей для одного интерфейса на 10 чипах составляет 1 виртуальную.
Собственно из-за распределённого характера этой виртуальной очереди от арбитра и здесь избавиться не получится - разным входным чипам всё же нужно знать, состояние выходной очереди. Поэтому даже несмотря на то, что выходная очередь - это FIFO, выходной чип всё ещё должен давать добро на отправку трафика.

Кстати, что касается трафика, который должен вернуться в интерфейс той же карты, на которую он пришёл изначально, то здесь никаких исключений - он томится в VOQ, пока чип не даст добро переложить его в выходную очередь. С тем только отличием, что пакет не будет отправляться на фабрику. Поэтому перед лицом перегрузок все равны.

Combined Input and Output Queueing w/ Virtual Output Queueing

Как сделать еще сложнее? Совместить обе вышепреведённые техники буферизации. VoQ применяется на фабрике, то есть только для виртуальных портов подключающих Switch Fabric к Egress Line Card NPU, таких портов в сторону NPU относительно немного и виртуальная очередь (VoQ) всё так же находится на Ingress Line Card.

Таким образом у каждого Ingress NPU есть виртуальные очереди для каждого из Egress NPU, таких очередей может быть несколько.

После того как трафик прошёл через фабрику минуя fabric VoQ, пакет в любом случае попадёт в очередь на исходящем интерфейсе (Egress Interface Output Queue) и или задержится там если в данный момент интерфейс перегружен, или сбросится если очередь заполнена полностью, или отправится дальше в исходящий интерфейс.

Данная техника не подвержена HoLB эффекту в той же мере что и классическая CIOQ, однако в случае когда трафик идёт на два разных исходящих интерфейса за одним NPU в случае затора на порту фабрики проблема HoLB всё таки может присутствовать.

CIOQ w/ VoQ fabric масштабируется гораздо лучше чем E2E VoQ из-за меньшего количества интерфейсов, а следовательно и очередей, поэтому подходит для PE, BNG и другого оборудования с большим количеством интерфейсов. Однако, недостаток двойной буферизации никуда не делся, как и недостаток размещения памяти в двух местах и как следствие бОльшего потребления энергии.

https://fs.linkmeup.ru/images/articles/buffers/cioqwvoq.png

На сегодняшний день End-to-End VOQ является наиболее прогрессивной технологией, но говорить о её безоговорочной победе пока не приходится. Картинка с NANOG65 (2015):

https://fs.linkmeup.ru/images/articles/buffers/queuing_arch.png

Дальнейшее чтиво: