Apache Spark instrumentation#4997
Conversation
92bd061 to
36bbe1a
Compare
36bbe1a to
f581020
Compare
dougqh
left a comment
There was a problem hiding this comment.
I haven't reviewed this closely yet, but there are several issues that I can already see need to be addressed...
First, in terms of Java style...
1 - The member variables aren't private - not idiomatic for Java
2 - Many member variables that should be final are not final
Second, memory management - the tracer is pretty strictly prohibited from having unbounded collections like those in this listener.
The first step towards fixing this is to remove elements from the map as those jobs & tasks finish.
The second step towards this to check that the map isn't over capacity as insertion.
Third, it is clear what the thread safety rules are the listener, so this code may not be safe. Please indicate the thread safety behavior of the Spark listener with a comment in the code.
| } | ||
|
|
||
| @Override | ||
| public void onApplicationStart(SparkListenerApplicationStart applicationStart) { |
There was a problem hiding this comment.
Are the listener methods always called in the same thread? If not, then this code isn't thread safe.
There was a problem hiding this comment.
Yes, they are all called from the same thread https://www.mail-archive.com/[email protected]/msg48812.html
- Removed serviceName, will be apply automatically - Added private/final to most of the attributes - Removed stageLongestTask, complexify the PR and might not be useful at first - Remove elements for the maps when receiving the end event - Adding capping on the number of elements in maps - Replaced Optional by a check if it is defined
paul-laffon-dd
left a comment
There was a problem hiding this comment.
Thanks for the review! I am not used to write java, I tried to adapt the style related to your comments
For the memory management, I added a check that we are not above a certain size and removing the spans on the end event
For the thread-safety, all the callbacks are called from the same thread on the driver, so we should be safe on that (added a comment about it in the code)
| } | ||
|
|
||
| @Override | ||
| public void onApplicationStart(SparkListenerApplicationStart applicationStart) { |
There was a problem hiding this comment.
Yes, they are all called from the same thread https://www.mail-archive.com/[email protected]/msg48812.html
| int stageId = stageSubmitted.stageInfo().stageId(); | ||
| int stageAttemptId = stageSubmitted.stageInfo().attemptNumber(); | ||
|
|
||
| if (!stageToJob.containsKey(stageId)) { |
There was a problem hiding this comment.
containsKey followed by get is redundant unless you store nulls into the map.
You can just do stageToJob.get(stageId). The only caveat is that you'll get back an Integer rather than int and then need to check against null.
There was a problem hiding this comment.
Changed it to here
Integer jobId = stageToJob.get(stageId);
if (jobId == null) {
return;
}| submissionTimeMs = System.currentTimeMillis(); | ||
| } | ||
|
|
||
| AgentSpan stageSpan = |
There was a problem hiding this comment.
Seeing withStartTimestamp made me realize that we probably want to exclude these spans from tracking by profiling, since these spans aren't actually represent work within this JVM.
I'll make a note to think about that. It doesn't need to be a blocker for this initial prototype.
| if (applicationStart.appAttemptId().isDefined()) | ||
| applicationSpan.setTag("app_attempt_id", applicationStart.appAttemptId().get()); | ||
|
|
||
| for (Tuple2<String, String> conf : sparkConf.getAll()) { |
There was a problem hiding this comment.
We actually don't to capture all values from config into a span.
That risks accidentally exposing sensitive information.
We need to build an inclusion list of config to include. We don't want to just include everything unconditionally.
It would also be fine to have the list be configurable.
There was a problem hiding this comment.
Add the SparkConfAllowList to only capture a safe subset of the config. Is it what you meant by have the list be configurable ?
| "spark_application_metrics.available_executor_time", availableExecutorTime); | ||
|
|
||
| applicationSpan.finish(applicationEnd.time() * 1000); | ||
| tracer.flush(); |
There was a problem hiding this comment.
You shouldn't explicitly flush the tracer that's bad for performance.
Also, tracer.flush doesn't provide the semantics that you probably want. As currently implemented, it is really only good for testing.
There was a problem hiding this comment.
Removed the flush(). The idea was that the JVM will be shut down by spark pretty quickly after the onApplicationEnd() event is received and this was to make sure that the spans are sent to datadog
Would you know if there is a way to ensure that ?
| public void onApplicationEnd(SparkListenerApplicationEnd applicationEnd) { | ||
| if (lastJobFailed) { | ||
| applicationSpan.setError(true); | ||
| applicationSpan.setTag(DDTags.ERROR_TYPE, "Spark Application Failed"); |
There was a problem hiding this comment.
Is there a better message available on the applicationEnd object?
There was a problem hiding this comment.
There is no error message in SparkListenerApplicationEnd, but we can provide the error message of the last job that failed
| .start(); | ||
|
|
||
| if (jobStart.properties() != null) { | ||
| for (final Map.Entry<Object, Object> entry : jobStart.properties().entrySet()) { |
There was a problem hiding this comment.
As with the config info, this needs to an inclusion list of know safe properties -- otherwise, there's a risk of leaking sensitive information.
| } | ||
|
|
||
| private int stageSpanKey(int stageId, int attemptId) { | ||
| return stageId * 100000 + attemptId; |
There was a problem hiding this comment.
Since you are dealing with two ints, you might as well just use a long if you want a composite key. Then you'll have no risk of accidental key collision.
It will also be more efficient to use shift operations / powers of 2 than multiplication.
There was a problem hiding this comment.
Changed it to here
private long stageSpanKey(int stageId, int attemptId) {
return ((long) stageId << 32) + attemptId;
}| // Some properties can change at runtime, so capturing properties of all jobs | ||
| if (jobStart.properties() != null) { | ||
| for (final Map.Entry<Object, Object> entry : jobStart.properties().entrySet()) { | ||
| if (SparkConfAllowList.canCaptureParameter(entry.getKey().toString())) { |
There was a problem hiding this comment.
Wouldn't it better to have two different allow lists? I imagine the parameters are different for app vs a job. Are they not?
|
|
||
| @Override | ||
| public void onJobEnd(SparkListenerJobEnd jobEnd) { | ||
| if (!jobSpans.containsKey(jobEnd.jobId())) { |
There was a problem hiding this comment.
You don't need the containsKey guard. You can just do a remove and test the result against null.
That's generally a better overall practice because it avoids rare race conditions when using maps synchronized across threads. It also happens to be a bit more efficient.
There was a problem hiding this comment.
Removed all the remaining containsKey in this commit f69e40c
dougqh
left a comment
There was a problem hiding this comment.
I think pretty much all uses of containsKey are unnecessary. You can just do a get or remove then check the return value.
That's a good habit to get into because it will race condition problems when dealing with synchronized maps. Plus it is a tad bit more efficient.
| isMethod() | ||
| .and(named("setupAndStartListenerBus")) | ||
| .and(takesNoArguments()) | ||
| .and(isDeclaredBy(named("org.apache.spark.SparkContext"))), |
There was a problem hiding this comment.
Since we're instrumenting a known type, org.apache.spark.SparkContext, you don't need isDeclaredBy here
|
|
||
| for (Tuple2<String, String> conf : sparkConf.getAll()) { | ||
| if (SparkConfAllowList.canCaptureParameter(conf._1)) { | ||
| applicationSpan.setTag("config." + conf._1.replace(".", "_"), conf._2); |
There was a problem hiding this comment.
this is another place we could use a small cache to reduce string creation
(note the .->_ replace can be made part of the caching function, so the lookup key is the original string)
| if (jobStart.properties() != null) { | ||
| for (final Map.Entry<Object, Object> entry : jobStart.properties().entrySet()) { | ||
| if (SparkConfAllowList.canCaptureParameter(entry.getKey().toString())) { | ||
| jobSpan.setTag("config." + entry.getKey().toString().replace('.', '_'), entry.getValue()); |
There was a problem hiding this comment.
this is another place we could use a small cache to reduce string creation
(note the .->_ replace can be made part of the caching function, so the lookup key is the original object)
| span.setTag(DDTags.ERROR_TYPE, "Spark Stage Failed"); | ||
| } | ||
|
|
||
| stageInfo.rddInfos().foreach(rdd -> span.setTag("rdd." + rdd.name(), rdd.toString())); |
There was a problem hiding this comment.
this is another place we could use a small cache to reduce string creation
|
Agree with the comments from @dougqh about avoiding Likewise patterns like this: can be simplified in Java 8 to: There's also the opportunity to add some caches to limit creation of duplicate strings - however, if the number of stages and jobs is small then you may decide adding these caches is not worthwhile (we typically use them for resource name transformations inside very hot methods.) |
|
I removed all usage of About the caching to limit creation of duplicate strings, I would think that they might not be worthwhile since there is not that many stages per spark application (generally less than 100 for applications running for tens of minutes) |
dougqh
left a comment
There was a problem hiding this comment.
The code around applicationSpan isn't obviously correct to me.
I think having a trace encompass the whole application might be the wrong choice, but it really depends on the lifetime of the application object.
Just based on the names, I think it would probably be better to have a trace per job -- not a trace per application.
|
The application is started when the spark driver is spin up and ends when the driver is shut down. Spark applications are launched with a spark-submit command to produce an output and the application lifecycle is tied to this command ./bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master local[8] \
/path/to/examples.jar \
100The application metrics are the most useful since they provide an overall picture of what happened during the entire program. Looking into Spark jobs is mainly useful for troubleshooting when the application has failed or performed poorly |
|
Excluding the spark instrumentation from semeru jdk since hadoop, on which spark in depending is not behaving well on it, https://issues.apache.org/jira/browse/HADOOP-18174 |
What Does This Do
Instrument apache spark applications by using the spark listener API to get notified when a spark job / stage / task started and finished. It is done using an entry advice on the method setupAndStartListenerBus to inject our custom listener that spark will call for every event
Tracing is done a bit differently than on regular instrumentations since everything can be done from the
SparkListenerinterface and mostly managed by spark itselfMotivation
Additional Notes
Only supporting spark >= 2.4.0 with scala 2.12 for now