Блокирует ли Golang (* http.ResponseWriter) метод Write () метод до тех пор, пока клиент не получит данные? - PullRequest
0 голосов
/ 01 мая 2020

Я задаю этот вопрос, потому что у меня было очень странное и удивительное впечатление, о котором я собираюсь рассказать.

Я настраиваю сервер HTTP API, чтобы наблюдать за его поведением при наличии задержки между сервером и клиентами , У меня была установка, состоящая из одного сервера и дюжины клиентов, связанных с 10Gbps Ethe rnet fabri c. Я измерил время, необходимое для обслуживания определенных запросов API в 5 сценариях ios. В каждом сценарии я устанавливаю задержку между сервером и клиентами на одно из значений: без задержки (я называю это базовым значением), 25 мс, 50 ​​мс, 250 мс или 400 мс, используя утилиту tc-netem(8).

, потому что Я использую сегменты гистограммы для количественной оценки времени обслуживания, я заметил, что все запросы были обработаны менее чем за 50 мс, независимо от сценария, что явно не имеет никакого смысла, как, например, в случае 400 мс, это должно быть по крайней мере около 400 мс (так как я измеряю только длительность с момента обращения запроса к серверу до момента возврата функции HTTP Write()). Обратите внимание, что размер объектов ответа составляет от 1 до 10 КБ.

Изначально у меня были сомнения, что функция *http.ResponsWriter Write() была асинхронной и возвращалась непосредственно перед тем, как данные были получены клиентом. Итак, я решил проверить эту гипотезу, написав игрушечный HTTP-сервер, который обслуживает содержимое файла, сгенерированного с использованием dd(1) и /dev/urandom, чтобы иметь возможность перенастроить размер ответа. Вот сервер:

var response []byte

func httpHandler(w http.ResponseWriter, r * http.Request) {

    switch r.Method {
        case "GET":
            now: = time.Now()
            w.Write(response)
            elapsed: = time.Since(now)
            mcs: = float64(elapsed / time.Microsecond)
            s: = elapsed.Seconds()
            log.Printf("Elapsed time in mcs: %v, sec:%v", mcs, s)
    }
}

func main() {
    response, _ = ioutil.ReadFile("BigFile")

    http.HandleFunc("/hd", httpHandler)
    http.ListenAndServe(":8089", nil)
}

Затем я запускаю сервер следующим образом: dd if=/dev/urandom of=BigFile bs=$VARIABLE_SIZE count=1 && ./server

со стороны клиента, я выдаю time curl -X GET $SERVER_IP:8089/hd --output /dev/null

Я пытался с много значений $ VARIABLE_SIZE из диапазона [1Kb, 500Mb] с использованием эмулированной задержки в 400 мс между сервером и каждым из клиентов. Короче говоря, я заметил, что метод Write() блокируется до тех пор, пока данные не будут отправлены, когда размер ответа будет достаточно большим, чтобы его можно было визуально заметить (порядка десятков мегабайт). Однако, когда размер ответа невелик, сервер не сообщает о ментально-нормальном времени обслуживания по сравнению со значением, сообщаемым клиентом. Для файла размером 10 КБ клиент сообщает о 1,6 секундах, а сервер сообщает об 67 микросекундах (что совершенно не имеет смысла, даже для меня, как человека, я заметил небольшую задержку порядка секунды, о чем сообщает клиент) ,

Чуть дальше go я попытался выяснить, начиная с какого размера ответа сервер возвращает мысленно приемлемое время. После многих испытаний с использованием алгоритма двоичного поиска я обнаружил, что сервер всегда возвращает несколько микросекунд [20us, 600us] для ответов размером менее 86501 байт и возвращает ожидаемое (приемлемое) время для запросов, которые> = 86501 байт (обычно половина времени сообщается клиентом). Например, для ответа 86501 байт клиент сообщил 4 секунды, а сервер - 365 микросекунд. Для 86502 байтов клиент сообщил о 4s, а сервер сообщил о 1.6s. Я повторил этот опыт много раз, используя разные серверы, поведение всегда одинаково. Число 86502 выглядит как волшебный c !!

Этот опыт объясняет странные наблюдения, которые у меня были изначально, потому что все ответы API имели размер менее 10 Кб. Однако это открывает дверь для серьезного вопроса. Что, черт возьми, происходит на земле и как объяснить это поведение?

