-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Automatically detect whether a v1 query could have run on the v2 query engine #13628
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
yashmayya
commented
Jul 16, 2024
- With the v2 / multi-stage query engine becoming more robust and feature complete (https://github.com/apache/pinot/pulls?q=is%3Apr+label%3Amulti-stage+is%3Aclosed), existing users might want to potentially migrate their existing v1 query workload to the multi-stage query engine.
- However, there are some differences between the two query engines in terms of supported syntax (some of which is documented in https://docs.pinot.apache.org/reference/troubleshooting/troubleshoot-multi-stage-query-engine).
- In case a user wants to migrate from the v1 query engine to the v2 query engine, it would be useful to be able to automatically detect whether the existing query workload would work as is without modification. This patch adds a counter metric (optionally enabled via a broker configuration that defaults to false for performance reasons) that is incremented each time a v1 query is run that fails during compilation with the v2 query engine. If this counter remains 0 during regular query workload execution, it signals that users can potentially migrate their query workload to the multi-stage query engine.
- Note that there could be situations where a query compiles successfully but fails during execution. There could also be cases where a query runs successfully but there are differences in the result between the v1 and the v2 query engine - in terms of result types, result set size etc.
- In order to address these above concerns, we plan to also add a tool / API in the near future that actually executes a given query against both the query engines and compares the results. This is much more heavyweight and shouldn't be done for every query during regular execution and is intended more for ad-hoc use cases.
51dd65e to
6f1bc80
Compare
|
Metric name and broker config are up for debate (naming is hard) 😄 |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #13628 +/- ##
============================================
+ Coverage 61.75% 61.99% +0.23%
+ Complexity 207 198 -9
============================================
Files 2436 2554 +118
Lines 133233 140652 +7419
Branches 20636 21879 +1243
============================================
+ Hits 82274 87192 +4918
- Misses 44911 46824 +1913
- Partials 6048 6636 +588
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
6f1bc80 to
9b8e2de
Compare
| if (sqlNode.getKind().equals(SqlKind.EXPLAIN)) { | ||
| sqlNode = ((SqlExplain) sqlNode).getExplicandum(); | ||
| } | ||
| compileAndOptimizeQuery(sqlNode, plannerContext); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the initial version, I'd skipped the optimize step here for perf reasons but turns out that there are some cases where bad queries are only caught in the optimize step. For instance, this test started failing because the bad query compiles fine but the PinotEvaluateLiteralRule catches the issue during the optimize query phase.
2fe750d to
7ad4e58
Compare
| _brokerMetrics.addMeteredGlobalValue(BrokerMeter.QUERIES_GLOBAL, 1); | ||
| _brokerMetrics.addValueToTableGauge(rawTableName, BrokerGauge.REQUEST_SIZE, query.length()); | ||
|
|
||
| if (!pinotQuery.isExplain() && _enableMultistageMigrationMetric) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also add a config to sample the queries we are going to compare. I mean, instead of compiling every query in V2, we can say something like: compile 1 every X queries or a X% of the queries.
Given the high QPS Pinot usually has, this sampling should be good enough to provide a good enough overview without having to parse all queries in V2, which may add some impact.
Alternatively we can delegate this into a background thread that may be reading from a concurrent queue, so running V1 queries won't be affected by the time spent in planning V2. The important part here is that the thread adding into the queue should be using Queue.offer so if the queue is full it just continues (the query won't be reported, but at the same time it won't be blocked).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestions! I do agree that we need to be careful about affecting query latency in the v1 engine due to this additional query compilation overhead. I liked your second option more because the first option can still introduce unpredictable latency in some queries which can increase the tail latency by a fair amount. I did some ad-hoc benchmarking and for some queries, the v2 engine query compilation time is actually comparable in order of magnitude to the overall v1 query execution time (and I'm sure in certain cases it could even be higher).
Also, I'm not a big fan of adding more user configuration than is necessary. I've gone with the second option for now - background thread processing (v2 query compiling) a queue of v1 query requests where the queue capacity is currently hardcoded / not configurable. We could make this queue capacity configurable in the future if we see a need for allowing this to be configurable by users - i.e., in order to tune the percentage of requests that are effectively sampled based on the query workload.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've gone with a simple single threaded executor and a blocking queue implementation for now, but on second thoughts I think we might run into contention issues in high QPS environments with the request processing threads all vying for the blocking queue's internal lock during the call to offer. I think we could potentially solve this using our own additional external lock and just drop requests if the lock isn't free (using Lock.tryLock). Although this would still introduce some additional overhead. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, we could potentially use a lock-free bounded queue (like BoundedFifoBuffer from commons-collections) with some additional logic on our end to avoid busy waiting on the consumer side when the buffer is empty. Or, we could use Java's ConcurrentLinkedQueue which is lock-free but unbounded and add some logic on our end to make it bounded (and also the logic for avoiding busy waiting on the consumer side when the queue is empty). Neither option is too appealing though 😄
Edit: for the waiting logic we'd need our own external lock in any case, so we could just wrap a regular non-synchronized lock-free bounded queue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that lock will be that problematic. Given this feature is behind a config flag, it can be enabled as needed. In case ends up being a performance issue we can just turn it off.
yashmayya
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review Gonzalo!
| _brokerMetrics.addMeteredGlobalValue(BrokerMeter.QUERIES_GLOBAL, 1); | ||
| _brokerMetrics.addValueToTableGauge(rawTableName, BrokerGauge.REQUEST_SIZE, query.length()); | ||
|
|
||
| if (!pinotQuery.isExplain() && _enableMultistageMigrationMetric) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestions! I do agree that we need to be careful about affecting query latency in the v1 engine due to this additional query compilation overhead. I liked your second option more because the first option can still introduce unpredictable latency in some queries which can increase the tail latency by a fair amount. I did some ad-hoc benchmarking and for some queries, the v2 engine query compilation time is actually comparable in order of magnitude to the overall v1 query execution time (and I'm sure in certain cases it could even be higher).
Also, I'm not a big fan of adding more user configuration than is necessary. I've gone with the second option for now - background thread processing (v2 query compiling) a queue of v1 query requests where the queue capacity is currently hardcoded / not configurable. We could make this queue capacity configurable in the future if we see a need for allowing this to be configurable by users - i.e., in order to tune the percentage of requests that are effectively sampled based on the query workload.
|
Approved. Waiting to fix the conflicts, which should be pretty simple given they are in the CommonConstants. |
ccca689 to
8639e71
Compare
…ion from broker v1 request processing threads
8639e71 to
bc4c32b
Compare