Как Sentinel ограничивает ток

Архитектура внешний интерфейс

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

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

И теперь у нас есть новый вариант, Sentinel, предоставленный Али.

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

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

1. Общий процесс

Давайте сначала разберемся в общем процессе:

(Цитата с официального сайта Sentinel)

Картинка выше взята с официального сайта.

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

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

2. Чтение исходного кода

1. Запись чтения исходного кода и общий процесс

Чтобы прочитать исходный код, вы должны сначала найти запись исходного кода. Мы часто используем @SentinelResource для обозначения метода, а метод, отмеченный @SentinelResource, можно рассматривать как ресурс Sentinel. Поэтому мы используем @SentinelResource как запись, находим его аспект и смотрим на проделанную работу после перехвата аспекта, после чего можно уточнить принцип работы Sentinel. Посмотрите непосредственно на код аспекта (SentinelResourceAspect) аннотации @SentinelResource.

Хорошо видно, как ведет себя Sentinel. После входа в аспект SentinelResource будет выполнен метод SphU.entry, в котором будет выполняться логическая обработка ограничения тока и фьюзинга на перехваченном методе.

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

Таким образом, обработка ограничения тока предохранителя Sentinel в основном выполняется методом SphU.entry, и его основная логика обработки показана в исходном коде ниже.

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

  • Получить контекст Sentinel (Контекст);
  • Получить цепочку ответственности, соответствующую ресурсу;
  • Создание учетных данных для вызова ресурсов (Entry);
  • Выполните каждый узел в цепочке ответственности.

Затем, вокруг этих аспектов, сделайте систематическое изложение механизма обслуживания Sentinel.

2. Получить контекст Sentinel (Контекст)

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

Давайте посмотрим на характеристики контекста:

  • Контекст удерживается потоком и привязывается к текущему потоку с помощью ThreadLocal.

  • Контент, содержащийся в контексте

Вот еще три важных концепции Sentinel:Контекст, узел, запись. Эти три класса являются основными классами Sentinel, которые предоставляют такую ​​информацию, как пути вызовов ресурсов и статистику вызовов ресурсов.

Context

Контекст — это контекст Sentinel, удерживаемый текущим потоком.

При входе в логику Sentinel сначала получит контекст текущего потока, если нет, то создаст новый. При выполнении задачи контекст текущего потока очищается. Контекст представляет собой контекст цепочки вызовов, который проходит через все элементы Entry в цепочке вызовов.

Контекст поддерживает такую ​​информацию, как узел входа (entryNode), текущий узел (curNode) текущей ссылки вызова и источник вызова (origin). Имя контекста — это имя записи ссылки вызова.

Node

Node — это статистическая оболочка вокруг ресурса с тегом @SentinelResource.

Узел входа текущего вызова ресурса потока записывается в Context.

Мы можем проследить вызов ресурсов через дочерний список узла входа. Каждый узел соответствует ресурсу, отмеченному @SentinelResource, и его статистике, такой как passQps, blockQps, rt и другие данные.

Entry

Entry — это учетные данные, используемые в Sentinel, чтобы указать, пройден ли текущий предел.Если он может нормально вернуться, это означает, что вы можете получить доступ к задней службе, защищенной Sentinel, в противном случае Sentinel выдаст исключение BlockException.

Кроме того, он сохраняет некоторую базовую информацию о выполнении метода entry(), включая контекст ресурса, узел и соответствующую цепочку ответственности.После завершения последующего вызова ресурса необходимо получить полученную запись для выполнения некоторых последующих действий. операций, включая выход из цепочки ответственности, соответствующей Входу, обновление некоторой статистической информации узла, очистку контекстной информации текущего потока и т. д.

3. Получить цепочку ответственности, соответствующую ресурсу, отмеченному @SentinelResource.

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

Давайте сначала посмотрим на состав цепочки ответственности по умолчанию:

Узлы обработки в цепочке ответственности по умолчанию включают NodeSelectorSlot, ClusterBuilderSlot, StatisticSlot, FlowSlot, DegradeSlot и т. д. Цепочка вызовов (ProcessorSlotChain) и все содержащиеся в ней слоты реализуют интерфейс ProcessorSlot, используют режим цепочки ответственности для выполнения логики обработки каждого узла и вызывают следующий узел.

У каждого узла есть своя роль, и позже мы увидим, что делают эти узлы.

