Skip to content
This repository was archived by the owner on Aug 22, 2025. It is now read-only.

Commit d52c016

Browse files
[CROSSDATA-2041] Incremental collect: limit performance improvement (#834) (#874)
1 parent 2d53fcb commit d52c016

File tree

8 files changed

+421
-15
lines changed

8 files changed

+421
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Only listing significant user-visible, not internal code cleanups and minor bug
4646
* [CROSSDATA-2039][CROSSDATA-2040] Fix bug in session catalog cache, inconsistent cache key generation
4747
* [CROSSDATA-2049] Allow crontab expression for "refresh-time" in partition refresh
4848
* [CROSSDATA-2050] Allow proper requestId Deserialization in Server, to keep both values equals
49+
* [CROSSDATA-2041] Incremental collect: limit performance improvement
4950

5051
## 2.15.0-24f463e (Built: January 14, 2019 | Released: January 17, 2019)
5152

common/src/main/scala/org/apache/spark/sql/crossdata/serializers/StreamedSuccessfulSQLResultSerializer.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,7 @@ class StreamedRowSerializer(schema: StructType) extends CustomSerializer[Interna
4242
PartialFunction.empty
4343
)
4444
)
45+
46+
case class CustomStreamedRow(streamedRow: StreamedValues)
47+
48+
case class StreamedValues(values: List[Any])

