Рубрики
Uncategorized

Две причины ошибки чтения при подключении

Автор оригинала: David Wong.

В последнее время онлайн-модуль PHP иногда считывает ошибку при подключении; конкретный журнал ошибок выглядит следующим образом

 Uncaught exception 'RedisException' with message 'read error on connection' 

После анализа и изучения были обнаружены две причины, которые могут привести к тому, что phpredis вернет “ошибку чтения при подключении”.:

  • Время ожидания выполнения
  • Используйте отключенные соединения

Следующие две ситуации будут подробно проанализированы.

I. Время ожидания выполнения

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

1.1 Имитированное Воспроизведение

1.1.1 Короткое время ожидания для настроек клиента

Выполнение операции get в тестовой среде занимает около 0,1 мс времени, поэтому клиент установил время выполнения равным 0,01 мс, а тестовый сценарий выглядит следующим образом:

pconnect("127.0.0.1", 6390);
    if ($ret == false) {
        echo "Connect return false";
        exit;
    }
    // Set timeout time to 0.1ms
    $rds->setOption(3,0.0001);
    $rds->get("aa");
} catch (Exception $e) {
    var_dump ($e);
}

При выполнении сценария вручную фиксируется исключение “ошибка чтения при подключении”.

1.1.2 Клиент не устанавливает тайм-аут, используйте тайм-аут по умолчанию

Клиент не устанавливает время ожидания, но в процессе выполнения команды время ожидания достигает значения по умолчанию, установленного php. Дополнительные сведения см. в разделе Анализ проблемы и решения тайм-аута подписки phpredis.

1.2 Анализ причин

1.2.1 анализ слоев

Глядя на процесс выполнения через strace, вы можете видеть, что после отправки инструкции get AA poll ожидает тайм-аута, когда он хочет получить событие POLLIN:

1.2.2 Анализ логики Кода

Соединение PHP redis использует расширение phpredis. Полнотекстовый поиск “ошибка чтения при подключении” в исходном коде phpredis показывает, что ошибка находится в функции redis_sock_gets файла phpredis/library.c, как описано в phpredis;

Функция Redis_sock_gets файла библиотеки.c phpredis

/*
 * Processing for variant reply types (think EVAL)
 */