Кроме того, цепочка ответственности, соответствующая одному и тому же ресурсу (метод, отмеченный @SentinelResource), непротиворечива. То есть каждому ресурсу соответствует отдельная цепочка ответственности, логику получения цепочки ответственности ресурса вы можете увидеть в исходном коде: сначала достать из кеша, если нет, то создать новый.

4. Генерация учетных данных для звонка. Вход

Результирующая запись является CtEntry. Его параметры построения включают оболочку ресурса (ResourceWrapper), цепочку ответственности, соответствующую ресурсу, и контекст текущего потока.

Видно, что вновь созданный CtEntry записывает цепочку ответственности и Контекст текущего ресурса, и в то же время обновляет Контекст, устанавливая текущую Запись Контекста на себя. Как видите, CtEntry — это двусвязный список, который строит ссылку вызова ресурсов Sentinel.

5. Выполнение цепочки ответственности

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

5.1 NodeSelectorSlot — получить узел, соответствующий текущему ресурсу, и построить дерево вызовов узла.

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

Знакомый стиль кода. Мы знаем, что ресурс соответствует цепочке ответственности. В каждой цепочке вызовов есть NodeSelectorSlot. Карта кэша узла в NodeSelectSlot является нестатической переменной, поэтому карта используется только для текущего ресурса.Кэш NodeSelectSlot и кэш узла, соответствующие разным ресурсам, различаются.Взаимосвязь между ресурсом и картой кэша узла можно увидеть на рисунок ниже.

Итак, роль NodeSelectorSlot такова:

  • Когда выполняется цепочка вызовов, соответствующая ресурсу, получается узел, соответствующий текущему контексту, и узел представляет состояние вызова ресурса.
  • Установите полученный узел в качестве текущего узла и добавьте его позади предыдущего узла, чтобы сформировать древовидный путь вызова. (через текущую запись в контексте)
  • Инициировать выполнение следующего слота.

Здесь возникает очень интересный вопрос, то есть когда мы получаем Node, соответствующий ресурсу в NodeSelectorSlot цепочки ответственности, почему мы используем имя Context вместо имени SentinelResource?

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

НапримерРеализация функции entinel может быть достигнута не только с помощью метода аннотации @SentinelResource, но и путем введения соответствующих зависимостей (sentinel-dubbo-adapter) и использования механизма Filter Dubbo для непосредственной защиты интерфейса DUBBO. Давайте сравним разницу между @SentinelResource и способом создания контекста в Dubbo:

@SentinelResource

Имя созданного контекста: sentinel_default_context. Контекст, соответствующий всем ресурсам, является этим значением.

Метод фильтра Даббо

Имя сгенерированного контекста — это имя интерфейса Dubbo или имя метода.

Если есть другие вызовы ресурсов SentinelResource, вложенные в метод Dubbo Filter, то для этих вызовов ресурсов появятся разные контексты.

Так вот такая ситуация, приходят разные dubbo интерфейсы, и эти dubbo интерфейсы вызывают один и тот же метод помеченный @SentinelResource, то соответствующий этому методу Context SentinelReource во время исполнения разный.

Другой вопрос, раз ресурсы разбиты на разные ноды по Контексту, что нам делать, если мы хотим видеть статистику общего количества ресурсов? Это касается ClusterNode. Дополнительные сведения см. в разделе ClusterBuilderSlot.

5.2 ClusterBuilderSlot — объединение узлов с разными контекстами одного и того же ресурса.

Этот узел отвечает за агрегирование узлов, соответствующих разным контекстам одного и того же ресурса, для последующего текущего ограничивающего суждения.

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

5.3 StatisticSlot -- статистика вызовов ресурсов

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

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

На приведенном выше рисунке четко описан процесс статистики данных StatisticSlot. Вы можете обратить внимание на ситуацию отсутствия исключений и блокирующих исключений, в основном количество потоков обновления, количество проходящих запросов и количество блокирующих запросов. Будь то DefaultNode или ClusterNode, оба наследуются от StatisticNode. Таким образом, обновление данных Node должно поступать в StatisticNode.

Ссылаясь на блок-схему статистики данных Sentinel, общий процесс обновления статистических данных узла описывается следующим образом:

Давайте начнем с метода StatisticNode.addPassRequest() и возьмем passQps в качестве примера, чтобы изучить, как StatisticNode обновляет количество запросов в секунду для переданных запросов.

