Continue conversation on use of buftok in input plugin - #235
Draft
donoghuc wants to merge 1 commit into
Draft
Conversation
Contributor
Changelog and Version ManagementNo changelog or version changes detected. You can either update them manually or use a comment command to update them automatically on merge:
Omit the entry line to use the PR title. If multiple commands are posted, the last one wins. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug
The HTTP input clones the codec per TCP connection, so different connections are isolated. Requests that arrive on the same connection are dispatched to a shared worker pool, so two requests on one connection can run at the same time against that connection's single
json_linesBufferedTokenizer. That concurrent access drops lines, duplicates lines, and can throwNoSuchElementException, which the input turns into an HTTP 500. A connection that sends one request at a time is safe. The trigger is concurrent, pipelined requests on one connection.Environment
Official image
docker.elastic.co/logstash/logstash:9.3.4. The plugin under test islogstash-input-http4.1.7 and the codec isjson_lines3.2.2, both the stock gems inside the image. Pipeline:How the driver builds a request
Each line is a small JSON object whose
idis globally unique across the whole run. A request body is a block of complete, newline terminated lines, so the server does exactly oneappendper request. The framing is a normal HTTP POST withContent-LengthandConnection: keep-alive.How the driver forces the concurrency
Everything goes over a single socket, which is a single TCP connection, so every request shares that connection's one cloned codec. The driver keeps a sliding window of
WINDOWrequests in flight. It first writesWINDOWrequests back to back without reading any response, then for every response it reads it sends one more. Because the requests are on the wire before their responses are read, the server's worker pool picks several of them up at once and runs them concurrently against the one buffer.Only requests that returned HTTP 200 are treated as accepted. The driver records the exact ids in each accepted request, so the accepted set is precise.
The default run is 4000 requests of 200 lines each, with a window of 16. The window is kept under the input's
max_pending_requestsof 200 so nothing is rejected for backpressure.How loss is measured
The output file holds one JSON line per emitted event. The analyzer compares the set of accepted ids against the set of emitted ids. Anything accepted over HTTP but missing from the output is silent loss. A repeated id is a duplicate.
The run also reads the pipeline
events.incounter from the monitoring API. Because a tokenizer drop happens before an event is created,events.infalling short of the accepted line count places the loss inside the codec rather than in the output.The fix
The change is in
decode_bodyinlib/logstash/inputs/http.rb. Give each request its own codec so its buffer is private and cannot be shared with a concurrent request.This works because
codec.clonere-instantiates the plugin and builds a freshBufferedTokenizer, so nothing stateful is shared across concurrent requests.How each build was tested
Three builds were driven through the identical single connection pipelined load and the same analyzer, so the only variable between rows is the build itself. Each build is a fresh container running the pipeline above, then the same driver, then the same accepted-versus-emitted comparison.
No fix. The stock image, unmodified. The harness starts the container and runs the load with nothing patched. This is the bug as it ships.
Logstash-core side fix. This approach adds
synchronizedto the tokenizer'sappendandflushmethods. The other public methods are already synchronized. It was tested in isolation by taking the exactBufferedTokenizer.javafrom the image, adding only those two keywords, compiling it against the image's own log4j jar, and swapping the resulting class files into a copy oflogstash-core.jarthat is mounted over the container's. Because the two keywords are the only difference from stock, this isolates the effect of that change. A recompiled but otherwise unmodified build reproduced the stock loss exactly, which confirms the swap itself changes nothing else.Plugin side fix. The per-request
cloneindecode_bodyshown above. It was tested by mounting the patchedhttp.rbover the plugin gem file in the container, with everything else identical.Results
Single connection pipelined load, 4000 requests of 200 lines each, repeated runs.
The logstash-core side fix removes the memory corruption, so duplicates and garbled tokens go to zero, but it does not stop the silent loss or the
NoSuchElementExceptionpath. Locking each method does not make the multi calldecodethenflushsequence atomic, and the shared buffer remains. The plugin side fix removes all of it. With the plugin fix every one of the 4000 requests returned HTTP 200, the pipelineevents.indelta equaled the 800,000 accepted lines, and the output contained all 800,000 ids with no duplicates.appendix
the repro code and attempt at profiling is at gist.github.com/donoghuc/a047177d3461ffce414ef908c445960d