Я пытался искать ответы, но ничего не нашел. Единственное, о чем я могу думать, это, может быть, это связано с размером сокетов Linux и с тем, делает ли Go системный вызов неблокирующим способом. Однако AFAIK, TCP пакеты, передающие ответы HTTP, должны быть подтверждены получателем (клиентом) до того, как отправитель (сервер) сможет вернуться! Нарушение этого предположения (как это выглядит в данном случае) может привести к бедствиям! Может кто-нибудь дать объяснение этому странному поведению?

Технические данные:

Go version: 12
OS: Debian Buster
Arch: x86_64

1 Ответ

2 голосов
/ 01 мая 2020

Я бы предположил, что на самом деле вопрос сформулирован недопустимо: кажется, вы гадаете о том, как работает HTTP, а не просматриваете весь стек.

Первое, что нужно рассмотреть, - это HTTP ( 1.0 и 1.1, которая является стандартной версией, поскольку долгое время go) не указывает никаких средств для подтверждения приема данных ни одной из сторон.
Существует неявное подтверждение того факта, что сервер получил запрос клиента - сервер ожидается, что он ответит на запрос, и когда он отвечает, клиент может быть разумно уверен, что сервер фактически получил запрос.
Хотя нет такой вещи, работающей в другом направлении: сервер не ожидает, что клиент каким-то образом «сообщить обратно» - на уровне HTTP - что ему удалось прочитать ответ всего сервера.

Второе, что следует учитывать, - это то, что HTTP передается по TCP-соединениям (или TLS, что на самом деле не отличается от он также использует TCP).

Часто забытый факт о TCP состоит в том, что не имеет кадров сообщения - , то есть TCP выполняет двунаправленную передачу непрозрачных байтов. TCP гарантирует только полное упорядочение байтов в этих потоках; он никоим образом не сохраняет случайные «пакетные операции», которые, естественно, могут возникнуть в результате того, как вы работаете с TCP через типичный интерфейс программирования - вызывая какую-то функцию «записать этот набор байтов».

Другой Что часто забывают о TCP, так это то, что, хотя он действительно использует подтверждения для отслеживания того, какая часть исходящего потока была фактически получена получателем, это подробности протокола, которые не раскрываются на уровне интерфейса программирования (по крайней мере, в общем реализация TCP мне известна).

Эти функции означают, что если кто-то хочет использовать TCP для обмена данными, ориентированного на сообщения, ему необходимо реализовать поддержку как границ сообщений (так называемое «кадрирование»), так и подтверждение приема отдельных сообщений в прокотол выше ПТС. HTTP - это протокол, который выше TCP, но, хотя он реализует кадрирование, он не реализует явное подтверждение, кроме того, что сервер отвечает клиенту, как описано выше.

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

Наконец, учтите, что наиболее часто используемые реализации TCP обеспечивают отправку данных в активное TCP-соединение посредством использования вызова, позволяющего передать фрагмент байтов произвольной длины. Учитывая описанную выше буферизацию, такой вызов обычно блокируется, пока все отправленные данные не будут скопированы в буфер отправки. Если в буфере нет места, вызов блокируется до тех пор, пока стеку TCP не удастся направить некоторое количество данных из этого буфера в соединение - освободить место для приема большего количества данных от клиента.

Что все выше означает, что net/http.ResponseWriter.Write взаимодействует с типичным современным стеком TCP / IP?

  • При вызове Write будет равномерно пытаться отправить указанные данные в стек TCP / IP.
  • Стек будет пытаться скопировать эти данные в буфер отправки соответствующего TCP-соединения - блокировка, пока все данные не удастся скопировать.
  • После этого вы по существу потеряли контроль над тем, что происходит с этим данные: в конечном итоге он может быть успешно доставлен получателю, или он может полностью потерпеть неудачу, или какая-то его часть может быть успешной, а остальные - нет.

Что это значит для вас, это когда net/http.ResponseWriter.Write блоков, он блокирует буфер отправки TCP-сокета, лежащего в основе HTTP-соединения, с которым вы работаете.


Обратите внимание, что если стек TCP / IP обнаруживает неисправимую проблему с соединением, лежащим в основе вашего обмена запросами / ответами HTTP - например, кадр с флагом RST, поступающим из удаленной части, означающий, что соединение было неожиданно разорвано down - эта проблема также вызовет стек HTTP Go, и Write вернет ошибку, отличную от nil. В этом случае вы будете знать, что клиент, вероятно, не смог получить полный ответ.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...