Как видно из исходного кода, переменные-счетчики rollCounterInSecond и rollCounterInMinute являются метриками, а временные измерения этих двух переменных — секунды и минуты соответственно. RollingCounterInSecond и RollingCounterInMinute используют класс реализации Metric ArrayMetric.

Отслеживание от ArrayMetric:

Статистика сохраняется в данные ArrayMetric, то есть LeapArray.

LeapArray — это массив временных окон. Базовая информация включает в себя: длину временного окна (мс, windowLengthInMs), количество выборок (то есть количество временных окон, sampleCount), временной интервал (мс, intervalInMs) и массив временного окна (массив). Длина временного окна, количество выборок и временной интервал связаны следующим образом:

windowLengthInMs = intervalInMs / sampleCount

Значение intervalInMs, используемое функцией RollCounterInSecond в коде, равно 1000 (мс), что равно 1 с, а sampleCount=2. Итак, длительность окна — windowLengthInMs = 500 мс. Интервал InMs, используемый при прокруткеCounterInMinute, равен 60 * 1000 (мс), что составляет 60 с. sampleCount=60, поэтому windowLengthInMs = 1000 мс, что равно 1 с.

Массив окна времени (массив) имеет тип AtomicReferenceArray, который показывает, что это ссылка на массив для атомарных операций. Тип элемента массива — WindowWrap. windowWrap — это оболочка для временного окна, включая время начала окна (windowStart), длину окна (windowLengthInMs) и счетчик этого окна (значение типа MetricBucket). Фактический подсчет окна выполняется MetricBucket, а информация о подсчете сохраняется в счетчиках (типа (LongAdder)) в MetricBucket. Вы можете взглянуть на блок-схему компонента подсчета на следующем рисунке:

Вернемся к методу StatisticNode.addPassRequest. Возьмем в качестве примера RollCounterInSecond.addPass(count), чтобы изучить, как Sentinel подсчитывает скользящее окно.

5.3.1 Получить текущее временное окно

(1) Возьмите индекс массива, соответствующий текущей метке времени

long timeId = time / windowLength

int idx = (int)(timeId % array.length());

time — текущее время, windowLength — длина временного окна, а длина временного окна rollCounterInSecond — 500 мс. Массив представляет собой количество временных окон в единицу времени, а количество временных окон в единицу времени (1 с) для rollCounterInSecond равно 2. timeId — это текущее время, деленное на временное окно. Каждый раз, когда продолжительность времени увеличивается на одну длину окна, timeId будет увеличиваться на 1, а временное окно сдвинется вперед на единицу.

(2) Рассчитать время начала окна

Время начала окна = текущее время (мс) - текущее время (мс) % длины окна времени (мс)

Все полученные времена начала окна являются целыми кратными времени окна.

(3) Получить временное окно

Сначала получаем временное окно из массива LeapArray по индексу массива.

  • Если полученное временное окно пусто, создайте новое временное окно (CAS).
  • Если полученное временное окно не пусто, и время начала временного окна равно рассчитанному нами времени начала, это означает, что текущее время находится в этом временном окне, и возвращается непосредственно временное окно.
  • Если полученное временное окно не пусто, и время начала временного окна меньше вычисленного нами времени начала, это означает, что временное окно истекло (сценарии, в которых временное окно было получено в течение длительного времени с момента последнего время), и временное окно необходимо обновить (операция блокировки), установить время начала временного окна равным расчетному времени начала и сбросить счетчик во временном окне на 0.
  • Если полученное временное окно не пусто, и время начала временного окна больше, чем рассчитанное нами время начала, создайте новое временное окно. В эту ветку это вообще не входит, потому что это означает, что текущее время отстало от временного окна, а полученное временное окно - это будущее время, что бессмысленно.

5.3.2 Накопление счетчика временного окна

Счетчик временного окна — это массив LongAdder, который используется для хранения таких данных, как количество переданных запросов, количество ненормальных запросов и количество заблокированных запросов. Как показано ниже:

Среди них счетчик проходов, счетчик блокировок и счетчик исключений обновляются при выполнении метода входа StatisticSlot. Счетчик успешных операций и время отклика обновляются при выполнении метода выхода StatisticSlot. По сути, это обновление соответствующих счетчиков до и после выполнения перехваченного метода. Конечно, addPass должен накапливаться на первом элементе массива count.

Тип элемента массива count — LongAdder. LongAdder был добавлен в JUC JDK8. Это потокобезопасный «счетчик» с более высокой производительностью, чем инструменты серии Atomic*.

5.4 FlowSlot — суждение об ограничении тока

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