PHP_REDIS_API int
redis_sock_gets(RedisSock *redis_sock, char *buf, int buf_size,
                size_t *line_size)
{
    // Handle EOF
    if(-1 == redis_check_eof(redis_sock, 0)) {
        return -1;
    }

    if(php_stream_get_line(redis_sock->stream, buf, buf_size, line_size)
                           == NULL)
    {
        char *errmsg = NULL;

        if (redis_sock->port < 0) {
            spprintf(&errmsg, 0, "read error on connection to %s", ZSTR_VAL(redis_sock->host));
        } else {
            spprintf(&errmsg, 0, "read error on connection to %s:%d", ZSTR_VAL(redis_sock->host), redis_sock->port);
        }
        // Close our socket
        redis_sock_disconnect(redis_sock, 1);

        // Throw a read error exception
        REDIS_THROW_EXCEPTION(errmsg, 0);
        efree(errmsg);
        return -1;
    }

    /* We don't need \r\n */
    *line_size-=2;
    buf[*line_size]='
/*
* Processing for variant reply types (think EVAL)
*/
PHP_REDIS_API int
redis_sock_gets(RedisSock *redis_sock, char *buf, int buf_size,
size_t *line_size)
{
// Handle EOF
if(-1 == redis_check_eof(redis_sock, 0)) {
return -1;
}
if(php_stream_get_line(redis_sock->stream, buf, buf_size, line_size)
== NULL)
{
char *errmsg = NULL;
if (redis_sock->port < 0) {
spprintf(&errmsg, 0, "read error on connection to %s", ZSTR_VAL(redis_sock->host));
} else {
spprintf(&errmsg, 0, "read error on connection to %s:%d", ZSTR_VAL(redis_sock->host), redis_sock->port);
}
// Close our socket
redis_sock_disconnect(redis_sock, 1);
// Throw a read error exception
REDIS_THROW_EXCEPTION(errmsg, 0);
efree(errmsg);
return -1;
}
/* We don't need \r\n */
*line_size-=2;
buf[*line_size]='\0';
/* Success! */
return 0;
}
'; /* Success! */ return 0; }

Вложение: Эта MSG выглядит более хостовой и портовой, чем онлайн-MSG, из-за недавнего объединения филиалов, как показано на рисунке.

Из исходного кода вы можете видеть, что если php_stream_get_line считывает данные потока как нулевые, это приводит к ошибке чтения при подключении. Итак, когда php_stream_get_line вернется к НУЛЮ, соответствующему файлу php-src/main/streams/streams.c исходного кода PHP, см. php-src для получения подробной информации;

/* If buf == NULL, the buffer will be allocated automatically and will be of an
 * appropriate length to hold the line, regardless of the line length, memory
 * permitting */
PHPAPI char *_php_stream_get_line(php_stream *stream, char *buf, size_t maxlen,
        size_t *returned_len)
{
    size_t avail = 0;
    size_t current_buf_size = 0;
    size_t total_copied = 0;
    int grow_mode = 0;
    char *bufstart = buf;

    if (buf == NULL) {
        grow_mode = 1;
    } else if (maxlen == 0) {
        return NULL;
    }

    /*
     * If the underlying stream operations block when no new data is readable,
     * we need to take extra precautions.
     *
     * If there is buffered data available, we check for a EOL. If it exists,
     * we pass the data immediately back to the caller. This saves a call
     * to the read implementation and will not block where blocking
     * is not necessary at all.
     *
     * If the stream buffer contains more data than the caller requested,
     * we can also avoid that costly step and simply return that data.
     */

    for (;;) {
        avail = stream->writepos - stream->readpos;

        if (avail > 0) {
            size_t cpysz = 0;
            char *readptr;
            const char *eol;
            int done = 0;

            readptr = (char*)stream->readbuf + stream->readpos;
            eol = php_stream_locate_eol(stream, NULL);

            if (eol) {
                cpysz = eol - readptr + 1;
                done = 1;
            } else {
                cpysz = avail;
            }

            if (grow_mode) {
                /* allow room for a NUL. If this realloc is really a realloc
                 * (ie: second time around), we get an extra byte. In most
                 * cases, with the default chunk size of 8K, we will only
                 * incur that overhead once.  When people have lines longer
                 * than 8K, we waste 1 byte per additional 8K or so.
                 * That seems acceptable to me, to avoid making this code
                 * hard to follow */
                bufstart = erealloc(bufstart, current_buf_size + cpysz + 1);
                current_buf_size += cpysz + 1;
                buf = bufstart + total_copied;
            } else {
                if (cpysz >= maxlen - 1) {
                    cpysz = maxlen - 1;
                    done = 1;
                }
            }

            memcpy(buf, readptr, cpysz);

            stream->position += cpysz;
            stream->readpos += cpysz;
            buf += cpysz;
            maxlen -= cpysz;
            total_copied += cpysz;

            if (done) {
                break;
            }
        } else if (stream->eof) {
            break;
        } else {
            /* XXX: Should be fine to always read chunk_size */
            size_t toread;

            if (grow_mode) {
                toread = stream->chunk_size;
            } else {
                toread = maxlen - 1;
                if (toread > stream->chunk_size) {
                    toread = stream->chunk_size;
                }
            }

            php_stream_fill_read_buffer(stream, toread);

            if (stream->writepos - stream->readpos == 0) {
                break;
            }
        }
    }

    if (total_copied == 0) {
        if (grow_mode) {
            assert(bufstart == NULL);
        }
        return NULL;
    }

    buf[0] = '
/* If buf == NULL, the buffer will be allocated automatically and will be of an
* appropriate length to hold the line, regardless of the line length, memory
* permitting */
PHPAPI char *_php_stream_get_line(php_stream *stream, char *buf, size_t maxlen,
size_t *returned_len)
{
size_t avail = 0;
size_t current_buf_size = 0;
size_t total_copied = 0;
int grow_mode = 0;
char *bufstart = buf;
if (buf == NULL) {
grow_mode = 1;
} else if (maxlen == 0) {
return NULL;
}
/*
* If the underlying stream operations block when no new data is readable,
* we need to take extra precautions.
*
* If there is buffered data available, we check for a EOL. If it exists,
* we pass the data immediately back to the caller. This saves a call
* to the read implementation and will not block where blocking
* is not necessary at all.
*
* If the stream buffer contains more data than the caller requested,
* we can also avoid that costly step and simply return that data.
*/
for (;;) {
avail = stream->writepos - stream->readpos;
if (avail > 0) {
size_t cpysz = 0;
char *readptr;
const char *eol;
int done = 0;
readptr = (char*)stream->readbuf + stream->readpos;
eol = php_stream_locate_eol(stream, NULL);
if (eol) {
cpysz = eol - readptr + 1;
done = 1;
} else {
cpysz = avail;
}
if (grow_mode) {
/* allow room for a NUL. If this realloc is really a realloc
* (ie: second time around), we get an extra byte. In most
* cases, with the default chunk size of 8K, we will only
* incur that overhead once.  When people have lines longer
* than 8K, we waste 1 byte per additional 8K or so.
* That seems acceptable to me, to avoid making this code
* hard to follow */
bufstart = erealloc(bufstart, current_buf_size + cpysz + 1);
current_buf_size += cpysz + 1;
buf = bufstart + total_copied;
} else {
if (cpysz >= maxlen - 1) {
cpysz = maxlen - 1;
done = 1;
}
}
memcpy(buf, readptr, cpysz);
stream->position += cpysz;
stream->readpos += cpysz;
buf += cpysz;
maxlen -= cpysz;
total_copied += cpysz;
if (done) {
break;
}
} else if (stream->eof) {
break;
} else {
/* XXX: Should be fine to always read chunk_size */
size_t toread;
if (grow_mode) {
toread = stream->chunk_size;
} else {
toread = maxlen - 1;
if (toread > stream->chunk_size) {
toread = stream->chunk_size;
}
}
php_stream_fill_read_buffer(stream, toread);
if (stream->writepos - stream->readpos == 0) {
break;
}
}
}
if (total_copied == 0) {
if (grow_mode) {
assert(bufstart == NULL);
}
return NULL;
}
buf[0] = '\0';
if (returned_len) {
*returned_len = total_copied;
}
return bufstart;
}
'; if (returned_len) { *returned_len = total_copied; } return bufstart; }

Из метода php_stream_get_line видно, что значение NULL возвращается только тогда, когда. указывает, что никакие данные, включая терминаторы, не принимаются в буфер и поток buf.

1.3 Решение

У клиента есть два способа установить разумный тайм-аут:

1.3.1 int_set

ini_set(‘default_socket_timeout’, -1);

1.3.2 Настройка

$redis->SetOption(Redis::OPT_READ_TIMEOUT, -1);

Примечание: -1 означает отсутствие тайм-аута, вы также можете установить тайм-аут в нужное время. Предыдущее время повторения установлено равным 0,01 мс.

Повторное использование отключенных соединений

Использование отключенных соединений также может привести к “ошибке чтения при подключении”, что требует различия между “Соединение закрыто” и “Соединение потеряно”.

2.1 отключение

2.1.1 Соединение закрыто

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

pconnect("127.0.0.1", 6390);
    if ($ret == false) {
        echo "Connect return false";
        exit;
    }

    $rds->close();
    
    var_dump($rds->get("aa"));
} catch (Exception $e) {
    var_dump ($e);
}

Результаты испытаний следующие:

2.1.2 Соединение потеряно

См. раздел Работа над PHP-ошибкой проверки живучести для написания тестового сценария. PHP следующим образом. После подключения redis завершите процесс redis перед выполнением команды:

pconnect("127.0.0.1", 6390);
    if ($ret == false) {
        echo "Connect return false";
        exit;
    }

    echo "Press any key to continue ...";
    fgetc(STDIN);
    var_dump($rds->get("aa"));
} catch (Exception $e) {
    var_dump ($e);
}

