Java HttpURLConnection InputStream.close () зависает (или работает слишком долго?)

Во-первых, немного предыстории. Есть рабочий, который раскрывает / разрешает кучу коротких URL:

http://t.co/example -> http://example.com

Итак, мы просто следим за перенаправлениями. Вот и все. Мы не читаем данные из соединения. Сразу после того, как мы получили 200, мы возвращаем конечный URL и закрываем InputStream.

Теперь сама проблема. На рабочем сервере один из потоков преобразователя зависает внутри вызова InputStream.close():

"ProcessShortUrlTask" prio=10 tid=0x00007f8810119000 nid=0x402b runnable [0x00007f882b044000]
   java.lang.Thread.State: RUNNABLE
        at java.io.BufferedInputStream.fill(BufferedInputStream.java:218)
        at java.io.BufferedInputStream.skip(BufferedInputStream.java:352)
        - locked <0x0000000561293aa0> (a java.io.BufferedInputStream)
        at sun.net.www.MeteredStream.skip(MeteredStream.java:134)
        - locked <0x0000000561293a70> (a sun.net.www.http.KeepAliveStream)
        at sun.net.www.http.KeepAliveStream.close(KeepAliveStream.java:76)
        at java.io.FilterInputStream.close(FilterInputStream.java:155)
        at sun.net.www.protocol.http.HttpURLConnection$HttpInputStream.close(HttpURLConnection.java:2735)
        at ru.twitter.times.http.URLProcessor.resolve(URLProcessor.java:131)
        at ru.twitter.times.http.URLProcessor.resolve(URLProcessor.java:55)
        at ...

После краткого исследования я понял, что skip() вызывается для очистки потока перед его отправкой обратно в пул подключений (если включена поддержка активности?). До сих пор не понимаю, как избежать этой ситуации. Более того, я сомневаюсь, что в нашем коде плохой дизайн или есть проблема в JDK.

Итак, вопросы:

  1. Можно ли избежать зависания на close()? Например, обеспечьте разумный тайм-аут.
  2. Можно ли вообще избежать чтения данных из соединения? Помните, мне нужен только конечный URL. На самом деле, я думаю, я вообще не хочу, чтобы skip() назывался ...

Обновление:

KeepAliveStream, строка 79, close() метод:

    // Skip past the data that's left in the Inputstream because
    // some sort of error may have occurred.
    // Do this ONLY if the skip won't block. The stream may have
    // been closed at the beginning of a big file and we don't want
    // to hang around for nothing. So if we can't skip without blocking
    // we just close the socket and, therefore, terminate the keepAlive
    // NOTE: Don't close super class
    try {
        if (expected > count) {
        long nskip = (long) (expected - count);
        if (nskip <= available()) {
            long n = 0;
            while (n < nskip) {
            nskip = nskip - n;
            n = skip(nskip);} ...

Мне все больше и больше кажется, что в самом JDK есть ошибка. К сожалению, воспроизвести это очень сложно ...


person Shcheklein    schedule 17.01.2013    source источник
comment
Вы пробовали работать напрямую с InputStream вместо BufferedInputStream?   -  person Fildor    schedule 17.01.2013
comment
Поток возвращается из вызова HttpURLConnection.getInputStream (). Я не контролирую это.   -  person Shcheklein    schedule 17.01.2013
comment
Я понимаю. Хорошо, это был просто выстрел в никуда.   -  person Fildor    schedule 17.01.2013
comment
Можете ли вы использовать HEAD вместо GET?   -  person artbristol    schedule 17.01.2013
comment
Да, в некоторых случаях мы можем использовать HEAD как очень хорошую оптимизацию. Нет, мы не можем полностью избавиться от GET. По двум причинам. Во-первых, не все сайты поддерживают HEAD. Во-вторых, нам нужно максимально имитировать поведение браузера. Даже если какой-то сайт поддерживает HEAD, он может возвращать что-то отличное от GET.   -  person Shcheklein    schedule 18.01.2013
comment
Я смущен. Сокращатели URL возвращают код перенаправления 30x, заголовок Location: и без тела. t.co, например, возвращает Content-Length: 0. Так нельзя просто прочитать весь поток (из 0 байтов) перед его закрытием?   -  person artbristol    schedule 18.01.2013
comment
Ах, HttpURLConnection автоматически следует за перенаправлением? Вы можете попробовать setInstanceFollowRedirects(false)?   -  person artbristol    schedule 18.01.2013
comment
Я думаю, что с большинством сокращателей URL-адресов мы можем даже использовать HEAD. Проблема в том, что нам тоже нужно следить за редиректами для обычных сайтов. Некоторые из них по той или иной причине выполняют редиректы. Так что в общем случае у вас в итоге будет GET для какого-то обычного сайта. И кажется, что для некоторых из этих сайтов JDK ведет себя странно.   -  person Shcheklein    schedule 18.01.2013
comment
Да, мы можем попробовать установить перенаправления на false и следить за ними вручную. Но воспроизвести это зависание очень-очень сложно (за год я вижу второй случай). Чего вы ждете от этого изменения? Почему это должно влиять на поведение close()?   -  person Shcheklein    schedule 18.01.2013
comment
Одно из наших серверных приложений столкнулось с той же проблемой при вызове службы Hessian: pastebin.com/TiDcNk9C.   -  person Derek Mahar    schedule 02.05.2014


Ответы (3)


Реализация KeepAliveStream, которую вы связали, нарушает контракт, в соответствии с которым available() и skip() гарантированно не блокируют и, следовательно, могут действительно блокировать.

Контракт available () гарантирует однократное неблокирование skip():

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

Где реализация вызывает skip() несколько раз за один вызов available():

    if (nskip <= available()) {
        long n = 0;
        // The loop below can iterate several times,
        // only the first call is guaranteed to be non-blocking. 
        while (n < nskip) { 
        nskip = nskip - n;
        n = skip(nskip);
        }

Это не доказывает, что ваше приложение блокируется из-за того, что KeepAliveStream неправильно использует InputStream. Некоторые реализации InputStream могут предоставить более надежные гарантии неблокирования, но я думаю, что это весьма вероятно.

РЕДАКТИРОВАТЬ: после небольшого дополнительного исследования это недавно исправленная ошибка в JDK: https://bugs.openjdk.java.net/browse/JDK-8004863?page=com.atlassian.jira.plugin.system.issuetabpanels%3aall-tabpanel. В отчете об ошибке говорится о бесконечном цикле, но блокировка skip() также может быть результатом. Похоже, что исправление решает обе проблемы (есть только один skip() на available()).

person Jan Wrobel    schedule 30.01.2013
comment
Спасибо, Ян. Действительно, это очень правдоподобное объяснение. Знаете ли вы, существует ли практика резервного копирования в сообществе разработчиков JDK? Есть ли шанс исправить это в JDK6 / 7? - person Shcheklein; 31.01.2013
comment
@Shcheklein согласно этому сайту oracle.com/technetwork/java/eol-135779. html «Выпуски Java SE обновляются с исправлениями ошибок, исправлениями безопасности и небольшими обновлениями на срок не менее 3 лет ...». Этот период заканчивается в феврале 2013 года для Java 6, но Java 7 должна получать исправления ошибок до июля 2014 года. Мне неясно, хотя это конкретное исправление является рыночной версией v8, возможно, оно считается слишком низким приоритетом (P4). Возможно, было бы полезно, если бы вы прокомментировали ошибку и объяснили, что она встречалась в реальных приложениях и планируют ли они исправить v7, потому что в соответствии с политикой они должны. - person Jan Wrobel; 31.01.2013
comment
Код, который вы показываете, находится в close (), а контракт close () не гарантирует неблокирующую способность. - person petertc; 22.10.2014

Я предполагаю, что это skip() на close() предназначено для поддержки Keep-Alive.

См. http://docs.oracle.com/javase/6/docs/technotes/guides/net/http-keepalive.html.

До Java SE 6, если приложение закрывает HTTP InputStream, когда остается прочитать более чем небольшой объем данных, соединение должно быть закрыто, а не кэшироваться. Теперь в Java SE 6 поведение заключается в чтении до 512 Кбайт из соединения в фоновом потоке, что позволяет повторно использовать соединение. Точный объем данных, который может быть прочитан, можно настроить с помощью системного свойства http.KeepAlive.remainingData.

Так что функцию keep alive можно эффективно отключить с помощью http.KeepAlive.remainingData=0 или http.keepAlive=false. Но это может отрицательно сказаться на производительности, если вы всегда обращаетесь к одному и тому же хосту http://t.co.

Как предположил @artbristol, использование HEAD вместо GET кажется здесь предпочтительным решением.

person Vadzim    schedule 17.01.2013
comment
Спасибо, что указали на вариант remainingData. Это хорошо знать. Хотя проблема не решена. Более того, я совершенно не понимаю, почему виснет close(). В документации сказано: чтение до 512 Кбайт в фоновом потоке. Даже если сайт настолько медленный, что не может дать нам 512 Кбайт за два дня, как может фоновый поток повесить приложение? - person Shcheklein; 18.01.2013
comment
Да, мы не хотим полностью отключать кеш подключений (мы знали о keepAlive=false и некоторых других настройках кеша, которые позволяют его отключить). См. Также мой комментарий к @artbristol о HEAD. - person Shcheklein; 18.01.2013

У меня возникла аналогичная проблема, когда я пытался сделать запрос «HEAD». Чтобы исправить это, я удалил метод "HEAD", потому что я просто хотел пропинговать URL-адрес

person Sabaat Ahmad    schedule 02.11.2014