Skip to content

Commit 3b249fa

Browse files
Use JNA to Speed up Snapshot Cache File Creation (#68687)
Use JNA to speed up snapshot cache file creation. Do this in `:server` to bypass the security filter and move necessary bits of code to `:server` to enable the logic. Fall back to trying to create the file by writing zeros if anything except for the step determining free disk space fails.
1 parent 114c396 commit 3b249fa

File tree

15 files changed

+187
-84
lines changed

15 files changed

+187
-84
lines changed

server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.elasticsearch.node.InternalSettingsPreparer;
4141
import org.elasticsearch.node.Node;
4242
import org.elasticsearch.node.NodeValidationException;
43+
import org.elasticsearch.snapshots.SnapshotsService;
4344

4445
import java.io.ByteArrayOutputStream;
4546
import java.io.IOException;
@@ -168,6 +169,18 @@ private void setup(boolean addShutdownHook, Environment environment) throws Boot
168169
BootstrapSettings.SYSTEM_CALL_FILTER_SETTING.get(settings),
169170
BootstrapSettings.CTRLHANDLER_SETTING.get(settings));
170171

172+
final long cacheSize = SnapshotsService.SNAPSHOT_CACHE_SIZE_SETTING.get(settings).getBytes();
173+
final long regionSize = SnapshotsService.SNAPSHOT_CACHE_REGION_SIZE_SETTING.get(settings).getBytes();
174+
final int numRegions = Math.toIntExact(cacheSize / regionSize);
175+
final long fileSize = numRegions * regionSize;
176+
if (fileSize > 0) {
177+
try {
178+
Natives.tryCreateCacheFile(environment, fileSize);
179+
} catch (Exception e) {
180+
throw new BootstrapException(e);
181+
}
182+
}
183+
171184
// initialize probes before the security manager is installed
172185
initializeProbes();
173186

server/src/main/java/org/elasticsearch/bootstrap/JNACLibrary.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ protected List<String> getFieldOrder() {
6161

6262
static native String strerror(int errno);
6363

64+
// TODO: Bind POSIX fallocate as well to support non-Linux? (this would only apply to OSX in practice?)
65+
static native int fallocate(int fd, int mode, long offset, long length);
66+
6467
private JNACLibrary() {
6568
}
6669
}

server/src/main/java/org/elasticsearch/bootstrap/JNANatives.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,17 @@
1414

1515
import org.apache.logging.log4j.LogManager;
1616
import org.apache.logging.log4j.Logger;
17+
import org.apache.logging.log4j.message.ParameterizedMessage;
1718
import org.apache.lucene.util.Constants;
19+
import org.elasticsearch.common.SuppressForbidden;
20+
import org.elasticsearch.env.Environment;
1821
import org.elasticsearch.monitor.jvm.JvmInfo;
22+
import org.elasticsearch.snapshots.SnapshotUtils;
1923

24+
import java.io.FileOutputStream;
25+
import java.io.IOException;
26+
import java.lang.reflect.Field;
27+
import java.nio.file.Files;
2028
import java.nio.file.Path;
2129