Если

Этапы реализации заключаются в следующем

  1. Терминал выполняет PHP-тест. PHP скрипт
  2. Откройте другой терминал, убейте процесс redis
  3. Произвольный ввод и возврат первого терминала

Там будет потеряна связь’

2.1.3 ошибка считывания при подключении

При подключении redis возвращается ошибка чтения при подключении, если соединение отключено во время выполнения команды. Тестовый сценарий выглядит следующим образом:

pconnect("127.0.0.1", 6390);
    if ($ret == false) {
        echo "Connect return false";
        exit;
    }

    while(1){
       $rds->get("aa");
    }
    
} catch (Exception $e) {
    var_dump ($e);
}

Если

Этапы реализации заключаются в следующем

  1. Терминал выполняет PHP-тест. PHP скрипт
  2. Откройте другой терминал, убейте процесс redis

В этот момент возникает исключение:

Или откройте новый терминал для подключения к серверу redis и выполните убийство клиента следующим образом:

Выполняемый PHP-скрипт также фиксирует ошибку чтения исключения при подключении.

2.2 php-fpm и pconnect

В режиме cli сервер redis подключается с помощью PHP через pconnect. Хотя бизнес-код показывает, что вызов закрыт, на самом деле соединение не отключено. FPM поддерживает соединение с redis. Когда следующий запрос должен снова выполнить pconnect, redis на самом деле не будет запрашиваться для установления соединения. Это также создает проблему. Если соединение было отключено, следующий запрос может напрямую использовать последнее отключенное соединение. Для этого у phpredis также есть комментарии к исходному коду, подробнее см. php-src.

Поэтому отключенное соединение повторного использования php-fpm может привести к таким ошибкам.

Самое простое решение в этой ситуации-заменить длинную ссылку на короткую.

Резюме

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

IV. Ссылка

[1] ошибка чтения redis при подключении и проверка ошибок сервера Redis удаляются

[2] Работа над PHP-ошибкой проверки живучести

[3] Проблема тайм-аута подписки Phpredis и ее решение

[4] php-src

[5] phпредис

Оригинал: “https://developpaper.com/two-reasons-for-read-error-on-connection/”