Skip to content

Commit 84c44b5

Browse files
author
Andrew Or
committed
[SPARK-12081] Make unified memory manager work with small heaps
The existing `spark.memory.fraction` (default 0.75) gives the system 25% of the space to work with. For small heaps, this is not enough: e.g. default 1GB leaves only 250MB system memory. This is especially a problem in local mode, where the driver and executor are crammed in the same JVM. Members of the community have reported driver OOM's in such cases. **New proposal.** We now reserve 300MB before taking the 75%. For 1GB JVMs, this leaves `(1024 - 300) * 0.75 = 543MB` for execution and storage. This is proposal (1) listed in the [JIRA](https://issues.apache.org/jira/browse/SPARK-12081). Author: Andrew Or <[email protected]> Closes #10081 from andrewor14/unified-memory-small-heaps. (cherry picked from commit d96f8c9) Signed-off-by: Andrew Or <[email protected]>
1 parent 72da2a2 commit 84c44b5

File tree

4 files changed

+41
-7
lines changed

4 files changed

+41
-7
lines changed

core/src/main/scala/org/apache/spark/memory/UnifiedMemoryManager.scala

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import org.apache.spark.storage.{BlockStatus, BlockId}
2626
* A [[MemoryManager]] that enforces a soft boundary between execution and storage such that
2727
* either side can borrow memory from the other.
2828
*
29-
* The region shared between execution and storage is a fraction of the total heap space
29+
* The region shared between execution and storage is a fraction of (the total heap space - 300MB)
3030
* configurable through `spark.memory.fraction` (default 0.75). The position of the boundary
3131
* within this space is further determined by `spark.memory.storageFraction` (default 0.5).
3232
* This means the size of the storage region is 0.75 * 0.5 = 0.375 of the heap space by default.
@@ -48,7 +48,7 @@ import org.apache.spark.storage.{BlockStatus, BlockId}
4848
*/
4949
private[spark] class UnifiedMemoryManager private[memory] (
5050
conf: SparkConf,
51-
maxMemory: Long,
51+
val maxMemory: Long,
5252
private val storageRegionSize: Long,
5353
numCores: Int)
5454
extends MemoryManager(
@@ -130,6 +130,12 @@ private[spark] class UnifiedMemoryManager private[memory] (
130130

131131
object UnifiedMemoryManager {
132132

133+
// Set aside a fixed amount of memory for non-storage, non-execution purposes.
134+
// This serves a function similar to `spark.memory.fraction`, but guarantees that we reserve
135+
// sufficient memory for the system even for small heaps. E.g. if we have a 1GB JVM, then
136+
// the memory used for execution and storage will be (1024 - 300) * 0.75 = 543MB by default.
137+
private val RESERVED_SYSTEM_MEMORY_BYTES = 300 * 1024 * 1024
138+
133139
def apply(conf: SparkConf, numCores: Int): UnifiedMemoryManager = {
134140
val maxMemory = getMaxMemory(conf)
135141
new UnifiedMemoryManager(
@@ -144,8 +150,16 @@ object UnifiedMemoryManager {
144150
* Return the total amount of memory shared between execution and storage, in bytes.
145151
*/
146152
private def getMaxMemory(conf: SparkConf): Long = {
147-
val systemMaxMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)
153+
val systemMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)
154+
val reservedMemory = conf.getLong("spark.testing.reservedMemory",
155+
if (conf.contains("spark.testing")) 0 else RESERVED_SYSTEM_MEMORY_BYTES)
156+
val minSystemMemory = reservedMemory * 1.5
157+
if (systemMemory < minSystemMemory) {
158+
throw new IllegalArgumentException(s"System memory $systemMemory must " +
159+
s"be at least $minSystemMemory. Please use a larger heap size.")
160+
}
161+
val usableMemory = systemMemory - reservedMemory
148162
val memoryFraction = conf.getDouble("spark.memory.fraction", 0.75)
149-
(systemMaxMemory * memoryFraction).toLong
163+
(usableMemory * memoryFraction).toLong
150164
}
151165
}

core/src/test/scala/org/apache/spark/memory/UnifiedMemoryManagerSuite.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,24 @@ class UnifiedMemoryManagerSuite extends MemoryManagerSuite with PrivateMethodTes
182182
assertEnsureFreeSpaceCalled(ms, 850L)
183183
}
184184

185+
test("small heap") {
186+
val systemMemory = 1024 * 1024
187+
val reservedMemory = 300 * 1024
188+
val memoryFraction = 0.8
189+
val conf = new SparkConf()
190+
.set("spark.memory.fraction", memoryFraction.toString)
191+
.set("spark.testing.memory", systemMemory.toString)
192+
.set("spark.testing.reservedMemory", reservedMemory.toString)
193+
val mm = UnifiedMemoryManager(conf, numCores = 1)
194+
val expectedMaxMemory = ((systemMemory - reservedMemory) * memoryFraction).toLong
195+
assert(mm.maxMemory === expectedMaxMemory)
196+
197+
// Try using a system memory that's too small
198+
val conf2 = conf.clone().set("spark.testing.memory", (reservedMemory / 2).toString)
199+
val exception = intercept[IllegalArgumentException] {
200+
UnifiedMemoryManager(conf2, numCores = 1)
201+
}
202+
assert(exception.getMessage.contains("larger heap size"))
203+
}
204+
185205
}

docs/configuration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -719,8 +719,8 @@ Apart from these, the following properties are also available, and may be useful
719719
<td><code>spark.memory.fraction</code></td>
720720
<td>0.75</td>
721721
<td>
722-
Fraction of the heap space used for execution and storage. The lower this is, the more
723-
frequently spills and cached data eviction occur. The purpose of this config is to set
722+
Fraction of (heap space - 300MB) used for execution and storage. The lower this is, the
723+
more frequently spills and cached data eviction occur. The purpose of this config is to set
724724
aside memory for internal metadata, user data structures, and imprecise size estimation
725725
in the case of sparse, unusually large records. Leaving this at the default value is
726726
recommended. For more detail, see <a href="tuning.html#memory-management-overview">

docs/tuning.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ variety of workloads without requiring user expertise of how memory is divided i
114114
Although there are two relevant configurations, the typical user should not need to adjust them
115115
as the default values are applicable to most workloads:
116116

117-
* `spark.memory.fraction` expresses the size of `M` as a fraction of the total JVM heap space
117+
* `spark.memory.fraction` expresses the size of `M` as a fraction of the (JVM heap space - 300MB)
118118
(default 0.75). The rest of the space (25%) is reserved for user data structures, internal
119119
metadata in Spark, and safeguarding against OOM errors in the case of sparse and unusually
120120
large records.

0 commit comments

Comments
 (0)