2230
import static org.elasticsearch.bootstrap.JNAKernel32Library.SizeT;
@@ -260,4 +268,39 @@ static void tryInstallSystemCallFilter(Path tmpFile) {
260268
logger.warn("unable to install syscall filter: ", e);
261269
}
262270
}
271+
272+
@SuppressForbidden(reason = "need access to fd on FileOutputStream")
273+
static void fallocateSnapshotCacheFile(Environment environment, long fileSize) throws IOException {
274+
if (Constants.LINUX == false) {
275+
logger.debug("not trying to create a shared cache file using fallocate on non-Linux platform");
276+
return;
277+
}
278+
Path cacheFile = SnapshotUtils.findCacheSnapshotCacheFilePath(environment, fileSize);
279+
if (cacheFile == null) {
280+
throw new IOException("could not find a directory with adequate free space for cache file");
281+
}
282+
boolean success = false;
283+
try (FileOutputStream fileChannel = new FileOutputStream(cacheFile.toFile())) {
284+
long currentSize = fileChannel.getChannel().size();
285+
if (currentSize < fileSize) {
286+
final Field field = fileChannel.getFD().getClass().getDeclaredField("fd");
287+
field.setAccessible(true);
288+
final int result = JNACLibrary.fallocate((int) field.get(fileChannel.getFD()), 0, currentSize, fileSize - currentSize);
289+
final int errno = result == 0 ? 0 : Native.getLastError();
290+
if (errno == 0) {
291+
success = true;
292+
logger.info("allocated cache file [{}] using fallocate", cacheFile);
293+
} else {
294+
logger.warn("failed to initialize cache file [{}] using fallocate errno [{}]", cacheFile, errno);
295+
}
296+
}
297+
} catch (Exception e) {
298+
logger.warn(new ParameterizedMessage("failed to initialize cache file [{}] using fallocate", cacheFile), e);
299+
} finally {
300+
if (success == false) {
301+
// if anything goes wrong, delete the potentially created file to not waste disk space
302+
Files.deleteIfExists(cacheFile);
303+
}
304+
}
305+
}
263306
}

server/src/main/java/org/elasticsearch/bootstrap/Natives.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111
import org.apache.logging.log4j.LogManager;
1212
import org.apache.logging.log4j.Logger;
13+
import org.elasticsearch.env.Environment;
1314

15+
import java.io.IOException;
1416
import java.nio.file.Path;
1517

1618
/**
@@ -132,4 +134,20 @@ static boolean isSystemCallFilterInstalled() {
132134
}
133135
return JNANatives.LOCAL_SYSTEM_CALL_FILTER;
134136
}
137+
138+
/**
139+
* On Linux, this method tries to create the searchable snapshot frozen cache file using fallocate if JNA is available. This enables
140+
* a much faster creation of the file than the fallback mechanism in the searchable snapshots plugin that will pre-allocate the cache
141+
* file by writing zeros to the file.
142+
*
143+
* @throws IOException on failure to determine free disk space for a data path
144+
*/
145+
public static void tryCreateCacheFile(Environment environment, long fileSize) throws IOException {
146+
if (JNA_AVAILABLE == false) {
147+
logger.warn("cannot use fallocate to create cache file because JNA is not available");
148+
return;
149+
}
150+
JNANatives.fallocateSnapshotCacheFile(environment, fileSize);
151+
}
152+
135153
}

server/src/main/java/org/elasticsearch/env/Environment.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,16 @@ public static FileStore getFileStore(final Path path) throws IOException {
300300
return new ESFileStore(Files.getFileStore(path));
301301
}
302302

303+
public static long getUsableSpace(Path path) throws IOException {
304+
long freeSpaceInBytes = Environment.getFileStore(path).getUsableSpace();
305+
306+
/* See: https://bugs.openjdk.java.net/browse/JDK-8162520 */
307+
if (freeSpaceInBytes < 0) {
308+
freeSpaceInBytes = Long.MAX_VALUE;
309+
}
310+
return freeSpaceInBytes;
311+
}
312+
303313
/**
304314
* asserts that the two environments are equivalent for all things the environment cares about (i.e., all but the setting
305315
* object which may contain different setting)

server/src/main/java/org/elasticsearch/snapshots/SnapshotUtils.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99

1010
import org.elasticsearch.action.support.IndicesOptions;
1111
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
12+
import org.elasticsearch.common.Nullable;
1213
import org.elasticsearch.common.regex.Regex;
14+
import org.elasticsearch.env.Environment;
1315
import org.elasticsearch.index.IndexNotFoundException;
1416

17+
import java.io.IOException;
18+
import java.nio.file.Files;
19+
import java.nio.file.Path;
1520
import java.util.Arrays;
1621
import java.util.HashSet;
1722
import java.util.List;
@@ -107,4 +112,29 @@ public static List<String> filterIndices(List<String> availableIndices, String[]
107112
}
108113
return List.copyOf(result);
109114
}
115+
116+
/**
117+
* Tries to find a suitable path to a searchable snapshots shared cache file in the data paths founds in the environment.
118+
*
119+
* @return path for the cache file or {@code null} if none could be found
120+
*/
121+
@Nullable
122+
public static Path findCacheSnapshotCacheFilePath(Environment environment, long fileSize) throws IOException {
123+
Path cacheFile = null;
124+
for (Path path : environment.dataFiles()) {
125+
Files.createDirectories(path);
126+
// TODO: be resilient to this check failing and try next path?
127+
long usableSpace = Environment.getUsableSpace(path);
128+
Path p = path.resolve(SnapshotsService.CACHE_FILE_NAME);
129+
if (Files.exists(p)) {
130+
usableSpace += Files.size(p);
131+
}
132+
// TODO: leave some margin for error here
133+
if (usableSpace > fileSize) {
134+
cacheFile = p;
135+
break;
136+
}
137+
}
138+
return cacheFile;
139+
}
110140
}

