-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
Multi node pipeline executor #4070
base: master
Are you sure you want to change the base?
Conversation
This allows passing an ExecutorService when creating a ClusterPipeline. The previous parallelization approach for pipeline syncing/closing would create a new executor service for each sync operation, resulting in excessive thread creation and termination. On an EC2 m5.12xlarge instance with ~100k single writes/sec, this thread creation consumed 40% CPU and increased operation latency. The change also optimizes thread usage when no ExecutorService is provided. Previously, even a single pipeline within a multipipeline would create 3 threads for syncing. This improvement removes that overhead, though callers are encouraged to provide their own ExecutorService for optimal CPU usage and latency.
@dolavb Thank you for your effort to improve Jedis. |
lets start considering on a pool of executorService instances. And discuss tradeoffs of each solution we have. For sure we are doing this for performance but we need to regard other aspects from API standpoint, like principles of information hiding and encapsulation . |
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.
|
||
syncing = false; | ||
private void closeConnection(Map.Entry<HostAndPort, Queue<Response<?>>> entry) { |
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.
The method name is misleading. It is actually reading the command responses rather than closing the connections
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, will address.
try { | ||
countDownLatch.await(); | ||
awaitAllCompleted.get(); | ||
if (executorService != this.executorService) { |
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 suggest a more explicit approach.
e.g, when creating the MultiNodePipelineBase, if an external executor is provided, have a flag useSharedExecutor = true
.
This way it will be clearer that lifecycle is managed externaly
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, will address.
@atakavci Yes, I think the API considerations are important. It is unfortunate that this now have an API that is essentially a global variable in the static field. Ohterwise, I would have chosen one of the typical approaches:
These approaches would keep changes to the API small by avoiding injecting the ExecutorService in the client's pipeline method. The executorService would be a field in the client config that would be injected from there when creating a pipeline. I did not go for any of these approach because of the unfortunate public static variable. There might be a way to do it, but I am not sure it would be simpler if we want to maintain backward compatibility. So do we want to break people who have started depending on that static field? As for information hiding, I am not sure I understand, a ThreadPool is not information it is resources, if it is the responsibility of the client to create it, it needs to be exposed in some way. I am running a system where resources are important, we monitor all of our threadpools for tuning, and we do this using JMX sensors. I would not use a lib that hides a threadpool. |
hey @dolavb i partially agree on what you stated. There are couple of ways to do it without introducing a breaking change. |
@atakavci I agree there are a couple of ways to do it without adding a breaking change, but I believe the complexity is not worth it. But your call. |
The downside mentioned is a critical one since there is no way to free the resources at will Providing the option for using external ExecutorService will allow for customisable Why do we consider this to be a breaking change if we preserve the default behaviour to be the existing one?
From API perspective, we are adding a new configuration option, which should not be a breaking change |
to get it clear, no breaking solutions proposed in this PR as i see. |
What I can suggest here is to document the API with a clear statement that when external ExecutorService is provided it's lifecycle is not managed by the Jedis client and is the user's responsibility for proper resource management (shutdown). In addition, I suggest wrapping the ExecutorService in simple PipelineExecutor interface with submit/shutdown methods, which will allow us some control in the future if we need to add logging and metrics or even implement an executor that performs the call in the caller thread directly.
@dolavb @atakavci |
/** | ||
* Sub-classes must call this method, if graph commands are going to be used. | ||
* @param connectionProvider connection provider | ||
*/ | ||
protected final void prepareGraphCommands(ConnectionProvider connectionProvider) { | ||
GraphCommandObjects graphCommandObjects = new GraphCommandObjects(connectionProvider); | ||
graphCommandObjects.setBaseCommandArgumentsCreator((comm) -> this.commandObjects.commandArguments(comm)); | ||
super.setGraphCommands(graphCommandObjects); | ||
} |
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.
Graph module support got removed with #4073
@ggivo I agree this is a better approach. @atakavci upon review of your gist I have the following question, why the countdownlatch? Since the introduction of CompletableFuture (JDK8), future composition is the idiomatic way to wait/merge threads. I don't see any backward compatibility concern here since JDK7 is EOL. |
i dont mind if its countdownlatch or Futures, i just did it in min lines of change with existing code. rest is the diff editor's trick. |
While working on this I saw this. Do I understand correctly that we shutdown the topologyRefreshExecutor in the JedisClusterInfoCache whenever we close a pipeline? I apologize for the lack of clean reference. I am not the most versed with GitHub, and could not find a way to better reference this here. |
I don't think that is the case, but let's keep the discussion in referred PR, not to clutter this one |
As part of this PR, should we mark
I would advise for having a default Executor to be provided in the future and this would be in preparation of that change. Otherwise I will keep the comment, but remove the deprecated annotation. |
This allow the configuration of ClusterPipelineExecutor to sync pipeline in parallel. The default implementation remain problematic. This new approach will allow clients to address the performance issue fo the default approach.
@ggivo This makes for a bigger change but offer a path away from the current static implementation. Let me know what you think. |
This allow clients to provide an ExecutorService at pipeline creation instead of having a new ExecutorService created at every pipeline Sync call. An Executor service creation will create new threads, which are expensive resource to creates. In a high throughput application developed internally, we are writing at a rate of ~100k set per seconds, on a six node cluster, on an instance equivalent to an EC2 m5.12xlarge. The creation of threads uses 40% of CPU, and adds substantial latency. This new approach will allow clients to send a pooled Executor that is tuned to there load patterns.
This change is non breaking, but will come with a slight optimization for the clients currently using the created thread pool. In the current approach even if a pipeline has a single connection to close the Executor service will create MULTI_NODE_PIPELINE_SYNC_WORKERS threads. In the default mode would mean wasting 2 thread creation.
From: https://stackoverflow.com/questions/5483047/why-is-creating-a-thread-said-to-be-expensive