Перейдем непосредственно к основной логике текущей операции ограничения — FlowRuleChecker:

К основным процессам относятся:

  • Получить текущее правило ограничения, соответствующее ресурсу
  • Проверьте, регулируется ли ток в соответствии с правилом ограничения тока.

Если он регулируется, создается исключение FlowException, ограничивающее поток. FlowException наследуется от BlockException.

Так как же FlowSlot проверяет, ограничен ли ток?

По умолчанию узел, используемый для регулирования, является узлом кластера текущего узла. Основным методом ограничения тока анализа является ограничение тока QPS. Давайте посмотрим на код клавиши ограничения тока (DefaultController):

  • Получить текущее количество запросов в секунду узла;
  • Определить, превышен ли порог после получения нового счетчика
  • Если порог превышен, он вернет false, указывая на то, что ток ограничен, и позже будет выброшено исключение FlowException. В противном случае он возвращает значение true и не регулируется.

Видно, что суждение о текущем пределе очень простое, нужно только проверить количество запросов в секунду. Это связано со статистикой данных, сделанной StatisticSlot.

5.5 Краткое изложение цепочки ответственности

Благодаря приведенному выше объяснению, а затем посмотрите на следующую картинку, все ли ясно?

(Цитата с официального сайта Sentinel)

NodeSelectorSlot используется для получения узла, соответствующего ресурсу, построения дерева вызовов узла и группировки ссылок вызовов SentinelSource в виде дерева узлов. ClusterBuilderSlot создает соответствующий ClusterNode для текущего Узла, агрегирует Узлы разных Контекстов, соответствующих одному и тому же ресурсу, и последующим текущим ограничивающим основанием является этот ClusterNode.

ClusterNode наследуется от StatisticNode и записывает некоторые статистические данные об обработке соответствующих ресурсов. StatisticSlot используется для обновления соответствующих счетчиков вызовов ресурсов для последующего текущего ограничения. FlowSlot решает, следует ли ограничивать ток, в соответствии со счетчиком вызовов соответствующего узла ресурса. На этом этапе логика выполнения цепочки ответственности Sentinel завершена.

6. Последние штрихи в Sentienl

Независимо от того, будет ли выполнение выполнено успешно или неудачно, или будет заблокировано, метод Entry.exit() будет выполнен.Давайте взглянем на этот метод.

  • Определить, является ли запись, из которой нужно выйти, текущей записью текущего контекста;
  • Если выходимая запись не является текущей записью текущего контекста, эта запись не завершается, но происходит выход из текущей записи контекста и всех ее родительских записей, и генерируется исключение;
  • Если запись, которую нужно закрыть, является текущей записью текущего контекста (это нормальная ситуация), сначала закройте все слоты в цепочке ответственности, соответствующей текущей записи. На этом шаге StatisticSlot обновит счетчик успехов узла и счетчик RT;
  • Установите текущую запись контекста в качестве родительской записи завершенной записи;
  • Если родительская запись завершенной записи пуста, а контекст является контекстом по умолчанию, контекст по умолчанию автоматически закрывается (Clear ThreadLocal).
  • Очистить контекстную ссылку завершенной записи

7. Резюме

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

  • Три основных компонента: Контекст, Запись и Узел, являются основными компонентами Sentinel.Все виды информации и вызовов ресурсов относятся к этим трем категориям;
  • Используйте модель цепочки ответственности для завершения информационной статистики Sentinel, слияния, ограничения тока и других операций;
  • В цепочке ответственности NodeSelectSlot отвечает за выбор Узла, соответствующего текущему ресурсу, и построение дерева вызовов узла;
  • ClusterBuilderSlot в цепочке ответственности отвечает за создание ClusterNode, соответствующего текущему узлу, который используется для объединения узлов одного и того же ресурса, соответствующих разным контекстам;
  • StatisticSlot в цепочке ответственности используется для подсчета обращений к текущим ресурсам и обновления различных статистических данных Node и его парного ClusterNode;
  • FlowSlot в цепочке ответственности ограничивает ток согласно статистике ClusterNode (по умолчанию), соответствующей текущему Node;
  • Статистика вызовов ресурсов (например, PassQps) подсчитывается с использованием скользящих временных окон;
  • После того, как вся работа будет выполнена, выполните процесс выхода, добавьте статистику и очистите контекст.

3. Ссылки

GitHub.com/alibaba/s ru…

Автор: Сунь И