Add per-role continuous JFR recording to the Helm chart - #19372
Open
gortiz wants to merge 2 commits into
Open
Conversation
Runs a continuous Java Flight Recorder recording in each Pinot JVM, so that when something goes wrong the profile of the minutes leading up to it is already on disk. The recording is started by the JVM through -XX:StartFlightRecording in JAVA_OPTS rather than from inside Pinot, which means it covers startup and does not depend on ZooKeeper or Helix being reachable. Enable it per role with `<role>.jfr.enabled`; the settings under the top-level `jfr` block are shared by every role that enables it. JFR's `maxsize` makes the recording roll like a log file, so nothing has to rotate files by hand. What it does not do is reclaim the repository left behind by a JVM that has already exited, and with `preserve-repository=true` those accumulate on a PersistentVolume until it is full. An init container reclaims them: init containers finish before the Pinot container starts, so every repository it sees belongs to a run that is already over. It also refuses to touch anything written to within `jfr.janitor.minIdleMinutes`, and always exits 0 - a failed cleanup must never keep a role from starting. Notes on the design: - Recordings go to a volume of their own, never a subdirectory of the role's data volume, so a runaway recording cannot eat the space Pinot needs for segments. - `jfr.persistence.enabled` defaults to false. An emptyDir applies with a plain rolling restart, while a PersistentVolume adds a volumeClaimTemplates entry that Kubernetes forbids changing in place. See UPGRADING.md. - The stateless minion is a Deployment and always uses an emptyDir: replicas cannot each have their own volume, and sharing one would let a starting pod's cleanup delete a running pod's recording. - All sizes are Kubernetes quantities. The chart converts them to the byte counts JFR wants, so JFR's own unit table never reaches values.yaml, and the cleanup script parses no units at all. - Values are validated at render time. A bad JFR option is not a warning - the JVM refuses to start - so `helm install` fails with a clear message instead of leaving a CrashLoopBackOff. That includes checking that the volume can hold the janitor's budget plus the runs that follow it.
The `pinot.jfr.*` cluster configs start a JFR recording from inside Pinot, after the component has connected to Helix. Setting the equivalent JVM arguments is better in three ways, so this deprecates the configs and points at the replacement, which the Helm chart now renders. - Coverage. This listener cannot run before cluster config has been read, so class loading, plugin init, segment preload and the ZooKeeper connect itself are never captured. - Availability. It depends on ZooKeeper being reachable, which is not a safe assumption during the incidents a profile would help with. - Data loss. `JFR.stop` without a filename deletes the whole repository, so changing any `pinot.jfr.*` key - including one that only affects cleanup - silently discards every chunk recorded so far. The configs keep working; nothing is removed. Two changes beyond the annotations: - Stand down when the JVM already owns the recorder. Both mechanisms drive the same per-JVM recorder and this one would win destructively: applyRuntimeOptions issues `JFR.configure repositorypath=...`, which is JVM-global and relocates a running recording off the volume it was meant to be written to. The check looks for -XX:StartFlightRecording and also -XX:FlightRecorderOptions, since the latter is what governs repositorypath. - Warn once, on the first config that enables the deprecated path, naming the JVM arguments to use instead.
gortiz
requested review from
xiangfu0 and
yashmayya
and removed request for
xiangfu0
August 26, 2026 14:24
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #19372 +/- ##
============================================
+ Coverage 67.44% 67.46% +0.02%
Complexity 1430 1430
============================================
Files 3485 3485
Lines 223874 223910 +36
Branches 35300 35305 +5
============================================
+ Hits 150987 151069 +82
+ Misses 60890 60859 -31
+ Partials 11997 11982 -15
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds continuous Java Flight Recorder recording
to the Helm chart, per role, and deprecates the
pinot.jfr.*cluster configs that did the same jobfrom inside the JVM.
The point is post-mortem profiling: when a server falls over, the profile of the minutes leading up
to it should already be on disk rather than something you go and enable afterwards.
That renders into the role's
JAVA_OPTS:Why JVM arguments rather than the existing
pinot.jfr.*cluster configsContinuousJfrStarterstarts the recording after the component has connected to Helix. Starting itfrom the command line is better in three ways:
plugin init, segment preload and the ZooKeeper connect itself are never captured.
incidents a profile would help with.
JFR.stopwithout a filename deletes the whole repository, so changing anypinot.jfr.*key — including one that only affects cleanup — silently discards every chunkrecorded so far.
Nothing is removed. The configs are
@Deprecated(forRemoval = true), still work, and warn oncenaming the JVM arguments to use instead.
Why there is an init container
JFR's
maxsizemakes the recording roll like a log file, so nothing needs to rotate files by hand.What JFR does not do is reclaim the repository of a JVM that has already exited — and
preserve-repository=true, which is what keeps recordings across a restart in the first place,means those directories survive on a PersistentVolume forever. Measured: a second run against the
same repository root leaves the first run's directory untouched, so each restart leaks up to a full
maxSize.jfr-janitorreclaims them. Running it as an init container is what makes it safe — init containersfinish before the Pinot container starts, so every repository it sees belongs to a run that is
already over. It also refuses to delete anything written to within
jfr.janitor.minIdleMinutes, andalways exits 0: a failed cleanup must never keep a role from starting.
Design notes
eat the space Pinot needs for segments.
jfr.persistence.enableddefaults to false. AnemptyDirapplies with a plain rollingrestart; a PersistentVolume adds a
volumeClaimTemplatesentry, which Kubernetes forbids changingin place.
UPGRADING.mdhas the one-time--cascade=orphanprocedure.emptyDir. It is a Deployment, so replicas cannot eachhave their own volume, and sharing one would let a starting pod's cleanup delete a running pod's
live recording during a rolling update.
JFR accepts. JFR's own unit table never reaches
values.yaml, and the cleanup script parses nounits at all.
so the chart fails
helm installwith a clear message instead of leaving a CrashLoopBackOff. Thatincludes checking the volume can hold the janitor's budget plus the runs that follow it.
Getting a recording out
Each
*.jfrchunk in the repository is a valid recording on its own and is named with its starttimestamp, so you can pull only the window you care about instead of the whole volume.
Testing
helm lint --strict,helm templateandkubeconform -strictover six configurations: defaults,all five roles on with
emptyDir, all five with a PVC, janitor off, a user-suppliedinitContainersentry alongside the janitor, and a largeprofilesetup.initContainers: []key is no longer emitted.helm templatefails:maxSize: 500m(a lowercasemis milli inKubernetes),
maxSize: 2GB,maxAge: P7D, a janitor with neither bound set, and anover-committed volume.
helm/pinot/scripts/jfr-janitor-test.sh— 13 fixture checks over the cleanup script: both passes,non-repository entries left alone, a recently written repository never deleted, a malformed budget
skipping the pass rather than deleting everything, and exit 0 on a read-only or missing directory.
shellcheck -s shclean.ContinuousJfrStarterTest— 29 cases, including a data provider over the JVM-argument detection(both flags, both
=/:forms, and-Dsomething=-XX:StartFlightRecordingwhich must notmatch).
recovered it with
jfr assemble, and ran the janitor extracted from the rendered manifest againstthe result.
Notes for reviewers
helm lint/helm templateappear in none of the workflows). Happyto add one in a follow-up if that is wanted — it would have caught two bugs found during review.
@Deprecated(forRemoval = true)names no removal release. Suggestions welcome on which one totarget.