Ошибка IncompleteRead при отправке пакета neo4j с удаленного сервера; искаженный HTTP-ответ

Я настроил neo4j на сервере A, и у меня есть приложение, работающее на сервере B, которое должно подключаться к нему.

Если я клонирую приложение на сервер A и запускаю модульные тесты, оно работает нормально. Но запустив их на сервере B, установка выполняется в течение 30 секунд и завершается с ошибкой IncompleteRead:

Traceback (most recent call last):
  File "/usr/local/lib/python2.7/site-packages/nose-1.3.1-py2.7.egg/nose/suite.py", line 208, in run
    self.setUp()
  File "/usr/local/lib/python2.7/site-packages/nose-1.3.1-py2.7.egg/nose/suite.py", line 291, in setUp
    self.setupContext(ancestor)
  File "/usr/local/lib/python2.7/site-packages/nose-1.3.1-py2.7.egg/nose/suite.py", line 314, in setupContext
    try_run(context, names)
  File "/usr/local/lib/python2.7/site-packages/nose-1.3.1-py2.7.egg/nose/util.py", line 469, in try_run
    return func()
  File "/comps/comps/webapp/tests/__init__.py", line 19, in setup
    create_graph.import_films(films)
  File "/comps/comps/create_graph.py", line 49, in import_films
    batch.submit()
  File "/usr/local/lib/python2.7/site-packages/py2neo-1.6.3-py2.7-linux-x86_64.egg/py2neo/neo4j.py", line 2643, in submit
    return [BatchResponse(rs).hydrated for rs in responses.json]
  File "/usr/local/lib/python2.7/site-packages/py2neo-1.6.3-py2.7-linux-x86_64.egg/py2neo/packages/httpstream/http.py", line 563, in json
    return json.loads(self.read().decode(self.encoding))
  File "/usr/local/lib/python2.7/site-packages/py2neo-1.6.3-py2.7-linux-x86_64.egg/py2neo/packages/httpstream/http.py", line 634, in read
    data = self._response.read()
  File "/usr/local/lib/python2.7/httplib.py", line 532, in read
    return self._read_chunked(amt)
  File "/usr/local/lib/python2.7/httplib.py", line 575, in _read_chunked
    raise IncompleteRead(''.join(value))
IncompleteRead: IncompleteRead(131072 bytes read)
-------------------- >> begin captured logging << --------------------
py2neo.neo4j.batch: INFO: Executing batch with 2 requests
py2neo.neo4j.batch: INFO: Executing batch with 1800 requests
--------------------- >> end captured logging << ---------------------

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

Если я использую batch.run() вместо .submit(), я не получаю ошибки, но тесты не проходят; кажется, что партия отклоняется молча. Если я использую .stream() и не перебираю результаты, происходит то же самое, что и .run(); если я перебираю их, я получаю ту же ошибку, что и .submit() (за исключением того, что «прочитано 0 байтов»).

Глядя на httplib.py, можно предположить, что мы получим эту ошибку, когда ответ HTTP имеет Transfer-Encoding: Chunked и не содержит размер фрагмента, который ожидается. Итак, я запустил tcpdump для тестов, и действительно, похоже, это то, что происходит. Последний фрагмент имеет длину 0x8000, а его последние байты равны

"http://10.210.\r\n
0\r\n
\r\n

(Для ясности после \n добавлены разрывы строк.) Это выглядит как правильное разбиение на фрагменты, но 0x8000-й байт — это первый «/», а не второй «.». На восемь байт раньше. Это также не полный ответ, так как это недопустимый JSON.

Интересно, что внутри этого чанка мы получаем следующие данные:

"all_relatio\r\n
1280\r\n
nships":

То есть выглядит как начало нового чанка, но встроенного в старый. Этот новый фрагмент закончился бы в правильном месте (второй «.» выше), если бы мы заметили, что он начинается. А если заголовка фрагмента не было, старый фрагмент заканчивался в правильном месте (восемь байтов позже).

