критиковать
Несколько дней назад я разговаривал с читателем и говорил о Dubbo.
Он сказал, что уже сталкивался с ямой Даббо.
Я спросил, что случилось?
Затем он описал мне причину и следствие и резюмировал их в восьми словах: по истечении тайм-аута он автоматически повторит попытку.
У меня есть две точки зрения на это.
- Читатели не знакомы с использованием фреймворка и не знают, что в Dubbo все еще есть автоматический повтор.
- Речь идет о функции автоматического повтора Dubbo.Я думаю, что отправная точка хороша, но дизайн не хорош.
Первый мало говорил, и плохо учился, поэтому продолжил учебу.
В основном говорят о втором.
Одна вещь, которую я хочу сказать, как пользователь, который использовал Dubbo в течение многих лет, согласно моему опыту, я думаю, что идея Dubbo, обеспечивающая функцию повтора, очень хороша, но это неправильно, потому что она не должна автоматически повторять попытки.
В большинстве случаев я вручную устанавливаю retries=0.
В качестве основы, конечно, от пользователей может потребоваться использовать ее после полного понимания соответствующих характеристик, включая функцию автоматического повтора, которая должна знать об этом.
Однако решение о повторной попытке должно приниматься пользователем, и фреймворк или класс инструментов не должны активно помогать пользователю в этом.
Подождите, это предложение слишком категорично. Я изменю это.
Пользователь должен решить, следует ли повторять попытку, после анализа сцены. Платформа или класс инструментов не должны вмешиваться на бизнес-уровне, чтобы помочь пользователю сделать это.
В этой статье для простого сравнения используются два знакомых примера.
Первый пример — стандартная стратегия отказоустойчивости кластера Dubbo, Failover Cluster, то есть автоматическое переключение при сбое.
Второй пример — это HttpClient от apache.
Один из них — это фреймворк, а другой — класс инструментов, оба из которых поддерживают повторную попытку и оба включают повторную попытку по умолчанию.
Но по моему опыту, автоматический повтор Dubbo задействован в бизнесе и воспринимается пользователями. Автоматический повтор HttpClient осуществляется на сетевом уровне и незаметен для пользователя.
Однако следует еще раз подчеркнуть, что:
На официальном сайте Dubbo четко указано, что по умолчанию он будет автоматически повторять попытку, что обычно используется для операций чтения.
Если вы используете его неправильно и вызываете ошибки в данных, вы не можете обвинять в этом чиновника, вы можете только сказать, что у этой конструкции есть преимущества и недостатки.
Дуббо повторите попытку несколько раз
Говорят, что Dubbo автоматически повторяет попытку, так сколько же раз она повторяется?
Сначала рассмотрим пример и продемонстрируем его.
Сначала посмотрите на определение интерфейса:
Видно, что в реализации интерфейса я сплю 5 с, цель — имитировать тайм-аут интерфейса.
XML-файл сервера настроен следующим образом, а тайм-аут установлен на 1000 мс:
XML-файл клиента настроен следующим образом, а тайм-аут также установлен на 1000 мс:
Затем мы имитируем удаленный вызов один раз в модульном тесте:
Это оригинальный проект Dubbo Demo. Так как у нас таймаут 1000 мс, то есть 1 с, а обработка интерфейса занимает 5 с, вызов точно истечет по таймауту.
Затем стратегия отказоустойчивости кластера Dubbo по умолчанию (отказоустойчивый кластер) повторит попытку сколько раз, запустит тестовый пример, и вы сразу увидите:
Вы посмотрите на время этого теста, он работал 3 с 226 мс, вы сначала вспомните это время, я расскажу об этом позже.
Сначала сосредоточимся на количестве повторных попыток.
Немного непонятно, поэтому вынесу кейлог отдельно на всеобщее обозрение:
Из лога видно, что клиент повторил попытку 3 раза. Время начала последней попытки: 2020-12-11 22:41:05.094.
Посмотрим на вывод с сервера:
Я вызываю его один раз, а база данных вставляется сюда три раза. прохладно.
А вы обратите внимание на время запроса, а запрос приходит раз в 1с.
Почему я продолжаю подчеркивать здесь время?
Потому что вот точка знаний:Тайм-аут 1000 мс — это один вызов, а не весь повторный запрос (три раза).
Во время предыдущего интервью кто-то задал мне этот вопрос о времени. Поэтому напишу отдельно.
Затем мы преобразуем xml-файл клиента и указываем retries=0:
позвони снова:
Как видите, делается только один вызов.
Пока что мы по-прежнему используем Dubbo как черный ящик. Было проверено, что его автоматические повторные попытки выполняются 3 раза, что можно указать в параметре повторов.
Далее давайте посмотрим на исходный код.
Исходный код отказоустойчивого кластера
Исходный код находится наorg.apache.dubbo.rpc.cluster.support.FailoverClusterInvoker
середина:
Из исходного кода мы можем знать, что количество попыток по умолчанию равно 2:
Подождите, это неправильно, то, что я только что сказал, было 3 раза, почему это 2 раза в мгновение ока?
Не волнуйтесь.
Вы видите "+1" в конце строки 61?
Ты думаешь об этом. Мы хотим повторить попытку n раз после сбоя вызова интерфейса, это n равно DEFAULT_RETRIES, значение по умолчанию — 2. Тогда наше общее количество вызовов равно n+1 раз.
Так что этот "+1" пришел вот так, маленькая точка знаний для всех.
Кроме того, место отмечено красной пятиконечной звездой ★, строки с 62 по 64. Тоже очень важное место. Для параметра retries описание на официальном сайте выглядит следующим образом:
Повторная попытка не требуется, установите значение 0 . Как мы анализировали ранее, когда для него установлено значение 0, он будет вызываться только один раз.
Но я также видел повторные попытки, настроенные как -1. -1+1=0. Вызов 0 раз, очевидно, является неправильным значением. Но программа тоже работает нормально и вызывается только один раз.
Это заслуга мест, отмеченных красной пентаграммой.
Защитное программирование. Даже если вы установите для него значение -10000, он будет вызываться только один раз.
На следующем рисунке показана моя исчерпывающая интерпретация метода doInvoke. По сути, каждая строка основного кода снабжена аннотациями. Вы можете нажать на большую картинку, чтобы просмотреть:
Как показано выше, основной рабочий процесс метода doInvoke FailoverClusterInvoker:
- Во-первых, получить количество повторных попыток, а затем сделать вызов цикла в соответствии с количеством повторных попыток.В теле цикла, если это не удается, выполняется повторная попытка.
- В теле цикла сначала вызовите метод select родительского класса AbstractClusterInvoker, выберите Invoker с помощью компонента балансировки нагрузки, а затем выполните удаленный вызов с помощью метода Invoker Invoker.
- Если это не удается, зарегистрируйте исключение и повторите попытку.
Обратите внимание на деталь: перед повторной попыткой повторите выборку последнего набора вызывающих элементов.Преимущество этого в том, что если служба зависает во время процесса повторной попытки, вы можете вызвать метод списка, чтобы убедиться, что copyInvokers является последним доступным списком вызывающих.
Весь процесс примерно такой, и понять его несложно.
Пример использования HttpClient
Далее давайте посмотрим, что происходит с повторными попытками в HttpClients apache.
То есть этот класс:org.apache.http.impl.client.HttpClients
.
Прежде всего, разговаривайте меньше ерунды, возьмите демонстрацию.
Сначала посмотрим на логику контроллера:
@RestController
public class TestController {
@PostMapping(value = "/testRetry")
public void testRetry() {
try {
System.out.println("时间:" + new Date() + ",数据库插入成功");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
То же самое — спать на 5 с, имитируя ситуацию тайм-аута.
HttpUtils инкапсулируется следующим образом:
public class HttpPostUtils {
public static String retryPostJson(String uri) throws Exception {
HttpPost post = new HttpPost(uri);
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(1000)
.setConnectionRequestTimeout(1000)
.setSocketTimeout(1000).build();
post.setConfig(config);
String responseContent = null;
CloseableHttpResponse response = null;
CloseableHttpClient client = null;
try {
client = HttpClients.custom().build();
response = client.execute(post, HttpClientContext.create());
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
responseContent = EntityUtils.toString(response.getEntity(), Consts.UTF_8.name());
}
} finally {
if (response != null) {
response.close();
}
if (client != null){
client.close();
}
}
return responseContent;
}
}
Давайте сначала объясним три параметра, установленные на 1000 мс:
connectTimeout: тайм-аут для установления соединения между клиентом и сервером
connectionRequestTimeout: время ожидания для получения соединения из пула соединений.
socketTimeout: тайм-аут для клиента, чтобы прочитать данные с сервера
Всем известно, что абстрактно http-запрос должен состоять из трех этапов.
- Первый: установить соединение
- Два: передача данных
- Три: Отключить
Если операция установления соединения не будет завершена в течение указанного времени (ConnectionTimeOut), произойдет сбой соединения и будет выдано исключение ConnectTimeoutException.
Последующее исключение SocketTimeOutException не должно возникать.
Когда соединение будет установлено, начнется передача данных, если данные не будут переданы в течение заданного времени (SocketTimeOut), будет выброшено исключение SocketTimeOutException. Если передача завершена, отключитесь.
Код основного метода теста выглядит следующим образом:
public class MainTest {
public static void main(String[] args) {
try {
String returnStr = HttpPostUtils.retryPostJson("http://127.0.0.1:8080/testRetry/");
System.out.println("returnStr = " + returnStr);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Во-первых, если мы не запустим службу, то, согласно только что проведенному анализу, время ожидания соединения между клиентом и сервером истечет, и будет выброшено исключение ConnectTimeoutException.
Выполните основной метод напрямую, и результат будет следующим:
В соответствии с нашими ожиданиями.
Теперь мы запускаем интерфейс контроллера.
Поскольку время, установленное нашим socketTimeout, составляет 1000 мс, в интерфейсе есть 5-секундный сон.
Согласно только что проведенному анализу, у клиента определенно истечет время ожидания для чтения данных с сервера, и будет выброшено исключение SocketTimeOutException.
После того, как интерфейс контроллера запущен, мы запускаем основной метод, и результат выглядит следующим образом:
В это время интерфейс фактически вызывается успешно, но клиент не получает возврата.
Эта ситуация аналогична ситуации с Dubbo, о которой мы упоминали ранее, и тайм-аут для клиента.
Даже если время ожидания клиента истечет, логика сервера будет продолжать выполняться для завершения обработки запроса.
Результат выполнения вызывает исключение SocketTimeOutException, как и ожидалось.
Но как насчет хорошей повторной попытки?
Повторите попытку для HttpClient
В HttpClients на самом деле есть функция повтора, и, как и в Dubbo, она включена по умолчанию.
Но почему мы не повторяем здесь оба исключения?
Если он может повторить попытку, сколько раз по умолчанию?
С вопросами мы все же идем в исходники, чтобы найти ответ.
Ответ скрыт в этом исходном коде,org.apache.http.impl.client.DefaultHttpRequestRetryHandler
.
DefaultHttpRequestRetryHandler — это стратегия повторных попыток по умолчанию для Apache HttpClients.
Как видно из его метода построения, по умолчанию он повторяется 3 раза:
Этот конструктор this вызывает этот метод:
Из комментариев и кода конструктора видно, что для этих четырех типов исключений повторные попытки выполняться не будут:
- Один: InterruptedIOException
- Два: UnknownHostException
- Три: ConnectException
- Четыре: SSLException
А упомянутые выше ConnectTimeoutException и SocketTimeOutException унаследованы от InterruptedIOException:
Давайте закроем интерфейс контроллера и нажмем точку останова, чтобы посмотреть:
Видно, что после решения if он вернет false и повторная попытка не будет инициирована.
Чтобы смоделировать ситуацию с повторной попыткой, нам нужно преобразовать HttpPostUtils в пользовательский HttpRequestRetryHandler:
public class HttpPostUtils {
public static String retryPostJson(String uri) throws Exception {
HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() {
@Override
public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
System.out.println("开始第" + executionCount + "次重试!");
if (executionCount > 3) {
System.out.println("重试次数大于3次,不再重试");
return false;
}
if (exception instanceof ConnectTimeoutException) {
System.out.println("连接超时,准备进行重新请求....");
return true;
}
HttpClientContext clientContext = HttpClientContext.adapt(context);
HttpRequest request = clientContext.getRequest();
boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
if (idempotent) {
return true;
}
return false;
}
};
HttpPost post = new HttpPost(uri);
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(1000)
.setConnectionRequestTimeout(1000)
.setSocketTimeout(1000).build();
post.setConfig(config);
String responseContent = null;
CloseableHttpResponse response = null;
CloseableHttpClient client = null;
try {
client = HttpClients.custom().setRetryHandler(httpRequestRetryHandler).build();
response = client.execute(post, HttpClientContext.create());
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
responseContent = EntityUtils.toString(response.getEntity(), Consts.UTF_8.name());
}
} finally {
if (response != null) {
response.close();
}
if (client != null) {
client.close();
}
}
return responseContent;
}
}
Внутри нашего пользовательского HttpRequestRetryHandler для ConnectTimeoutException я позволяю повторить запрос.
Когда мы не запускаем интерфейс Controller, программа автоматически повторяет попытку 3 раза:
Приведенное выше демонстрирует стратегию повторных попыток по умолчанию для Apache HttpClients. Приведенный выше код можно напрямую вынуть и запустить.
Если вы хотите узнать весь процесс вызова, вы можете увидеть ссылку вызова в режиме отладки:
Автоматический повтор HttpClients также включен по умолчанию, но мы не знаем об этом во время использования.
Поскольку условия повторных попыток также относительно жесткие, повторные попытки на сетевом уровне не мешают бизнесу.
Будьте осторожны и будьте осторожны.
Для функций, которые необходимо перепроверить, мы должны быть осторожными и осторожными в процессе разработки.
Например, повторная попытка Dubbo по умолчанию, я думаю, ее отправной точкой является обеспечение высокой доступности сервисов.
Обычно наши микросервисы имеют как минимум два узла. Когда один из узлов не может предоставить услуги, стратегия отказоустойчивости кластера автоматически повторяет попытку другого.
Но в случае тайм-аута вызова службы Dubbo также считает, что его необходимо повторить, что эквивалентно вторжению в бизнес.
Ранее мы говорили, что тайм-аут вызова службы для клиента. Даже если время ожидания вызова клиента истекло, сервер по-прежнему выполняет запрос в обычном режиме.
Итак, в официальной документации говорится, что «обычно используется для операций чтения»:
http://dubbo.apache.org/zh/docs/v2.7/user/examples/fault-tolerent-strategy/
Операция чтения, что означает, что по умолчанию она является идемпотентной. Итак, не забудьте установить retries=0, если ваш метод интерфейса не является идемпотентным.
Эта вещь, я дам вам настоящую сцену.
Предположим, вы звоните в платежный интерфейс WeChat, но время ожидания звонка истекло.
Что вы делаете в это время?
Просто попробовать еще раз? Пожалуйста, вернитесь и дождитесь уведомления.
Он должен быть вызван интерфейсом запроса, чтобы определить, был ли получен текущий запрос другой стороной, чтобы выполнить дальнейшие операции.
Для HttpClients его автоматический повтор не мешает бизнесу, а находится на сетевом уровне.
Так что в большинстве случаев наша система безразлична к его автоматическому повтору.
Нам даже нужно реализовать в программе функцию автоматического повтора.
Поскольку ваше преобразование находится в самом низу метода HttpClients, вам следует обратить внимание на один момент: вам нужно определить, поддерживает ли запрос повторную попытку после исключения.
Вы не можете просто повторить попытку, не подумав.
Для платформы повторных попыток вы можете узнать о Guava-Retry и Spring-Retry.
Анекдот
Я знаю, что это та часть, которая всем нравится больше всего.
Взгляните на журнал фиксации для FailoverClusterInvoker:
Сдан дважды в 2020 году. Промежуток времени совсем небольшой.
Коммит от 9 февраля был исправлением ошибки 5686.
В этом выпуске внесены исправления для номеров 5684 и 5654:
https://github.com/apache/dubbo/issues/5654
Все они указывают на проблему:
Балансировка нагрузки нескольких реестров не действует.
После того, как официалы исправили эту проблему, тут же всплыла еще одна большая проблема:
В версии 2.7.6 стратегия балансировки нагрузки отказоустойчивости недействительна.
Вы думаете, я знаю, что один из моих интерфейсов не может дать сбой и повторить попытку, поэтому я намеренно изменил его на отказоустойчивую стратегию.
Но фактическая структура все еще использует отказоустойчивость и повторяет попытки 2 раза?
На самом деле ситуация еще хуже: в версии 2.7.6 стратегия балансировки нагрузки поддерживает только отказоустойчивость.
Эта вещь немного похожа на дыру.
И этот баг не исправляли до версии 2.7.8.
Итак, если вы используете Dubbo версии 2.7.5 или 2.7.6. Обязательно обратите внимание на то, используются ли другие стратегии отказоустойчивости кластера. Если он используется, он фактически не вступает в силу.
Можно сказать, что это действительно относительно большая ошибка.
Но проекты с открытым исходным кодом поддерживаются вместе.
Конечно, мы знаем, что Dubbo — не идеальный фреймворк, но мы также знаем, что за ним стоит группа инженеров, которые знают, что он не идеален, но все же не сдаются и не сдаются.
Они пытаются обновить его и сделать его идеальным.
Как пользователи, мы меньше «плюемся», больше подбадриваем и вносим содержательные предложения.
Только так я могу с гордостью сказать, что мы внесли небольшой вклад в мир открытого исходного кода, и мы верим, что завтра он станет лучше.
Приветствую открытый исходный код, приветствую инженеров с открытым исходным кодом.
В общем, круто.
Хорошо, это все для этой статьи.
Это всего лишь неглубоко, у него неизбежно будет утечка, если вы обнаружите неправильное место, вы можете упомянуть об этом, я изменю его.
Спасибо за прочтение, настаиваю на оригинальности, очень приветствую и благодарю за внимание.
Я почему, литературный творец, которого задержал код, теплый и информативный сычуаньский человек.
Кроме того, пожалуйста, следуйте за мной.