我想使用a Stream
来并行处理一组未知数量的远程存储的JSON文件的异类(文件数量事先未知)。文件的大小可以相差很大,从每个文件1个JSON记录到其他文件中的100,000个记录。一个JSON记录在这种情况下是指表示为文件中的一条线的自包含JSON对象。
我真的很想为此使用Streams,所以我实现了这一点Spliterator
:
public abstract class JsonStreamSpliterator<METADATA, RECORD> extends AbstractSpliterator<RECORD> {
abstract protected JsonStreamSupport<METADATA> openInputStream(String path);
abstract protected RECORD parse(METADATA metadata, Map<String, Object> json);
private static final int ADDITIONAL_CHARACTERISTICS = Spliterator.IMMUTABLE | Spliterator.DISTINCT | Spliterator.NONNULL;
private static final int MAX_BUFFER = 100;
private final Iterator<String> paths;
private JsonStreamSupport<METADATA> reader = null;
public JsonStreamSpliterator(Iterator<String> paths) {
this(Long.MAX_VALUE, ADDITIONAL_CHARACTERISTICS, paths);
}
private JsonStreamSpliterator(long est, int additionalCharacteristics, Iterator<String> paths) {
super(est, additionalCharacteristics);
this.paths = paths;
}
private JsonStreamSpliterator(long est, int additionalCharacteristics, Iterator<String> paths, String nextPath) {
this(est, additionalCharacteristics, paths);
open(nextPath);
}
@Override
public boolean tryAdvance(Consumer<? super RECORD> action) {
if(reader == null) {
String path = takeNextPath();
if(path != null) {
open(path);
}
else {
return false;
}
}
Map<String, Object> json = reader.readJsonLine();
if(json != null) {
RECORD item = parse(reader.getMetadata(), json);
action.accept(item);
return true;
}
else {
reader.close();
reader = null;
return tryAdvance(action);
}
}
private void open(String path) {
reader = openInputStream(path);
}
private String takeNextPath() {
synchronized(paths) {
if(paths.hasNext()) {
return paths.next();
}
}
return null;
}
@Override
public Spliterator<RECORD> trySplit() {
String nextPath = takeNextPath();
if(nextPath != null) {
return new JsonStreamSpliterator<METADATA,RECORD>(Long.MAX_VALUE, ADDITIONAL_CHARACTERISTICS, paths, nextPath) {
@Override
protected JsonStreamSupport<METADATA> openInputStream(String path) {
return JsonStreamSpliterator.this.openInputStream(path);
}
@Override
protected RECORD parse(METADATA metaData, Map<String,Object> json) {
return JsonStreamSpliterator.this.parse(metaData, json);
}
};
}
else {
List<RECORD> records = new ArrayList<RECORD>();
while(tryAdvance(records::add) && records.size() < MAX_BUFFER) {
// loop
}
if(records.size() != 0) {
return records.spliterator();
}
else {
return null;
}
}
}
}
我遇到的问题是,虽然流一开始漂亮地并行化,但最终最大的文件仍留在单个线程中处理。我认为,近端原因已得到充分证明:分离器“不平衡”。
更具体地讲,似乎trySplit
在Stream.forEach
生命周期中的某个点之后没有调用该方法,因此在末尾分发小批量的额外逻辑trySplit
很少执行。
请注意,从trySplit返回的所有拆分paths
器如何共享同一个迭代器。我认为这是在所有拆分器之间平衡工作的非常聪明的方法,但是还不足以实现完全并行。
我希望并行处理首先在文件之间进行,然后在仍然拆分几个大文件时,我希望在剩余文件的大块之间进行并行处理。这就是else
区块末尾的意图trySplit
。
解决这个问题有简单/简单/规范的方法吗?
Long.MAX_VALUE
会导致过多和不必要的拆分,而任何估计Long.MAX_VALUE
都会导致进一步的拆分停止,从而杀死并行性。返回准确的估计值混合似乎不会导致任何智能的优化。
AbstractSpliterator
但被覆盖trySplit()
,这对于除之外的其他任何事情都是不好的组合Long.MAX_VALUE
,因为您没有适应中的大小估计trySplit()
。之后trySplit()
,大小估算值应减少已拆分元素的数量。