Skip to content

Commit 3dc00db

Browse files
authored
Quicker shared cache file preallocation (#79447)
Reworks preallocation of the shared_cache file, which was very slow on Windows. Instead of manually filling the file with 0's, this new approach uses the RandomAccessFile.setLength() method, which can quickly allocate a file of the given size (tested that this took only seconds to preallocate TB-size file on Windows).
1 parent fef1e43 commit 3dc00db

File tree

7 files changed

+76
-46
lines changed

7 files changed

+76
-46
lines changed

x-pack/plugin/searchable-snapshots/preallocate/src/main/java/org/elasticsearch/xpack/searchablesnapshots/preallocate/LinuxPreallocator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
final class LinuxPreallocator implements Preallocator {
1717

1818
@Override
19-
public boolean available() {
19+
public boolean useNative() {
2020
return Natives.NATIVES_AVAILABLE;
2121
}
2222

x-pack/plugin/searchable-snapshots/preallocate/src/main/java/org/elasticsearch/xpack/searchablesnapshots/preallocate/MacOsPreallocator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
final class MacOsPreallocator implements Preallocator {
2121

2222
@Override
23-
public boolean available() {
23+
public boolean useNative() {
2424
return Natives.NATIVES_AVAILABLE;
2525
}
2626

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77

88
package org.elasticsearch.xpack.searchablesnapshots.preallocate;
99

10-
final class UnsupportedPreallocator implements Preallocator {
10+
final class NoNativePreallocator implements Preallocator {
1111

1212
@Override
13-
public boolean available() {
13+
public boolean useNative() {
1414
return false;
1515
}
1616

x-pack/plugin/searchable-snapshots/preallocate/src/main/java/org/elasticsearch/xpack/searchablesnapshots/preallocate/Preallocate.java

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
import org.apache.logging.log4j.Logger;
1212
import org.apache.logging.log4j.message.ParameterizedMessage;
1313
import org.apache.lucene.util.Constants;
14+
import org.elasticsearch.common.unit.ByteSizeValue;
1415
import org.elasticsearch.core.SuppressForbidden;
1516

1617
import java.io.FileOutputStream;
1718
import java.io.IOException;
19+
import java.io.RandomAccessFile;
1820
import java.lang.reflect.Field;
1921
import java.nio.file.Files;
2022
import java.nio.file.Path;
@@ -31,35 +33,53 @@ public static void preallocate(final Path cacheFile, final long fileSize) throws
3133
} else if (Constants.MAC_OS_X) {
3234
preallocate(cacheFile, fileSize, new MacOsPreallocator());
3335
} else {
34-
preallocate(cacheFile, fileSize, new UnsupportedPreallocator());
36+
preallocate(cacheFile, fileSize, new NoNativePreallocator());
3537
}
3638
}
3739

3840
@SuppressForbidden(reason = "need access to fd on FileOutputStream")
3941
private static void preallocate(final Path cacheFile, final long fileSize, final Preallocator prealloactor) throws IOException {
40-
if (prealloactor.available() == false) {
41-
logger.warn("failed to pre-allocate cache file [{}] as native methods are not available", cacheFile);
42-
}
4342
boolean success = false;
44-
try (FileOutputStream fileChannel = new FileOutputStream(cacheFile.toFile())) {
45-
long currentSize = fileChannel.getChannel().size();
46-
if (currentSize < fileSize) {
47-
final Field field = AccessController.doPrivileged(new FileDescriptorFieldAction(fileChannel));
48-
final int errno = prealloactor.preallocate((int) field.get(fileChannel.getFD()), currentSize, fileSize - currentSize);
49-
if (errno == 0) {
43+
try {
44+
if (prealloactor.useNative()) {
45+
try (FileOutputStream fileChannel = new FileOutputStream(cacheFile.toFile())) {
46+
long currentSize = fileChannel.getChannel().size();
47+
if (currentSize < fileSize) {
48+
logger.info("pre-allocating cache file [{}] ({}) using native methods", cacheFile, new ByteSizeValue(fileSize));
49+
final Field field = AccessController.doPrivileged(new FileDescriptorFieldAction(fileChannel));
50+
final int errno = prealloactor.preallocate(
51+
(int) field.get(fileChannel.getFD()),
52+
currentSize,
53+
fileSize - currentSize
54+
);
55+
if (errno == 0) {
56+
success = true;
57+
logger.debug("pre-allocated cache file [{}] using native methods", cacheFile);
58+
} else {
59+
logger.warn(
60+
"failed to pre-allocate cache file [{}] using native methods, errno: [{}], error: [{}]",
61+
cacheFile,
62+
errno,
63+
prealloactor.error(errno)
64+
);
65+
}
66+
}
67+
} catch (final Exception e) {
68+
logger.warn(new ParameterizedMessage("failed to pre-allocate cache file [{}] using native methods", cacheFile), e);
69+
}
70+
}
71+
// even if allocation was successful above, verify again here
72+
try (RandomAccessFile raf = new RandomAccessFile(cacheFile.toFile(), "rw")) {
73+
if (raf.length() != fileSize) {
74+
logger.info("pre-allocating cache file [{}] ({}) using setLength method", cacheFile, new ByteSizeValue(fileSize));
75+
raf.setLength(fileSize);
5076
success = true;
51-
logger.debug("pre-allocated cache file [{}] using native methods", cacheFile);
52-
} else {
53-
logger.warn(
54-
"failed to pre-allocate cache file [{}] using native methods, errno: [{}], error: [{}]",
55-
cacheFile,
56-
errno,
57-
prealloactor.error(errno)
58-
);
77+
logger.debug("pre-allocated cache file [{}] using setLength method", cacheFile);
5978
}
79+
} catch (final Exception e) {
80+
logger.warn(new ParameterizedMessage("failed to pre-allocate cache file [{}] using setLength method", cacheFile), e);
81+
throw e;
6082
}
61-
} catch (final Exception e) {
62-
logger.warn(new ParameterizedMessage("failed to pre-allocate cache file [{}] using native methods", cacheFile), e);
6383
} finally {
6484
if (success == false) {
6585
// if anything goes wrong, delete the potentially created file to not waste disk space

x-pack/plugin/searchable-snapshots/preallocate/src/main/java/org/elasticsearch/xpack/searchablesnapshots/preallocate/Preallocator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ interface Preallocator {
1717
*
1818
* @return true if native methods are available, otherwise false
1919
*/
20-
boolean available();
20+
boolean useNative();
2121

2222
/**
2323
* Pre-allocate a file of given current size to the specified size using the given file descriptor.

x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/cache/shared/SharedBytes.java

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
import org.apache.logging.log4j.LogManager;
1111
import org.apache.logging.log4j.Logger;
12-
import org.elasticsearch.common.io.Channels;
1312
import org.elasticsearch.common.unit.ByteSizeValue;
1413
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
1514
import org.elasticsearch.core.AbstractRefCounted;
@@ -61,27 +60,8 @@ public class SharedBytes extends AbstractRefCounted {
6160
if (fileSize > 0) {
6261
cacheFile = findCacheSnapshotCacheFilePath(environment, fileSize);
6362
Preallocate.preallocate(cacheFile, fileSize);
64-
// TODO: maybe make this faster by allocating a larger direct buffer if this is too slow for very large files
65-
// We fill either the full file or the bytes between its current size and the desired size once with zeros to fully allocate
66-
// the file up front
67-
final ByteBuffer fillBytes = ByteBuffer.allocate(Channels.WRITE_CHUNK_SIZE);
6863
this.fileChannel = FileChannel.open(cacheFile, OPEN_OPTIONS);
69-
long written = fileChannel.size();
70-
if (fileSize < written) {
71-
logger.info("creating shared snapshot cache file [size={}, path={}]", fileSize, cacheFile);
72-
} else if (fileSize == written) {
73-
logger.debug("reusing existing shared snapshot cache file [size={}, path={}]", fileSize, cacheFile);
74-
}
75-
fileChannel.position(written);
76-
while (written < fileSize) {
77-
final int toWrite = Math.toIntExact(Math.min(fileSize - written, Channels.WRITE_CHUNK_SIZE));
78-
fillBytes.position(0).limit(toWrite);
79-
Channels.writeToChannel(fillBytes, fileChannel);
80-
written += toWrite;
81-
}
82-
if (written > fileChannel.size()) {
83-
fileChannel.truncate(fileSize);
84-
}
64+
assert this.fileChannel.size() == fileSize : "expected file size " + fileSize + " but was " + fileChannel.size();
8565
} else {
8666
this.fileChannel = null;
8767
for (Path path : environment.nodeDataPaths()) {

x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/xpack/searchablesnapshots/cache/shared/FrozenCacheServiceTests.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.elasticsearch.cluster.node.DiscoveryNodeRole;
1111
import org.elasticsearch.common.settings.Settings;
1212
import org.elasticsearch.common.settings.SettingsException;
13+
import org.elasticsearch.common.unit.ByteSizeUnit;
1314
import org.elasticsearch.common.unit.ByteSizeValue;
1415
import org.elasticsearch.common.unit.RatioValue;
1516
import org.elasticsearch.common.unit.RelativeByteSizeValue;
@@ -346,4 +347,33 @@ private static CacheKey generateCacheKey() {
346347
randomAlphaOfLength(10)
347348
);
348349
}
350+
351+
public void testCacheSizeChanges() throws IOException {
352+
ByteSizeValue val1 = new ByteSizeValue(randomIntBetween(1, 5), ByteSizeUnit.MB);
353+
Settings settings = Settings.builder()
354+
.put(NODE_NAME_SETTING.getKey(), "node")
355+
.put(FrozenCacheService.SNAPSHOT_CACHE_SIZE_SETTING.getKey(), val1.getStringRep())
356+
.put(FrozenCacheService.SNAPSHOT_CACHE_REGION_SIZE_SETTING.getKey(), new ByteSizeValue(size(100)).getStringRep())
357+
.put("path.home", createTempDir())
358+
.build();
359+
final DeterministicTaskQueue taskQueue = new DeterministicTaskQueue();
360+
try (
361+
NodeEnvironment environment = new NodeEnvironment(settings, TestEnvironment.newEnvironment(settings));
362+
FrozenCacheService cacheService = new FrozenCacheService(environment, settings, taskQueue.getThreadPool())
363+
) {
364+
assertEquals(val1.getBytes(), cacheService.getStats().getSize());
365+
}
366+
367+
ByteSizeValue val2 = new ByteSizeValue(randomIntBetween(1, 5), ByteSizeUnit.MB);
368+
settings = Settings.builder()
369+
.put(settings)
370+
.put(FrozenCacheService.SNAPSHOT_CACHE_SIZE_SETTING.getKey(), val2.getStringRep())
371+
.build();
372+
try (
373+
NodeEnvironment environment = new NodeEnvironment(settings, TestEnvironment.newEnvironment(settings));
374+
FrozenCacheService cacheService = new FrozenCacheService(environment, settings, taskQueue.getThreadPool())
375+
) {
376+
assertEquals(val2.getBytes(), cacheService.getStats().getSize());
377+
}
378+
}
349379
}

0 commit comments

Comments
 (0)