server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.elasticsearch.cluster.RestoreInProgress;
3838
import org.elasticsearch.cluster.SnapshotDeletionsInProgress;
3939
import org.elasticsearch.cluster.SnapshotsInProgress;
40+
import org.elasticsearch.common.unit.ByteSizeValue;
4041
import org.elasticsearch.common.util.CollectionUtils;
4142
import org.elasticsearch.repositories.RepositoryShardId;
4243
import org.elasticsearch.cluster.SnapshotsInProgress.ShardSnapshotStatus;
@@ -132,6 +133,26 @@ public class SnapshotsService extends AbstractLifecycleComponent implements Clus
132133

133134
public static final String UPDATE_SNAPSHOT_STATUS_ACTION_NAME = "internal:cluster/snapshot/update_snapshot_status";
134135

136+
public static final String SHARED_CACHE_SETTINGS_PREFIX = "xpack.searchable.snapshot.shared_cache.";
137+
138+
public static final Setting<ByteSizeValue> SHARED_CACHE_RANGE_SIZE_SETTING = Setting.byteSizeSetting(
139+
SHARED_CACHE_SETTINGS_PREFIX + "range_size",
140+
ByteSizeValue.ofMb(16), // default
141+
Setting.Property.NodeScope
142+
);
143+
public static final Setting<ByteSizeValue> SNAPSHOT_CACHE_REGION_SIZE_SETTING = Setting.byteSizeSetting(
144+
SHARED_CACHE_SETTINGS_PREFIX + "region_size",
145+
SHARED_CACHE_RANGE_SIZE_SETTING,
146+
Setting.Property.NodeScope
147+
);
148+
public static final Setting<ByteSizeValue> SNAPSHOT_CACHE_SIZE_SETTING = Setting.byteSizeSetting(
149+
SHARED_CACHE_SETTINGS_PREFIX + "size",
150+
ByteSizeValue.ZERO,
151+
Setting.Property.NodeScope
152+
);
153+
154+
public static final String CACHE_FILE_NAME = "shared_snapshot_cache";
155+
135156
private final ClusterService clusterService;
136157

137158
private final IndexNameExpressionResolver indexNameExpressionResolver;

x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/process/NativeStorageProvider.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,8 @@ public ByteSizeValue getMinLocalStorageAvailable() {
134134
return minLocalStorageAvailable;
135135
}
136136

137+
// non-static indirection to enable mocking in tests
137138
long getUsableSpace(Path path) throws IOException {
138-
long freeSpaceInBytes = Environment.getFileStore(path).getUsableSpace();
139-
140-
/* See: https://bugs.openjdk.java.net/browse/JDK-8162520 */
141-
if (freeSpaceInBytes < 0) {
142-
freeSpaceInBytes = Long.MAX_VALUE;
143-
}
144-
return freeSpaceInBytes;
139+
return Environment.getUsableSpace(path);
145140
}
146141
}

