-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-34309][BUILD][CORE][SQL][K8S]Use Caffeine instead of Guava Cache #31517
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 48 commits
d4e3901
a3d794a
85dc7b9
399f5b2
dbb46dc
01f99fe
18813e9
3f13689
39032e6
674af31
5f08d0f
0c5382a
4761a5b
4b49b84
1805f34
adc6d92
bdb522b
ac35393
b116f87
bd46b67
8842fa3
8985d6d
b568fcc
ca9d58d
97c3c74
f47cdb8
425c345
e0006c6
c02bc5b
c1c3cef
c9f45ae
24c361b
2b3072e
636bfb5
825b69a
d1315c3
32d9ce0
8d6ad81
cf30234
5eb75fa
7129802
045d3dc
554b5a5
f488a3b
89abb61
1a24ed4
96e6cc8
7028ffa
3fad6ef
aa742d3
95d5c2e
3ba2574
8995d72
f61b041
6146529
7c53f73
a069bff
1cbebd3
52037e1
8391e8b
0e3c5fb
4cb428d
35bea2d
f314403
994fcee
a64b280
a93e37e
56d41f1
d7610ac
93784cc
db1de65
47b0bf8
34d31fd
93330ce
e3b81c5
74e2ac3
7506999
68193a9
ee05314
db9cc56
5423f36
c6b9dc6
0743e3b
c5c84cd
f2a656a
d69df8e
9cd9c35
7b360a7
33f5353
87def1a
6c74fc6
5a75b2c
706a68d
81f863f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| ================================================================================================ | ||
| Loading Cache | ||
| ================================================================================================ | ||
|
|
||
| OpenJDK 64-Bit Server VM 11.0.8+10-LTS on Mac OS X 10.15.7 | ||
| Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz | ||
| Loading Cache: Best Time(ms) Avg Time(ms) Stdev(ms) Rate(M/s) Per Row(ns) Relative | ||
| -------------------------------------------------------------------------------------------------------------------------- | ||
| Guava Cache 5 6 0 15.2 65.6 1.0X | ||
| Caffeine 2 2 1 46.9 21.3 3.1X | ||
| CaffeinatedGuava with Guava CacheLoader 2 2 0 46.0 21.8 3.0X | ||
| CaffeinatedGuava with Caffeine CacheLoader 2 2 0 46.1 21.7 3.0X | ||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| ================================================================================================ | ||
| Loading Cache | ||
| ================================================================================================ | ||
|
|
||
| OpenJDK 64-Bit Server VM 1.8.0_232-b18 on Mac OS X 10.15.7 | ||
| Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz | ||
| Loading Cache: Best Time(ms) Avg Time(ms) Stdev(ms) Rate(M/s) Per Row(ns) Relative | ||
| -------------------------------------------------------------------------------------------------------------------------- | ||
| Guava Cache 5 6 2 15.2 65.7 1.0X | ||
| Caffeine 2 2 1 41.2 24.3 2.7X | ||
| CaffeinatedGuava with Guava CacheLoader 2 2 0 44.0 22.7 2.9X | ||
| CaffeinatedGuava with Caffeine CacheLoader 2 2 0 44.2 22.6 2.9X | ||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,7 +25,9 @@ import javax.servlet.http.{HttpServletRequest, HttpServletResponse} | |
| import scala.collection.JavaConverters._ | ||
|
|
||
| import com.codahale.metrics.{Counter, MetricRegistry, Timer} | ||
| import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache, RemovalListener, RemovalNotification} | ||
| import com.github.benmanes.caffeine.cache.{Caffeine, RemovalCause, RemovalListener} | ||
| import com.github.benmanes.caffeine.guava.CaffeinatedGuava | ||
| import com.google.common.cache.{CacheLoader, LoadingCache} | ||
| import com.google.common.util.concurrent.UncheckedExecutionException | ||
| import org.eclipse.jetty.servlet.FilterHolder | ||
|
|
||
|
|
@@ -62,21 +64,27 @@ private[history] class ApplicationCache( | |
|
|
||
| /** | ||
| * Removal event notifies the provider to detach the UI. | ||
| * @param rm removal notification | ||
| * @param key removal key | ||
| * @param value removal value | ||
| */ | ||
| override def onRemoval(rm: RemovalNotification[CacheKey, CacheEntry]): Unit = { | ||
| override def onRemoval(key: CacheKey, value: CacheEntry, | ||
| cause: RemovalCause): Unit = { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe mention RemovalCause in the scaladoc?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: move this to the previous line?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok ~ It doesn't look exceeds 100 characters ~ |
||
| metrics.evictionCount.inc() | ||
| val key = rm.getKey | ||
| logDebug(s"Evicting entry ${key}") | ||
| operations.detachSparkUI(key.appId, key.attemptId, rm.getValue().loadedUI.ui) | ||
| logDebug(s"Evicting entry $key") | ||
| operations.detachSparkUI(key.appId, key.attemptId, value.loadedUI.ui) | ||
| } | ||
| } | ||
|
|
||
| private val appCache: LoadingCache[CacheKey, CacheEntry] = { | ||
| CacheBuilder.newBuilder() | ||
| .maximumSize(retainedApplications) | ||
| .removalListener(removalListener) | ||
| .build(appLoader) | ||
| val builder = Caffeine.newBuilder() | ||
| .maximumSize(retainedApplications) | ||
| .removalListener(removalListener) | ||
| // SPARK-34309: Use custom Executor to compatible with | ||
| // the data eviction behavior of Guava cache | ||
| .executor((command: Runnable) => command.run()) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this going to run in the same thread? Is that what the old behaviour would have been?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it's a compromise for compatibility with old behaviour at present. In the future, I will optimize this behaviour through other pr. |
||
| // Wrapping as CaffeinatedGuava to be compatible with | ||
| // the exception behavior of Guava cache | ||
| CaffeinatedGuava.build(builder, appLoader) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason to preserve behavior of guava cache here ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My uninformed review is that you can drop the guava adapters. The catch in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @ben-manes !
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can remove the
For example, there are some differences in the processing of error, when the
For example, |
||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,9 @@ package org.apache.spark.storage | |
|
|
||
| import java.io.{Externalizable, IOException, ObjectInput, ObjectOutput} | ||
|
|
||
| import com.google.common.cache.{CacheBuilder, CacheLoader} | ||
| import com.github.benmanes.caffeine.cache.Caffeine | ||
| import com.github.benmanes.caffeine.guava.CaffeinatedGuava | ||
| import com.google.common.cache.CacheLoader | ||
|
|
||
| import org.apache.spark.SparkContext | ||
| import org.apache.spark.annotation.DeveloperApi | ||
|
|
@@ -136,11 +138,14 @@ private[spark] object BlockManagerId { | |
| * The max cache size is hardcoded to 10000, since the size of a BlockManagerId | ||
| * object is about 48B, the total memory cost should be below 1MB which is feasible. | ||
| */ | ||
| val blockManagerIdCache = CacheBuilder.newBuilder() | ||
| .maximumSize(10000) | ||
| .build(new CacheLoader[BlockManagerId, BlockManagerId]() { | ||
| override def load(id: BlockManagerId) = id | ||
| }) | ||
| val blockManagerIdCache = { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: maybe change this to: val blockManagerIdCache = Caffeine.newBuilder()
.maximumSize(10000)
.build[BlockManagerId, BlockManagerId](identity)?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 7b360a7 revert this change |
||
| val builder = Caffeine.newBuilder() | ||
| .maximumSize(10000) | ||
| CaffeinatedGuava.build(builder, | ||
| new CacheLoader[BlockManagerId, BlockManagerId]() { | ||
| override def load(id: BlockManagerId): BlockManagerId = id | ||
| }) | ||
| } | ||
|
|
||
| def getCachedBlockManagerId(id: BlockManagerId): BlockManagerId = { | ||
| blockManagerIdCache.get(id) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| /* | ||
| * 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 scala.util.Random | ||
|
|
||
| import com.github.benmanes.caffeine.cache.{CacheLoader => CaffeineCacheLoader, Caffeine} | ||
| import com.github.benmanes.caffeine.guava.CaffeinatedGuava | ||
| import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache} | ||
|
|
||
| import org.apache.spark.benchmark.{Benchmark, BenchmarkBase} | ||
|
|
||
| /** | ||
| * Benchmark for Guava Cache vs Caffeine. | ||
| * To run this benchmark: | ||
| * {{{ | ||
| * 1. without sbt: | ||
| * bin/spark-submit --class <this class> --jars <spark core test jar> | ||
| * 2. build/sbt "core/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/KryoBenchmark-results.txt". | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. update this
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
| * }}} | ||
| */ | ||
| object LocalCacheBenchmark extends BenchmarkBase { | ||
|
|
||
| override def runBenchmarkSuite(mainArgs: Array[String]): Unit = { | ||
| runBenchmark("Loading Cache") { | ||
| val size = 10000 | ||
| val parallelism = 8 | ||
| val guavaCacheConcurrencyLevel = 8 | ||
| val dataset = (1 to parallelism) | ||
| .map(_ => Random.shuffle(List.range(0, size))) | ||
| .map(list => list.map(i => TestData(i))) | ||
|
Comment on lines
+50
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This distribution is uniformly distributed with only single key overlaps. This means that there are not hot and cold entries, e.g. random eviction has an optimal hit rate. In reality, some entries will be used much more often and follows a power law curve. That is fairly generous distribution for a cache like guava, which uses coarse locking of multiple hash tables. That way the access distribution matches the hash distribution, so ideally spread across all of the locks. In reality, while the hash distribution will be uniform the access distribution is not so a lock holding hot entries will be used much more frequently. In Caffeine's benchmarks, it uses a scrambled Zipfian distribution (YCSB's generator). That would show an even larger speedup. More just an fyi that your benchmarks are conservative and you may see a larger gain. Of course, if the caches are not a bottleneck you might not see any benefit except if the eviction policy improves the hit rates in your workloads.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for your advice. I think we should avoid introducing more dependencies, so I'll try to implement this data generator in spark code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think your code is fine as is. Maybe just document the simplification? I mostly wanted to let you know since writing a good benchmark is hard, not that you should change it. Your code served its purpose, and you might not get much more out of improving it. |
||
| val guavaCacheLoader = new CacheLoader[TestData, TestData]() { | ||
| override def load(id: TestData): TestData = { | ||
| id | ||
| } | ||
| } | ||
| val caffeineCacheLoader = new CaffeineCacheLoader[TestData, TestData]() { | ||
| override def load(id: TestData): TestData = { | ||
| id | ||
| } | ||
| } | ||
|
|
||
| val benchmark = new Benchmark("Loading Cache", size * parallelism, 3, output = output) | ||
| benchmark.addCase("Guava Cache") { _ => | ||
| val cache = CacheBuilder.newBuilder() | ||
| .concurrencyLevel(guavaCacheConcurrencyLevel).build[TestData, TestData](guavaCacheLoader) | ||
| dataset.par.foreach(dataList => dataList.foreach(key => cache.get(key))) | ||
| cache.cleanUp() | ||
| } | ||
|
|
||
| benchmark.addCase("Caffeine") { _ => | ||
| val cache = Caffeine.newBuilder().build[TestData, TestData](caffeineCacheLoader) | ||
| dataset.par.foreach(dataList => dataList.foreach(key => cache.get(key))) | ||
| cache.cleanUp() | ||
| } | ||
|
|
||
| benchmark.addCase("CaffeinatedGuava with Guava CacheLoader") { _ => | ||
| val cache: LoadingCache[TestData, TestData] = | ||
| CaffeinatedGuava.build(Caffeine.newBuilder(), guavaCacheLoader) | ||
| dataset.par.foreach(dataList => dataList.foreach(key => cache.get(key))) | ||
| cache.cleanUp() | ||
| } | ||
|
|
||
| benchmark.addCase("CaffeinatedGuava with Caffeine CacheLoader") { _ => | ||
| val cache: LoadingCache[TestData, TestData] = | ||
| CaffeinatedGuava.build(Caffeine.newBuilder(), caffeineCacheLoader) | ||
| dataset.par.foreach(dataList => dataList.foreach(key => cache.get(key))) | ||
| cache.cleanUp() | ||
| } | ||
|
|
||
| benchmark.run() | ||
| } | ||
| } | ||
|
|
||
| case class TestData(content: Int) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we avoid this since it depends on Guava and guava conflicts can be a pain to resolve.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good idea, but there are some interface differences between Caffeine and Guava Cache, and spark just uses these interfaces. For example,
SessionCatalogusesV get(K key, Callable<? extends V> valueLoader)method only define in Guava Cache APIUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you use Caffeine's
V get(K key, Function<? extends K, ? extends V> mappingFunction)instead? That should be equivalent but be friendly for method references.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I'm trying to get rid of the dependence on
com.github.ben-manes.caffeine:guavain this wayThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@holdenk @ben-manes All the dependencies related to
com.github.ben-manes.caffeine: guavahave been removed.If all test passed , I will try to upgrade caffeine from 2.9.0 to 2.9.1 in this pr