Затем я извлек POST-запрос пакета и запустил его, используя cat batch-request.txt | nc $SERVER_A 7474. Ответ на это был действительным ответом HTTP с фрагментами, содержащим полный действительный объект JSON.

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

cat batch-request.txt | perl -ne 'BEGIN { $| = 1 } for (split //) { select(undef, undef, undef, 0.1) unless int(rand(50)); print }' | nc $SERVER_A 7474

Но он продолжал работать, несмотря на то, что стал намного медленнее.

Я также пытался выполнить tcpdump на сервере A, но запросы к локальному хосту не проходят через tcp.

У меня все еще есть несколько путей, которые я не исследовал: я не выяснил, насколько надежно запрос завершается ошибкой или при каких именно условиях (однажды я видел, как это удается с пакетом, который обычно терпит неудачу, но я не исследовал границы ). И я не пробовал делать запрос напрямую из python, минуя py2neo. Но я не особенно ожидаю, что любой из них будет очень информативным. И я не смотрел внимательно на дамп TCP, за исключением использования wireshark «follow TCP stream» для извлечения HTTP-разговора; Я действительно не знаю, что я буду там искать. Есть большой раздел, который Wireshark выделяет черным цветом в неудачном дампе, и только отдельные строки черного цвета в успешном дампе, может быть, это имеет значение?

Так что пока: кто-нибудь знает, что может происходить? Что-нибудь еще я должен попытаться диагностировать проблему?

Дампы TCP находятся здесь: failed и успешно.

EDIT: я начинаю понимать неудачный дамп TCP. Весь разговор занимает ~ 30 секунд, и есть ~ 28-секундный промежуток, в течение которого оба сервера отправляют TCP-кадры ZeroWindow — это черные линии, о которых я упоминал.

Во-первых, py2neo заполняет окно neo4j; neo4j отправляет кадр с сообщением «мое окно заполнено», а затем еще один кадр, который заполняет окно py2neo. Затем мы тратим ~ 28 секунд, и каждый из них просто говорит: «Ага, мое окно все еще заполнено». В конце концов neo4j снова открывает свое окно, py2neo отправляет еще немного данных, а затем py2neo открывает свое окно. Оба они отправляют немного больше данных, затем py2neo заканчивает отправку своего запроса, а neo4j отправляет больше данных, прежде чем также закончить.

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

Это интересно, но я не уверен, что это значит, что предпоследний кадр TCP, отправленный с neo4j на py2neo, начинается с \r\n1280\r\n — начала поддельного чанка. \r\n8000\r\n, с которого начинается настоящий фрагмент, просто появляется в середине ничем не примечательного кадра TCP. (Это был третий фрейм, отправленный после того, как py2neo завершил отправку почтового запроса.)

EDIT 2: я проверил, где именно висит python. Неудивительно, что это было при отправке запроса, поэтому BatchRequestList._execute() не возвращается до тех пор, пока neo4j не сдастся, поэтому ни .run(), ни .stream() не справились лучше, чем .submit().


person philh    schedule 20.03.2014    source источник


Ответы (1)


Похоже, обходным путем является установка заголовка X-Stream: true;format=pretty. (По умолчанию это просто true; раньше оно было красивым, но оно было удалено из-за эта ошибка (которая выглядит так, как будто это ошибка neo4j, и все еще кажется открытой, но в настоящее время это не проблема для меня).

Похоже, установив format=pretty, мы заставляем neo4j не отправлять никаких данных, пока он не обработает весь ввод. Таким образом, он не пытается отправить данные, не блокируется при отправке и не отказывается читать, пока что-то не отправит.

Полное удаление заголовка X-Stream или установка для него значения false, по-видимому, имеет тот же эффект, что и установка format=pretty (например, заставляет neo4j отправлять ответ, который разбит на фрагменты, красиво напечатан, не содержит кодов состояния и не получает отправляется до тех пор, пока весь запрос не будет обработан), что довольно странно.

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

batch._batch._headers['X-Stream'] = 'true;format=pretty'

Или установите глобальные заголовки с помощью

neo4j._add_header('X-Stream', 'true;format=pretty')
person philh    schedule 21.03.2014