Skip to content
Closed
62 changes: 47 additions & 15 deletions core/src/main/scala/org/apache/spark/MapOutputTracker.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@

package org.apache.spark

import java.io._
import java.io.{ByteArrayInputStream, ObjectInputStream, ObjectOutputStream}
import java.util.concurrent.{ConcurrentHashMap, LinkedBlockingQueue, ThreadPoolExecutor, TimeUnit}
import java.util.concurrent.locks.ReentrantReadWriteLock
import java.util.zip.{GZIPInputStream, GZIPOutputStream}

import scala.collection.JavaConverters._
import scala.collection.mutable.{HashMap, ListBuffer, Map}
Expand All @@ -29,6 +28,10 @@ import scala.concurrent.duration.Duration
import scala.reflect.ClassTag
import scala.util.control.NonFatal

import com.github.luben.zstd.ZstdInputStream
import com.github.luben.zstd.ZstdOutputStream
import org.apache.commons.io.output.{ByteArrayOutputStream => ApacheByteArrayOutputStream}

import org.apache.spark.broadcast.{Broadcast, BroadcastManager}
import org.apache.spark.internal.Logging
import org.apache.spark.internal.config._
Expand Down Expand Up @@ -357,8 +360,8 @@ private[spark] abstract class MapOutputTracker(conf: SparkConf) extends Logging
*/
private[spark] class MapOutputTrackerMaster(
conf: SparkConf,
broadcastManager: BroadcastManager,
isLocal: Boolean)
private[spark] val broadcastManager: BroadcastManager,
private[spark] val isLocal: Boolean)
extends MapOutputTracker(conf) {

// The size at which we use Broadcast to send the map output statuses to the executors
Expand Down Expand Up @@ -807,13 +810,18 @@ private[spark] object MapOutputTracker extends Logging {
private val BROADCAST = 1

// Serialize an array of map output locations into an efficient byte format so that we can send
// it to reduce tasks. We do this by compressing the serialized bytes using GZIP. They will
// it to reduce tasks. We do this by compressing the serialized bytes using Zstd. They will
// generally be pretty compressible because many map outputs will be on the same hostname.
def serializeMapStatuses(statuses: Array[MapStatus], broadcastManager: BroadcastManager,
isLocal: Boolean, minBroadcastSize: Int): (Array[Byte], Broadcast[Array[Byte]]) = {
val out = new ByteArrayOutputStream
out.write(DIRECT)
val objOut = new ObjectOutputStream(new GZIPOutputStream(out))
// Using `org.apache.commons.io.output.ByteArrayOutputStream` instead of the standard one
// This implementation doesn't reallocate the whole memory block but allocates
// additional buffers. This way no buffers need to be garbage collected and
// the contents don't have to be copied to the new buffer.
val out = new ApacheByteArrayOutputStream()
val compressedOut = new ApacheByteArrayOutputStream()

val objOut = new ObjectOutputStream(out)
Utils.tryWithSafeFinally {
// Since statuses can be modified in parallel, sync on it
statuses.synchronized {
Expand All @@ -822,18 +830,42 @@ private[spark] object MapOutputTracker extends Logging {
} {
objOut.close()
}
val arr = out.toByteArray

val arr: Array[Byte] = {
val zos = new ZstdOutputStream(compressedOut)
Comment thread
dongjoon-hyun marked this conversation as resolved.
Utils.tryWithSafeFinally {
compressedOut.write(DIRECT)
// `out.writeTo(zos)` will write the uncompressed data from `out` to `zos`
// without copying to avoid unnecessary allocation and copy of byte[].
out.writeTo(zos)
} {
zos.close()
}
compressedOut.toByteArray
}
if (arr.length >= minBroadcastSize) {
// Use broadcast instead.
// Important arr(0) is the tag == DIRECT, ignore that while deserializing !
val bcast = broadcastManager.newBroadcast(arr, isLocal)
// toByteArray creates copy, so we can reuse out
Comment thread
dongjoon-hyun marked this conversation as resolved.
out.reset()
out.write(BROADCAST)
val oos = new ObjectOutputStream(new GZIPOutputStream(out))
oos.writeObject(bcast)
oos.close()
val outArr = out.toByteArray
val oos = new ObjectOutputStream(out)
Utils.tryWithSafeFinally {
oos.writeObject(bcast)
} {
oos.close()
}
val outArr = {
compressedOut.reset()
val zos = new ZstdOutputStream(compressedOut)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, @dbtsai , I am back-porting this into our internal repo. Looks like this compression is unnecessary since arr is already compressed by zstd. Compress again with already compressed byte[] is a waste of cpu time. WDYT?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actually value of the data (which is already compressed) will not be in the serialized form of out.writeTo(zos) as it's transient. Here, we are just serializing the reference to the actual data, and the actual data will be broadcast through TorrentBroadcast. See the next log, "Broadcast mapstatuses size = " + outArr.length + ", actual size = " + arr.length for your real data. The broadcast one is very small.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your clarification. It's indeed not including the compressed data.

Utils.tryWithSafeFinally {
compressedOut.write(BROADCAST)
out.writeTo(zos)
} {
zos.close()
}
compressedOut.toByteArray
}
logInfo("Broadcast mapstatuses size = " + outArr.length + ", actual size = " + arr.length)
(outArr, bcast)
} else {
Expand All @@ -846,7 +878,7 @@ private[spark] object MapOutputTracker extends Logging {
assert (bytes.length > 0)

def deserializeObject(arr: Array[Byte], off: Int, len: Int): AnyRef = {
val objIn = new ObjectInputStream(new GZIPInputStream(
val objIn = new ObjectInputStream(new ZstdInputStream(
new ByteArrayInputStream(arr, off, len)))
Utils.tryWithSafeFinally {
objIn.readObject()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,5 +332,4 @@ class MapOutputTrackerSuite extends SparkFunSuite {
tracker.stop()
rpcEnv.shutdown()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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.spark

import org.apache.spark.benchmark.Benchmark
import org.apache.spark.benchmark.BenchmarkBase
import org.apache.spark.scheduler.CompressedMapStatus
import org.apache.spark.storage.BlockManagerId

/**
* Benchmark for MapStatuses serialization performance.
* {{{
* To run this benchmark:
* 1. without sbt: bin/spark-submit --class <this class>
* --jars <catalyst test jar>,<core test jar>,<spark-avro jar> <avro test jar>
* 2. build/sbt "avro/test:runMain <this class>"
* 3. generate result: SPARK_GENERATE_BENCHMARK_FILES=1 build/sbt "core/test:runMain <this class>"
* Results will be written to "benchmarks/AvroReadBenchmark-results.txt".
Comment thread
dongjoon-hyun marked this conversation as resolved.
Outdated
* }}}
*/
object MapStatusesSerializationBenchmark extends BenchmarkBase {

var sc: SparkContext = null

def serializationBenchmark(numMaps: Int, blockSize: Int,
minBroadcastSize: Int = Int.MaxValue): Unit = {
val benchmark = new Benchmark(s"MapStatuses Serialization with $numMaps MapOutput",
numMaps, output = output)

val shuffleId = 10
val tracker = sc.env.mapOutputTracker.asInstanceOf[MapOutputTrackerMaster]
val rpcEnv = sc.env.rpcEnv
val masterEndpoint = new MapOutputTrackerMasterEndpoint(rpcEnv, tracker, sc.getConf)
rpcEnv.stop(tracker.trackerEndpoint)
rpcEnv.setupEndpoint(MapOutputTracker.ENDPOINT_NAME, masterEndpoint)


tracker.registerShuffle(shuffleId, numMaps)
val r = new scala.util.Random(912)
(0 until numMaps).foreach { i =>
tracker.registerMapOutput(shuffleId, i,
new CompressedMapStatus(BlockManagerId(s"node$i", s"node$i.spark.apache.org", 1000),
Array.range(0, 500).map(i => math.abs(r.nextLong())), i))
}

val shuffleStatus = tracker.shuffleStatuses.get(shuffleId).head


var serializedMapStatusSizes = 0
var serializedBroadcastSizes = 0

val (serializedMapStatus, serializedBroadcast) = MapOutputTracker.serializeMapStatuses(
shuffleStatus.mapStatuses, tracker.broadcastManager, tracker.isLocal, minBroadcastSize)
serializedMapStatusSizes = serializedMapStatus.length
if (serializedBroadcast != null) {
serializedBroadcastSizes = serializedBroadcast.value.length
}


benchmark.addCase("Serialization") { _ =>
MapOutputTracker.serializeMapStatuses(
shuffleStatus.mapStatuses, tracker.broadcastManager, tracker.isLocal, minBroadcastSize)
}

benchmark.run()
tracker.unregisterShuffle(shuffleId)
tracker.stop()
}

override def runBenchmarkSuite(mainArgs: Array[String]): Unit = {
createSparkContext()
serializationBenchmark(200000, 500)
}

def createSparkContext(): Unit = {
val conf = new SparkConf()
if (sc != null) {
sc.stop()
}
sc = new SparkContext("local", "MapStatusesSerializationBenchmark", conf)
}

override def afterAll(): Unit = {
if (sc != null) {
sc.stop()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ private[spark] class Benchmark(
val stdev = if (runTimes.size > 1) {
math.sqrt(runTimes.map(time => (time - avg) * (time - avg)).sum / (runTimes.size - 1))
} else 0
Result(avg / 1000000.0, num / (best / 1000.0), best / 1000000.0, stdev / 1000000.0)
Result(avg / 1E6, num / (best / 1E3), best / 1E6, stdev / 1E6)
}
}

Expand Down