-
Notifications
You must be signed in to change notification settings - Fork 3.8k
fix: dedicated ephemeral storage loading pool for tasks #19658
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,9 +20,12 @@ | |
| package org.apache.druid.indexing.common; | ||
|
|
||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.google.common.annotations.VisibleForTesting; | ||
| import com.google.inject.Inject; | ||
| import org.apache.druid.guice.annotations.EphemeralStorageLoading; | ||
| import org.apache.druid.guice.annotations.Json; | ||
| import org.apache.druid.segment.IndexIO; | ||
| import org.apache.druid.segment.loading.AcquireMode; | ||
| import org.apache.druid.segment.loading.LeastBytesUsedStorageLocationSelectorStrategy; | ||
| import org.apache.druid.segment.loading.SegmentCacheManager; | ||
| import org.apache.druid.segment.loading.SegmentLoaderConfig; | ||
|
|
@@ -43,26 +46,45 @@ public class SegmentCacheManagerFactory | |
| { | ||
| private final IndexIO indexIO; | ||
| private final ObjectMapper jsonMapper; | ||
| private final StorageLoadingThreadPool loadingThreadPool; | ||
|
|
||
| @Inject | ||
| public SegmentCacheManagerFactory( | ||
| IndexIO indexIO, | ||
| @Json ObjectMapper mapper | ||
| @Json ObjectMapper mapper, | ||
| @EphemeralStorageLoading StorageLoadingThreadPool loadingThreadPool | ||
| ) | ||
| { | ||
| this.indexIO = indexIO; | ||
| this.jsonMapper = mapper; | ||
| this.loadingThreadPool = loadingThreadPool; | ||
| } | ||
|
|
||
| /** | ||
| * Creates a new {@link SegmentCacheManager} backed by a new storage location in {@code storageDir}, and a new | ||
| * loading thread pool of default size. | ||
| * Convenience constructor for tests and manual construction. Builds a private, always-virtual loading pool of default | ||
| * size rather than sharing the process-wide {@link EphemeralStorageLoading} pool. The pool is not lifecycle-managed; | ||
| * this is fine for short-lived test JVMs. | ||
| */ | ||
| @VisibleForTesting | ||
| public SegmentCacheManagerFactory( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe that |
||
| IndexIO indexIO, | ||
| ObjectMapper mapper | ||
| ) | ||
| { | ||
| this(indexIO, mapper, StorageLoadingThreadPool.createForEphemeral(new SegmentLoaderConfig())); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a new {@link SegmentCacheManager} backed by a new storage location in {@code storageDir}. When | ||
| * {@code virtualStorage} is true, the returned manager uses this factory's process-wide | ||
| * {@link EphemeralStorageLoading} loading pool (shared across all per-task caches and stopped by the lifecycle) | ||
| * rather than creating its own. | ||
| * | ||
| * @param storageDir storage location | ||
| * @param maxSize size limit, or null for no limit | ||
| * @param virtualStorage whether to configure the cache manager in ephemeral virtual storage mode. In this mode, | ||
| * loading is triggered by {@link SegmentCacheManager#acquireSegment(DataSegment)}, and | ||
| * segment files are deleted as soon as all holds are closed. | ||
| * loading is triggered by {@link SegmentCacheManager#acquireSegment(DataSegment, AcquireMode)}, | ||
| * and segment files are deleted as soon as all holds are closed. | ||
| */ | ||
| public SegmentCacheManager manufacturate(File storageDir, Long maxSize, boolean virtualStorage) | ||
| { | ||
|
|
@@ -77,11 +99,10 @@ public SegmentCacheManager manufacturate(File storageDir, Long maxSize, boolean | |
| .setVirtualStorage(virtualStorage) | ||
| .setVirtualStorageIsEphemeral(virtualStorage); | ||
| final List<StorageLocation> storageLocations = loaderConfig.toStorageLocations(); | ||
| final StorageLoadingThreadPool loadingThreadPool = StorageLoadingThreadPool.createFromConfig(loaderConfig); | ||
| return new SegmentLocalCacheManager( | ||
| storageLocations, | ||
| loaderConfig, | ||
| loadingThreadPool, | ||
| virtualStorage ? loadingThreadPool : StorageLoadingThreadPool.none(), | ||
| new LeastBytesUsedStorageLocationSelectorStrategy(storageLocations), | ||
| indexIO, | ||
| jsonMapper | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
|
|
||
| package org.apache.druid.indexing.common; | ||
|
|
||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import org.apache.druid.java.util.emitter.EmittingLogger; | ||
| import org.apache.druid.segment.TestHelper; | ||
| import org.apache.druid.segment.TestIndex; | ||
| import org.apache.druid.segment.loading.SegmentCacheManager; | ||
| import org.apache.druid.segment.loading.SegmentLoaderConfig; | ||
| import org.apache.druid.segment.loading.StorageLoadingThreadPool; | ||
| import org.apache.druid.server.metrics.NoopServiceEmitter; | ||
| import org.junit.jupiter.api.Assertions; | ||
| import org.junit.jupiter.api.BeforeAll; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.api.io.TempDir; | ||
|
|
||
| import java.io.File; | ||
|
|
||
| class SegmentCacheManagerFactoryTest | ||
| { | ||
| @TempDir | ||
| File tempDir; | ||
|
|
||
| private ObjectMapper jsonMapper; | ||
|
|
||
| @BeforeAll | ||
| static void setUpClass() | ||
| { | ||
| EmittingLogger.registerEmitter(new NoopServiceEmitter()); | ||
| } | ||
|
|
||
| @BeforeEach | ||
| void setUp() | ||
| { | ||
| jsonMapper = TestHelper.makeJsonMapper(); | ||
| } | ||
|
|
||
| @Test | ||
| void testVirtualStorageManagersShareTheInjectedPool() | ||
| { | ||
| final StorageLoadingThreadPool shared = StorageLoadingThreadPool.createForEphemeral(new SegmentLoaderConfig()); | ||
| try { | ||
| final SegmentCacheManagerFactory factory = new SegmentCacheManagerFactory(TestIndex.INDEX_IO, jsonMapper, shared); | ||
| final SegmentCacheManager m1 = factory.manufacturate(new File(tempDir, "a"), null, true); | ||
| final SegmentCacheManager m2 = factory.manufacturate(new File(tempDir, "b"), null, true); | ||
|
|
||
| Assertions.assertSame(shared, m1.getLoadingThreadPool()); | ||
| Assertions.assertSame(shared, m2.getLoadingThreadPool()); | ||
| } | ||
| finally { | ||
| shared.stop(); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| void testNonVirtualStorageManagerDoesNotUseTheSharedPool() | ||
| { | ||
| final StorageLoadingThreadPool shared = StorageLoadingThreadPool.createForEphemeral(new SegmentLoaderConfig()); | ||
| try { | ||
| final SegmentCacheManagerFactory factory = new SegmentCacheManagerFactory(TestIndex.INDEX_IO, jsonMapper, shared); | ||
| final SegmentCacheManager m = factory.manufacturate(new File(tempDir, "c"), null, false); | ||
|
|
||
| Assertions.assertNotSame(shared, m.getLoadingThreadPool()); | ||
| Assertions.assertFalse(m.getLoadingThreadPool().isAvailable()); | ||
| } | ||
| finally { | ||
| shared.stop(); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| void testConvenienceConstructorBuildsOneAvailablePoolPerFactory() | ||
| { | ||
| final SegmentCacheManagerFactory factory = new SegmentCacheManagerFactory(TestIndex.INDEX_IO, jsonMapper); | ||
| final SegmentCacheManager m1 = factory.manufacturate(new File(tempDir, "d"), null, true); | ||
| final SegmentCacheManager m2 = factory.manufacturate(new File(tempDir, "e"), null, true); | ||
| try { | ||
| Assertions.assertTrue(m1.getLoadingThreadPool().isAvailable()); | ||
| // The convenience constructor builds one pool and shares it across the factory's manufacturate calls. | ||
| Assertions.assertSame(m1.getLoadingThreadPool(), m2.getLoadingThreadPool()); | ||
| } | ||
| finally { | ||
| m1.getLoadingThreadPool().stop(); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
|
|
||
| package org.apache.druid.guice.annotations; | ||
|
|
||
| import com.google.inject.BindingAnnotation; | ||
|
|
||
| import java.lang.annotation.ElementType; | ||
| import java.lang.annotation.Retention; | ||
| import java.lang.annotation.RetentionPolicy; | ||
| import java.lang.annotation.Target; | ||
|
|
||
| /** | ||
| * Binding annotation for the {@code StorageLoadingThreadPool} used by ephemeral, per-task segment caches | ||
| */ | ||
| @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) | ||
| @Retention(RetentionPolicy.RUNTIME) | ||
| @BindingAnnotation | ||
| public @interface EphemeralStorageLoading | ||
| { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -63,49 +63,59 @@ public StorageLoadingThreadPool( | |
|
|
||
| public static StorageLoadingThreadPool createFromConfig(final SegmentLoaderConfig config) | ||
| { | ||
| final ListeningExecutorService exec; | ||
| return new StorageLoadingThreadPool(config.isVirtualStorage() ? createOnDemandLoadingExecutor(config) : null); | ||
| } | ||
|
|
||
| /** | ||
| * Build a pool configured for virtual-storage on-demand loading <b>regardless of</b> | ||
| * {@link SegmentLoaderConfig#isVirtualStorage()}. Used for the process-wide loading pool shared by ephemeral, | ||
| * per-task segment caches, whose host process may not itself run in virtual-storage mode. The executor is created | ||
| * eagerly but spawns no threads until work is submitted, so an unused pool is cheap. | ||
| * | ||
| * @see org.apache.druid.guice.annotations.EphemeralStorageLoading | ||
| */ | ||
| public static StorageLoadingThreadPool createForEphemeral(final SegmentLoaderConfig config) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO it would be cleaner to remove this method, add This will simplify the logic in |
||
| { | ||
| return new StorageLoadingThreadPool(createOnDemandLoadingExecutor(config)); | ||
| } | ||
|
|
||
| if (config.isVirtualStorage()) { | ||
| if (config.getVirtualStorageLoadThreads() <= 0) { | ||
| throw DruidException.forPersona(DruidException.Persona.OPERATOR) | ||
| .ofCategory(DruidException.Category.INVALID_INPUT) | ||
| .build( | ||
| "virtualStorageLoadThreads must be greater than 0, got [%d]", | ||
| config.getVirtualStorageLoadThreads() | ||
| ); | ||
| } | ||
| if (config.isVirtualStorageUseVirtualThreads()) { | ||
| log.info( | ||
| "Using virtual storage mode with virtual threads - max concurrent on demand loads: [%d].", | ||
| config.getVirtualStorageLoadThreads() | ||
| ); | ||
| exec = new PermitBoundedListeningExecutorService( | ||
| MoreExecutors.listeningDecorator( | ||
| Executors.newThreadPerTaskExecutor( | ||
| Thread.ofVirtual() | ||
| .name("VirtualStorageOnDemandLoadingThread-", 0) | ||
| .factory() | ||
| ) | ||
| ), | ||
| new Semaphore(config.getVirtualStorageLoadThreads()) | ||
| ); | ||
| } else { | ||
| log.info( | ||
| "Using virtual storage mode with fixed platform thread pool - on demand load threads: [%d].", | ||
| config.getVirtualStorageLoadThreads() | ||
| ); | ||
| exec = MoreExecutors.listeningDecorator( | ||
| Executors.newFixedThreadPool( | ||
| config.getVirtualStorageLoadThreads(), | ||
| Execs.makeThreadFactory("VirtualStorageOnDemandLoadingThread-%s") | ||
| ) | ||
| ); | ||
| } | ||
| private static ListeningExecutorService createOnDemandLoadingExecutor(final SegmentLoaderConfig config) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider value of adding a "purpose" parameter that can enrich the logs based on if it is the ephemeral create or the create from config. would help avoid confusion for an operator who doesn't have process level virtual storage flipped on but still sees logs for it due to ephemeral create. not blocking |
||
| { | ||
| if (config.getVirtualStorageLoadThreads() <= 0) { | ||
| throw DruidException.forPersona(DruidException.Persona.OPERATOR) | ||
| .ofCategory(DruidException.Category.INVALID_INPUT) | ||
| .build( | ||
| "virtualStorageLoadThreads must be greater than 0, got [%d]", | ||
| config.getVirtualStorageLoadThreads() | ||
| ); | ||
| } | ||
| if (config.isVirtualStorageUseVirtualThreads()) { | ||
| log.info( | ||
| "Using virtual storage mode with virtual threads - max concurrent on demand loads: [%d].", | ||
| config.getVirtualStorageLoadThreads() | ||
| ); | ||
| return new PermitBoundedListeningExecutorService( | ||
| MoreExecutors.listeningDecorator( | ||
| Executors.newThreadPerTaskExecutor( | ||
| Thread.ofVirtual() | ||
| .name("VirtualStorageOnDemandLoadingThread-", 0) | ||
| .factory() | ||
| ) | ||
| ), | ||
| new Semaphore(config.getVirtualStorageLoadThreads()) | ||
| ); | ||
| } else { | ||
| exec = null; | ||
| log.info( | ||
| "Using virtual storage mode with fixed platform thread pool - on demand load threads: [%d].", | ||
| config.getVirtualStorageLoadThreads() | ||
| ); | ||
| return MoreExecutors.listeningDecorator( | ||
| Executors.newFixedThreadPool( | ||
| config.getVirtualStorageLoadThreads(), | ||
| Execs.makeThreadFactory("VirtualStorageOnDemandLoadingThread-%s") | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| return new StorageLoadingThreadPool(exec); | ||
| } | ||
|
|
||
| /** | ||
|
|
||
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.
Hmm. The
SegmentCacheManagerFactoryis injected intoDruidInputSource, which will cause the pool to be created anywhere that aDruidInputSourceis instantiated. If used in task specs, that would be Overlord, MM, Peon, Indexer, and possibly even Broker. I don't think we want theEphemeralStorageLoadingpool to exist in all these places. It won't even be used by theDruidInputSourceanyway, sincemanufacturateis called withvirtualStorage = false.Maybe this can be fixed by injecting
IndexIOand@Json ObjectMapperdirectly intoDruidInputSource, rather than injectingSegmentCacheManagerFactory?