core/src/main/scala/org/apache/spark/sql/crossdata/catalyst/catalog/persistent/GovernanceMetadataRetriever.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ object GovernanceMetadataRetriever {
426426
logger.warn(s"Table ${database.name}.${success.tableName} failed while refreshing partitions: ${th.getMessage}", th)
427427
}
428428
}
429+
case _ => //Do nothing
429430
}
430431
droppableTableNames.foreach {
431432
name =>

core/src/main/scala/org/apache/spark/sql/crossdata/execution/XDPlan.scala

Lines changed: 159 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,26 @@
55
*/
66
package org.apache.spark.sql.crossdata.execution
77

8+
import java.io._
9+
810
import com.stratio.common.utils.components.logger.impl.Slf4jLoggerComponent
911
import com.stratio.crossdata.common.profiler.PerformanceLogger
1012
import com.stratio.crossdata.connector.NativeScan
1113
import com.stratio.crossdata.metrics.{MetricsGlossary, MetricsRegister}
14+
import org.apache.spark.SparkEnv
15+
import org.apache.spark.io.CompressionCodec
1216
import org.apache.spark.rdd.RDD
1317
import org.apache.spark.sql.catalyst.InternalRow
14-
import org.apache.spark.sql.catalyst.expressions.{Alias, Attribute, AttributeSet, GetMapValue, GetStructField, Literal}
18+
import org.apache.spark.sql.catalyst.expressions.{Alias, Attribute, AttributeSet, GetMapValue, GetStructField, Literal, UnsafeProjection, UnsafeRow}
1519
import org.apache.spark.sql.catalyst.plans.QueryPlan
1620
import org.apache.spark.sql.catalyst.plans.logical._
1721
import org.apache.spark.sql.crossdata.execution.command.XDExplainCommand
18-
import org.apache.spark.sql.execution.SparkPlan
22+
import org.apache.spark.sql.crossdata.serializers.CustomStreamedRow
1923
import org.apache.spark.sql.execution.datasources._
24+
import org.apache.spark.sql.execution.{DeserializeToObjectExec, InputAdapter, LocalLimitExec, MapPartitionsExec, SerializeFromObjectExec, SparkPlan, WholeStageCodegenExec}
25+
import org.apache.spark.sql.types.IntegerType
2026

27+
import scala.collection.mutable.ArrayBuffer
2128
import scala.util.{Failure, Success, Try}
2229

2330
case class XDPlan(@transient xdQueryExecution: XDQueryExecution,
@@ -31,6 +38,8 @@ case class XDPlan(@transient xdQueryExecution: XDQueryExecution,
3138

3239
private lazy val nativeQueryExecutor: Option[NativeScan] = findNativeQueryExecutor(analyzedPlan, isNativeQueriesEnabled)
3340

41+
private lazy val applyXDLimitRule = xdQueryExecution.sparkSession.sparkContext.conf.getBoolean("spark.sql.crossdata.limitRule", false)
42+
3443
private lazy val usablePlan: QueryPlan[_] =
3544
if(nativeQueryExecutor.exists(x => supportedPlan(x, analyzedPlan))) {
3645
analyzedPlan
@@ -81,6 +90,147 @@ case class XDPlan(@transient xdQueryExecution: XDQueryExecution,
8190
}
8291
}
8392

93+
/**
94+
* Decode the byte arrays back to UnsafeRows and put them into buffer.
95+
*
96+
* NOTE: Great part of this code is a copy from the [[SparkPlan]].decodeUnsafeRows(bytes).
97+
*
98+
*/
99+
private def decodeUnsafeRows(bytes: Array[Byte]): Iterator[InternalRow] = {
100+
val nFields = schema.length
101+
102+
val codec = CompressionCodec.createCodec(SparkEnv.get.conf)
103+
val bis = new ByteArrayInputStream(bytes)
104+
val ins = new DataInputStream(codec.compressedInputStream(bis))
105+
106+
new Iterator[InternalRow] {
107+
private var sizeOfNextRow = ins.readInt()
108+
override def hasNext: Boolean = sizeOfNextRow >= 0
109+
override def next(): InternalRow = {
110+
val bs = new Array[Byte](sizeOfNextRow)
111+
ins.readFully(bs)
112+
val row = new UnsafeRow(nFields)
113+
row.pointTo(bs, sizeOfNextRow)
114+
sizeOfNextRow = ins.readInt()
115+
row
116+
}
117+
}
118+
}
119+
120+
/**
121+
* Packing the UnsafeRows into byte array for faster serialization.
122+
* The byte arrays are in the following format:
123+
* [size] [bytes of UnsafeRow] [size] [bytes of UnsafeRow] ... [-1]
124+
*
125+
* UnsafeRow is highly compressible (at least 8 bytes for any column), the byte array is also
126+
* compressed.
127+
*
128+
* NOTE: Great part of this code is a copy from the [[SparkPlan]].getByteArrayRdd(n).
129+
*
130+
* @param executedPlan usable executed plan.
131+
* @param limit global limit.
132+
* @return executed plan result.
133+
*/
134+
private def getByteArrayRdd(executedPlan: SparkPlan, limit: Int = -1): RDD[Array[Byte]] = {
135+
executedPlan.execute().mapPartitionsInternal { iter =>
136+
var count = 0
137+
val buffer = new Array[Byte](4 << 10) // 4K
138+
val codec = CompressionCodec.createCodec(SparkEnv.get.conf)
139+
val bos = new ByteArrayOutputStream()
140+
val out = new DataOutputStream(codec.compressedOutputStream(bos))
141+
while (iter.hasNext && (limit < 0 || count < limit)) {
142+
val row = iter.next().asInstanceOf[UnsafeRow]
143+
out.writeInt(row.getSizeInBytes)
144+
row.writeToStream(out, buffer)
145+
count += 1
146+
}
147+
out.writeInt(-1)
148+
out.flush()
149+
out.close()
150+
Iterator(bos.toByteArray)
151+
}
152+
}
153+
154+
/**
155+
* Fetch data partition by partition from a specific plan until the number of rows reaches a specific limit.
156+
*
157+
* NOTE: Great part of this code is a copy from the [[SparkPlan.executeTake(n)]].
158+
*
159+
* @param executedPlan usable executed plan.
160+
* @param limit global limit.
161+
* @return executed plan result.
162+
*/
163+
private def incrementalExecute(executedPlan: SparkPlan, limit: Int): Array[InternalRow] = {
164+
if (limit == 0) {
165+
return new Array[InternalRow](0)
166+
}
167+
168+
import org.json4s._
169+
import org.json4s.jackson.JsonMethods._
170+
import org.json4s.jackson.Serialization._
171+
implicit val formats: Formats = DefaultFormats
172+
173+
val buf = new ArrayBuffer[InternalRow]
174+
val childRDD = getByteArrayRdd(executedPlan, limit)
175+
val totalParts = childRDD.partitions.length
176+
var partsScanned = 0
177+
var numOfRows = 0
178+
while (numOfRows < limit && partsScanned < totalParts) {
179+
// The number of partitions to try in this iteration. It is ok for this number to be
180+
// greater than totalParts because we actually cap it at totalParts in runJob.
181+
var numPartsToTry = 1L
182+
if (partsScanned > 0) {
183+
// If we didn't find any rows after the previous iteration, quadruple and retry.
184+
// Otherwise, interpolate the number of partitions we need to try, but overestimate
185+
// it by 50%. We also cap the estimation in the end.
186+
val limitScaleUpFactor = Math.max(sqlContext.conf.limitScaleUpFactor, 2)
187+
if (buf.isEmpty) {
188+
numPartsToTry = partsScanned * limitScaleUpFactor
189+
} else {
190+
// the left side of max is >=1 whenever partsScanned >= 2
191+
numPartsToTry = Math.max((1.5 * limit * partsScanned / buf.size).toInt - partsScanned, 1)
192+
numPartsToTry = Math.min(numPartsToTry, partsScanned * limitScaleUpFactor)
193+
}
194+
}
195+
196+
val p = partsScanned.until(math.min(partsScanned + numPartsToTry, totalParts).toInt)
197+
val sc = sqlContext.sparkContext
198+
199+
val jobRDD = if (numOfRows == 0) childRDD else getByteArrayRdd(executedPlan, limit-numOfRows)
200+
201+
val res = sc.runJob(jobRDD, (it: Iterator[Array[Byte]]) => if (it.hasNext) it.next() else Array.empty[Byte], p)
202+
203+
res.foreach{ pres =>
204+
val iter = if(numOfRows < limit){
205+
decodeUnsafeRows(pres)
206+
} else {
207+
Iterator.empty
208+
}
209+
210+
while(iter.hasNext && numOfRows < limit){
211+
val row = iter.next()
212+
val numElements = row.getInt(0)
213+
214+
if(numOfRows + numElements <= limit){
215+
numOfRows += numElements
216+
buf += row
217+
} else {
218+
collectFirst{
219+
case SerializeFromObjectExec(serializer, _) =>
220+
val remainingRows = limit - numOfRows
221+
val projection = UnsafeProjection.create(serializer)
222+
projection.initialize(0)
223+
numOfRows += remainingRows
224+
buf += projection(InternalRow.apply((remainingRows, write((parse(row.getString(1)).extract[List[CustomStreamedRow]]).take(remainingRows)))))
225+
}
226+
}
227+
}
228+
}
229+
partsScanned += p.size
230+
}
231+
buf.toArray
232+
}
233+
84234
override def executeCollect(): Array[InternalRow] = {
85235
nativeQueryExecutor match {
86236
case Some(nqe) =>
@@ -109,7 +259,13 @@ case class XDPlan(@transient xdQueryExecution: XDQueryExecution,
109259
case None =>
110260
logPerformance("[XDPlan][executeSpark]") {
111261
MetricsRegister.countExecution(MetricsGlossary.Counter.`current_queries_spark_total`, discountWhenFinished = true) {
112-
Try(child.executeCollect())
262+
child match {
263+
case WholeStageCodegenExec(SerializeFromObjectExec(_, InputAdapter(MapPartitionsExec(_, _, DeserializeToObjectExec(_, _, WholeStageCodegenExec(LocalLimitExec(limit, _))))))) if applyXDLimitRule =>
264+
logger.debug("Resolving query with incremental execution")
265+
Try(incrementalExecute(child, limit))
266+
case _ =>
267+
Try(child.executeCollect())
268+
}
113269
}
114270
} match {
115271
case Success(r) => r

core/src/main/scala/org/apache/spark/sql/crossdata/session/XDSessionStateBuilder.scala

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,18 @@
55
*/
66
package org.apache.spark.sql.crossdata.session
77

8-
import org.apache.spark.sql.{SparkSession, Strategy}
98
import org.apache.spark.sql.catalyst.analysis.Analyzer
109
import org.apache.spark.sql.catalyst.catalog.SessionCatalog
1110
import org.apache.spark.sql.catalyst.parser.ParserInterface
1211
import org.apache.spark.sql.catalyst.plans.logical._
1312
import org.apache.spark.sql.catalyst.rules.Rule
1413
import org.apache.spark.sql.crossdata.XDSession
15-
import org.apache.spark.sql.crossdata.catalyst.catalog.{XDCatalogWrapper, XDSessionCatalog}
1614
import org.apache.spark.sql.crossdata.catalyst.catalog.temporary.implementations.DefaultTemporaryCatalog
15+
import org.apache.spark.sql.crossdata.catalyst.catalog.{XDCatalogWrapper, XDSessionCatalog}
1716
import org.apache.spark.sql.crossdata.execution.{XDQueryExecution, XDSparkSqlParser}
1817
import org.apache.spark.sql.execution.{QueryExecution, SparkPlanner}
19-
import org.apache.spark.sql.internal.SQLConf.buildConf
20-
import org.apache.spark.sql.internal.{BaseSessionStateBuilder, SQLConf, SessionState}
18+
import org.apache.spark.sql.internal.{BaseSessionStateBuilder, SessionState}
19+
import org.apache.spark.sql.{SparkSession, Strategy}
2120

2221

2322
class XDSessionStateBuilder(sparkSession: SparkSession, parentState: Option[SessionState] = None) extends BaseSessionStateBuilder(sparkSession, parentState){
@@ -34,12 +33,13 @@ class XDSessionStateBuilder(sparkSession: SparkSession, parentState: Option[Sess
3433
val applyXDLimitRule = sparkSession.sparkContext.conf.getBoolean("spark.sql.crossdata.limitRule", false)
3534
logDebug(s"applyXDLimitRule? $applyXDLimitRule")
3635
override def apply(plan: LogicalPlan): LogicalPlan = plan transform {
37-
case s@SerializeFromObject(serializer, MapPartitions(func, outputObjAttr, DeserializeToObject(des, attrs, gl@GlobalLimit(_, _)))) =>
38-
if(applyXDLimitRule){
39-
SerializeFromObject(serializer, MapPartitions(func, outputObjAttr, DeserializeToObject(des, attrs, ReturnAnswer(gl))))
40-
} else {
41-
s
42-
}
36+
case s@SerializeFromObject(_, MapPartitions(_, _, DeserializeToObject(_, _, Limit(_, Project(_, Sort(_, _, _)))))) if applyXDLimitRule =>
37+
s // Skip third case
38+
case s@SerializeFromObject(_, MapPartitions(_, _, DeserializeToObject(_, _, Limit(_, Sort(_, _, _))))) if applyXDLimitRule =>
39+
s // Skip third case
40+
case SerializeFromObject(serializer, MapPartitions(func, outputObjAttr, DeserializeToObject(des, attrs, Limit(exp, child)))) if applyXDLimitRule =>
41+
logDebug("Applying XDLimitRule")
42+
SerializeFromObject(serializer, MapPartitions(func, outputObjAttr, DeserializeToObject(des, attrs, LocalLimit(exp, child))))
4343
}
4444
}
4545

server/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,12 @@ This software – including all its source code – contains proprietary informa
243243
<type>test-jar</type>
244244
<scope>test</scope>
245245
</dependency>
246+
<dependency>
247+
<groupId>org.apache.httpcomponents</groupId>
248+
<artifactId>fluent-hc</artifactId>
249+
<version>${httpclient.version}</version>
250+
<scope>test</scope>
251+
</dependency>
246252
</dependencies>
247253

248254
<build>

0 commit comments

Comments
 (0)