предисловие
Возможно, многие Java-программисты понимают TCP только в трехстороннем и четырехстороннем рукопожатии.Я думаю, основная причина этого в том, что сам протокол TCP немного абстрактен (по сравнению с протоколом HTTP на прикладном уровне); во-вторых , разработчики, не работающие с фреймворком Нет необходимости касаться некоторых деталей TCP. На самом деле, я лично не до конца понимаю многие детали TCP.Эта статья в основном посвящена проблемам с длинным соединением и сердцебиением, поднятым некоторыми людьми в группе обмена WeChat, и представляет собой единую договоренность.
В Java при использовании связи TCP, скорее всего, будут задействованы Socket и Netty, в этой статье будут заимствованы некоторые из их API и заданы параметры, чтобы облегчить введение.
Длинные и короткие соединения
Сам TCP не имеет разницы между длинными и короткими соединениями., длинный он или нет, полностью зависит от того, как мы его используем.
- Короткое соединение: создайте сокет для каждого соединения, вызовите socket.close(), когда соединение завершится. Это короткое соединение в общем смысле.Преимущество короткого соединения в том, что им относительно просто управлять, а существующие соединения - это все доступные соединения, и никаких дополнительных средств управления не требуется.
- Длинное соединение: после завершения каждой связи соединение не будет закрыто, поэтому соединение можно будет использовать повторно.Преимущество длинного соединения в том, что оно экономит время на создание соединения.
Преимущества коротких соединений и длинных соединений являются недостатками друг друга. Если вам нужна простая диаграмма и вы не стремитесь к высокой производительности, уместно использовать короткие соединения, чтобы нам не нужно было беспокоиться об управлении состоянием соединения; если вы хотите добиться производительности и использовать длинные соединения, мы нужно беспокоиться о различных проблемах: таких какОбслуживание сквозного соединения, поддержание соединения в рабочем состоянии.
Длинные соединения также часто используются для передачи данных.Большую часть времени мы понимаем связь по-прежнему как модель запроса/ответа, но природа дуплексной связи TCP определяет, что ее также можно использовать для двусторонней связи. При длинном соединении может быть легко реализована модель push.Эта особенность длинного соединения не будет обсуждаться в этой статье.Заинтересованные студенты могут искать статьи по теме.
О коротких соединениях говорить особо нечего, поэтому ниже мы сосредоточимся на некоторых проблемах, связанных с длинными соединениями. Это слишком монотонно, чтобы говорить чисто теоретически, поэтому я начну обсуждение TCP с помощью некоторых практик фреймворка RPC.
Постоянные соединения в структуре управления службами
Как упоминалось ранее, в погоне за производительностью вы определенно решите использовать длинное соединение, чтобы вы могли хорошо понимать TCP с Dubbo. Открываем два приложения Dubbo, один сервер отвечает за прослушивание локального порта 20880 (как мы все знаем, это порт по умолчанию протокола Dubbo), а один клиент отвечает за отправку запросов в цикле. воплощать в жизньlsof -i:20880
Команда может просмотреть соответствующее использование порта:
-
*:20880 (LISTEN)
Это показывает, что Dubbo прослушивает локальный порт 20880 и обрабатывает запросы, отправленные на локальный порт 20880. - Последние два сообщения описывают отправку запроса и подтверждают, что TCP является процессом двусторонней связи.Поскольку я открыл два приложения Dubbo на одном компьютере, вы можете видеть, что локальный порт 53078 обменивается данными с портом 20880. Мы не устанавливали клиентский порт 53078 вручную, это было случайно. Через эти две части информации объясняется факт:Даже сторона, отправляющая запрос, должна занимать порт.
- Поговорим немного о параметре FD, который представляетдескриптор файла, каждое новое соединение будет занимать новый дескриптор файла, если вы используете соединение TCP
open too many files
исключение, вы должны проверить, не создали ли вы слишком много подключений, не закрывая их. Внимательные читатели также подумают о другом преимуществе длинных соединений, а именно о том, что они занимают меньше файловых дескрипторов.
Обслуживание длинных соединений
Поскольку служба, запрошенная клиентом, может быть распределена по нескольким серверам, клиенту, естественно, необходимо создать несколько постоянных соединений с одноранговым узлом.Первая проблема, с которой мы сталкиваемся, заключается в том, как поддерживать постоянные соединения.
// 客户端
public class NettyHandler extends SimpleChannelHandler {
private final Map<String, Channel> channels = new ConcurrentHashMap<String, Channel>(); // <ip:port, channel>
}
// 服务端
public class NettyServer extends AbstractServer implements Server {
private Map<String, Channel> channels; // <ip:port, channel>
}
В Dubbo и клиент, и сервер используютip:port
Поддерживая сквозное длинное соединение, Channel является абстракцией соединения. В основном мы фокусируемся на длинных соединениях в NettyHandler.Сервер одновременно поддерживает набор длинных соединений, что является дополнительной конструкцией Dubbo, о которой мы упомянем позже.
Вставьте здесь предложение, объясняющее, почему я считаю набор соединений клиента более важным. TCP — это протокол двусторонней связи, любая из сторон может быть отправителем или получателем, так зачем абстрагировать клиента и сервер? так какУстановление связи - это как говорить о любви. Должна быть активная сторона. Если вы проявите инициативу, у нас будет история.. Клиента можно понимать как сторону, которая активно устанавливает соединение Фактически, статус двух концов можно понимать как равный.
поддержание соединения
Эта тема обсуждалась, и она будет включать больше очков знаний. Прежде всего, должно быть ясно, зачем вам нужно поддерживать связь? Когда две стороны установили соединение, но соединение заблокировано из-за проблем с сетью, длительное соединение использовать нельзя. Одна вещь, которую необходимо прояснить, это то, что состояние соединения просматривается с помощью таких команд, как netstat и lsof.ESTABLISHED
Статус - штука не очень надежная, потому что связь может быть мертвой, но системой она не воспринимается, не говоря уже о неизлечимой болезни анабиоза. Это техническая работа, чтобы гарантировать, что длинное соединение доступно.
Поддержка подключений: KeepAlive
Первое, что приходит на ум, это механизм KeepAlive в TCP. KeepAlive не является частью протокола TCP, но большинство операционных систем реализуют этот механизм (поэтому соответствующие параметры KeepAlive необходимо устанавливать на уровне операционной системы). После включения механизма KeepAlive в течение определенного периода времени (обычно 7200 с) параметрtcp_keepalive_time
) При отсутствии передачи данных по каналу уровень TCP отправит соответствующий KeepAlive probe, чтобы определить доступность соединения, и повторит попытку 10 (параметрtcp_keepalive_probes
) раз, каждый временной интервал равен 75 с (параметрtcp_keepalive_intvl
), текущее соединение считается недоступным до тех пор, пока не произойдет сбой всех зондов.
Включите KeepAlive в Netty:
bootstrap.option(ChannelOption.SO_KEEPALIVE, true)
Установите параметры, связанные с KeepAlive, в операционной системе Linux, измените/etc/sysctl.conf
документ:
net.ipv4.tcp_keepalive_time=90
net.ipv4.tcp_keepalive_intvl=15
net.ipv4.tcp_keepalive_probes=2
Механизм KeepAlive обеспечивает доступность соединения на сетевом уровне., но на уровне фреймворка приложения мы думаем, что этого недостаточно. В основном это проявляется в трех аспектах:
- Переключатель KeepAlive включается на уровне приложения, но настройка конкретных параметров (таких как тест повторных попыток, интервал повторных попыток) находится на уровне операционной системы, расположенной в операционной системе.
/etc/sysctl.conf
конфигурации, это недостаточно гибко для приложения. - Механизм проверки активности KeepAlive будет работать только тогда, когда ссылка простаивает.Если в это время отправляются данные, а физическая ссылка больше недоступна, статус ссылки на стороне операционной системы остается прежним.
ESTABLISHED
, что тогда будет? Естественно, будет использоваться механизм TCP retransmission.Необходимо знать, что используемый по умолчанию TCP timeout retransmission и алгоритм экспоненциального отсрочки также является достаточно длительным процессом. - Сам KeepAlive ориентирован на сеть, а не на приложение.Когда соединение недоступно, это может быть связано с частым GC самого приложения и высокой нагрузкой на систему, но сеть все еще подключена.В это время приложение потеряло свою активности, и соединение должно считаться недоступным.
Мы достаточно подготовились к поддержке соединения на уровне приложения, давайте посмотрим, как сделать поддержку соединения на уровне приложения.
Поддержание соединения: пульсация прикладного уровня
Наконец, тема упоминается, как указано в названии статьи.сердцебиениеЭто еще один важный аспект знаний, который мы хотим подчеркнуть в этой статье. Как мы объяснили в предыдущем разделе, KeepAlive на сетевом уровне недостаточно для поддержки доступности соединения на уровне приложения.В этом разделе мы поговорим о механизме сердцебиения прикладного уровня для поддержания активности соединения.
Как понять сердцебиение прикладного уровня? Проще говоря, клиент будет запускать временную задачу, чтобы периодически отправлять запрос к одноранговому приложению, установившему соединение (запрос здесь — это специальный запрос сердцебиения), а серверу нужно специально обработать запрос и вернуть ответ. . Если пульсация продолжается много раз, а ответ не получен, клиент будет считать соединение недоступным и активно отключится. Разные платформы управления сервисами имеют разные стратегии для механизмов пульса, установления соединения, отключения и шантажа, но большинство фреймворков управления сервисами выполняют пульсацию на прикладном уровне, и Dubbo/HSF не является исключением.
Детали дизайна сердцебиения прикладного уровня
Возьмем, к примеру, Dubbo, который поддерживает пульсацию прикладного уровня, и как клиент, так и сервер будут открыватьHeartBeatTask
, клиент находится вHeaderExchangeClient
включен, сервер будетHeaderExchangeServer
на. В начале статьи зарыта яма: почему Dubbo поддерживается на стороне сервера при этомMap<String,Channel>
Шерстяная ткань? Основная цель состоит в том, чтобы способствовать пульсу. Когда задача синхронизации пульса обнаруживает, что соединение недоступно, она переходит к разным ветвям в зависимости от того, является ли он в настоящее время клиентом или сервером. Если клиент обнаруживает, что он недоступен, он переподключится; если сервер обнаружит, что он недоступен, он сразу же закроется.
// HeartBeatTask
if (channel instanceof Client) {
((Client) channel).reconnect();
} else {
channel.close();
}
По сравнению с версией 2.6.x, в Dubbo 2.7.x оптимизирована синхронизация пульса, используя
HashedWheelTimer
Более точный контроль отправки тактов только тогда, когда соединение не используется.
Глядя на реализацию HSF, сердцебиение прикладного уровня не установлено, если быть точным, оно предоставляется Netty после HSF2.2.IdleStateHandler
Сердцебиение приложения реализовано более изящно.
ch.pipeline()
.addLast("clientIdleHandler", new IdleStateHandler(getHbSentInterval(), 0, 0));
иметь дело сuserEventTriggered
серединаIdleStateEvent
событие
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
callConnectionIdleListeners(client, (ClientStream) StreamUtils.streamOfChannel(ctx.channel()));
} else {
super.userEventTriggered(ctx, evt);
}
}
Для клиентов HSF используетSendHeartbeat
Для выполнения пульса каждый сбой накапливает время сбоя пульса и отключает случайные соединения при превышении максимального предела; для использования HSF на стороне сервераCloseIdle
Для обработки неактивных соединений закройте соединение напрямую. Вообще говоря, время простоя сервера будет установлено немного дольше.
Учащиеся, знакомые с другими инфраструктурами RPC, обнаружат, что механизм сердцебиения в разных инфраструктурах действительно сильно отличается. План пульса также связан с созданием соединения, механизмом повторного соединения и соединениями из черного списка и требует специального анализа структуры.
Помимо проектирования задач на время, также необходимо поддерживать пульсации на уровне протокола. Простейший пример может относиться к проверке работоспособности nginx.Для протокола Dubbo естественно требуется поддержка heartbeat.Если запрос heartbeat идентифицируется как обычный трафик, это вызовет проблемы с давлением на стороне сервера, помехи и ограничение тока и многое другое проблемы.
Среди них Flag представляет бит флага протокола Dubbo с 8 битами адреса. Нижние четыре бита используются для указания типа инструмента сериализации, используемого для данных тела сообщения (гессе по умолчанию).Среди старших четырех битов первый бит равен 1, чтобы указать запрос запроса, а второй бит равен 1, чтобы указать два -способ передачи (то есть ответ возвращается),Третий бит равен 1, что означает, что это событие сердцебиения..
Запросы Heartbeat следует обрабатывать иначе, чем обычные запросы.
Обратите внимание, что он обрабатывается иначе, чем KeepAlive HTTP.
- Целью KeepAlive протокола HTTP является мультиплексирование соединений, а данные запроса-ответа передаются последовательно по одному и тому же соединению.
- Механизм TCP KeepAlive предназначен для поддержания работоспособности, пульса и обнаружения ошибок подключения.
Это в основном два понятия.
Распространенные ошибки KeepAlive
Приложения, поддерживающие TCP KeepAlive, обычно могут обнаруживать следующие типы ошибок.
-
Ошибка тайм-аута ETIMEOUT, исключение, возникающее, когда подтверждение ACK не получено после отправки пакета защиты зонда после (tcp_keepalive_time + tcp_keepalive_intvl * tcp_keepalive_probes) времени, когда сокет закрыт
java.io.IOException: Connection timed out
-
Ошибка EHOSTUNREACH host unreachable (хост недоступен), об этом следует сообщить вышестоящему приложению по ICMP.
java.io.IOException: No route to host
-
После сброса ссылки терминал может глючить и зависать.После перезапуска он получает сообщение от сервера.Однако дела обстоят иначе.Раньше об этом можно было объявить только беспомощным сбросом.
java.io.IOException: Connection reset by peer
Суммировать
Существует три практических сценария использования KeepAlive:
- По умолчанию период проверки активности составляет 2 часа. Если вы не решите изменить его, это будет категория неправильного использования, что приведет к пустой трате ресурсов: ядро будет открывать таймер поддержки активности для каждого соединения, а N подключений будет открывать N проверок активности. таймеры. . Преимущества очевидны:
- Механизм проверки активности на уровне протокола TCP, ядро системы полностью автоматически выполняется для приложения верхнего уровня.
- Таймеры уровня ядра более эффективны, чем приложения верхнего уровня.
- Приложению верхнего уровня нужно только обрабатывать отправку и получение данных и уведомление об исключении соединения.
- Пакеты будут более компактными
- Отключите TCP KeepAlive и полностью используйте механизм подтверждения активности на уровне приложений. Приложение управляет сердцебиением, что является более гибким и управляемым.Например, цикл сердцебиения можно настроить на уровне приложения для адаптации к частным протоколам.
- Heartbeat службы + TCP KeepAlive используются вместе, чтобы дополнять друг друга, но цикл обнаружения TCP keepalive и цикл подтверждения активности приложения должны быть скоординированы, чтобы дополнять друг друга, и разрыв не может быть слишком большим, иначе ожидаемый эффект не будет достигнут.
Каждый фреймворк отличается друг от друга, например, Dubbo использует схему 3, но внутренний фреймворк HSF Alibaba не устанавливает протокол TCP KeepAlive и поддерживается только пульсацией приложения. Как и стратегия сердцебиения, это связано с общим дизайном фреймворка.
Добро пожаловать в мой общедоступный аккаунт WeChat: «Обмен технологиями Кирито».