This is an automated email from the ASF dual-hosted git repository.

neuyilan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/master by this push:
     new 910b184  [IOTDB-1117][Distributed]Batched creation and fetch of 
RemoteSeriesReader(#2875)
910b184 is described below

commit 910b184f2a157c28adf6e863d48b6184bb49e6c0
Author: wangchao316 <[email protected]>
AuthorDate: Thu Mar 25 20:18:06 2021 +0800

    [IOTDB-1117][Distributed]Batched creation and fetch of 
RemoteSeriesReader(#2875)
---
 .../cluster/query/ClusterDataQueryExecutor.java    |  80 ++++++
 .../iotdb/cluster/query/LocalQueryExecutor.java    | 143 ++++++++++
 .../cluster/query/reader/ClusterReaderFactory.java | 295 +++++++++++++++++++++
 .../query/reader/mult/AbstractMultPointReader.java |  70 +++++
 .../reader/mult/AssignPathManagedMergeReader.java  |  92 +++++++
 .../reader/mult/AssignPathPriorityMergeReader.java |  66 +++++
 .../query/reader/mult/IMultBatchReader.java        |  31 +++
 .../cluster/query/reader/mult/MultBatchReader.java |  73 +++++
 .../query/reader/mult/MultDataSourceInfo.java      | 264 ++++++++++++++++++
 .../cluster/query/reader/mult/MultEmptyReader.java |  52 ++++
 .../reader/mult/MultSeriesRawDataPointReader.java  |  55 ++++
 .../query/reader/mult/RemoteMultSeriesReader.java  | 222 ++++++++++++++++
 .../iotdb/cluster/server/DataClusterServer.java    |  37 +++
 .../cluster/server/service/DataAsyncService.java   |  27 ++
 .../cluster/server/service/DataSyncService.java    |  21 ++
 .../iotdb/cluster/common/TestAsyncDataClient.java  |  35 +++
 .../mult/AssignPathManagedMergeReaderTest.java     | 190 +++++++++++++
 .../mult/MultSeriesRawDataPointReaderTest.java     |  67 +++++
 .../reader/mult/RemoteMultSeriesReaderTest.java    | 286 ++++++++++++++++++++
 .../universal/CachedPriorityMergeReader.java       |   3 +-
 .../reader/universal/DescPriorityMergeReader.java  |   4 +-
 .../iotdb/db/query/reader/universal/Element.java   |  72 +++++
 .../reader/universal/PriorityMergeReader.java      |  39 +--
 thrift/src/main/thrift/cluster.thrift              |  27 ++
 24 files changed, 2212 insertions(+), 39 deletions(-)

diff --git 
a/cluster/src/main/java/org/apache/iotdb/cluster/query/ClusterDataQueryExecutor.java
 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/ClusterDataQueryExecutor.java
index da371d9..9ebde27 100644
--- 
a/cluster/src/main/java/org/apache/iotdb/cluster/query/ClusterDataQueryExecutor.java
+++ 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/ClusterDataQueryExecutor.java
@@ -24,6 +24,8 @@ import 
org.apache.iotdb.cluster.exception.EmptyIntervalException;
 import org.apache.iotdb.cluster.partition.PartitionGroup;
 import org.apache.iotdb.cluster.query.reader.ClusterReaderFactory;
 import org.apache.iotdb.cluster.query.reader.ClusterTimeGenerator;
+import org.apache.iotdb.cluster.query.reader.mult.AbstractMultPointReader;
+import org.apache.iotdb.cluster.query.reader.mult.AssignPathManagedMergeReader;
 import org.apache.iotdb.cluster.server.member.DataGroupMember;
 import org.apache.iotdb.cluster.server.member.MetaGroupMember;
 import org.apache.iotdb.db.exception.StorageEngineException;
@@ -43,9 +45,11 @@ import 
org.apache.iotdb.tsfile.read.query.dataset.QueryDataSet;
 import org.apache.iotdb.tsfile.read.query.timegenerator.TimeGenerator;
 import org.apache.iotdb.tsfile.read.reader.IPointReader;
 
+import com.google.common.collect.Lists;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -65,6 +69,82 @@ public class ClusterDataQueryExecutor extends 
RawDataQueryExecutor {
     this.readerFactory = new ClusterReaderFactory(metaGroupMember);
   }
 
+  /**
+   * use mult batch query for without value filter
+   *
+   * @param context query context
+   * @return query data set
+   * @throws StorageEngineException
+   */
+  @Override
+  public QueryDataSet executeWithoutValueFilter(QueryContext context)
+      throws StorageEngineException {
+    QueryDataSet dataSet = needRedirect(context, false);
+    if (dataSet != null) {
+      return dataSet;
+    }
+    try {
+      List<ManagedSeriesReader> readersOfSelectedSeries = 
initMultSeriesReader(context);
+      return new RawQueryDataSetWithoutValueFilter(
+          context.getQueryId(),
+          queryPlan.getDeduplicatedPaths(),
+          queryPlan.getDeduplicatedDataTypes(),
+          readersOfSelectedSeries,
+          queryPlan.isAscending());
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new StorageEngineException(e.getMessage());
+    } catch (IOException | EmptyIntervalException | QueryProcessException e) {
+      throw new StorageEngineException(e.getMessage());
+    }
+  }
+
+  private List<ManagedSeriesReader> initMultSeriesReader(QueryContext context)
+      throws StorageEngineException, IOException, EmptyIntervalException, 
QueryProcessException {
+    Filter timeFilter = null;
+    if (queryPlan.getExpression() != null) {
+      timeFilter = ((GlobalTimeExpression) 
queryPlan.getExpression()).getFilter();
+    }
+
+    // make sure the partition table is new
+    try {
+      metaGroupMember.syncLeaderWithConsistencyCheck(false);
+    } catch (CheckConsistencyException e) {
+      throw new StorageEngineException(e);
+    }
+    List<ManagedSeriesReader> readersOfSelectedSeries = Lists.newArrayList();
+    List<AbstractMultPointReader> multPointReaders = Lists.newArrayList();
+
+    multPointReaders =
+        readerFactory.getMultSeriesReader(
+            queryPlan.getDeduplicatedPaths(),
+            queryPlan.getDeviceToMeasurements(),
+            queryPlan.getDeduplicatedDataTypes(),
+            timeFilter,
+            null,
+            context,
+            queryPlan.isAscending());
+
+    // combine reader of different partition group of the same path
+    // into a MultManagedMergeReader
+    for (int i = 0; i < queryPlan.getDeduplicatedPaths().size(); i++) {
+      PartialPath partialPath = queryPlan.getDeduplicatedPaths().get(i);
+      TSDataType dataType = queryPlan.getDeduplicatedDataTypes().get(i);
+      AssignPathManagedMergeReader assignPathManagedMergeReader =
+          new AssignPathManagedMergeReader(partialPath.getFullPath(), 
dataType);
+      for (AbstractMultPointReader multPointReader : multPointReaders) {
+        if (multPointReader.getAllPaths().contains(partialPath.getFullPath())) 
{
+          assignPathManagedMergeReader.addReader(multPointReader, 0);
+        }
+      }
+      readersOfSelectedSeries.add(assignPathManagedMergeReader);
+    }
+    if (logger.isDebugEnabled()) {
+      logger.debug("Initialized {} readers for {}", 
readersOfSelectedSeries.size(), queryPlan);
+    }
+    return readersOfSelectedSeries;
+  }
+
   @Override
   protected List<ManagedSeriesReader> initManagedSeriesReader(QueryContext 
context)
       throws StorageEngineException {
diff --git 
a/cluster/src/main/java/org/apache/iotdb/cluster/query/LocalQueryExecutor.java 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/LocalQueryExecutor.java
index 24c7847..f078519 100644
--- 
a/cluster/src/main/java/org/apache/iotdb/cluster/query/LocalQueryExecutor.java
+++ 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/LocalQueryExecutor.java
@@ -26,9 +26,11 @@ import 
org.apache.iotdb.cluster.partition.slot.SlotPartitionTable;
 import org.apache.iotdb.cluster.query.filter.SlotTsFileFilter;
 import org.apache.iotdb.cluster.query.manage.ClusterQueryManager;
 import org.apache.iotdb.cluster.query.reader.ClusterReaderFactory;
+import org.apache.iotdb.cluster.query.reader.mult.IMultBatchReader;
 import org.apache.iotdb.cluster.rpc.thrift.GetAggrResultRequest;
 import org.apache.iotdb.cluster.rpc.thrift.GroupByRequest;
 import org.apache.iotdb.cluster.rpc.thrift.LastQueryRequest;
+import org.apache.iotdb.cluster.rpc.thrift.MultSeriesQueryRequest;
 import org.apache.iotdb.cluster.rpc.thrift.Node;
 import org.apache.iotdb.cluster.rpc.thrift.PreviousFillRequest;
 import org.apache.iotdb.cluster.rpc.thrift.PullSchemaRequest;
@@ -70,6 +72,8 @@ import org.apache.iotdb.tsfile.utils.Pair;
 import org.apache.iotdb.tsfile.write.schema.MeasurementSchema;
 import org.apache.iotdb.tsfile.write.schema.TimeseriesSchema;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -79,6 +83,7 @@ import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import static org.apache.iotdb.session.Config.DEFAULT_FETCH_SIZE;
@@ -151,6 +156,45 @@ public class LocalQueryExecutor {
   }
 
   /**
+   * Fetch a batch from the reader whose id is "readerId".
+   *
+   * @param readerId reader id
+   * @param paths mult series path
+   */
+  public Map<String, ByteBuffer> fetchMultSeries(long readerId, List<String> 
paths)
+      throws ReaderNotFoundException, IOException {
+    IMultBatchReader reader =
+        (IMultBatchReader) 
dataGroupMember.getQueryManager().getReader(readerId);
+    if (reader == null) {
+      throw new ReaderNotFoundException(readerId);
+    }
+
+    Map<String, ByteBuffer> pathByteBuffers = Maps.newHashMap();
+
+    for (String path : paths) {
+      ByteBuffer byteBuffer = null;
+      if (reader.hasNextBatch(path)) {
+        BatchData batchData = reader.nextBatch(path);
+
+        ByteArrayOutputStream byteArrayOutputStream = new 
ByteArrayOutputStream();
+        DataOutputStream dataOutputStream = new 
DataOutputStream(byteArrayOutputStream);
+
+        SerializeUtils.serializeBatchData(batchData, dataOutputStream);
+        logger.debug(
+            "{}: Send results of reader {}, size:{}",
+            dataGroupMember.getName(),
+            readerId,
+            batchData.length());
+        byteBuffer = ByteBuffer.wrap(byteArrayOutputStream.toByteArray());
+      } else {
+        byteBuffer = ByteBuffer.allocate(0);
+      }
+      pathByteBuffers.put(path, byteBuffer);
+    }
+    return pathByteBuffers;
+  }
+
+  /**
    * Create an IBatchReader of a path, register it in the query manager to get 
a reader id for it
    * and send the id back to the requester. If the reader does not have any 
data, an id of -1 will
    * be returned.
@@ -237,6 +281,105 @@ public class LocalQueryExecutor {
   }
 
   /**
+   * Create an IBatchReader of a path, register it in the query manager to get 
a reader id for it
+   * and send the id back to the requester. If the reader does not have any 
data, an id of -1 will
+   * be returned.
+   *
+   * @param request
+   */
+  public long queryMultSeries(MultSeriesQueryRequest request)
+      throws CheckConsistencyException, QueryProcessException, 
StorageEngineException, IOException {
+    logger.debug(
+        "{}: {} is querying {}, queryId: {}",
+        name,
+        request.getRequester(),
+        request.getPath(),
+        request.getQueryId());
+    dataGroupMember.syncLeaderWithConsistencyCheck(false);
+
+    List<PartialPath> paths = Lists.newArrayList();
+    request
+        .getPath()
+        .forEach(
+            fullPath -> {
+              try {
+                paths.add(new PartialPath(fullPath));
+              } catch (IllegalPathException e) {
+                logger.warn("Failed to create partial path, fullPath is {}.", 
fullPath, e);
+              }
+            });
+
+    List<TSDataType> dataTypes = Lists.newArrayList();
+    request
+        .getDataTypeOrdinal()
+        .forEach(
+            dataType -> {
+              dataTypes.add(TSDataType.values()[dataType]);
+            });
+
+    Filter timeFilter = null;
+    Filter valueFilter = null;
+    if (request.isSetTimeFilterBytes()) {
+      timeFilter = FilterFactory.deserialize(request.timeFilterBytes);
+    }
+    if (request.isSetValueFilterBytes()) {
+      valueFilter = FilterFactory.deserialize(request.valueFilterBytes);
+    }
+    Map<String, Set<String>> deviceMeasurements = 
request.getDeviceMeasurements();
+
+    // the same query from a requester correspond to a context here
+    RemoteQueryContext queryContext =
+        queryManager.getQueryContext(
+            request.getRequester(),
+            request.getQueryId(),
+            request.getFetchSize(),
+            request.getDeduplicatedPathNum());
+    logger.debug(
+        "{}: local queryId for {}#{} is {}",
+        name,
+        request.getQueryId(),
+        request.getPath(),
+        queryContext.getQueryId());
+    IBatchReader batchReader =
+        readerFactory.getMultSeriesBatchReader(
+            paths,
+            deviceMeasurements,
+            dataTypes,
+            timeFilter,
+            valueFilter,
+            queryContext,
+            dataGroupMember,
+            request.ascending);
+
+    // if the reader contains no data, send a special id of -1 to prevent the 
requester from
+    // meaninglessly fetching data
+    if (batchReader != null && batchReader.hasNextBatch()) {
+      long readerId = queryManager.registerReader(batchReader);
+      queryContext.registerLocalReader(readerId);
+      logger.debug(
+          "{}: Build a reader of {} for {}#{}, readerId: {}",
+          name,
+          paths,
+          request.getRequester(),
+          request.getQueryId(),
+          readerId);
+      return readerId;
+    } else {
+      logger.debug(
+          "{}: There is no data of {} for {}#{}",
+          name,
+          paths,
+          request.getRequester(),
+          request.getQueryId());
+
+      if (batchReader != null) {
+        batchReader.close();
+      }
+      return -1;
+    }
+  }
+
+  /**
    * Send the timeseries schemas of some prefix paths to the requester. The 
schemas will be sent in
    * the form of a list of MeasurementSchema, but notice the measurements in 
them are the full
    * paths.
diff --git 
a/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/ClusterReaderFactory.java
 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/ClusterReaderFactory.java
index 77ffc01..35fabe3 100644
--- 
a/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/ClusterReaderFactory.java
+++ 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/ClusterReaderFactory.java
@@ -33,7 +33,14 @@ import org.apache.iotdb.cluster.query.RemoteQueryContext;
 import org.apache.iotdb.cluster.query.filter.SlotTsFileFilter;
 import org.apache.iotdb.cluster.query.groupby.RemoteGroupByExecutor;
 import org.apache.iotdb.cluster.query.manage.QueryCoordinator;
+import org.apache.iotdb.cluster.query.reader.mult.AbstractMultPointReader;
+import org.apache.iotdb.cluster.query.reader.mult.MultBatchReader;
+import org.apache.iotdb.cluster.query.reader.mult.MultDataSourceInfo;
+import org.apache.iotdb.cluster.query.reader.mult.MultEmptyReader;
+import org.apache.iotdb.cluster.query.reader.mult.MultSeriesRawDataPointReader;
+import org.apache.iotdb.cluster.query.reader.mult.RemoteMultSeriesReader;
 import org.apache.iotdb.cluster.rpc.thrift.GroupByRequest;
+import org.apache.iotdb.cluster.rpc.thrift.MultSeriesQueryRequest;
 import org.apache.iotdb.cluster.rpc.thrift.Node;
 import org.apache.iotdb.cluster.rpc.thrift.SingleSeriesQueryRequest;
 import org.apache.iotdb.cluster.server.RaftServer;
@@ -64,6 +71,9 @@ import org.apache.iotdb.tsfile.read.filter.basic.Filter;
 import org.apache.iotdb.tsfile.read.reader.IBatchReader;
 import org.apache.iotdb.tsfile.read.reader.IPointReader;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
 import org.apache.thrift.TException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -72,6 +82,7 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 @SuppressWarnings("java:S107")
@@ -202,6 +213,140 @@ public class ClusterReaderFactory {
   }
 
   /**
+   * Create a MultSeriesReader that can read the data of "path" with filters 
in the whole cluster.
+   * The data groups that should be queried will be determined by the 
timeFilter, then for each
+   * group a series reader will be created, and finally all such readers will 
be merged into one.
+   *
+   * @param paths all path
+   * @param deviceMeasurements device to measurements
+   * @param dataTypes data type
+   * @param timeFilter time filter
+   * @param valueFilter value filter
+   * @param context query context
+   * @param ascending asc or aesc
+   * @return
+   * @throws StorageEngineException
+   * @throws EmptyIntervalException
+   */
+  public List<AbstractMultPointReader> getMultSeriesReader(
+      List<PartialPath> paths,
+      Map<String, Set<String>> deviceMeasurements,
+      List<TSDataType> dataTypes,
+      Filter timeFilter,
+      Filter valueFilter,
+      QueryContext context,
+      boolean ascending)
+      throws StorageEngineException, EmptyIntervalException, 
QueryProcessException {
+
+    Map<PartitionGroup, List<PartialPath>> partitionGroupListMap = 
Maps.newHashMap();
+    for (PartialPath partialPath : paths) {
+      List<PartitionGroup> partitionGroups = 
metaGroupMember.routeFilter(timeFilter, partialPath);
+      partitionGroups.forEach(
+          partitionGroup -> {
+            partitionGroupListMap
+                .computeIfAbsent(partitionGroup, n -> new ArrayList<>())
+                .add(partialPath);
+          });
+    }
+
+    List<AbstractMultPointReader> multPointReaders = Lists.newArrayList();
+
+    // different path of the same partition group are constructed as a 
AbstractMultPointReader
+    // if be local partition, constructed a MultBatchReader
+    // if be a remote partition, constructed a RemoteMultSeriesReader
+    for (Map.Entry<PartitionGroup, List<PartialPath>> entityPartitionGroup :
+        partitionGroupListMap.entrySet()) {
+      List<PartialPath> partialPaths = entityPartitionGroup.getValue();
+      Map<String, Set<String>> partitionGroupDeviceMeasurements = 
Maps.newHashMap();
+      List<TSDataType> partitionGroupTSDataType = Lists.newArrayList();
+      partialPaths.forEach(
+          partialPath -> {
+            Set<String> measurements =
+                deviceMeasurements.getOrDefault(partialPath.getDevice(), 
Collections.emptySet());
+            partitionGroupDeviceMeasurements.put(partialPath.getFullPath(), 
measurements);
+            
partitionGroupTSDataType.add(dataTypes.get(paths.lastIndexOf(partialPath)));
+          });
+
+      AbstractMultPointReader abstractMultPointReader =
+          getMultSeriesReader(
+              entityPartitionGroup.getKey(),
+              partialPaths,
+              partitionGroupTSDataType,
+              partitionGroupDeviceMeasurements,
+              timeFilter,
+              valueFilter,
+              context,
+              ascending);
+      multPointReaders.add(abstractMultPointReader);
+    }
+    return multPointReaders;
+  }
+
+  /**
+   * Query one node in "partitionGroup" for data of "path" with "timeFilter" 
and "valueFilter". If
+   * "partitionGroup" contains the local node, a local reader will be 
returned. Otherwise a remote
+   * reader will be returned.
+   *
+   * @param timeFilter nullable
+   * @param valueFilter nullable
+   */
+  private AbstractMultPointReader getMultSeriesReader(
+      PartitionGroup partitionGroup,
+      List<PartialPath> partialPaths,
+      List<TSDataType> dataTypes,
+      Map<String, Set<String>> deviceMeasurements,
+      Filter timeFilter,
+      Filter valueFilter,
+      QueryContext context,
+      boolean ascending)
+      throws StorageEngineException, QueryProcessException {
+    if (partitionGroup.contains(metaGroupMember.getThisNode())) {
+      // the target storage group contains this node, perform a local query
+      DataGroupMember dataGroupMember =
+          metaGroupMember.getLocalDataMember(
+              partitionGroup.getHeader(),
+              String.format(
+                  "Query: %s, time filter: %s, queryId: %d",
+                  partialPaths, timeFilter, context.getQueryId()));
+      Map<String, IPointReader> partialPathPointReaderMap = Maps.newHashMap();
+      for (int i = 0; i < partialPaths.size(); i++) {
+        PartialPath partialPath = partialPaths.get(i);
+        IPointReader seriesPointReader =
+            getSeriesPointReader(
+                partialPath,
+                deviceMeasurements.get(partialPath.getFullPath()),
+                dataTypes.get(i),
+                timeFilter,
+                valueFilter,
+                context,
+                dataGroupMember,
+                ascending);
+        partialPathPointReaderMap.put(partialPath.getFullPath(), 
seriesPointReader);
+      }
+
+      if (logger.isDebugEnabled()) {
+        logger.debug(
+            "{}: creating a local reader for {}#{} of {}",
+            metaGroupMember.getName(),
+            partialPaths,
+            context.getQueryId(),
+            partitionGroup.getHeader());
+      }
+      return new MultSeriesRawDataPointReader(partialPathPointReaderMap);
+    } else {
+      return getRemoteMultSeriesPointReader(
+          timeFilter,
+          valueFilter,
+          dataTypes,
+          partialPaths,
+          deviceMeasurements,
+          partitionGroup,
+          context,
+          ascending);
+    }
+  }
+
+  /**
    * Create a ManagedSeriesReader that can read the data of "path" with 
filters in the whole
    * cluster. The data groups that should be queried will be determined by the 
timeFilter, then for
    * each group a series reader will be created, and finally all such readers 
will be merged into
@@ -395,6 +540,68 @@ public class ClusterReaderFactory {
    * @param timeFilter nullable
    * @param valueFilter nullable
    */
+  private AbstractMultPointReader getRemoteMultSeriesPointReader(
+      Filter timeFilter,
+      Filter valueFilter,
+      List<TSDataType> dataType,
+      List<PartialPath> paths,
+      Map<String, Set<String>> deviceMeasurements,
+      PartitionGroup partitionGroup,
+      QueryContext context,
+      boolean ascending)
+      throws StorageEngineException {
+    MultSeriesQueryRequest request =
+        constructMultQueryRequest(
+            timeFilter,
+            valueFilter,
+            dataType,
+            paths,
+            deviceMeasurements,
+            partitionGroup,
+            context,
+            ascending);
+
+    // reorder the nodes such that the nodes that suit the query best (have 
lowest latenct or
+    // highest throughput) will be put to the front
+    List<Node> orderedNodes = 
QueryCoordinator.getINSTANCE().reorderNodes(partitionGroup);
+
+    MultDataSourceInfo dataSourceInfo =
+        new MultDataSourceInfo(
+            partitionGroup,
+            paths,
+            dataType,
+            request,
+            (RemoteQueryContext) context,
+            metaGroupMember,
+            orderedNodes);
+
+    boolean hasClient = dataSourceInfo.hasNextDataClient(Long.MIN_VALUE);
+    if (hasClient) {
+      return new RemoteMultSeriesReader(dataSourceInfo);
+    } else if (dataSourceInfo.isNoData()) {
+      // there is no satisfying data on the remote node
+      Set<String> fullPaths = Sets.newHashSet();
+      dataSourceInfo
+          .getPartialPaths()
+          .forEach(
+              partialPath -> {
+                fullPaths.add(partialPath.getFullPath());
+              });
+      return new MultEmptyReader(fullPaths);
+    }
+    throw new StorageEngineException(
+        new RequestTimeOutException("Query " + paths + " in " + 
partitionGroup));
+  }
+
+  /**
+   * Query a remote node in "partitionGroup" to get the reader of "path" with 
"timeFilter" and
+   * "valueFilter". Firstly, a request will be sent to that node to construct 
a reader there, then
+   * the id of the reader will be returned so that we can fetch data from that 
node using the reader
+   * id.
+   *
+   * @param timeFilter nullable
+   * @param valueFilter nullable
+   */
   private IPointReader getRemoteSeriesPointReader(
       Filter timeFilter,
       Filter valueFilter,
@@ -441,6 +648,45 @@ public class ClusterReaderFactory {
         new RequestTimeOutException("Query " + path + " in " + 
partitionGroup));
   }
 
+  private MultSeriesQueryRequest constructMultQueryRequest(
+      Filter timeFilter,
+      Filter valueFilter,
+      List<TSDataType> dataTypes,
+      List<PartialPath> paths,
+      Map<String, Set<String>> deviceMeasurements,
+      PartitionGroup partitionGroup,
+      QueryContext context,
+      boolean ascending) {
+    MultSeriesQueryRequest request = new MultSeriesQueryRequest();
+    if (timeFilter != null) {
+      request.setTimeFilterBytes(SerializeUtils.serializeFilter(timeFilter));
+    }
+    if (valueFilter != null) {
+      request.setValueFilterBytes(SerializeUtils.serializeFilter(valueFilter));
+    }
+
+    List<String> fullPaths = Lists.newArrayList();
+    paths.forEach(
+        path -> {
+          fullPaths.add(path.getFullPath());
+        });
+
+    List<Integer> dataTypeOrdinals = Lists.newArrayList();
+    dataTypes.forEach(
+        dataType -> {
+          dataTypeOrdinals.add(dataType.ordinal());
+        });
+
+    request.setPath(fullPaths);
+    request.setHeader(partitionGroup.getHeader());
+    request.setQueryId(context.getQueryId());
+    request.setRequester(metaGroupMember.getThisNode());
+    request.setDataTypeOrdinal(dataTypeOrdinals);
+    request.setDeviceMeasurements(deviceMeasurements);
+    request.setAscending(ascending);
+    return request;
+  }
+
   private SingleSeriesQueryRequest constructSingleQueryRequest(
       Filter timeFilter,
       Filter valueFilter,
@@ -711,6 +957,55 @@ public class ClusterReaderFactory {
   }
 
   /**
+   * Create an IBatchReader of "path" with “timeFilter” and "valueFilter". A 
synchronization with
+   * the leader will be performed according to consistency level
+   *
+   * @param paths
+   * @param dataTypes
+   * @param timeFilter nullable
+   * @param valueFilter nullable
+   * @param context
+   * @return an IBatchReader or null if there is no satisfying data
+   * @throws StorageEngineException
+   */
+  public IBatchReader getMultSeriesBatchReader(
+      List<PartialPath> paths,
+      Map<String, Set<String>> allSensors,
+      List<TSDataType> dataTypes,
+      Filter timeFilter,
+      Filter valueFilter,
+      QueryContext context,
+      DataGroupMember dataGroupMember,
+      boolean ascending)
+      throws StorageEngineException, QueryProcessException, IOException {
+    // pull the newest data
+    try {
+      dataGroupMember.syncLeaderWithConsistencyCheck(false);
+    } catch (CheckConsistencyException e) {
+      throw new StorageEngineException(e);
+    }
+
+    Map<String, IBatchReader> partialPathBatchReaderMap = Maps.newHashMap();
+
+    for (int i = 0; i < paths.size(); i++) {
+      PartialPath partialPath = paths.get(i);
+      SeriesReader seriesReader =
+          getSeriesReader(
+              partialPath,
+              allSensors.get(partialPath.getFullPath()),
+              dataTypes.get(i),
+              timeFilter,
+              valueFilter,
+              context,
+              dataGroupMember.getHeader(),
+              ascending);
+      partialPathBatchReaderMap.put(
+          partialPath.getFullPath(), new 
SeriesRawDataBatchReader(seriesReader));
+    }
+    return new MultBatchReader(partialPathBatchReaderMap);
+  }
+
+  /**
    * Create an IReaderByTimestamp of "path". A synchronization with the leader 
will be performed
    * according to consistency level
    *
diff --git 
a/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/AbstractMultPointReader.java
 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/AbstractMultPointReader.java
new file mode 100644
index 0000000..0cd4d89
--- /dev/null
+++ 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/AbstractMultPointReader.java
@@ -0,0 +1,70 @@
+/*
+ * 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.iotdb.cluster.query.reader.mult;
+
+import org.apache.iotdb.tsfile.read.TimeValuePair;
+import org.apache.iotdb.tsfile.read.reader.IPointReader;
+
+import java.io.IOException;
+import java.util.Set;
+
+public abstract class AbstractMultPointReader implements IPointReader {
+
+  public abstract boolean hasNextTimeValuePair(String fullPath) throws 
IOException;
+
+  public abstract TimeValuePair nextTimeValuePair(String fullPath) throws 
IOException;
+
+  public abstract Set<String> getAllPaths();
+
+  /**
+   * do not support this method
+   *
+   * @return only false
+   * @throws IOException
+   */
+  @Override
+  @Deprecated
+  public boolean hasNextTimeValuePair() throws IOException {
+    return false;
+  }
+
+  /**
+   * do not support this method
+   *
+   * @return only null
+   * @throws IOException
+   */
+  @Override
+  @Deprecated
+  public TimeValuePair nextTimeValuePair() throws IOException {
+    return null;
+  }
+
+  /**
+   * do not support this method
+   *
+   * @return only null
+   * @throws IOException
+   */
+  @Override
+  @Deprecated
+  public TimeValuePair currentTimeValuePair() throws IOException {
+    return null;
+  }
+}
diff --git 
a/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/AssignPathManagedMergeReader.java
 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/AssignPathManagedMergeReader.java
new file mode 100644
index 0000000..34ecc13
--- /dev/null
+++ 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/AssignPathManagedMergeReader.java
@@ -0,0 +1,92 @@
+/*
+ * 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.iotdb.cluster.query.reader.mult;
+
+import org.apache.iotdb.db.query.reader.series.ManagedSeriesReader;
+import org.apache.iotdb.tsfile.file.metadata.enums.TSDataType;
+import org.apache.iotdb.tsfile.read.TimeValuePair;
+import org.apache.iotdb.tsfile.read.common.BatchData;
+
+import java.io.IOException;
+import java.util.NoSuchElementException;
+
+public class AssignPathManagedMergeReader extends AssignPathPriorityMergeReader
+    implements ManagedSeriesReader {
+
+  private static final int BATCH_SIZE = 4096;
+  private volatile boolean managedByPool;
+  private volatile boolean hasRemaining;
+
+  private BatchData batchData;
+  private TSDataType dataType;
+
+  public AssignPathManagedMergeReader(String fullPath, TSDataType dataType) {
+    super(fullPath);
+    this.dataType = dataType;
+  }
+
+  @Override
+  public boolean isManagedByQueryManager() {
+    return managedByPool;
+  }
+
+  @Override
+  public void setManagedByQueryManager(boolean managedByQueryManager) {
+    this.managedByPool = managedByQueryManager;
+  }
+
+  @Override
+  public boolean hasRemaining() {
+    return hasRemaining;
+  }
+
+  @Override
+  public void setHasRemaining(boolean hasRemaining) {
+    this.hasRemaining = hasRemaining;
+  }
+
+  @Override
+  public boolean hasNextBatch() throws IOException {
+    if (batchData != null) {
+      return true;
+    }
+    constructBatch();
+    return batchData != null;
+  }
+
+  private void constructBatch() throws IOException {
+    if (hasNextTimeValuePair()) {
+      batchData = new BatchData(dataType);
+      while (hasNextTimeValuePair() && batchData.length() < BATCH_SIZE) {
+        TimeValuePair next = nextTimeValuePair();
+        batchData.putAnObject(next.getTimestamp(), next.getValue().getValue());
+      }
+    }
+  }
+
+  @Override
+  public BatchData nextBatch() throws IOException {
+    if (!hasNextBatch()) {
+      throw new NoSuchElementException();
+    }
+    BatchData ret = batchData;
+    batchData = null;
+    return ret;
+  }
+}
diff --git 
a/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/AssignPathPriorityMergeReader.java
 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/AssignPathPriorityMergeReader.java
new file mode 100644
index 0000000..30c6f70
--- /dev/null
+++ 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/AssignPathPriorityMergeReader.java
@@ -0,0 +1,66 @@
+/*
+ * 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.iotdb.cluster.query.reader.mult;
+
+import org.apache.iotdb.db.query.reader.universal.Element;
+import org.apache.iotdb.db.query.reader.universal.PriorityMergeReader;
+import org.apache.iotdb.tsfile.read.TimeValuePair;
+
+import java.io.IOException;
+
+/**
+ * This class extends {@link extends PriorityMergeReader} for data sources 
with different
+ * priorities.
+ */
+public class AssignPathPriorityMergeReader extends PriorityMergeReader {
+
+  private String fullPath;
+
+  public AssignPathPriorityMergeReader(String fullPath) {
+    super();
+    this.fullPath = fullPath;
+  }
+
+  public void addReader(AbstractMultPointReader reader, long priority) throws 
IOException {
+    if (reader.hasNextTimeValuePair(fullPath)) {
+      heap.add(
+          new MultElement(
+              reader, reader.nextTimeValuePair(fullPath), new 
MergeReaderPriority(priority, 0)));
+    } else {
+      reader.close();
+    }
+  }
+
+  public class MultElement extends Element {
+    public MultElement(
+        AbstractMultPointReader reader, TimeValuePair timeValuePair, 
MergeReaderPriority priority) {
+      super(reader, timeValuePair, priority);
+    }
+
+    @Override
+    public boolean hasNext() throws IOException {
+      return ((AbstractMultPointReader) reader).hasNextTimeValuePair(fullPath);
+    }
+
+    @Override
+    public void next() throws IOException {
+      timeValuePair = ((AbstractMultPointReader) 
reader).nextTimeValuePair(fullPath);
+    }
+  }
+}
diff --git 
a/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/IMultBatchReader.java
 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/IMultBatchReader.java
new file mode 100644
index 0000000..07c63cf
--- /dev/null
+++ 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/IMultBatchReader.java
@@ -0,0 +1,31 @@
+/*
+ * 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.iotdb.cluster.query.reader.mult;
+
+import org.apache.iotdb.tsfile.read.common.BatchData;
+import org.apache.iotdb.tsfile.read.reader.IBatchReader;
+
+import java.io.IOException;
+
+public interface IMultBatchReader extends IBatchReader {
+
+  boolean hasNextBatch(String fullPath) throws IOException;
+
+  BatchData nextBatch(String fullPath) throws IOException;
+}
diff --git 
a/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/MultBatchReader.java
 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/MultBatchReader.java
new file mode 100644
index 0000000..dd7f21b
--- /dev/null
+++ 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/MultBatchReader.java
@@ -0,0 +1,73 @@
+/*
+ * 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.iotdb.cluster.query.reader.mult;
+
+import org.apache.iotdb.tsfile.read.common.BatchData;
+import org.apache.iotdb.tsfile.read.reader.IBatchReader;
+
+import java.io.IOException;
+import java.util.Map;
+
+public class MultBatchReader implements IMultBatchReader {
+
+  private Map<String, IBatchReader> pathBatchReaders;
+
+  public MultBatchReader(Map<String, IBatchReader> pathBatchReaders) {
+    this.pathBatchReaders = pathBatchReaders;
+  }
+
+  /**
+   * reader has next batch data
+   *
+   * @return true if only one reader has next batch data, otherwise false
+   * @throws IOException
+   */
+  @Override
+  public boolean hasNextBatch() throws IOException {
+    for (IBatchReader reader : pathBatchReaders.values()) {
+      if (reader.hasNextBatch()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public boolean hasNextBatch(String fullPath) throws IOException {
+    return pathBatchReaders.get(fullPath).hasNextBatch();
+  }
+
+  @Override
+  public BatchData nextBatch(String fullPath) throws IOException {
+    return pathBatchReaders.get(fullPath).nextBatch();
+  }
+
+  @Override
+  public BatchData nextBatch() throws IOException {
+    throw new UnsupportedOperationException();
+  }
+
+  /**
+   * close in query resource
+   *
+   * @throws IOException
+   */
+  @Override
+  public void close() throws IOException {}
+}
diff --git 
a/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/MultDataSourceInfo.java
 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/MultDataSourceInfo.java
new file mode 100644
index 0000000..27c9f59
--- /dev/null
+++ 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/MultDataSourceInfo.java
@@ -0,0 +1,264 @@
+/*
+ * 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.iotdb.cluster.query.reader.mult;
+
+import org.apache.iotdb.cluster.client.async.AsyncDataClient;
+import org.apache.iotdb.cluster.client.sync.SyncDataClient;
+import org.apache.iotdb.cluster.config.ClusterDescriptor;
+import org.apache.iotdb.cluster.partition.PartitionGroup;
+import org.apache.iotdb.cluster.query.RemoteQueryContext;
+import org.apache.iotdb.cluster.rpc.thrift.MultSeriesQueryRequest;
+import org.apache.iotdb.cluster.rpc.thrift.Node;
+import org.apache.iotdb.cluster.server.RaftServer;
+import org.apache.iotdb.cluster.server.handlers.caller.GenericHandler;
+import org.apache.iotdb.cluster.server.member.MetaGroupMember;
+import org.apache.iotdb.db.metadata.PartialPath;
+import org.apache.iotdb.db.utils.SerializeUtils;
+import org.apache.iotdb.tsfile.file.metadata.enums.TSDataType;
+import org.apache.iotdb.tsfile.read.filter.TimeFilter;
+import org.apache.iotdb.tsfile.read.filter.basic.Filter;
+import org.apache.iotdb.tsfile.read.filter.factory.FilterFactory;
+import org.apache.iotdb.tsfile.read.filter.operator.AndFilter;
+
+import org.apache.thrift.TException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * provide client which could connect to all nodes of the partitionGroup, and 
mult reader Notice:
+ * methods like getter should be called only after nextDataClient() has been 
called
+ */
+public class MultDataSourceInfo {
+
+  private static final Logger logger = 
LoggerFactory.getLogger(MultDataSourceInfo.class);
+
+  private long readerId;
+  private Node curSource;
+  private PartitionGroup partitionGroup;
+  private List<PartialPath> partialPaths;
+  private List<TSDataType> dataTypes;
+  private MultSeriesQueryRequest request;
+  private RemoteQueryContext context;
+  private MetaGroupMember metaGroupMember;
+  private List<Node> nodes;
+  private int curPos;
+  private boolean isNoData = false;
+  private boolean isNoClient = false;
+
+  public MultDataSourceInfo(
+      PartitionGroup group,
+      List<PartialPath> partialPaths,
+      List<TSDataType> dataTypes,
+      MultSeriesQueryRequest request,
+      RemoteQueryContext context,
+      MetaGroupMember metaGroupMember,
+      List<Node> nodes) {
+    this.readerId = -1;
+    this.partitionGroup = group;
+    this.partialPaths = partialPaths;
+    this.dataTypes = dataTypes;
+    this.request = request;
+    this.context = context;
+    this.metaGroupMember = metaGroupMember;
+    this.nodes = nodes;
+    // set to the last node so after nextDataClient() is called it will scan 
from the first node
+    this.curPos = nodes.size() - 1;
+    this.curSource = nodes.get(curPos);
+  }
+
+  public boolean hasNextDataClient(long timestamp) {
+    if (this.nodes.isEmpty()) {
+      this.isNoData = false;
+      return false;
+    }
+
+    int nextNodePos = (this.curPos + 1) % this.nodes.size();
+    while (true) {
+      Node node = nodes.get(nextNodePos);
+      logger.debug("querying {} from {} of {}", request.path, node, 
partitionGroup.getHeader());
+      try {
+        Long newReaderId = getReaderId(node, timestamp);
+        if (newReaderId != null) {
+          logger.debug("get a readerId {} for {} from {}", newReaderId, 
request.path, node);
+          if (newReaderId != -1) {
+            // register the node so the remote resources can be released
+            context.registerRemoteNode(node, partitionGroup.getHeader());
+            this.readerId = newReaderId;
+            this.curSource = node;
+            this.curPos = nextNodePos;
+            return true;
+          } else {
+            // the id being -1 means there is no satisfying data on the remote 
node, create an
+            // empty reader to reduce further communication
+            this.isNoClient = true;
+            this.isNoData = true;
+            return false;
+          }
+        }
+      } catch (TException | IOException e) {
+        logger.error("Cannot query {} from {}", this.request.path, node, e);
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+        logger.error("Cannot query {} from {}", this.request.path, node, e);
+      }
+      nextNodePos = (nextNodePos + 1) % this.nodes.size();
+      if (nextNodePos == this.curPos) {
+        // has iterate over all nodes
+        isNoClient = true;
+        break;
+      }
+    }
+    // all nodes are failed
+    this.isNoData = false;
+    return false;
+  }
+
+  public List<PartialPath> getPartialPaths() {
+    return partialPaths;
+  }
+
+  private Long getReaderId(Node node, long timestamp)
+      throws TException, InterruptedException, IOException {
+    if (ClusterDescriptor.getInstance().getConfig().isUseAsyncServer()) {
+      return applyForReaderIdAsync(node, timestamp);
+    }
+    return applyForReaderIdSync(node, timestamp);
+  }
+
+  private Long applyForReaderIdAsync(Node node, long timestamp)
+      throws TException, InterruptedException, IOException {
+    AsyncDataClient client =
+        this.metaGroupMember
+            .getClientProvider()
+            .getAsyncDataClient(node, RaftServer.getReadOperationTimeoutMS());
+    AtomicReference<Long> result = new AtomicReference<>();
+    GenericHandler<Long> handler = new GenericHandler<>(client.getNode(), 
result);
+    Filter newFilter;
+    // add timestamp to as a timeFilter to skip the data which has been read
+    if (request.isSetTimeFilterBytes()) {
+      Filter timeFilter = FilterFactory.deserialize(request.timeFilterBytes);
+      newFilter = new AndFilter(timeFilter, TimeFilter.gt(timestamp));
+    } else {
+      newFilter = TimeFilter.gt(timestamp);
+    }
+    request.setTimeFilterBytes(SerializeUtils.serializeFilter(newFilter));
+    client.queryMultSeries(request, handler);
+    synchronized (result) {
+      if (result.get() == null && handler.getException() == null) {
+        result.wait(RaftServer.getReadOperationTimeoutMS());
+      }
+    }
+    return result.get();
+  }
+
+  private Long applyForReaderIdSync(Node node, long timestamp) throws 
TException {
+
+    Long newReaderId;
+    try (SyncDataClient client =
+        this.metaGroupMember
+            .getClientProvider()
+            .getSyncDataClient(node, RaftServer.getReadOperationTimeoutMS())) {
+
+      Filter newFilter;
+      // add timestamp to as a timeFilter to skip the data which has been read
+      if (request.isSetTimeFilterBytes()) {
+        Filter timeFilter = FilterFactory.deserialize(request.timeFilterBytes);
+        newFilter = new AndFilter(timeFilter, TimeFilter.gt(timestamp));
+      } else {
+        newFilter = TimeFilter.gt(timestamp);
+      }
+      request.setTimeFilterBytes(SerializeUtils.serializeFilter(newFilter));
+      newReaderId = client.queryMultSeries(request);
+      return newReaderId;
+    }
+  }
+
+  public long getReaderId() {
+    return this.readerId;
+  }
+
+  public List<TSDataType> getDataTypes() {
+    return this.dataTypes;
+  }
+
+  public Node getHeader() {
+    return partitionGroup.getHeader();
+  }
+
+  AsyncDataClient getCurAsyncClient(int timeout) throws IOException {
+    return isNoClient
+        ? null
+        : 
metaGroupMember.getClientProvider().getAsyncDataClient(this.curSource, timeout);
+  }
+
+  SyncDataClient getCurSyncClient(int timeout) throws TException {
+    return isNoClient
+        ? null
+        : 
metaGroupMember.getClientProvider().getSyncDataClient(this.curSource, timeout);
+  }
+
+  public boolean isNoData() {
+    return this.isNoData;
+  }
+
+  private boolean isNoClient() {
+    return this.isNoClient;
+  }
+
+  @Override
+  public String toString() {
+    return "DataSourceInfo{"
+        + "readerId="
+        + readerId
+        + ", curSource="
+        + curSource
+        + ", partitionGroup="
+        + partitionGroup
+        + ", request="
+        + request
+        + '}';
+  }
+
+  /**
+   * Check if there is still any available client and there is still any left 
data.
+   *
+   * @return true if there is an available client and data to read, false all 
data has been read.
+   * @throws IOException if all clients are unavailable.
+   */
+  boolean checkCurClient() throws IOException {
+    if (isNoClient()) {
+      if (!isNoData()) {
+        throw new IOException("no available client.");
+      } else {
+        // no data
+        return false;
+      }
+    }
+    return true;
+  }
+
+  Node getCurrentNode() {
+    return this.curSource;
+  }
+}
diff --git 
a/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/MultEmptyReader.java
 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/MultEmptyReader.java
new file mode 100644
index 0000000..2bb463c
--- /dev/null
+++ 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/MultEmptyReader.java
@@ -0,0 +1,52 @@
+/*
+ * 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.iotdb.cluster.query.reader.mult;
+
+import org.apache.iotdb.tsfile.read.TimeValuePair;
+
+import java.io.IOException;
+import java.util.Set;
+
+/** empty mult reader */
+public class MultEmptyReader extends AbstractMultPointReader {
+
+  private Set<String> fullPaths;
+
+  public MultEmptyReader(Set<String> fullPaths) {
+    this.fullPaths = fullPaths;
+  }
+
+  @Override
+  public boolean hasNextTimeValuePair(String fullPath) throws IOException {
+    return false;
+  }
+
+  @Override
+  public TimeValuePair nextTimeValuePair(String fullPath) throws IOException {
+    return null;
+  }
+
+  @Override
+  public Set<String> getAllPaths() {
+    return fullPaths;
+  }
+
+  @Override
+  public void close() {}
+}
diff --git 
a/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/MultSeriesRawDataPointReader.java
 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/MultSeriesRawDataPointReader.java
new file mode 100644
index 0000000..0561cfe
--- /dev/null
+++ 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/MultSeriesRawDataPointReader.java
@@ -0,0 +1,55 @@
+/*
+ * 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.iotdb.cluster.query.reader.mult;
+
+import org.apache.iotdb.tsfile.read.TimeValuePair;
+import org.apache.iotdb.tsfile.read.reader.IPointReader;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+/** mult reader of local partition group */
+public class MultSeriesRawDataPointReader extends AbstractMultPointReader {
+  private Map<String, IPointReader> partitalPathReaders;
+
+  public MultSeriesRawDataPointReader(Map<String, IPointReader> 
partitalPathReaders) {
+    this.partitalPathReaders = partitalPathReaders;
+  }
+
+  @Override
+  public boolean hasNextTimeValuePair(String fullPath) throws IOException {
+    IPointReader seriesRawDataPointReader = partitalPathReaders.get(fullPath);
+    return seriesRawDataPointReader.hasNextTimeValuePair();
+  }
+
+  @Override
+  public TimeValuePair nextTimeValuePair(String fullPath) throws IOException {
+    IPointReader seriesRawDataPointReader = partitalPathReaders.get(fullPath);
+    return seriesRawDataPointReader.nextTimeValuePair();
+  }
+
+  @Override
+  public Set<String> getAllPaths() {
+    return partitalPathReaders.keySet();
+  }
+
+  @Override
+  public void close() {}
+}
diff --git 
a/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/RemoteMultSeriesReader.java
 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/RemoteMultSeriesReader.java
new file mode 100644
index 0000000..9c09bcb
--- /dev/null
+++ 
b/cluster/src/main/java/org/apache/iotdb/cluster/query/reader/mult/RemoteMultSeriesReader.java
@@ -0,0 +1,222 @@
+/*
+ * 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.iotdb.cluster.query.reader.mult;
+
+import org.apache.iotdb.cluster.client.sync.SyncDataClient;
+import org.apache.iotdb.cluster.config.ClusterDescriptor;
+import org.apache.iotdb.cluster.server.RaftServer;
+import org.apache.iotdb.cluster.server.handlers.caller.GenericHandler;
+import org.apache.iotdb.db.utils.SerializeUtils;
+import org.apache.iotdb.tsfile.file.metadata.enums.TSDataType;
+import org.apache.iotdb.tsfile.read.TimeValuePair;
+import org.apache.iotdb.tsfile.read.common.BatchData;
+import org.apache.iotdb.tsfile.utils.TsPrimitiveType;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.apache.thrift.TException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** mult reader without value filter that reads points from a remote side. */
+public class RemoteMultSeriesReader extends AbstractMultPointReader {
+
+  private static final Logger logger = 
LoggerFactory.getLogger(RemoteMultSeriesReader.class);
+  private static final int FETCH_BATCH_DATA_SIZE = 10;
+
+  private MultDataSourceInfo sourceInfo;
+
+  private Map<String, Queue<BatchData>> cachedBatchs;
+
+  private AtomicReference<Map<String, ByteBuffer>> fetchResult = new 
AtomicReference<>();
+  private GenericHandler<Map<String, ByteBuffer>> handler;
+
+  private BatchStrategy batchStrategy;
+
+  private Map<String, BatchData> currentBatchDatas;
+
+  private Map<String, TSDataType> pathToDataType;
+
+  public RemoteMultSeriesReader(MultDataSourceInfo sourceInfo) {
+    this.sourceInfo = sourceInfo;
+    this.handler = new GenericHandler<>(sourceInfo.getCurrentNode(), 
fetchResult);
+    this.currentBatchDatas = Maps.newHashMap();
+    this.batchStrategy = new DefaultBatchStrategy();
+
+    this.cachedBatchs = Maps.newHashMap();
+    this.pathToDataType = Maps.newHashMap();
+    for (int i = 0; i < sourceInfo.getPartialPaths().size(); i++) {
+      String fullPath = sourceInfo.getPartialPaths().get(i).getFullPath();
+      this.cachedBatchs.put(fullPath, new ConcurrentLinkedQueue<>());
+      this.pathToDataType.put(fullPath, sourceInfo.getDataTypes().get(i));
+    }
+  }
+
+  @Override
+  public boolean hasNextTimeValuePair(String fullPath) throws IOException {
+    BatchData batchData = currentBatchDatas.get(fullPath);
+    if (batchData != null && batchData.hasCurrent()) {
+      return true;
+    }
+    fetchBatch();
+    return checkPathBatchData(fullPath);
+  }
+
+  private boolean checkPathBatchData(String fullPath) {
+    BatchData batchData = cachedBatchs.get(fullPath).peek();
+    if (batchData != null && !batchData.isEmpty()) {
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public TimeValuePair nextTimeValuePair(String fullPath) throws IOException {
+    BatchData batchData = currentBatchDatas.get(fullPath);
+    if ((batchData == null || !batchData.hasCurrent()) && 
checkPathBatchData(fullPath)) {
+      batchData = cachedBatchs.get(fullPath).poll();
+      currentBatchDatas.put(fullPath, batchData);
+    }
+
+    if (!hasNextTimeValuePair(fullPath)) {
+      throw new NoSuchElementException();
+    }
+
+    TimeValuePair timeValuePair =
+        new TimeValuePair(
+            batchData.currentTime(),
+            TsPrimitiveType.getByType(pathToDataType.get(fullPath), 
batchData.currentValue()));
+    batchData.next();
+    return timeValuePair;
+  }
+
+  @Override
+  public Set<String> getAllPaths() {
+    return cachedBatchs.keySet();
+  }
+
+  /** query resource deal close there is not dealing. */
+  @Override
+  public void close() {}
+
+  private void fetchBatch() throws IOException {
+    if (!sourceInfo.checkCurClient()) {
+      cachedBatchs = null;
+      return;
+    }
+    List<String> paths = batchStrategy.selectBatchPaths(this.cachedBatchs);
+    if (paths.isEmpty()) return;
+
+    Map<String, ByteBuffer> result;
+    if (ClusterDescriptor.getInstance().getConfig().isUseAsyncServer()) {
+      result = fetchResultAsync(paths);
+    } else {
+      result = fetchResultSync(paths);
+    }
+
+    if (result == null) return;
+
+    for (String path : result.keySet()) {
+
+      BatchData batchData = 
SerializeUtils.deserializeBatchData(result.get(path));
+      if (logger.isDebugEnabled()) {
+        logger.debug(
+            "Fetched a batch from {}, size:{}",
+            sourceInfo.getCurrentNode(),
+            batchData == null ? 0 : batchData.length());
+      }
+      // if data query end, batchData is null,
+      // will create empty BatchData, and add queue.
+      if (batchData == null) {
+        batchData = new BatchData();
+      }
+      cachedBatchs
+          .computeIfAbsent(path, n -> new ConcurrentLinkedQueue<BatchData>())
+          .add(batchData);
+    }
+  }
+
+  @SuppressWarnings("java:S2274") // enable timeout
+  private Map<String, ByteBuffer> fetchResultAsync(List<String> paths) throws 
IOException {
+    synchronized (fetchResult) {
+      fetchResult.set(null);
+      try {
+        sourceInfo
+            .getCurAsyncClient(RaftServer.getReadOperationTimeoutMS())
+            .fetchMultSeries(sourceInfo.getHeader(), sourceInfo.getReaderId(), 
paths, handler);
+        fetchResult.wait(RaftServer.getReadOperationTimeoutMS());
+      } catch (TException | InterruptedException e) {
+        logger.error("Failed to fetch result async, connect to {}", 
sourceInfo, e);
+        return null;
+      }
+    }
+    return fetchResult.get();
+  }
+
+  private Map<String, ByteBuffer> fetchResultSync(List<String> paths) throws 
IOException {
+
+    try (SyncDataClient curSyncClient =
+        sourceInfo.getCurSyncClient(RaftServer.getReadOperationTimeoutMS()); ) 
{
+
+      return curSyncClient.fetchMultSeries(sourceInfo.getHeader(), 
sourceInfo.getReaderId(), paths);
+    } catch (TException e) {
+      logger.error("Failed to fetch result sync, connect to {}", sourceInfo, 
e);
+      return null;
+    }
+  }
+
+  /** select path, which could batch-fetch result */
+  interface BatchStrategy {
+    List<String> selectBatchPaths(Map<String, Queue<BatchData>> cacheBatchs);
+  }
+
+  static class DefaultBatchStrategy implements BatchStrategy {
+
+    @Override
+    public List<String> selectBatchPaths(Map<String, Queue<BatchData>> 
cacheBatchs) {
+      List<String> paths = Lists.newArrayList();
+
+      for (String path : cacheBatchs.keySet()) {
+        Queue<BatchData> batchDataQueue = cacheBatchs.get(path);
+        BatchData batchData = batchDataQueue.peek();
+
+        // data read finished, so can not batch get data
+        if (batchData != null && batchData.isEmpty()) {
+          continue;
+        }
+
+        if (batchDataQueue.size() < FETCH_BATCH_DATA_SIZE) {
+          paths.add(path);
+        }
+      }
+      return paths;
+    }
+  }
+}
diff --git 
a/cluster/src/main/java/org/apache/iotdb/cluster/server/DataClusterServer.java 
b/cluster/src/main/java/org/apache/iotdb/cluster/server/DataClusterServer.java
index de815b3..a54de23 100644
--- 
a/cluster/src/main/java/org/apache/iotdb/cluster/server/DataClusterServer.java
+++ 
b/cluster/src/main/java/org/apache/iotdb/cluster/server/DataClusterServer.java
@@ -39,6 +39,7 @@ import org.apache.iotdb.cluster.rpc.thrift.GroupByRequest;
 import org.apache.iotdb.cluster.rpc.thrift.HeartBeatRequest;
 import org.apache.iotdb.cluster.rpc.thrift.HeartBeatResponse;
 import org.apache.iotdb.cluster.rpc.thrift.LastQueryRequest;
+import org.apache.iotdb.cluster.rpc.thrift.MultSeriesQueryRequest;
 import org.apache.iotdb.cluster.rpc.thrift.Node;
 import org.apache.iotdb.cluster.rpc.thrift.PreviousFillRequest;
 import org.apache.iotdb.cluster.rpc.thrift.PullSchemaRequest;
@@ -335,6 +336,17 @@ public class DataClusterServer extends RaftServer
   }
 
   @Override
+  public void queryMultSeries(
+      MultSeriesQueryRequest request, AsyncMethodCallback<Long> resultHandler) 
throws TException {
+    DataAsyncService service =
+        getDataAsyncService(
+            request.getHeader(), resultHandler, "Query series:" + 
request.getPath());
+    if (service != null) {
+      service.queryMultSeries(request, resultHandler);
+    }
+  }
+
+  @Override
   public void fetchSingleSeries(
       Node header, long readerId, AsyncMethodCallback<ByteBuffer> 
resultHandler) {
     DataAsyncService service =
@@ -345,6 +357,20 @@ public class DataClusterServer extends RaftServer
   }
 
   @Override
+  public void fetchMultSeries(
+      Node header,
+      long readerId,
+      List<String> paths,
+      AsyncMethodCallback<Map<String, ByteBuffer>> resultHandler)
+      throws TException {
+    DataAsyncService service =
+        getDataAsyncService(header, resultHandler, "Fetch reader:" + readerId);
+    if (service != null) {
+      service.fetchMultSeries(header, readerId, paths, resultHandler);
+    }
+  }
+
+  @Override
   public void getAllPaths(
       Node header,
       List<String> paths,
@@ -738,11 +764,22 @@ public class DataClusterServer extends RaftServer
   }
 
   @Override
+  public long queryMultSeries(MultSeriesQueryRequest request) throws 
TException {
+    return getDataSyncService(request.getHeader()).queryMultSeries(request);
+  }
+
+  @Override
   public ByteBuffer fetchSingleSeries(Node header, long readerId) throws 
TException {
     return getDataSyncService(header).fetchSingleSeries(header, readerId);
   }
 
   @Override
+  public Map<String, ByteBuffer> fetchMultSeries(Node header, long readerId, 
List<String> paths)
+      throws TException {
+    return getDataSyncService(header).fetchMultSeries(header, readerId, paths);
+  }
+
+  @Override
   public long querySingleSeriesByTimestamp(SingleSeriesQueryRequest request) 
throws TException {
     return 
getDataSyncService(request.getHeader()).querySingleSeriesByTimestamp(request);
   }
diff --git 
a/cluster/src/main/java/org/apache/iotdb/cluster/server/service/DataAsyncService.java
 
b/cluster/src/main/java/org/apache/iotdb/cluster/server/service/DataAsyncService.java
index 256feb2..49c5a1e 100644
--- 
a/cluster/src/main/java/org/apache/iotdb/cluster/server/service/DataAsyncService.java
+++ 
b/cluster/src/main/java/org/apache/iotdb/cluster/server/service/DataAsyncService.java
@@ -28,6 +28,7 @@ import 
org.apache.iotdb.cluster.rpc.thrift.GetAggrResultRequest;
 import org.apache.iotdb.cluster.rpc.thrift.GetAllPathsResult;
 import org.apache.iotdb.cluster.rpc.thrift.GroupByRequest;
 import org.apache.iotdb.cluster.rpc.thrift.LastQueryRequest;
+import org.apache.iotdb.cluster.rpc.thrift.MultSeriesQueryRequest;
 import org.apache.iotdb.cluster.rpc.thrift.Node;
 import org.apache.iotdb.cluster.rpc.thrift.PreviousFillRequest;
 import org.apache.iotdb.cluster.rpc.thrift.PullSchemaRequest;
@@ -52,6 +53,7 @@ import org.slf4j.LoggerFactory;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 public class DataAsyncService extends BaseAsyncService implements 
TSDataService.AsyncIface {
@@ -175,6 +177,16 @@ public class DataAsyncService extends BaseAsyncService 
implements TSDataService.
   }
 
   @Override
+  public void queryMultSeries(
+      MultSeriesQueryRequest request, AsyncMethodCallback<Long> resultHandler) 
throws TException {
+    try {
+      
resultHandler.onComplete(dataGroupMember.getLocalQueryExecutor().queryMultSeries(request));
+    } catch (Exception e) {
+      resultHandler.onError(e);
+    }
+  }
+
+  @Override
   public void querySingleSeriesByTimestamp(
       SingleSeriesQueryRequest request, AsyncMethodCallback<Long> 
resultHandler) {
     try {
@@ -207,6 +219,21 @@ public class DataAsyncService extends BaseAsyncService 
implements TSDataService.
   }
 
   @Override
+  public void fetchMultSeries(
+      Node header,
+      long readerId,
+      List<String> paths,
+      AsyncMethodCallback<Map<String, ByteBuffer>> resultHandler)
+      throws TException {
+    try {
+      resultHandler.onComplete(
+          dataGroupMember.getLocalQueryExecutor().fetchMultSeries(readerId, 
paths));
+    } catch (ReaderNotFoundException | IOException e) {
+      resultHandler.onError(e);
+    }
+  }
+
+  @Override
   public void fetchSingleSeriesByTimestamps(
       Node header,
       long readerId,
diff --git 
a/cluster/src/main/java/org/apache/iotdb/cluster/server/service/DataSyncService.java
 
b/cluster/src/main/java/org/apache/iotdb/cluster/server/service/DataSyncService.java
index 77ba4db..94ac92f 100644
--- 
a/cluster/src/main/java/org/apache/iotdb/cluster/server/service/DataSyncService.java
+++ 
b/cluster/src/main/java/org/apache/iotdb/cluster/server/service/DataSyncService.java
@@ -28,6 +28,7 @@ import 
org.apache.iotdb.cluster.rpc.thrift.GetAggrResultRequest;
 import org.apache.iotdb.cluster.rpc.thrift.GetAllPathsResult;
 import org.apache.iotdb.cluster.rpc.thrift.GroupByRequest;
 import org.apache.iotdb.cluster.rpc.thrift.LastQueryRequest;
+import org.apache.iotdb.cluster.rpc.thrift.MultSeriesQueryRequest;
 import org.apache.iotdb.cluster.rpc.thrift.Node;
 import org.apache.iotdb.cluster.rpc.thrift.PreviousFillRequest;
 import org.apache.iotdb.cluster.rpc.thrift.PullSchemaRequest;
@@ -52,6 +53,7 @@ import org.slf4j.LoggerFactory;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 public class DataSyncService extends BaseSyncService implements 
TSDataService.Iface {
@@ -197,6 +199,15 @@ public class DataSyncService extends BaseSyncService 
implements TSDataService.If
   }
 
   @Override
+  public long queryMultSeries(MultSeriesQueryRequest request) throws 
TException {
+    try {
+      return dataGroupMember.getLocalQueryExecutor().queryMultSeries(request);
+    } catch (Exception e) {
+      throw new TException(e);
+    }
+  }
+
+  @Override
   public long querySingleSeriesByTimestamp(SingleSeriesQueryRequest request) 
throws TException {
     try {
       return 
dataGroupMember.getLocalQueryExecutor().querySingleSeriesByTimestamp(request);
@@ -224,6 +235,16 @@ public class DataSyncService extends BaseSyncService 
implements TSDataService.If
   }
 
   @Override
+  public Map<String, ByteBuffer> fetchMultSeries(Node header, long readerId, 
List<String> paths)
+      throws TException {
+    try {
+      return dataGroupMember.getLocalQueryExecutor().fetchMultSeries(readerId, 
paths);
+    } catch (ReaderNotFoundException | IOException e) {
+      throw new TException(e);
+    }
+  }
+
+  @Override
   public ByteBuffer fetchSingleSeriesByTimestamps(Node header, long readerId, 
List<Long> timestamps)
       throws TException {
     try {
diff --git 
a/cluster/src/test/java/org/apache/iotdb/cluster/common/TestAsyncDataClient.java
 
b/cluster/src/test/java/org/apache/iotdb/cluster/common/TestAsyncDataClient.java
index 65d4847..c5461dc 100644
--- 
a/cluster/src/test/java/org/apache/iotdb/cluster/common/TestAsyncDataClient.java
+++ 
b/cluster/src/test/java/org/apache/iotdb/cluster/common/TestAsyncDataClient.java
@@ -26,6 +26,7 @@ import org.apache.iotdb.cluster.rpc.thrift.ExecutNonQueryReq;
 import org.apache.iotdb.cluster.rpc.thrift.GetAggrResultRequest;
 import org.apache.iotdb.cluster.rpc.thrift.GetAllPathsResult;
 import org.apache.iotdb.cluster.rpc.thrift.GroupByRequest;
+import org.apache.iotdb.cluster.rpc.thrift.MultSeriesQueryRequest;
 import org.apache.iotdb.cluster.rpc.thrift.Node;
 import org.apache.iotdb.cluster.rpc.thrift.PreviousFillRequest;
 import org.apache.iotdb.cluster.rpc.thrift.PullSchemaRequest;
@@ -44,6 +45,7 @@ import org.apache.iotdb.db.qp.executor.PlanExecutor;
 import org.apache.iotdb.db.qp.physical.PhysicalPlan;
 import org.apache.iotdb.service.rpc.thrift.TSStatus;
 
+import org.apache.thrift.TException;
 import org.apache.thrift.async.AsyncMethodCallback;
 
 import java.io.File;
@@ -79,6 +81,24 @@ public class TestAsyncDataClient extends AsyncDataClient {
   }
 
   @Override
+  public void fetchMultSeries(
+      Node header,
+      long readerId,
+      List<String> paths,
+      AsyncMethodCallback<Map<String, ByteBuffer>> resultHandler) {
+    new Thread(
+            () -> {
+              try {
+                new DataAsyncService(dataGroupMemberMap.get(header))
+                    .fetchMultSeries(header, readerId, paths, resultHandler);
+              } catch (TException e) {
+                e.printStackTrace();
+              }
+            })
+        .start();
+  }
+
+  @Override
   public void getAggrResult(
       GetAggrResultRequest request, AsyncMethodCallback<List<ByteBuffer>> 
resultHandler) {
     new Thread(
@@ -99,6 +119,21 @@ public class TestAsyncDataClient extends AsyncDataClient {
   }
 
   @Override
+  public void queryMultSeries(
+      MultSeriesQueryRequest request, AsyncMethodCallback<Long> resultHandler) 
{
+    new Thread(
+            () -> {
+              try {
+                new 
DataAsyncService(dataGroupMemberMap.get(request.getHeader()))
+                    .queryMultSeries(request, resultHandler);
+              } catch (TException e) {
+                e.printStackTrace();
+              }
+            })
+        .start();
+  }
+
+  @Override
   public void fetchSingleSeriesByTimestamps(
       Node header,
       long readerId,
diff --git 
a/cluster/src/test/java/org/apache/iotdb/cluster/query/reader/mult/AssignPathManagedMergeReaderTest.java
 
b/cluster/src/test/java/org/apache/iotdb/cluster/query/reader/mult/AssignPathManagedMergeReaderTest.java
new file mode 100644
index 0000000..5864a99
--- /dev/null
+++ 
b/cluster/src/test/java/org/apache/iotdb/cluster/query/reader/mult/AssignPathManagedMergeReaderTest.java
@@ -0,0 +1,190 @@
+/*
+ * 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.iotdb.cluster.query.reader.mult;
+
+import org.apache.iotdb.cluster.client.DataClientProvider;
+import org.apache.iotdb.cluster.client.async.AsyncDataClient;
+import org.apache.iotdb.cluster.common.TestMetaGroupMember;
+import org.apache.iotdb.cluster.common.TestUtils;
+import org.apache.iotdb.cluster.config.ClusterDescriptor;
+import org.apache.iotdb.cluster.partition.PartitionGroup;
+import org.apache.iotdb.cluster.query.RemoteQueryContext;
+import org.apache.iotdb.cluster.rpc.thrift.MultSeriesQueryRequest;
+import org.apache.iotdb.cluster.rpc.thrift.Node;
+import org.apache.iotdb.cluster.server.member.MetaGroupMember;
+import org.apache.iotdb.db.exception.StorageEngineException;
+import org.apache.iotdb.db.exception.metadata.IllegalPathException;
+import org.apache.iotdb.db.metadata.PartialPath;
+import org.apache.iotdb.db.query.control.QueryResourceManager;
+import org.apache.iotdb.db.utils.SerializeUtils;
+import org.apache.iotdb.tsfile.file.metadata.enums.TSDataType;
+import org.apache.iotdb.tsfile.read.TimeValuePair;
+import org.apache.iotdb.tsfile.read.common.BatchData;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.apache.thrift.TException;
+import org.apache.thrift.async.AsyncMethodCallback;
+import org.apache.thrift.protocol.TBinaryProtocol.Factory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentSkipListSet;
+
+import static junit.framework.TestCase.assertEquals;
+
+public class AssignPathManagedMergeReaderTest {
+
+  private AssignPathManagedMergeReader assignPathManagedMergeReader;
+  private RemoteMultSeriesReader reader;
+  private List<BatchData> batchData;
+  private boolean batchUsed;
+  private MetaGroupMember metaGroupMember;
+  private Set<Node> failedNodes = new ConcurrentSkipListSet<>();
+  private boolean prevUseAsyncServer;
+  private List<PartialPath> paths;
+  private List<TSDataType> dataTypes;
+
+  @Before
+  public void setUp() throws IllegalPathException {
+    paths = Lists.newArrayList();
+    dataTypes = Lists.newArrayList();
+    PartialPath partialPath = new PartialPath("root.a.b");
+    paths.add(partialPath);
+    partialPath = new PartialPath("root.a.c");
+    paths.add(partialPath);
+    dataTypes.add(TSDataType.DOUBLE);
+    dataTypes.add(TSDataType.INT32);
+    prevUseAsyncServer = 
ClusterDescriptor.getInstance().getConfig().isUseAsyncServer();
+    batchData = Lists.newArrayList();
+    batchData.add(TestUtils.genBatchData(TSDataType.DOUBLE, 0, 100));
+    batchData.add(TestUtils.genBatchData(TSDataType.INT32, 0, 100));
+    batchUsed = false;
+    metaGroupMember = new TestMetaGroupMember();
+    assignPathManagedMergeReader = new 
AssignPathManagedMergeReader("root.a.b", TSDataType.DOUBLE);
+  }
+
+  @After
+  public void tearDown() {
+    
ClusterDescriptor.getInstance().getConfig().setUseAsyncServer(prevUseAsyncServer);
+  }
+
+  @Test
+  public void testMultManagerMergeRemoteSeriesReader() throws IOException, 
StorageEngineException {
+    ClusterDescriptor.getInstance().getConfig().setUseAsyncServer(true);
+    PartitionGroup group = new PartitionGroup();
+    setAsyncDataClient();
+    group.add(TestUtils.getNode(0));
+    group.add(TestUtils.getNode(1));
+    group.add(TestUtils.getNode(2));
+
+    MultSeriesQueryRequest request = new MultSeriesQueryRequest();
+    RemoteQueryContext context = new RemoteQueryContext(1);
+
+    try {
+      MultDataSourceInfo sourceInfo =
+          new MultDataSourceInfo(group, paths, dataTypes, request, context, 
metaGroupMember, group);
+      sourceInfo.hasNextDataClient(Long.MIN_VALUE);
+
+      reader = new RemoteMultSeriesReader(sourceInfo);
+      assignPathManagedMergeReader.addReader(reader, 0);
+
+      for (int i = 0; i < 100; i++) {
+        assertEquals(true, 
assignPathManagedMergeReader.hasNextTimeValuePair());
+        TimeValuePair pair = assignPathManagedMergeReader.nextTimeValuePair();
+        assertEquals(i, pair.getTimestamp());
+        assertEquals(i * 1.0, pair.getValue().getDouble(), 0.00001);
+      }
+      assertEquals(false, assignPathManagedMergeReader.hasNextTimeValuePair());
+
+    } finally {
+      QueryResourceManager.getInstance().endQuery(context.getQueryId());
+    }
+  }
+
+  private void setAsyncDataClient() {
+    metaGroupMember.setClientProvider(
+        new DataClientProvider(new Factory()) {
+          @Override
+          public AsyncDataClient getAsyncDataClient(Node node, int timeout) 
throws IOException {
+            return new AsyncDataClient(null, null, node, null) {
+              @Override
+              public void fetchMultSeries(
+                  Node header,
+                  long readerId,
+                  List<String> paths,
+                  AsyncMethodCallback<Map<String, ByteBuffer>> resultHandler)
+                  throws TException {
+                if (failedNodes.contains(node)) {
+                  throw new TException("Node down.");
+                }
+
+                new Thread(
+                        () -> {
+                          Map<String, ByteBuffer> stringByteBufferMap = 
Maps.newHashMap();
+                          if (batchUsed) {
+                            paths.forEach(
+                                path -> {
+                                  stringByteBufferMap.put(path, 
ByteBuffer.allocate(0));
+                                });
+                          } else {
+                            batchUsed = true;
+
+                            for (int i = 0; i < batchData.size(); i++) {
+                              stringByteBufferMap.put(
+                                  paths.get(i), 
generateByteBuffer(batchData.get(i)));
+                            }
+
+                            resultHandler.onComplete(stringByteBufferMap);
+                          }
+                        })
+                    .start();
+              }
+
+              @Override
+              public void queryMultSeries(
+                  MultSeriesQueryRequest request, AsyncMethodCallback<Long> 
resultHandler)
+                  throws TException {
+                if (failedNodes.contains(node)) {
+                  throw new TException("Node down.");
+                }
+
+                new Thread(() -> resultHandler.onComplete(1L)).start();
+              }
+            };
+          }
+        });
+  }
+
+  private ByteBuffer generateByteBuffer(BatchData batchData) {
+    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+    DataOutputStream dataOutputStream = new 
DataOutputStream(byteArrayOutputStream);
+    SerializeUtils.serializeBatchData(batchData, dataOutputStream);
+    ByteBuffer byteBuffer = 
ByteBuffer.wrap(byteArrayOutputStream.toByteArray());
+    return byteBuffer;
+  }
+}
diff --git 
a/cluster/src/test/java/org/apache/iotdb/cluster/query/reader/mult/MultSeriesRawDataPointReaderTest.java
 
b/cluster/src/test/java/org/apache/iotdb/cluster/query/reader/mult/MultSeriesRawDataPointReaderTest.java
new file mode 100644
index 0000000..9d30d4c
--- /dev/null
+++ 
b/cluster/src/test/java/org/apache/iotdb/cluster/query/reader/mult/MultSeriesRawDataPointReaderTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.iotdb.cluster.query.reader.mult;
+
+import org.apache.iotdb.cluster.common.TestUtils;
+import org.apache.iotdb.db.exception.StorageEngineException;
+import org.apache.iotdb.db.exception.metadata.IllegalPathException;
+import org.apache.iotdb.db.query.reader.series.SeriesRawDataPointReader;
+import org.apache.iotdb.tsfile.file.metadata.enums.TSDataType;
+import org.apache.iotdb.tsfile.read.TimeValuePair;
+import org.apache.iotdb.tsfile.read.common.BatchData;
+import org.apache.iotdb.tsfile.read.reader.IPointReader;
+
+import com.google.common.collect.Maps;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.util.Map;
+
+import static junit.framework.TestCase.assertEquals;
+
+public class MultSeriesRawDataPointReaderTest {
+
+  private MultSeriesRawDataPointReader reader;
+
+  @Before
+  public void setUp() throws IllegalPathException, IOException {
+    BatchData batchData = TestUtils.genBatchData(TSDataType.DOUBLE, 0, 100);
+    Map<String, IPointReader> pointReaderMap = Maps.newHashMap();
+    SeriesRawDataPointReader seriesRawDataBatchReader =
+        Mockito.mock(SeriesRawDataPointReader.class);
+    
Mockito.when(seriesRawDataBatchReader.hasNextTimeValuePair()).thenReturn(true);
+    TimeValuePair timeValuePair =
+        new TimeValuePair(batchData.currentTime(), 
batchData.currentTsPrimitiveType());
+    
Mockito.when(seriesRawDataBatchReader.nextTimeValuePair()).thenReturn(timeValuePair);
+    pointReaderMap.put("root.a.b", seriesRawDataBatchReader);
+    pointReaderMap.put("root.a.c", seriesRawDataBatchReader);
+    reader = new MultSeriesRawDataPointReader(pointReaderMap);
+  }
+
+  @Test
+  public void testMultSeriesReader() throws IOException, 
StorageEngineException {
+    boolean hasNext = this.reader.hasNextTimeValuePair("root.a.b");
+    assertEquals(true, hasNext);
+    TimeValuePair timeValuePair = this.reader.nextTimeValuePair("root.a.b");
+    assertEquals(0, timeValuePair.getTimestamp());
+    assertEquals(0 * 1.0, timeValuePair.getValue().getDouble(), 0.0001);
+  }
+}
diff --git 
a/cluster/src/test/java/org/apache/iotdb/cluster/query/reader/mult/RemoteMultSeriesReaderTest.java
 
b/cluster/src/test/java/org/apache/iotdb/cluster/query/reader/mult/RemoteMultSeriesReaderTest.java
new file mode 100644
index 0000000..73e1a5d
--- /dev/null
+++ 
b/cluster/src/test/java/org/apache/iotdb/cluster/query/reader/mult/RemoteMultSeriesReaderTest.java
@@ -0,0 +1,286 @@
+/*
+ * 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.iotdb.cluster.query.reader.mult;
+
+import org.apache.iotdb.cluster.client.DataClientProvider;
+import org.apache.iotdb.cluster.client.async.AsyncDataClient;
+import org.apache.iotdb.cluster.client.sync.SyncDataClient;
+import org.apache.iotdb.cluster.common.TestMetaGroupMember;
+import org.apache.iotdb.cluster.common.TestUtils;
+import org.apache.iotdb.cluster.config.ClusterDescriptor;
+import org.apache.iotdb.cluster.partition.PartitionGroup;
+import org.apache.iotdb.cluster.query.RemoteQueryContext;
+import org.apache.iotdb.cluster.rpc.thrift.MultSeriesQueryRequest;
+import org.apache.iotdb.cluster.rpc.thrift.Node;
+import org.apache.iotdb.cluster.server.member.MetaGroupMember;
+import org.apache.iotdb.db.exception.StorageEngineException;
+import org.apache.iotdb.db.exception.metadata.IllegalPathException;
+import org.apache.iotdb.db.metadata.PartialPath;
+import org.apache.iotdb.db.query.control.QueryResourceManager;
+import org.apache.iotdb.db.utils.SerializeUtils;
+import org.apache.iotdb.tsfile.file.metadata.enums.TSDataType;
+import org.apache.iotdb.tsfile.read.TimeValuePair;
+import org.apache.iotdb.tsfile.read.common.BatchData;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.apache.thrift.TException;
+import org.apache.thrift.async.AsyncMethodCallback;
+import org.apache.thrift.protocol.TBinaryProtocol.Factory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ConcurrentSkipListSet;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertTrue;
+
+public class RemoteMultSeriesReaderTest {
+
+  private RemoteMultSeriesReader reader;
+  private List<BatchData> batchData;
+  private boolean batchUsed;
+  private MetaGroupMember metaGroupMember;
+  private Set<Node> failedNodes = new ConcurrentSkipListSet<>();
+  private boolean prevUseAsyncServer;
+  private List<PartialPath> paths;
+  private List<TSDataType> dataTypes;
+
+  @Before
+  public void setUp() throws IllegalPathException {
+    paths = Lists.newArrayList();
+    dataTypes = Lists.newArrayList();
+    PartialPath partialPath = new PartialPath("root.a.b");
+    paths.add(partialPath);
+    partialPath = new PartialPath("root.a.c");
+    paths.add(partialPath);
+    dataTypes.add(TSDataType.DOUBLE);
+    dataTypes.add(TSDataType.INT32);
+    prevUseAsyncServer = 
ClusterDescriptor.getInstance().getConfig().isUseAsyncServer();
+    batchData = Lists.newArrayList();
+    batchData.add(TestUtils.genBatchData(TSDataType.DOUBLE, 0, 100));
+    batchData.add(TestUtils.genBatchData(TSDataType.INT32, 0, 100));
+    batchUsed = false;
+    metaGroupMember = new TestMetaGroupMember();
+  }
+
+  @After
+  public void tearDown() {
+    
ClusterDescriptor.getInstance().getConfig().setUseAsyncServer(prevUseAsyncServer);
+  }
+
+  @Test
+  public void testAsyncMultSeriesReader() throws IOException, 
StorageEngineException {
+    ClusterDescriptor.getInstance().getConfig().setUseAsyncServer(true);
+    PartitionGroup group = new PartitionGroup();
+    setAsyncDataClient();
+    group.add(TestUtils.getNode(0));
+    group.add(TestUtils.getNode(1));
+    group.add(TestUtils.getNode(2));
+
+    MultSeriesQueryRequest request = new MultSeriesQueryRequest();
+    RemoteQueryContext context = new RemoteQueryContext(1);
+
+    try {
+      MultDataSourceInfo sourceInfo =
+          new MultDataSourceInfo(group, paths, dataTypes, request, context, 
metaGroupMember, group);
+      sourceInfo.hasNextDataClient(Long.MIN_VALUE);
+
+      reader = new RemoteMultSeriesReader(sourceInfo);
+
+      for (int i = 0; i < 100; i++) {
+        assertTrue(reader.hasNextTimeValuePair(paths.get(0).getFullPath()));
+        TimeValuePair pair = 
reader.nextTimeValuePair(paths.get(0).getFullPath());
+        assertEquals(i, pair.getTimestamp());
+        assertEquals(i * 1.0, pair.getValue().getDouble(), 0.00001);
+      }
+      assertFalse(reader.hasNextTimeValuePair(paths.get(0).getFullPath()));
+
+    } finally {
+      QueryResourceManager.getInstance().endQuery(context.getQueryId());
+    }
+  }
+
+  @Test
+  public void testSyncMultSeriesReader() throws IOException, 
StorageEngineException {
+    ClusterDescriptor.getInstance().getConfig().setUseAsyncServer(false);
+    setSyncDataClient();
+    PartitionGroup group = new PartitionGroup();
+    group.add(TestUtils.getNode(0));
+    group.add(TestUtils.getNode(1));
+    group.add(TestUtils.getNode(2));
+
+    MultSeriesQueryRequest request = new MultSeriesQueryRequest();
+    RemoteQueryContext context = new RemoteQueryContext(1);
+
+    try {
+      MultDataSourceInfo sourceInfo =
+          new MultDataSourceInfo(group, paths, dataTypes, request, context, 
metaGroupMember, group);
+      sourceInfo.hasNextDataClient(Long.MIN_VALUE);
+
+      reader = new RemoteMultSeriesReader(sourceInfo);
+
+      for (int i = 0; i < 100; i++) {
+        assertTrue(reader.hasNextTimeValuePair(paths.get(0).getFullPath()));
+        TimeValuePair pair = 
reader.nextTimeValuePair(paths.get(0).getFullPath());
+        assertEquals(i, pair.getTimestamp());
+        assertEquals(i * 1.0, pair.getValue().getDouble(), 0.00001);
+      }
+      assertFalse(reader.hasNextTimeValuePair(paths.get(0).getFullPath()));
+
+      for (int i = 0; i < 100; i++) {
+        assertTrue(reader.hasNextTimeValuePair(paths.get(1).getFullPath()));
+        TimeValuePair pair = 
reader.nextTimeValuePair(paths.get(1).getFullPath());
+        assertEquals(i, pair.getTimestamp());
+        assertEquals(i * 1.0, pair.getValue().getInt(), 0.00001);
+      }
+      assertFalse(reader.hasNextTimeValuePair(paths.get(1).getFullPath()));
+    } finally {
+      QueryResourceManager.getInstance().endQuery(context.getQueryId());
+    }
+  }
+
+  @Test
+  public void testDefaultBatchStrategySelect() {
+    RemoteMultSeriesReader.DefaultBatchStrategy defaultBatchStrategy =
+        new RemoteMultSeriesReader.DefaultBatchStrategy();
+    Map cachedBatches = Maps.newHashMap();
+    Queue queue = new ConcurrentLinkedQueue<BatchData>();
+    batchData.forEach(
+        data -> {
+          queue.add(data);
+        });
+    cachedBatches.put("root.a.b", queue);
+    assertEquals(1, 
defaultBatchStrategy.selectBatchPaths(cachedBatches).size());
+  }
+
+  private void setAsyncDataClient() {
+    metaGroupMember.setClientProvider(
+        new DataClientProvider(new Factory()) {
+          @Override
+          public AsyncDataClient getAsyncDataClient(Node node, int timeout) 
throws IOException {
+            return new AsyncDataClient(null, null, node, null) {
+              @Override
+              public void fetchMultSeries(
+                  Node header,
+                  long readerId,
+                  List<String> paths,
+                  AsyncMethodCallback<Map<String, ByteBuffer>> resultHandler)
+                  throws TException {
+                if (failedNodes.contains(node)) {
+                  throw new TException("Node down.");
+                }
+
+                new Thread(
+                        () -> {
+                          Map<String, ByteBuffer> stringByteBufferMap = 
Maps.newHashMap();
+                          if (batchUsed) {
+                            paths.forEach(
+                                path -> {
+                                  stringByteBufferMap.put(path, 
ByteBuffer.allocate(0));
+                                });
+                          } else {
+                            batchUsed = true;
+
+                            for (int i = 0; i < batchData.size(); i++) {
+                              stringByteBufferMap.put(
+                                  paths.get(i), 
generateByteBuffer(batchData.get(i)));
+                            }
+
+                            resultHandler.onComplete(stringByteBufferMap);
+                          }
+                        })
+                    .start();
+              }
+
+              @Override
+              public void queryMultSeries(
+                  MultSeriesQueryRequest request, AsyncMethodCallback<Long> 
resultHandler)
+                  throws TException {
+                if (failedNodes.contains(node)) {
+                  throw new TException("Node down.");
+                }
+
+                new Thread(() -> resultHandler.onComplete(1L)).start();
+              }
+            };
+          }
+        });
+  }
+
+  private void setSyncDataClient() {
+    metaGroupMember.setClientProvider(
+        new DataClientProvider(new Factory()) {
+          @Override
+          public SyncDataClient getSyncDataClient(Node node, int timeout) {
+            return new SyncDataClient(null) {
+              @Override
+              public Map<String, ByteBuffer> fetchMultSeries(
+                  Node header, long readerId, List<String> paths) throws 
TException {
+                if (failedNodes.contains(node)) {
+                  throw new TException("Node down.");
+                }
+
+                Map<String, ByteBuffer> stringByteBufferMap = 
Maps.newHashMap();
+                if (batchUsed) {
+                  paths.forEach(
+                      path -> {
+                        stringByteBufferMap.put(path, ByteBuffer.allocate(0));
+                      });
+                } else {
+                  batchUsed = true;
+                  for (int i = 0; i < batchData.size(); i++) {
+                    stringByteBufferMap.put(paths.get(i), 
generateByteBuffer(batchData.get(i)));
+                  }
+                }
+                return stringByteBufferMap;
+              }
+
+              @Override
+              public long queryMultSeries(MultSeriesQueryRequest request) 
throws TException {
+                if (failedNodes.contains(node)) {
+                  throw new TException("Node down.");
+                }
+
+                return 1L;
+              }
+            };
+          }
+        });
+  }
+
+  private ByteBuffer generateByteBuffer(BatchData batchData) {
+    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+    DataOutputStream dataOutputStream = new 
DataOutputStream(byteArrayOutputStream);
+    SerializeUtils.serializeBatchData(batchData, dataOutputStream);
+    ByteBuffer byteBuffer = 
ByteBuffer.wrap(byteArrayOutputStream.toByteArray());
+    return byteBuffer;
+  }
+}
diff --git 
a/server/src/main/java/org/apache/iotdb/db/query/reader/universal/CachedPriorityMergeReader.java
 
b/server/src/main/java/org/apache/iotdb/db/query/reader/universal/CachedPriorityMergeReader.java
index 69c67c4..2f5a956 100644
--- 
a/server/src/main/java/org/apache/iotdb/db/query/reader/universal/CachedPriorityMergeReader.java
+++ 
b/server/src/main/java/org/apache/iotdb/db/query/reader/universal/CachedPriorityMergeReader.java
@@ -55,7 +55,8 @@ public class CachedPriorityMergeReader extends 
PriorityMergeReader {
     while (!heap.isEmpty() && cacheLimit < CACHE_SIZE) {
       Element top = heap.peek();
       if (lastTimestamp == null || top.currTime() != lastTimestamp) {
-        TimeValuePairUtils.setTimeValuePair(top.timeValuePair, 
timeValuePairCache[cacheLimit++]);
+        TimeValuePairUtils.setTimeValuePair(
+            top.getTimeValuePair(), timeValuePairCache[cacheLimit++]);
         lastTimestamp = top.currTime();
       }
       // remove duplicates
diff --git 
a/server/src/main/java/org/apache/iotdb/db/query/reader/universal/DescPriorityMergeReader.java
 
b/server/src/main/java/org/apache/iotdb/db/query/reader/universal/DescPriorityMergeReader.java
index d73e72b..d73fc14 100644
--- 
a/server/src/main/java/org/apache/iotdb/db/query/reader/universal/DescPriorityMergeReader.java
+++ 
b/server/src/main/java/org/apache/iotdb/db/query/reader/universal/DescPriorityMergeReader.java
@@ -31,8 +31,8 @@ public class DescPriorityMergeReader extends 
PriorityMergeReader {
         new PriorityQueue<>(
             (o1, o2) -> {
               int timeCompare =
-                  Long.compare(o2.timeValuePair.getTimestamp(), 
o1.timeValuePair.getTimestamp());
-              return timeCompare != 0 ? timeCompare : 
o2.priority.compareTo(o1.priority);
+                  Long.compare(o2.currPair().getTimestamp(), 
o1.currPair().getTimestamp());
+              return timeCompare != 0 ? timeCompare : 
o2.getPriority().compareTo(o1.getPriority());
             });
   }
 
diff --git 
a/server/src/main/java/org/apache/iotdb/db/query/reader/universal/Element.java 
b/server/src/main/java/org/apache/iotdb/db/query/reader/universal/Element.java
new file mode 100644
index 0000000..c5b3ad5
--- /dev/null
+++ 
b/server/src/main/java/org/apache/iotdb/db/query/reader/universal/Element.java
@@ -0,0 +1,72 @@
+/*
+ * 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.iotdb.db.query.reader.universal;
+
+import org.apache.iotdb.tsfile.read.TimeValuePair;
+import org.apache.iotdb.tsfile.read.reader.IPointReader;
+
+import java.io.IOException;
+
+public class Element {
+
+  public PriorityMergeReader.MergeReaderPriority priority;
+  protected IPointReader reader;
+  public TimeValuePair timeValuePair;
+
+  public Element(
+      IPointReader reader,
+      TimeValuePair timeValuePair,
+      PriorityMergeReader.MergeReaderPriority priority) {
+    this.reader = reader;
+    this.timeValuePair = timeValuePair;
+    this.priority = priority;
+  }
+
+  public long currTime() {
+    return timeValuePair.getTimestamp();
+  }
+
+  public TimeValuePair currPair() {
+    return timeValuePair;
+  }
+
+  public boolean hasNext() throws IOException {
+    return reader.hasNextTimeValuePair();
+  }
+
+  public void next() throws IOException {
+    timeValuePair = reader.nextTimeValuePair();
+  }
+
+  public void close() throws IOException {
+    reader.close();
+  }
+
+  public IPointReader getReader() {
+    return reader;
+  }
+
+  public TimeValuePair getTimeValuePair() {
+    return timeValuePair;
+  }
+
+  public PriorityMergeReader.MergeReaderPriority getPriority() {
+    return priority;
+  }
+}
diff --git 
a/server/src/main/java/org/apache/iotdb/db/query/reader/universal/PriorityMergeReader.java
 
b/server/src/main/java/org/apache/iotdb/db/query/reader/universal/PriorityMergeReader.java
index de377e4..0982f6d 100644
--- 
a/server/src/main/java/org/apache/iotdb/db/query/reader/universal/PriorityMergeReader.java
+++ 
b/server/src/main/java/org/apache/iotdb/db/query/reader/universal/PriorityMergeReader.java
@@ -91,7 +91,7 @@ public class PriorityMergeReader implements IPointReader {
   @Override
   public TimeValuePair nextTimeValuePair() throws IOException {
     Element top = heap.poll();
-    TimeValuePair ret = top.timeValuePair;
+    TimeValuePair ret = top.getTimeValuePair();
     TimeValuePair topNext = null;
     if (top.hasNext()) {
       top.next();
@@ -108,10 +108,10 @@ public class PriorityMergeReader implements IPointReader {
 
   @Override
   public TimeValuePair currentTimeValuePair() throws IOException {
-    return heap.peek().timeValuePair;
+    return heap.peek().getTimeValuePair();
   }
 
-  private void updateHeap(long topTime, long topNextTime) throws IOException {
+  protected void updateHeap(long topTime, long topNextTime) throws IOException 
{
     while (!heap.isEmpty() && heap.peek().currTime() == topTime) {
       Element e = heap.poll();
       if (!e.hasNext()) {
@@ -143,39 +143,6 @@ public class PriorityMergeReader implements IPointReader {
     }
   }
 
-  static class Element {
-
-    IPointReader reader;
-    TimeValuePair timeValuePair;
-    MergeReaderPriority priority;
-
-    Element(IPointReader reader, TimeValuePair timeValuePair, 
MergeReaderPriority priority) {
-      this.reader = reader;
-      this.timeValuePair = timeValuePair;
-      this.priority = priority;
-    }
-
-    long currTime() {
-      return timeValuePair.getTimestamp();
-    }
-
-    TimeValuePair currPair() {
-      return timeValuePair;
-    }
-
-    boolean hasNext() throws IOException {
-      return reader.hasNextTimeValuePair();
-    }
-
-    void next() throws IOException {
-      timeValuePair = reader.nextTimeValuePair();
-    }
-
-    void close() throws IOException {
-      reader.close();
-    }
-  }
-
   public static class MergeReaderPriority implements 
Comparable<MergeReaderPriority> {
     long version;
     long offset;
diff --git a/thrift/src/main/thrift/cluster.thrift 
b/thrift/src/main/thrift/cluster.thrift
index 1c2b95a..c8edbe3 100644
--- a/thrift/src/main/thrift/cluster.thrift
+++ b/thrift/src/main/thrift/cluster.thrift
@@ -186,6 +186,20 @@ struct SingleSeriesQueryRequest {
   11: required int deduplicatedPathNum
 }
 
+struct MultSeriesQueryRequest {
+  1: required list<string> path
+  2: optional binary timeFilterBytes
+  3: optional binary valueFilterBytes
+  4: required long queryId
+  5: required Node requester
+  6: required Node header
+  7: required list<int> dataTypeOrdinal
+  8: required map<string,set<string>> deviceMeasurements
+  9: required bool ascending
+  10: required int fetchSize
+  11: required int deduplicatedPathNum
+}
+
 struct PreviousFillRequest {
   1: required string path
   2: required long queryTime
@@ -331,12 +345,25 @@ service TSDataService extends RaftService {
   long querySingleSeries(1:SingleSeriesQueryRequest request)
 
   /**
+  * Query mult time series without value filter.
+  * @return a readerId >= 0 if the query succeeds, otherwise the query fails
+  **/
+  long queryMultSeries(1:MultSeriesQueryRequest request)
+
+  /**
   * Fetch at max fetchSize time-value pairs using the resultSetId generated by 
querySingleSeries.
   * @return a ByteBuffer containing the serialized time-value pairs or an 
empty buffer if there
   * are not more results.
   **/
   binary fetchSingleSeries(1:Node header, 2:long readerId)
 
+    /**
+    * Fetch mult series at max fetchSize time-value pairs using the 
resultSetId generated by querySingleSeries.
+    * @return a map containing key-value,the serialized time-value pairs or an 
empty buffer if there
+    * are not more results.
+    **/
+    map<string,binary> fetchMultSeries(1:Node header, 2:long readerId, 
list<string> paths)
+
    /**
    * Query a time series and generate an IReaderByTimestamp.
    * @return a readerId >= 0 if the query succeeds, otherwise the query fails

Reply via email to