Ruby Net::HTTP не отправляет пакеты TCP Keep-Alive, что приводит к ошибке Errno::ECONNRESET — сброс соединения из-за исключений одноранговых узлов

Я взаимодействую с API, который принимает запросы POST через HTTPS и отвечает XML. Генерация данных удаленным сервером занимает много времени, а это означает, что мой локальный клиент ждет несколько минут между отправкой запроса POST и получением ответа. Иногда ответ возвращается, как и ожидалось, но в других случаях Ruby (2.3.1p112) после долгой паузы вызывает следующее исключение:

Exception occurred: Errno::ECONNRESET - Connection reset by peer

/usr/share/ruby/openssl/buffering.rb:178:in `sysread_nonblock'
/usr/share/ruby/openssl/buffering.rb:178:in `read_nonblock'
/usr/share/ruby/net/protocol.rb:154:in `rbuf_fill'
/usr/share/ruby/net/protocol.rb:136:in `readuntil'
/usr/share/ruby/net/protocol.rb:146:in `readline'
/usr/share/ruby/net/http/response.rb:40:in `read_status_line'
/usr/share/ruby/net/http/response.rb:29:in `read_new'
/usr/share/ruby/net/http.rb:1437:in `block in transport_request'
/usr/share/ruby/net/http.rb:1434:in `catch'
/usr/share/ruby/net/http.rb:1434:in `transport_request'
/usr/share/ruby/net/http.rb:1407:in `request'

Из любопытства я попытался сделать те же запросы с помощью cURL и обнаружил, что каждый раз получаю ответ, никогда не сталкиваясь со сбросом соединения, инициированным удаленным хостом. Запросы cURL и Ruby выполнялись на одном компьютере, поэтому я решил, что это не низкоуровневая система или проблема с сетью. Пытаясь найти какую-то разницу, я запустил Wireshark и посмотрел на пакеты, отправляемые туда и обратно во время запросов.

Сначала с Руби:

1302978 3988.123708  [local-ip] → [remote-ip]  SSL 286 Client Hello
1302981 3988.189299  [remote-ip] → [local-ip]  TLSv1.2 463 Server Hello, Certificate
1302982 3988.189388  [local-ip] → [remote-ip]  TCP 54 55265 → 443 [ACK] Seq=233 Ack=1361 Win=65535 Len=0
1302983 3988.189389  [local-ip] → [remote-ip]  TCP 54 55265 → 443 [ACK] Seq=233 Ack=2721 Win=65535 Len=0
1302984 3988.189389  [local-ip] → [remote-ip]  TCP 54 55265 → 443 [ACK] Seq=233 Ack=3130 Win=65535 Len=0
1302985 3988.194978  [remote-ip] → [local-ip]  TLSv1.2 396 Server Key Exchange, Server Hello Done
1302986 3988.195047  [local-ip] → [remote-ip]  TCP 54 55265 → 443 [ACK] Seq=233 Ack=3472 Win=65535 Len=0
1302987 3988.195812  [local-ip] → [remote-ip]  TLSv1.2 180 Client Key Exchange, Change Cipher Spec, Encrypted Handshake Message
1302988 3988.257643  [remote-ip] → [local-ip]  TCP 60 443 → 55265 [ACK] Seq=3472 Ack=359 Win=35098 Len=0
1302989 3988.272131  [remote-ip] → [local-ip]  TLSv1.2 105 Change Cipher Spec, Encrypted Handshake Message
1302990 3988.272200  [local-ip] → [remote-ip]  TCP 54 55265 → 443 [ACK] Seq=359 Ack=3523 Win=65535 Len=0
1302994 3988.339450  [remote-ip] → [local-ip]  TCP 60 443 → 55265 [ACK] Seq=3523 Ack=945 Win=34512 Len=0
1302995 3988.339455  [remote-ip] → [local-ip]  TCP 60 443 → 55265 [ACK] Seq=3523 Ack=1206 Win=34251 Len=0
1406186 4376.828078  [remote-ip] → [local-ip]  TCP 54 443 → 55265 [RST, ACK] Seq=3523 Ack=1206 Win=9300 Len=0

Затем с помощью cURL:

51468 268.062527  [local-ip] → [remote-ip]  SSL 292 Client Hello
51472 268.125416  [remote-ip] → [local-ip]  TLSv1.2 463 Server Hello, Certificate
51473 268.125530  [local-ip] → [remote-ip]  TCP 54 53819 → 443 [ACK] Seq=239 Ack=1361 Win=65535 Len=0
51474 268.125531  [local-ip] → [remote-ip]  TCP 54 53819 → 443 [ACK] Seq=239 Ack=2721 Win=65535 Len=0
51475 268.125531  [local-ip] → [remote-ip]  TCP 54 53819 → 443 [ACK] Seq=239 Ack=3130 Win=65535 Len=0
51476 268.132509  [remote-ip] → [local-ip]  TLSv1.2 396 Server Key Exchange, Server Hello Done
51477 268.132604  [local-ip] → [remote-ip]  TCP 54 53819 → 443 [ACK] Seq=239 Ack=3472 Win=65535 Len=0
51479 268.158620  [local-ip] → [remote-ip]  TLSv1.2 129 Client Key Exchange
51481 268.220146  [remote-ip] → [local-ip]  TCP 60 443 → 53819 [ACK] Seq=3472 Ack=314 Win=35137 Len=0
51482 268.220216  [local-ip] → [remote-ip]  TLSv1.2 105 Change Cipher Spec, Encrypted Handshake Message
51483 268.281636  [remote-ip] → [local-ip]  TCP 60 443 → 53819 [ACK] Seq=3472 Ack=365 Win=35086 Len=0
51484 268.281642  [remote-ip] → [local-ip]  TLSv1.2 105 Change Cipher Spec, Encrypted Handshake Message
51485 268.281718  [local-ip] → [remote-ip]  TCP 54 53819 → 443 [ACK] Seq=365 Ack=3523 Win=65535 Len=0
51487 268.344020  [remote-ip] → [local-ip]  TCP 60 443 → 53819 [ACK] Seq=3523 Ack=770 Win=34681 Len=0
62427 328.950531  [local-ip] → [remote-ip]  TCP 54 [TCP Keep-Alive] 53819 → 443 [ACK] Seq=769 Ack=3523 Win=65535 Len=0
62435 329.012004  [remote-ip] → [local-ip]  TCP 60 [TCP Window Update] 443 → 53819 [ACK] Seq=3523 Ack=770 Win=65535 Len=0
72644 389.584563  [local-ip] → [remote-ip]  TCP 54 [TCP Keep-Alive] 53819 → 443 [ACK] Seq=769 Ack=3523 Win=65535 Len=0
72647 389.647037  [remote-ip] → [local-ip]  TCP 60 [TCP Keep-Alive ACK] 443 → 53819 [ACK] Seq=3523 Ack=770 Win=65535 Len=0

Соответствующее отличие, которое я обнаружил, заключается в том, что cURL отправляет пакет TCP Keep-Alive каждую минуту, ожидая ответа, сообщая серверу, что он все еще активен и хочет сохранить свое соединение открытым. Ruby Net::HTTP не отправляет эти пакеты и в конечном итоге получает TCP RST (сброс) от удаленного хоста, что вызывает исключение Errno::ECONNRESET.

Итак, мой вопрос: есть ли способ настроить Net::HTTP для отправки этих пакетов TCP Keep-Alive и оставить мое соединение открытым? Спасибо!


Изменить: мне удалось обойти это, переключившись на Typhoeus (который обертывает cURL) и передав параметр tcp_keepalive: true в мои запросы, который является флагом, который указывает cURL отправлять проверки активности.

Я оставлю этот вопрос открытым на тот случай, если кто-то поймет, можно ли получить такое же поведение от Net::HTTP.


person Joey Schoblaska    schedule 08.05.2017    source источник


Ответы (1)


Вы можете указать Keep Alive в заголовке запроса, хотя это не относится к nethttp! https://tools.ietf.org/id/draft-thomson-hybi-http-timeout-01.html

person Robert Nicholas    schedule 09.05.2017
comment
Я попытался установить этот заголовок, но удаленный хост все же отправил RST-пакет и не предложил Net::HTTP начать отправку тестов Keep-Alive. AFAICT, этот заголовок определяет ожидания хоста относительно того, как должен вести себя другой хост, чтобы соединение оставалось активным. - person Joey Schoblaska; 09.05.2017