x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/BaseSearchableSnapshotsIntegTestCase.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.elasticsearch.index.IndexSettings;
2424
import org.elasticsearch.plugins.Plugin;
2525
import org.elasticsearch.snapshots.AbstractSnapshotIntegTestCase;
26+
import org.elasticsearch.snapshots.SnapshotsService;
2627
import org.elasticsearch.xpack.core.searchablesnapshots.MountSearchableSnapshotAction;
2728
import org.elasticsearch.xpack.core.searchablesnapshots.MountSearchableSnapshotRequest;
2829
import org.elasticsearch.xpack.searchablesnapshots.cache.CacheService;
@@ -72,22 +73,22 @@ protected Settings nodeSettings(int nodeOrdinal) {
7273
);
7374
}
7475
builder.put(
75-
FrozenCacheService.SNAPSHOT_CACHE_SIZE_SETTING.getKey(),
76+
SnapshotsService.SNAPSHOT_CACHE_SIZE_SETTING.getKey(),
7677
rarely()
7778
? randomBoolean()
7879
? new ByteSizeValue(randomIntBetween(0, 10), ByteSizeUnit.KB)
7980
: new ByteSizeValue(randomIntBetween(0, 1000), ByteSizeUnit.BYTES)
8081
: new ByteSizeValue(randomIntBetween(1, 10), ByteSizeUnit.MB)
8182
);
8283
builder.put(
83-
FrozenCacheService.SNAPSHOT_CACHE_REGION_SIZE_SETTING.getKey(),
84+
SnapshotsService.SNAPSHOT_CACHE_REGION_SIZE_SETTING.getKey(),
8485
rarely()
8586
? new ByteSizeValue(randomIntBetween(4, 1024), ByteSizeUnit.KB)
8687
: new ByteSizeValue(randomIntBetween(1, 10), ByteSizeUnit.MB)
8788
);
8889
if (randomBoolean()) {
8990
builder.put(
90-
FrozenCacheService.FROZEN_CACHE_RANGE_SIZE_SETTING.getKey(),
91+
SnapshotsService.SHARED_CACHE_RANGE_SIZE_SETTING.getKey(),
9192
rarely()
9293
? new ByteSizeValue(randomIntBetween(4, 1024), ByteSizeUnit.KB)
9394
: new ByteSizeValue(randomIntBetween(1, 10), ByteSizeUnit.MB)

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import org.elasticsearch.rest.RestController;
5858
import org.elasticsearch.rest.RestHandler;
5959
import org.elasticsearch.script.ScriptService;
60+
import org.elasticsearch.snapshots.SnapshotsService;
6061
import org.elasticsearch.snapshots.SourceOnlySnapshotRepository;
6162
import org.elasticsearch.threadpool.ExecutorBuilder;
6263
import org.elasticsearch.threadpool.ScalingExecutorBuilder;
@@ -250,9 +251,9 @@ public List<Setting<?>> getSettings() {
250251
CacheService.SNAPSHOT_CACHE_MAX_FILES_TO_SYNC_AT_ONCE_SETTING,
251252
CacheService.SNAPSHOT_CACHE_SYNC_SHUTDOWN_TIMEOUT,
252253
SearchableSnapshotEnableAllocationDecider.SEARCHABLE_SNAPSHOTS_ALLOCATE_ON_ROLLING_RESTART,
253-
FrozenCacheService.SNAPSHOT_CACHE_SIZE_SETTING,
254-
FrozenCacheService.SNAPSHOT_CACHE_REGION_SIZE_SETTING,
255-
FrozenCacheService.FROZEN_CACHE_RANGE_SIZE_SETTING,
254+
SnapshotsService.SNAPSHOT_CACHE_SIZE_SETTING,
255+
SnapshotsService.SNAPSHOT_CACHE_REGION_SIZE_SETTING,
256+
SnapshotsService.SHARED_CACHE_RANGE_SIZE_SETTING,
256257
FrozenCacheService.FROZEN_CACHE_RECOVERY_RANGE_SIZE_SETTING,
257258
FrozenCacheService.SNAPSHOT_CACHE_MAX_FREQ_SETTING,
258259
FrozenCacheService.SNAPSHOT_CACHE_DECAY_INTERVAL_SETTING,

0 commit comments

Comments
 (0)