<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://rangareddy.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://rangareddy.github.io/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-09-11T15:33:05+05:30</updated><id>https://rangareddy.github.io/feed.xml</id><title type="html">Ranga Reddy</title><subtitle>Hands-on notes, production troubleshooting and interactive tools for Apache Hudi, Iceberg, Spark, Kafka and the lakehouse stack underneath them.</subtitle><author><name>Ranga Reddy</name></author><entry><title type="html">Apache XTable incremental sync: how to keep your conversions fast</title><link href="https://rangareddy.github.io/XTableIncrementalSync/" rel="alternate" type="text/html" title="Apache XTable incremental sync: how to keep your conversions fast" /><published>2026-09-10T11:00:00+05:30</published><updated>2026-09-10T11:00:00+05:30</updated><id>https://rangareddy.github.io/XTableIncrementalSync</id><content type="html" xml:base="https://rangareddy.github.io/XTableIncrementalSync/"><![CDATA[<ul id="markdown-toc">
  <li><a href="#what-xtable-gives-you" id="markdown-toc-what-xtable-gives-you">What XTable gives you</a></li>
  <li><a href="#architecture-the-internal-model-and-the-two-sync-paths" id="markdown-toc-architecture-the-internal-model-and-the-two-sync-paths">Architecture: the internal model and the two sync paths</a></li>
  <li><a href="#the-decision-from-source" id="markdown-toc-the-decision-from-source">The decision, from source</a></li>
  <li><a href="#what-safe-from-this-instant-means-per-format" id="markdown-toc-what-safe-from-this-instant-means-per-format">What “safe from this instant” means per format</a></li>
  <li><a href="#running-it" id="markdown-toc-running-it">Running it</a></li>
  <li><a href="#confirming-you-are-on-the-incremental-path" id="markdown-toc-confirming-you-are-on-the-incremental-path">Confirming you are on the incremental path</a></li>
  <li><a href="#where-xtable-fits-best" id="markdown-toc-where-xtable-fits-best">Where XTable fits best</a></li>
  <li><a href="#production-tips" id="markdown-toc-production-tips">Production tips</a></li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
</ul>

<blockquote>
  <p><strong>TL;DR</strong></p>

  <ul>
    <li>XTable (incubating) converts table <em>metadata</em>, not data. One set of Parquet files gets a second and third set of metadata so Iceberg and Delta readers can see a Hudi table without a copy.</li>
    <li><code class="language-plaintext highlighter-rouge">SyncMode</code> has two values, <code class="language-plaintext highlighter-rouge">FULL</code> and <code class="language-plaintext highlighter-rouge">INCREMENTAL</code>. <code class="language-plaintext highlighter-rouge">INCREMENTAL</code> is a request that XTable validates against the source before honouring it, which is what makes the result trustworthy.</li>
    <li>The decision is made <strong>per target format</strong>. In one run Iceberg can sync incrementally while Delta rebuilds a full snapshot.</li>
    <li>Incremental needs the source to still hold history back to the last sync. <code class="language-plaintext highlighter-rouge">isIncrementalSyncSafeFrom</code> checks that per format: a live commit on the Hudi timeline, an unbroken Iceberg snapshot chain, an active Delta commit at or before the instant.</li>
    <li>Keeping source retention comfortably longer than your sync interval is the one setting that keeps you on the incremental path, and a single log string confirms it.</li>
  </ul>
</blockquote>

<h2 id="what-xtable-gives-you">What XTable gives you</h2>

<p>A lakehouse table is Parquet files plus metadata that says which files are live,
what the schema is, and how the table has changed over time. The files are
ordinary Parquet. The metadata is what makes it a Hudi, Iceberg or Delta table.</p>

<p><a href="https://xtable.apache.org/">Apache XTable</a> (incubating) works from that
observation. Rather than copying data between formats, it reads the source
table’s metadata into a format-agnostic internal model and writes out metadata in
the target formats alongside it. The Parquet files are never rewritten and never
duplicated. Point Trino at the Iceberg metadata and Databricks at the Delta
metadata and both read the same bytes.</p>

<p>That design means correctness is not the thing you tune. The lever worth
understanding is cost: whether XTable appends the last few commits to the target
metadata or rebuilds it from scratch, which is the difference between a sync that
finishes in seconds and one that reads the whole table.</p>

<p>This post is written against <strong>XTable 0.4.0-incubating</strong>. Class and method
references are to the
<a href="https://github.com/apache/incubator-xtable/tree/0.4.0-incubating"><code class="language-plaintext highlighter-rouge">0.4.0-incubating</code></a>
tag.</p>

<p>At that release, <code class="language-plaintext highlighter-rouge">TableFormat</code> declares <code class="language-plaintext highlighter-rouge">HUDI</code>, <code class="language-plaintext highlighter-rouge">ICEBERG</code>, <code class="language-plaintext highlighter-rouge">DELTA</code>, <code class="language-plaintext highlighter-rouge">PAIMON</code> and
<code class="language-plaintext highlighter-rouge">PARQUET</code>, with <code class="language-plaintext highlighter-rouge">values()</code> returning the first four. Prerequisites: the
<a href="https://xtable.apache.org/docs/how-to">XTable docs</a> and a working knowledge of
at least one of the three main formats.</p>

<h2 id="architecture-the-internal-model-and-the-two-sync-paths">Architecture: the internal model and the two sync paths</h2>

<p>XTable is deliberately not an N-by-N set of converters. Every source is read into
one internal model, and every target is written from that model, so adding a
format is two adapters rather than six.</p>

<pre><code class="language-mermaid">flowchart LR
  H[Hudi timeline] --&gt; S[ConversionSource]
  I[Iceberg snapshots] --&gt; S
  D[Delta log] --&gt; S
  S --&gt; M[Internal model:&lt;br/&gt;schema, partitioning,&lt;br/&gt;files, commits]
  M --&gt; C{ConversionController}
  C --&gt;|INCREMENTAL&lt;br/&gt;per target| INC[syncIncrementalChanges]
  C --&gt;|FULL or&lt;br/&gt;declined| FULL[syncSnapshot]
  INC --&gt; T[ConversionTarget&lt;br/&gt;per format]
  FULL --&gt; T
  T --&gt; OUT[Iceberg metadata/&lt;br/&gt;Delta _delta_log/&lt;br/&gt;Hudi .hoodie]
</code></pre>

<p><code class="language-plaintext highlighter-rouge">ConversionController</code> is where the mode is chosen, and the two paths it can take
are <code class="language-plaintext highlighter-rouge">syncIncrementalChanges</code> and <code class="language-plaintext highlighter-rouge">syncSnapshot</code>:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">syncSnapshot</code></strong> extracts an <code class="language-plaintext highlighter-rouge">InternalSnapshot</code>: every file relevant to the
table at a point in time. The comment on <code class="language-plaintext highlighter-rouge">SyncMode.FULL</code> puts it plainly, it
“will create a checkpoint of ALL the files relevant at a certain point in
time”. Cost scales with the table.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">syncIncrementalChanges</code></strong> extracts the differential structures needed to
move the target from its last synced instant to the current one. Cost scales
with what changed.</li>
</ul>

<p>On a table with a million files, that difference is the difference between a
sync that finishes in a scheduled window and one that does not.</p>

<h2 id="the-decision-from-source">The decision, from source</h2>

<p>Two methods decide this. The first, <code class="language-plaintext highlighter-rouge">getFormatsToSyncIncrementally</code>, filters the
target formats down to those that can go incremental:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="o">(</span><span class="n">syncMode</span> <span class="o">==</span> <span class="nc">SyncMode</span><span class="o">.</span><span class="na">FULL</span><span class="o">)</span> <span class="o">{</span>
  <span class="c1">// Full sync requested by config, hence no incremental sync.</span>
  <span class="k">return</span> <span class="nc">Collections</span><span class="o">.</span><span class="na">emptyMap</span><span class="o">();</span>
<span class="o">}</span>
<span class="k">return</span> <span class="n">conversionTargetByFormat</span><span class="o">.</span><span class="na">entrySet</span><span class="o">().</span><span class="na">stream</span><span class="o">()</span>
    <span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">entry</span> <span class="o">-&gt;</span> <span class="o">{</span>
      <span class="nc">Optional</span><span class="o">&lt;</span><span class="nc">Instant</span><span class="o">&gt;</span> <span class="n">lastSyncInstant</span> <span class="o">=</span> <span class="o">...</span><span class="na">getLastInstantSynced</span><span class="o">();</span>
      <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Instant</span><span class="o">&gt;</span> <span class="n">pendingInstants</span> <span class="o">=</span> <span class="o">...</span><span class="na">getInstantsToConsiderForNextSync</span><span class="o">();</span>
      <span class="k">return</span> <span class="nf">isIncrementalSyncSufficient</span><span class="o">(</span><span class="n">conversionSource</span><span class="o">,</span> <span class="n">lastSyncInstant</span><span class="o">,</span> <span class="n">pendingInstants</span><span class="o">);</span>
    <span class="o">})</span>
    <span class="o">.</span><span class="na">collect</span><span class="o">(</span><span class="nc">Collectors</span><span class="o">.</span><span class="na">toMap</span><span class="o">(</span><span class="nc">Map</span><span class="o">.</span><span class="na">Entry</span><span class="o">::</span><span class="n">getKey</span><span class="o">,</span> <span class="nc">Map</span><span class="o">.</span><span class="na">Entry</span><span class="o">::</span><span class="n">getValue</span><span class="o">));</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">.filter</code> is worth reading closely, because it is a nice piece of design:
the predicate is evaluated <strong>per target format</strong>. Each target carries its own
<code class="language-plaintext highlighter-rouge">TableSyncMetadata</code> with its own last-synced instant, so one target catching up
never forces the others to redo work. Add Delta as a new target to a job that has
been syncing Iceberg for six months and that run will bootstrap Delta from a full
snapshot while Iceberg continues incrementally, in the same job.</p>

<p>The second method, <code class="language-plaintext highlighter-rouge">isIncrementalSyncSufficient</code>, is the actual test:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Optional</span><span class="o">&lt;</span><span class="nc">Instant</span><span class="o">&gt;</span> <span class="n">earliestInstant</span> <span class="o">=</span>
    <span class="n">lastSyncInstant</span>
        <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="n">instant</span> <span class="o">-&gt;</span> <span class="nc">Stream</span><span class="o">.</span><span class="na">concat</span><span class="o">(</span><span class="nc">Stream</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">instant</span><span class="o">),</span> <span class="n">pendingInstantsStream</span><span class="o">)</span>
            <span class="o">.</span><span class="na">min</span><span class="o">(</span><span class="nl">Instant:</span><span class="o">:</span><span class="n">compareTo</span><span class="o">))</span>
        <span class="o">.</span><span class="na">orElseGet</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">pendingInstantsStream</span><span class="o">.</span><span class="na">min</span><span class="o">(</span><span class="nl">Instant:</span><span class="o">:</span><span class="n">compareTo</span><span class="o">));</span>

<span class="k">if</span> <span class="o">(!</span><span class="n">earliestInstant</span><span class="o">.</span><span class="na">isPresent</span><span class="o">())</span> <span class="o">{</span>
  <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"No previous InternalTable sync for target. Falling back to snapshot sync."</span><span class="o">);</span>
  <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
<span class="o">}</span>

<span class="kt">boolean</span> <span class="n">isIncrementalSafeFromInstant</span> <span class="o">=</span>
    <span class="n">conversionSource</span><span class="o">.</span><span class="na">isIncrementalSyncSafeFrom</span><span class="o">(</span><span class="n">earliestInstant</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">isIncrementalSafeFromInstant</span><span class="o">)</span> <span class="o">{</span>
  <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"Incremental sync is not safe from instant {}. Falling back to snapshot sync."</span><span class="o">,</span>
      <span class="n">earliestInstant</span><span class="o">);</span>
  <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
<span class="o">}</span>
<span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
</code></pre></div></div>

<p>Note that the instant it validates is the <em>earliest</em> of the last synced instant
and any instants left pending from a previous run. That is the conservative and
correct choice, and it is worth knowing about: pending instants widen the history
window the source needs to retain, so clearing them keeps the window tight.</p>

<p>There are exactly two conditions under which XTable chooses a snapshot instead,
and both announce themselves with a log line ending in “Falling back to snapshot
sync”.</p>

<h2 id="what-safe-from-this-instant-means-per-format">What “safe from this instant” means per format</h2>

<p><code class="language-plaintext highlighter-rouge">isIncrementalSyncSafeFrom</code> is implemented by each source, and each
implementation maps onto a retention setting you already manage, which makes the
requirement concrete.</p>

<p><strong>Hudi</strong> requires the commit to still be on the timeline and to be untouched by
cleaning:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isIncrementalSyncSafeFrom</span><span class="o">(</span><span class="nc">Instant</span> <span class="n">instant</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">return</span> <span class="nf">doesCommitExistsAsOfInstant</span><span class="o">(</span><span class="n">instant</span><span class="o">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">isAffectedByCleanupProcess</span><span class="o">(</span><span class="n">instant</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Both halves matter. Archival moves old instants off the active timeline, and
cleaning removes old file slices, so a replayable commit needs both to have left
it alone. Keeping <code class="language-plaintext highlighter-rouge">hoodie.cleaner.commits.retained</code> generous relative to your sync
interval is all it takes to stay on the incremental path.</p>

<p><strong>Iceberg</strong> walks the snapshot parent chain backwards from the current snapshot
looking for one at or before the instant, and gives up if the chain runs out or
is broken:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Snapshot</span> <span class="n">parentSnapshot</span> <span class="o">=</span> <span class="n">iceTable</span><span class="o">.</span><span class="na">snapshot</span><span class="o">(</span><span class="n">parentSnapshotId</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">parentSnapshot</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
  <span class="c1">// chain is broken due to expired snapshot</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">expire_snapshots</code> governs this. Whatever <code class="language-plaintext highlighter-rouge">history.expire.max-snapshot-age-ms</code>
you have chosen, XTable needs the chain intact back to its last sync, so those
two numbers are worth setting together.</p>

<p><strong>Delta</strong> asks the history manager for the active commit at that time and checks
it really is at or before the instant, because the API returns the earliest
commit when you ask for something older than the table:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">DeltaHistoryManager</span><span class="o">.</span><span class="na">Commit</span> <span class="n">deltaCommitAtOrBeforeInstant</span> <span class="o">=</span>
    <span class="n">deltaLog</span><span class="o">.</span><span class="na">history</span><span class="o">().</span><span class="na">getActiveCommitAtTime</span><span class="o">(</span><span class="nc">Timestamp</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">instant</span><span class="o">),</span> <span class="kc">true</span><span class="o">,</span> <span class="kc">false</span><span class="o">,</span> <span class="kc">true</span><span class="o">);</span>
<span class="c1">// There is a chance earliest commit of the table is returned if the instant is before the</span>
<span class="c1">// earliest commit of the table, hence the additional check.</span>
<span class="nc">Instant</span> <span class="n">deltaCommitInstant</span> <span class="o">=</span> <span class="nc">Instant</span><span class="o">.</span><span class="na">ofEpochMilli</span><span class="o">(</span><span class="n">deltaCommitAtOrBeforeInstant</span><span class="o">.</span><span class="na">getTimestamp</span><span class="o">());</span>
<span class="k">return</span> <span class="n">deltaCommitInstant</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">instant</span><span class="o">)</span> <span class="o">||</span> <span class="n">deltaCommitInstant</span><span class="o">.</span><span class="na">isBefore</span><span class="o">(</span><span class="n">instant</span><span class="o">);</span>
</code></pre></div></div>

<p>Log retention and <code class="language-plaintext highlighter-rouge">VACUUM</code> govern this one.</p>

<p>One rule covers all three: <strong>keep the source’s history retention comfortably
longer than your sync interval.</strong> It is a single, checkable relationship between
two numbers, and getting it right is what keeps every run incremental.</p>

<h2 id="running-it">Running it</h2>

<p>The <code class="language-plaintext highlighter-rouge">RunSync</code> utility takes a YAML dataset config and a small set of options.
The config shape comes from <code class="language-plaintext highlighter-rouge">RunSync.DatasetConfig</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># trips-sync.yaml</span>
<span class="na">sourceFormat</span><span class="pi">:</span> <span class="s">HUDI</span>
<span class="na">targetFormats</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">ICEBERG</span>
  <span class="pi">-</span> <span class="s">DELTA</span>
<span class="na">datasets</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">tableBasePath</span><span class="pi">:</span> <span class="s">s3://lakehouse-prod/warehouse/trips</span>
    <span class="na">tableName</span><span class="pi">:</span> <span class="s">trips</span>
    <span class="na">partitionSpec</span><span class="pi">:</span> <span class="s">city_id:VALUE</span>
    <span class="na">namespace</span><span class="pi">:</span> <span class="s">analytics</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">partitionSpec</code> is a comma-separated list of <code class="language-plaintext highlighter-rouge">field:TRANSFORM</code> entries, with an
optional third <code class="language-plaintext highlighter-rouge">:format</code> part. The transform comes from <code class="language-plaintext highlighter-rouge">PartitionTransformType</code>,
which at this release is <code class="language-plaintext highlighter-rouge">YEAR</code>, <code class="language-plaintext highlighter-rouge">MONTH</code>, <code class="language-plaintext highlighter-rouge">DAY</code>, <code class="language-plaintext highlighter-rouge">HOUR</code>, <code class="language-plaintext highlighter-rouge">VALUE</code> or <code class="language-plaintext highlighter-rouge">BUCKET</code>, so
<code class="language-plaintext highlighter-rouge">city_id:VALUE</code> means “partitioned by the literal value of <code class="language-plaintext highlighter-rouge">city_id</code>” and
<code class="language-plaintext highlighter-rouge">event_date:DAY:yyyy-MM-dd</code> would describe a day-partitioned table with an
explicit date format.</p>

<p><code class="language-plaintext highlighter-rouge">sourceFormat</code> can be auto-detected, but the field’s own documentation
recommends setting it explicitly “for cases where the directory contains
metadata of multiple formats”, which is precisely the situation XTable creates.
Set it.</p>

<p>The utilities bundle is not published to Maven Central, so build it from the
release tag. Only <code class="language-plaintext highlighter-rouge">xtable-api</code>, <code class="language-plaintext highlighter-rouge">xtable-core_2.12</code>, <code class="language-plaintext highlighter-rouge">xtable-spark-runtime_2.12</code>
and a few others are published; <code class="language-plaintext highlighter-rouge">xtable-utilities</code> is not among them.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/apache/incubator-xtable.git
<span class="nb">cd </span>incubator-xtable
git checkout 0.4.0-incubating
mvn clean package <span class="nt">-DskipTests</span>

java <span class="nt">-jar</span> xtable-utilities/target/xtable-utilities_2.12-0.4.0-incubating-bundled.jar <span class="se">\</span>
  <span class="nt">--datasetConfig</span> trips-sync.yaml <span class="se">\</span>
  <span class="nt">--hadoopConfig</span> /etc/hadoop/conf/core-site.xml <span class="se">\</span>
  <span class="nt">--icebergCatalogConfig</span> iceberg-catalog.yaml
</code></pre></div></div>

<p>The full option set at 0.4.0-incubating:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Option</th>
      <th style="text-align: left">Short</th>
      <th style="text-align: left">Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">--datasetConfig</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">-d</code></td>
      <td style="text-align: left">The YAML above. Required</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">--hadoopConfig</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">-p</code></td>
      <td style="text-align: left">Hadoop XML for filesystem access, overrides defaults</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">--convertersConfig</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">-c</code></td>
      <td style="text-align: left">Override the built-in converter configurations</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">--icebergCatalogConfig</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">-i</code></td>
      <td style="text-align: left">Catalog config used for any Iceberg source or target</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">--continuousMode</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">-m</code></td>
      <td style="text-align: left">Run on a scheduled loop instead of once</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">--continuousModeInterval</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">-t</code></td>
      <td style="text-align: left">Loop interval in seconds, default 5</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">--help</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">-h</code></td>
      <td style="text-align: left">Usage</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">--continuousMode</code> is the option to reach for. It reloads the config file on
every iteration, so tables can be added to or removed from a running job without
a restart, and it keeps the sync interval short, which is the most effective way
to stay comfortably inside your source’s retention window.</p>

<h2 id="confirming-you-are-on-the-incremental-path">Confirming you are on the incremental path</h2>

<p>XTable tells you which path it took, which makes this easy to monitor. A
snapshot sync is announced with an <code class="language-plaintext highlighter-rouge">INFO</code> line, and there are only two of them to
know:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Log line</th>
      <th style="text-align: left">What it means</th>
      <th style="text-align: left">What to do</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">No previous InternalTable sync for target. Falling back to snapshot sync.</code></td>
      <td style="text-align: left">First sync for this target format, so it is bootstrapping</td>
      <td style="text-align: left">Expected once per target. Seeing it settle after the first run is the signal you want</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Incremental sync is not safe from instant ... Falling back to snapshot sync.</code></td>
      <td style="text-align: left">The source no longer holds history back to that instant</td>
      <td style="text-align: left">Lengthen source retention or shorten the sync interval, then it stays incremental</td>
    </tr>
  </tbody>
</table>

<p>One command confirms a healthy job:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">grep</span> <span class="nt">-c</span> <span class="s2">"Falling back to snapshot sync"</span> xtable-sync.log
</code></pre></div></div>

<p>Zero on a steady-state job means every target is on the incremental path, which
is exactly what you are aiming for. A count matching your number of target
formats on the first run and zero afterwards is the normal, healthy pattern.</p>

<p>Because the decision is per target, the message also names the format, so you can
tell a target that is still bootstrapping from a retention window worth widening.</p>

<h2 id="where-xtable-fits-best">Where XTable fits best</h2>

<p>XTable is at its best when several readers need different metadata over the same
files, and it solves that cleanly. A Hudi ingest pipeline serving an Iceberg
based query engine, or a Databricks team reading Delta over data another team
writes as Hudi, are exactly the shapes it was built for, and it handles them
without a second copy of the data.</p>

<p>Two adjacent problems have better tools, and knowing the boundary makes XTable
more useful rather than less.</p>

<p>If you have decided to move formats permanently, a one-off migration is simpler
than a sync you run forever, since it leaves you with one metadata tree to
maintain instead of two or three.</p>

<p>If writers on both sides need to write, keep the source format authoritative.
XTable’s targets are derived metadata, rebuilt from the source, so the clean
pattern is one writer on the source and readers everywhere else.</p>

<p>And if a single engine needs a single format it does not read natively, check for
a connector first. When one exists it is less machinery than a sync job with its
own schedule.</p>

<h2 id="production-tips">Production tips</h2>

<ul>
  <li><strong>Set <code class="language-plaintext highlighter-rouge">sourceFormat</code> explicitly.</strong> After the first sync the directory holds
metadata for several formats, so naming the source removes any ambiguity.</li>
  <li><strong>Make source retention exceed the sync interval with margin.</strong> For Hudi that
is the cleaner and archival configs, for Iceberg <code class="language-plaintext highlighter-rouge">expire_snapshots</code>, for Delta
log retention and <code class="language-plaintext highlighter-rouge">VACUUM</code>.</li>
  <li><strong>Alert on “Falling back to snapshot sync”</strong> rather than on job duration. It
is the leading indicator, and it is a single string to match.</li>
  <li><strong>Prefer <code class="language-plaintext highlighter-rouge">--continuousMode</code> with a short interval</strong> over an external scheduler
with a long one, both for the shorter history window and the config reload.</li>
  <li><strong>Add all target formats at once</strong> where you can, so the one-off bootstrap
snapshot happens once for every target rather than on separate days.</li>
  <li><strong>Keep pending instants clear.</strong> The safety check uses the earliest of the
last synced and pending instants, so an empty pending list keeps the required
history window as tight as possible.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>The useful mental model for XTable is that <code class="language-plaintext highlighter-rouge">INCREMENTAL</code> is a request rather than
a switch. You ask for it in config, <code class="language-plaintext highlighter-rouge">ConversionController</code> checks per target
format whether the source can still support it, and when it cannot it produces a
correct result the slower way. That is the right trade to make by default: you
never get a subtly wrong target table, and the conservative path is always
available as a fallback.</p>

<p>What makes this worth understanding rather than just monitoring is that the one
thing standing between you and permanently cheap syncs lives outside XTable.
Incremental sync works while the source still holds history back to your last
sync, which puts the Hudi cleaner config, the Iceberg snapshot expiry and the
Delta vacuum schedule squarely in your control. They are ordinary settings you
already own, and aligning them with your sync interval is a one-time
conversation with whoever runs the source pipeline.</p>

<p>So two numbers are worth writing down for any XTable deployment: the source’s
effective history retention, and the sync interval. Keep the first comfortably
larger than the second and every run takes the incremental path. Add one alert on
“Falling back to snapshot sync” and you will know immediately if that ever
changes.</p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://xtable.apache.org/docs/how-to">Apache XTable documentation</a> for setup and the dataset config</li>
  <li><a href="https://github.com/apache/incubator-xtable/blob/0.4.0-incubating/xtable-core/src/main/java/org/apache/xtable/conversion/ConversionController.java"><code class="language-plaintext highlighter-rouge">ConversionController.java</code> at 0.4.0-incubating</a>, where the per-target FULL versus INCREMENTAL decision is made</li>
  <li><a href="https://github.com/apache/incubator-xtable/blob/0.4.0-incubating/xtable-api/src/main/java/org/apache/xtable/model/sync/SyncMode.java"><code class="language-plaintext highlighter-rouge">SyncMode.java</code> at 0.4.0-incubating</a>, the two modes and their definitions</li>
  <li><a href="https://github.com/apache/incubator-xtable/blob/0.4.0-incubating/xtable-utilities/src/main/java/org/apache/xtable/utilities/RunSync.java"><code class="language-plaintext highlighter-rouge">RunSync.java</code> at 0.4.0-incubating</a> for the CLI options and the dataset config schema</li>
  <li><a href="https://hudi.apache.org/docs/indexes">Hudi indexing documentation</a>, if the source side of your sync is a Hudi table you are still tuning</li>
</ul>]]></content><author><name>Ranga Reddy</name></author><category term="Lakehouse" /><category term="XTable" /><category term="Hudi" /><category term="Iceberg" /><category term="Delta" /><category term="Lakehouse" /><summary type="html"><![CDATA[XTable exposes one Hudi, Iceberg or Delta table as the other two by writing metadata rather than copying data. It picks FULL or INCREMENTAL per target format on every run, and one setting on your side keeps it on the fast path. Here is how the decision works, from source, and how to confirm it in the log.]]></summary></entry><entry><title type="html">A Spark JVM playbook: custom logging, class loading, stack size and proxies</title><link href="https://rangareddy.github.io/SparkTroubleshootingPlaybook/" rel="alternate" type="text/html" title="A Spark JVM playbook: custom logging, class loading, stack size and proxies" /><published>2026-09-10T09:00:00+05:30</published><updated>2026-09-10T09:00:00+05:30</updated><id>https://rangareddy.github.io/SparkTroubleshootingPlaybook</id><content type="html" xml:base="https://rangareddy.github.io/SparkTroubleshootingPlaybook/"><![CDATA[<ul id="markdown-toc">
  <li><a href="#why-these-four-belong-together" id="markdown-toc-why-these-four-belong-together">Why these four belong together</a></li>
  <li><a href="#architecture-how-a-jvm-flag-reaches-the-driver-and-the-executors" id="markdown-toc-architecture-how-a-jvm-flag-reaches-the-driver-and-the-executors">Architecture: how a JVM flag reaches the driver and the executors</a></li>
  <li><a href="#custom-logging-and-the-log4j-2-boundary" id="markdown-toc-custom-logging-and-the-log4j-2-boundary">Custom logging, and the Log4j 2 boundary</a>    <ul>
      <li><a href="#the-log4j-2-boundary-at-spark-330" id="markdown-toc-the-log4j-2-boundary-at-spark-330">The Log4j 2 boundary at Spark 3.3.0</a></li>
      <li><a href="#write-the-log4j-2-config" id="markdown-toc-write-the-log4j-2-config">Write the Log4j 2 config</a></li>
      <li><a href="#ship-it-to-the-driver-and-the-executors" id="markdown-toc-ship-it-to-the-driver-and-the-executors">Ship it to the driver and the executors</a></li>
      <li><a href="#different-levels-on-driver-and-executors" id="markdown-toc-different-levels-on-driver-and-executors">Different levels on driver and executors</a></li>
    </ul>
  </li>
  <li><a href="#tracing-class-loading-with--verboseclass" id="markdown-toc-tracing-class-loading-with--verboseclass">Tracing class loading with -verbose:class</a></li>
  <li><a href="#javalangstackoverflowerror" id="markdown-toc-javalangstackoverflowerror">java.lang.StackOverflowError</a></li>
  <li><a href="#routing-spark-through-an-http-proxy" id="markdown-toc-routing-spark-through-an-http-proxy">Routing Spark through an HTTP proxy</a></li>
  <li><a href="#production-tips" id="markdown-toc-production-tips">Production tips</a></li>
  <li><a href="#confirming-each-change-took-effect" id="markdown-toc-confirming-each-change-took-effect">Confirming each change took effect</a></li>
  <li><a href="#where-to-look-instead" id="markdown-toc-where-to-look-instead">Where to look instead</a></li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
</ul>

<blockquote>
  <p><strong>TL;DR</strong></p>

  <ul>
    <li>Nearly every JVM-level Spark change lands in one of two configs: <code class="language-plaintext highlighter-rouge">spark.driver.extraJavaOptions</code> or <code class="language-plaintext highlighter-rouge">spark.executor.extraJavaOptions</code>. Learn where each applies and the rest is detail.</li>
    <li>Spark 3.3.0 moved to Log4j 2, so the file is <code class="language-plaintext highlighter-rouge">log4j2.properties</code> and the flag is <code class="language-plaintext highlighter-rouge">-Dlog4j.configurationFile=</code>. Start from the template Spark ships and your config is picked up first time.</li>
    <li><code class="language-plaintext highlighter-rouge">-verbose:class</code> turns a <code class="language-plaintext highlighter-rouge">NoClassDefFoundError</code> into a direct answer, because it prints which JAR each class actually came from.</li>
    <li>A <code class="language-plaintext highlighter-rouge">java.lang.StackOverflowError</code> in Spark usually points at a deep query plan rather than a bug in your loop. Raising <code class="language-plaintext highlighter-rouge">-Xss</code> on the side that threw resolves most of them.</li>
    <li>In client mode the driver JVM is already running by the time your <code class="language-plaintext highlighter-rouge">SparkConf</code> executes, so pass driver JVM options with <code class="language-plaintext highlighter-rouge">--driver-java-options</code> or the properties file.</li>
  </ul>
</blockquote>

<h2 id="why-these-four-belong-together">Why these four belong together</h2>

<p>I spent four and a half years as Cloudera’s Spark backline engineer and now do
similar work on Apache Hudi. The same four techniques come up again and again,
and each one is a small investment that pays off repeatedly: getting exactly the
logs you want, finding out which JAR a class came from, giving a deep query plan
the stack it needs, and getting the JVM through a corporate proxy.</p>

<p>Those four tasks look unrelated, and they are really one task. All four are JVM
flags delivered through the same pair of Spark configs, so the thing worth
learning is where a flag goes and when. Learn that once and every future JVM
setting is a lookup in the JVM documentation.</p>

<p>This post is written against <strong>Spark 4.2.0</strong> (Java 17, Scala 2.13.18, Hadoop
3.5.0 per its <a href="https://github.com/apache/spark/blob/v4.2.0/pom.xml"><code class="language-plaintext highlighter-rouge">pom.xml</code></a>).
Everything except the logging section applies unchanged back to Spark 2.x. The
logging section has a hard version boundary at 3.3.0, called out below.</p>

<p>Prerequisites: the Spark <a href="https://spark.apache.org/docs/latest/configuration.html">configuration
reference</a> and, if you
are on YARN, <a href="https://spark.apache.org/docs/latest/running-on-yarn.html">Running Spark on
YARN</a>.</p>

<h2 id="architecture-how-a-jvm-flag-reaches-the-driver-and-the-executors">Architecture: how a JVM flag reaches the driver and the executors</h2>

<p>Spark runs your code in two kinds of JVM. The driver builds the plan and
schedules work. The executors run the tasks. A JVM flag affects exactly one of
them, so the first question on any of these problems is <em>which JVM is failing</em>.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Config</th>
      <th style="text-align: left">Applies to</th>
      <th style="text-align: left">Set it with</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">spark.driver.extraJavaOptions</code></td>
      <td style="text-align: left">The driver JVM</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">--driver-java-options</code>, <code class="language-plaintext highlighter-rouge">--conf</code>, or <code class="language-plaintext highlighter-rouge">spark-defaults.conf</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">spark.executor.extraJavaOptions</code></td>
      <td style="text-align: left">Every executor JVM</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">--conf</code> or <code class="language-plaintext highlighter-rouge">spark-defaults.conf</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">spark.driver.defaultJavaOptions</code></td>
      <td style="text-align: left">The driver JVM, prepended to the above</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">spark-defaults.conf</code>, set by admins</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">spark.executor.defaultJavaOptions</code></td>
      <td style="text-align: left">Executor JVMs, prepended to the above</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">spark-defaults.conf</code>, set by admins</td>
    </tr>
  </tbody>
</table>

<p>Two rules worth committing to memory:</p>

<p><strong>Deploy mode changes where the driver lives.</strong> In <code class="language-plaintext highlighter-rouge">client</code> mode the driver runs
in the <code class="language-plaintext highlighter-rouge">spark-submit</code> process on the machine you typed the command on. In
<code class="language-plaintext highlighter-rouge">cluster</code> mode it runs inside the cluster, as the YARN ApplicationMaster or a
Kubernetes driver pod. Any file the driver needs must be shipped there.</p>

<p><strong>In client mode the driver JVM has already started.</strong> Spark’s own
documentation for <code class="language-plaintext highlighter-rouge">spark.driver.extraJavaOptions</code> is explicit: in client mode
the config “must not be set through the <code class="language-plaintext highlighter-rouge">SparkConf</code> directly in your
application, because the driver JVM has already started at that point,” and you
should use <code class="language-plaintext highlighter-rouge">--driver-java-options</code> or the properties file instead. Pass it on the
command line and it applies cleanly.</p>

<p>One more: it is illegal to set the maximum heap size (<code class="language-plaintext highlighter-rouge">-Xmx</code>) through
<code class="language-plaintext highlighter-rouge">extraJavaOptions</code>. Use <code class="language-plaintext highlighter-rouge">spark.driver.memory</code> and <code class="language-plaintext highlighter-rouge">spark.executor.memory</code>, or
<code class="language-plaintext highlighter-rouge">--driver-memory</code> and <code class="language-plaintext highlighter-rouge">--executor-memory</code>. <code class="language-plaintext highlighter-rouge">-Xss</code>, the flag in the stack-size
section below, is a different flag and is perfectly legal here.</p>

<h2 id="custom-logging-and-the-log4j-2-boundary">Custom logging, and the Log4j 2 boundary</h2>

<p>By default Spark reads its logging config from <code class="language-plaintext highlighter-rouge">$SPARK_HOME/conf</code>, which is set
at the cluster level. What you usually want while investigating is <code class="language-plaintext highlighter-rouge">DEBUG</code> on two
specific packages for one application, leaving the cluster default and everyone
else’s jobs untouched. Spark supports exactly that.</p>

<h3 id="the-log4j-2-boundary-at-spark-330">The Log4j 2 boundary at Spark 3.3.0</h3>

<p>Spark used Log4j 1.x up to and including 3.2.x, then switched to Log4j 2. You
can see the switch in the shipped templates:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Spark</th>
      <th style="text-align: left">Template in <code class="language-plaintext highlighter-rouge">conf/</code></th>
      <th style="text-align: left">Config property syntax</th>
      <th style="text-align: left">JVM flag</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">3.2.4 and earlier</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">log4j.properties.template</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">log4j.rootLogger=...</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">-Dlog4j.configuration=</code></td>
    </tr>
    <tr>
      <td style="text-align: left">3.3.0 and later</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">log4j2.properties.template</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">rootLogger.level = ...</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">-Dlog4j.configurationFile=</code></td>
    </tr>
  </tbody>
</table>

<p>The practical consequence is worth knowing. Log4j 2 ignores the older
<code class="language-plaintext highlighter-rouge">-Dlog4j.configuration</code> property rather than raising an error, and falls back to
its normal configuration lookup, so an application keeps running and logs at the
cluster default. If a custom config ever seems to have no effect, checking the
file name and the flag against the table above is the quickest thing to try, and
a snippet saved before 2022 is worth refreshing against it.</p>

<p>Spark 4.2.0 also ships <code class="language-plaintext highlighter-rouge">log4j2-json-layout.properties.template</code> and a
<code class="language-plaintext highlighter-rouge">spark.log.structuredLogging.enabled</code> config (default <code class="language-plaintext highlighter-rouge">false</code>) if you want JSON
logs rather than the pattern layout.</p>

<h3 id="write-the-log4j-2-config">Write the Log4j 2 config</h3>

<p>Start from the shipped template rather than from memory, because the property
names are not guessable:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cp</span> /opt/spark/conf/log4j2.properties.template /tmp/log4j2-debug.properties
</code></pre></div></div>

<p>Then set the levels you actually want. This raises the root logger to <code class="language-plaintext highlighter-rouge">DEBUG</code>,
turns Spark’s SQL execution and Hudi client packages up, and keeps the noisy
third-party loggers down so the output stays readable:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">rootLogger.level</span> <span class="p">=</span> <span class="s">debug</span>
<span class="py">rootLogger.appenderRef.stdout.ref</span> <span class="p">=</span> <span class="s">console</span>

<span class="py">appender.console.type</span> <span class="p">=</span> <span class="s">Console</span>
<span class="py">appender.console.name</span> <span class="p">=</span> <span class="s">console</span>
<span class="py">appender.console.target</span> <span class="p">=</span> <span class="s">SYSTEM_ERR</span>
<span class="py">appender.console.layout.type</span> <span class="p">=</span> <span class="s">PatternLayout</span>
<span class="c"># %ex rather than the implicit %xEx: the extended form resolves the JAR each
# stack frame came from, which is measurable overhead on a hot error path.
</span><span class="py">appender.console.layout.pattern</span> <span class="p">=</span> <span class="s">%d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n%ex</span>

<span class="c"># The two packages under investigation.
</span><span class="py">logger.sqlexec.name</span> <span class="p">=</span> <span class="s">org.apache.spark.sql.execution</span>
<span class="py">logger.sqlexec.level</span> <span class="p">=</span> <span class="s">debug</span>

<span class="py">logger.hudiclient.name</span> <span class="p">=</span> <span class="s">org.apache.hudi.client</span>
<span class="py">logger.hudiclient.level</span> <span class="p">=</span> <span class="s">debug</span>

<span class="c"># Third-party loggers that make DEBUG unusable if left at the root level.
</span><span class="py">logger.jetty.name</span> <span class="p">=</span> <span class="s">org.sparkproject.jetty</span>
<span class="py">logger.jetty.level</span> <span class="p">=</span> <span class="s">warn</span>

<span class="py">logger.parquet.name</span> <span class="p">=</span> <span class="s">org.apache.parquet</span>
<span class="py">logger.parquet.level</span> <span class="p">=</span> <span class="s">error</span>

<span class="py">logger.hmshandler.name</span> <span class="p">=</span> <span class="s">org.apache.hadoop.hive.metastore.RetryingHMSHandler</span>
<span class="py">logger.hmshandler.level</span> <span class="p">=</span> <span class="s">fatal</span>
</code></pre></div></div>

<h3 id="ship-it-to-the-driver-and-the-executors">Ship it to the driver and the executors</h3>

<p>The file has to exist on every JVM that reads it. <code class="language-plaintext highlighter-rouge">--files</code> uploads it and
places it in each container’s working directory, which is why the flag value on
the executor side is a bare filename with no path.</p>

<p>Cluster mode, where the driver is also a container:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spark-submit <span class="se">\</span>
  <span class="nt">--master</span> yarn <span class="se">\</span>
  <span class="nt">--deploy-mode</span> cluster <span class="se">\</span>
  <span class="nt">--files</span> /tmp/log4j2-debug.properties <span class="se">\</span>
  <span class="nt">--conf</span> spark.driver.extraJavaOptions<span class="o">=</span><span class="s2">"-Dlog4j.configurationFile=log4j2-debug.properties"</span> <span class="se">\</span>
  <span class="nt">--conf</span> spark.executor.extraJavaOptions<span class="o">=</span><span class="s2">"-Dlog4j.configurationFile=log4j2-debug.properties"</span> <span class="se">\</span>
  <span class="nt">--class</span> com.rangareddy.pipeline.TripsIngest <span class="se">\</span>
  s3a://lakehouse-prod/artifacts/trips-ingest-2.4.1.jar <span class="se">\</span>
  <span class="nt">--input</span> s3a://lakehouse-prod/raw/trips/ <span class="se">\</span>
  <span class="nt">--table</span> s3a://lakehouse-prod/warehouse/trips/
</code></pre></div></div>

<p>Client mode, where the driver reads the file straight off local disk and only
the executors need the upload:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spark-submit <span class="se">\</span>
  <span class="nt">--master</span> yarn <span class="se">\</span>
  <span class="nt">--deploy-mode</span> client <span class="se">\</span>
  <span class="nt">--files</span> /tmp/log4j2-debug.properties <span class="se">\</span>
  <span class="nt">--driver-java-options</span> <span class="s2">"-Dlog4j.configurationFile=/tmp/log4j2-debug.properties"</span> <span class="se">\</span>
  <span class="nt">--conf</span> spark.executor.extraJavaOptions<span class="o">=</span><span class="s2">"-Dlog4j.configurationFile=log4j2-debug.properties"</span> <span class="se">\</span>
  <span class="nt">--class</span> com.rangareddy.pipeline.TripsIngest <span class="se">\</span>
  s3a://lakehouse-prod/artifacts/trips-ingest-2.4.1.jar <span class="se">\</span>
  <span class="nt">--input</span> s3a://lakehouse-prod/raw/trips/ <span class="se">\</span>
  <span class="nt">--table</span> s3a://lakehouse-prod/warehouse/trips/
</code></pre></div></div>

<p>Note the asymmetry, which is the one detail to get right: in client mode the
driver flag carries an absolute local path, while the executor flag carries a
bare filename resolved inside the container. Keep those two straight and the
config lands on both sides first time.</p>

<h3 id="different-levels-on-driver-and-executors">Different levels on driver and executors</h3>

<p>Sometimes you want <code class="language-plaintext highlighter-rouge">DEBUG</code> on the driver, where planning happens, and <code class="language-plaintext highlighter-rouge">WARN</code> on
two thousand executors, because <code class="language-plaintext highlighter-rouge">DEBUG</code> across all of them will fill the disk.
Ship two files and point each side at its own:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spark-submit <span class="se">\</span>
  <span class="nt">--master</span> yarn <span class="se">\</span>
  <span class="nt">--deploy-mode</span> cluster <span class="se">\</span>
  <span class="nt">--files</span> /tmp/log4j2-driver.properties,/tmp/log4j2-executor.properties <span class="se">\</span>
  <span class="nt">--conf</span> spark.driver.extraJavaOptions<span class="o">=</span><span class="s2">"-Dlog4j.configurationFile=log4j2-driver.properties"</span> <span class="se">\</span>
  <span class="nt">--conf</span> spark.executor.extraJavaOptions<span class="o">=</span><span class="s2">"-Dlog4j.configurationFile=log4j2-executor.properties"</span> <span class="se">\</span>
  <span class="nt">--class</span> com.rangareddy.pipeline.TripsIngest <span class="se">\</span>
  s3a://lakehouse-prod/artifacts/trips-ingest-2.4.1.jar
</code></pre></div></div>

<p>On YARN, if you switch to a file appender, write it under YARN’s own log
directory so log aggregation still collects it and a long-running streaming job
does not fill the local disk:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">appender.file_appender.fileName</span> <span class="p">=</span> <span class="s">${sys:spark.yarn.app.container.log.dir}/spark.log</span>
</code></pre></div></div>

<h2 id="tracing-class-loading-with--verboseclass">Tracing class loading with -verbose:class</h2>

<p><code class="language-plaintext highlighter-rouge">ClassNotFoundException</code> tells you a class was absent. <code class="language-plaintext highlighter-rouge">NoClassDefFoundError</code>
tells you something more specific: the class was present at compile time, and at
runtime either it is missing or a <em>different version</em> of it loaded first. On a
cluster with Hadoop, Hive, Spark and connector JARs all on the classpath, that
second case is the common one, and it is very answerable.</p>

<p><code class="language-plaintext highlighter-rouge">-verbose:class</code> is a JVM flag, not a Spark one. It makes the JVM print every
class it loads and the source it loaded from, which turns the guess into a
lookup:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spark-submit <span class="se">\</span>
  <span class="nt">--master</span> yarn <span class="se">\</span>
  <span class="nt">--deploy-mode</span> cluster <span class="se">\</span>
  <span class="nt">--conf</span> spark.driver.extraJavaOptions<span class="o">=</span><span class="s2">"-verbose:class"</span> <span class="se">\</span>
  <span class="nt">--conf</span> spark.executor.extraJavaOptions<span class="o">=</span><span class="s2">"-verbose:class"</span> <span class="se">\</span>
  <span class="nt">--class</span> com.rangareddy.pipeline.TripsIngest <span class="se">\</span>
  s3a://lakehouse-prod/artifacts/trips-ingest-2.4.1.jar
</code></pre></div></div>

<p>Then search the container log for the class in the stack trace. The path after
<code class="language-plaintext highlighter-rouge">source:</code> is the JAR that won. Once you can name that JAR the fix follows
directly: shade the dependency, set <code class="language-plaintext highlighter-rouge">spark.driver.userClassPathFirst</code>, or remove
the duplicate.</p>

<p>Two similarly named things, both useful:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">--verbose</code> is a <code class="language-plaintext highlighter-rouge">spark-submit</code> flag. It prints the resolved Spark
configuration, the classpath and the parsed arguments before launch. Use it on
every escalation; it costs nothing and answers “was my config even applied?”.</li>
  <li><code class="language-plaintext highlighter-rouge">-verbose:class</code> is a JVM flag passed through <code class="language-plaintext highlighter-rouge">extraJavaOptions</code>. It prints
class-loader activity for the life of the JVM.</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">-verbose:class</code> buys you the loader’s ground truth at the cost of a very large
log. Turn it on for one reproduction, take the answer, turn it off. On a busy
executor it can add hundreds of megabytes, which is a fine trade for one run.</p>

<h2 id="javalangstackoverflowerror">java.lang.StackOverflowError</h2>

<p>The failure looks like this, on either the driver or an executor:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>java.lang.StackOverflowError
  at scala.collection.immutable.List.foreach(List.scala:431)
  at org.apache.spark.sql.catalyst.trees.TreeNode.mapChildren(TreeNode.scala:...)
  at org.apache.spark.sql.catalyst.trees.TreeNode.mapChildren(TreeNode.scala:...)
  at org.apache.spark.sql.catalyst.trees.TreeNode.mapChildren(TreeNode.scala:...)
</code></pre></div></div>

<p>A wall of repeating frames from <code class="language-plaintext highlighter-rouge">TreeNode</code>, <code class="language-plaintext highlighter-rouge">Catalyst</code>, or an Avro or Parquet
schema walker is the signature, and it is good news: it usually means your code
is fine. What it describes is a recursive walk over a structure deeper than the
thread’s stack, from hundreds of columns, a deeply nested struct, a plan built by
chaining <code class="language-plaintext highlighter-rouge">union</code> or <code class="language-plaintext highlighter-rouge">withColumn</code> in a loop, or a long chain of predicates.</p>

<p>Find the side that threw it first. A stack trace in the driver log means the
driver’s plan walk overflowed, and the knob is the driver’s. A trace in an
executor log means the knob is the executor’s. If the logs do not make it
obvious, set both:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spark-submit <span class="se">\</span>
  <span class="nt">--master</span> yarn <span class="se">\</span>
  <span class="nt">--deploy-mode</span> cluster <span class="se">\</span>
  <span class="nt">--conf</span> spark.driver.extraJavaOptions<span class="o">=</span><span class="s2">"-Xss4m"</span> <span class="se">\</span>
  <span class="nt">--conf</span> spark.executor.extraJavaOptions<span class="o">=</span><span class="s2">"-Xss4m"</span> <span class="se">\</span>
  <span class="nt">--class</span> com.rangareddy.pipeline.TripsIngest <span class="se">\</span>
  s3a://lakehouse-prod/artifacts/trips-ingest-2.4.1.jar
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-Xss</code> sets the stack size per thread, so budget for it: an executor with many
task threads pays the increase on each one, out of off-heap memory that is not
counted against <code class="language-plaintext highlighter-rouge">spark.executor.memory</code>. Raise <code class="language-plaintext highlighter-rouge">spark.executor.memoryOverhead</code>
alongside it and YARN stays happy.</p>

<p>Escalate in steps: <code class="language-plaintext highlighter-rouge">4m</code>, <code class="language-plaintext highlighter-rouge">8m</code>, <code class="language-plaintext highlighter-rouge">16m</code>, <code class="language-plaintext highlighter-rouge">32m</code>. Most plans are comfortable well
before the top of that range. If you find yourself needing hundreds of megabytes
of stack, the plan itself is the better thing to simplify, and there are three
clean ways to do it: collapse a <code class="language-plaintext highlighter-rouge">union</code> chain into one <code class="language-plaintext highlighter-rouge">unionByName</code> over a
collected sequence, checkpoint the DataFrame to truncate its lineage, or flatten
the nested schema.</p>

<h2 id="routing-spark-through-an-http-proxy">Routing Spark through an HTTP proxy</h2>

<p>When executors have to reach an external endpoint, a schema registry, a cloud
metadata service, a REST catalog, and the network only allows it through a
proxy, the proxy settings are standard JVM system properties. Spark has no
config of its own for them, which is why they go through <code class="language-plaintext highlighter-rouge">extraJavaOptions</code>.</p>

<p>Per application:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spark-submit <span class="se">\</span>
  <span class="nt">--master</span> yarn <span class="se">\</span>
  <span class="nt">--deploy-mode</span> cluster <span class="se">\</span>
  <span class="nt">--conf</span> spark.driver.extraJavaOptions<span class="o">=</span><span class="s2">"-Dhttp.proxyHost=proxy.corp.internal -Dhttp.proxyPort=8080 -Dhttps.proxyHost=proxy.corp.internal -Dhttps.proxyPort=8443 -Dhttp.nonProxyHosts=localhost|127.0.0.1|*.corp.internal"</span> <span class="se">\</span>
  <span class="nt">--conf</span> spark.executor.extraJavaOptions<span class="o">=</span><span class="s2">"-Dhttp.proxyHost=proxy.corp.internal -Dhttp.proxyPort=8080 -Dhttps.proxyHost=proxy.corp.internal -Dhttps.proxyPort=8443 -Dhttp.nonProxyHosts=localhost|127.0.0.1|*.corp.internal"</span> <span class="se">\</span>
  <span class="nt">--class</span> com.rangareddy.pipeline.TripsIngest <span class="se">\</span>
  s3a://lakehouse-prod/artifacts/trips-ingest-2.4.1.jar
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">http.nonProxyHosts</code> is the part worth including from the start. It keeps
in-cluster traffic off the proxy, so calls to the NameNode or a local metastore
continue to go direct.</p>

<p>For every application on the cluster, put the same values in
<code class="language-plaintext highlighter-rouge">$SPARK_HOME/conf/spark-defaults.conf</code>:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">spark.driver.extraJavaOptions</span>   <span class="py">-Dhttp.proxyHost</span><span class="p">=</span><span class="s">proxy.corp.internal -Dhttp.proxyPort=8080 -Dhttps.proxyHost=proxy.corp.internal -Dhttps.proxyPort=8443 -Dhttp.nonProxyHosts=localhost|127.0.0.1|*.corp.internal</span>
<span class="err">spark.executor.extraJavaOptions</span> <span class="py">-Dhttp.proxyHost</span><span class="p">=</span><span class="s">proxy.corp.internal -Dhttp.proxyPort=8080 -Dhttps.proxyHost=proxy.corp.internal -Dhttps.proxyPort=8443 -Dhttp.nonProxyHosts=localhost|127.0.0.1|*.corp.internal</span>
</code></pre></div></div>

<p>Note that <code class="language-plaintext highlighter-rouge">extraJavaOptions</code> in <code class="language-plaintext highlighter-rouge">spark-defaults.conf</code> is a single string, and a
<code class="language-plaintext highlighter-rouge">--conf spark.driver.extraJavaOptions=...</code> on the command line replaces it rather
than appending. Spark has a purpose-built answer for this:
<code class="language-plaintext highlighter-rouge">spark.driver.defaultJavaOptions</code>. Put cluster-wide settings such as the proxy
there, leave <code class="language-plaintext highlighter-rouge">extraJavaOptions</code> free for users, and Spark prepends the defaults to
whatever a user passes. Both layers then apply together.</p>

<h2 id="production-tips">Production tips</h2>

<ul>
  <li><strong>Always pass <code class="language-plaintext highlighter-rouge">--verbose</code>.</strong> It costs nothing and confirms up front that the
config you set is in the resolved configuration.</li>
  <li><strong>Put cluster-wide JVM policy in <code class="language-plaintext highlighter-rouge">defaultJavaOptions</code>.</strong> Spark prepends it to
whatever a user passes in <code class="language-plaintext highlighter-rouge">extraJavaOptions</code>, so both layers apply.</li>
  <li><strong>Raise <code class="language-plaintext highlighter-rouge">spark.executor.memoryOverhead</code> when you raise <code class="language-plaintext highlighter-rouge">-Xss</code>.</strong> Thread stacks
are off-heap and YARN counts them.</li>
  <li><strong>Copy the shipped <code class="language-plaintext highlighter-rouge">log4j2.properties.template</code></strong> instead of writing a Log4j 2
config from scratch or adapting a Log4j 1.x one.</li>
  <li><strong>Keep the executor-side config filename bare</strong> (<code class="language-plaintext highlighter-rouge">log4j2-debug.properties</code>),
and the client-mode driver-side path absolute.</li>
  <li><strong>Scope <code class="language-plaintext highlighter-rouge">DEBUG</code> to the packages you care about</strong> rather than the root logger.
On a large cluster this keeps the output readable and the disks healthy.</li>
  <li><strong>Turn <code class="language-plaintext highlighter-rouge">-verbose:class</code> off once you have your answer.</strong> It is a diagnostic
rather than a permanent setting.</li>
  <li><strong>On YARN, send file appenders to <code class="language-plaintext highlighter-rouge">${sys:spark.yarn.app.container.log.dir}</code></strong>
so aggregation picks them up.</li>
</ul>

<h2 id="confirming-each-change-took-effect">Confirming each change took effect</h2>

<p>Three of these four apply without printing anything, so it is worth knowing the
one-second check for each. All of them are quick:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Change</th>
      <th style="text-align: left">How you confirm it worked</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Custom Log4j 2 config</td>
      <td style="text-align: left">Your own pattern layout appears on the driver log lines</td>
    </tr>
    <tr>
      <td style="text-align: left">Executor-side logging flag</td>
      <td style="text-align: left">An executor log shows the level you set, not the cluster default</td>
    </tr>
    <tr>
      <td style="text-align: left">Driver options in client mode</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">--verbose</code> lists them in the resolved configuration</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">-Xss</code> raised</td>
      <td style="text-align: left">The job passes the stage that previously overflowed, and containers stay within their limits</td>
    </tr>
    <tr>
      <td style="text-align: left">Proxy settings</td>
      <td style="text-align: left">External calls succeed and in-cluster calls stay direct, thanks to <code class="language-plaintext highlighter-rouge">nonProxyHosts</code></td>
    </tr>
  </tbody>
</table>

<p>For the two logging rows the check is the same and takes one glance: if your
custom pattern layout is on the log lines, the file was read. Running with
<code class="language-plaintext highlighter-rouge">--verbose</code> first makes all five of these visible before the job even starts.</p>

<h2 id="where-to-look-instead">Where to look instead</h2>

<p>These four are diagnostics and last-mile plumbing, and knowing their boundary
saves time.</p>

<p>For a slow job rather than a broken one, the productive settings live in
<code class="language-plaintext highlighter-rouge">spark.sql.*</code>: shuffle partition counts, join strategies, AQE and file sizing.
The Spark UI’s SQL tab will point you at the right one faster than any JVM flag.
For memory pressure, <code class="language-plaintext highlighter-rouge">spark.executor.memory</code> and <code class="language-plaintext highlighter-rouge">spark.memory.fraction</code> are the
knobs, and Spark helpfully rejects <code class="language-plaintext highlighter-rouge">-Xmx</code> here so you cannot set it in the wrong
place.</p>

<p>For logging you want on permanently, edit <code class="language-plaintext highlighter-rouge">$SPARK_CONF_DIR/log4j2.properties</code> and
let Spark upload it for every job, rather than threading <code class="language-plaintext highlighter-rouge">--files</code> through each
one.</p>

<h2 id="conclusion">Conclusion</h2>

<p>These four techniques belong in one post because the delivery mechanism is the
real lesson. There are two JVMs, they take flags through two configs, the deploy
mode decides where the driver’s copy of a file lives, and in client mode the
driver has already started by the time your application code runs. Once that
model is in your head, any future JVM flag is a lookup in the JVM documentation
and a one-line config change.</p>

<p>The Log4j 2 boundary is the one version detail worth remembering alongside it.
Spark 3.3.0 moved to Log4j 2, so on any current cluster the file is
<code class="language-plaintext highlighter-rouge">log4j2.properties</code> and the flag is <code class="language-plaintext highlighter-rouge">-Dlog4j.configurationFile=</code>. Guidance
written for Spark 3.2 and earlier will say otherwise, so start from the template
Spark ships in <code class="language-plaintext highlighter-rouge">conf/</code> and you will get the logging you asked for on the first
run.</p>

<p>Next, if you are sizing the JVMs rather than instrumenting them, the
<a href="/SparkConfigurationGenerator/">Spark Configuration Generator</a>
turns a node count, core count and memory-per-node into executor settings, and
the <a href="/SparkSubmitFormatter/">Spark Submit Command Formatter</a>
will break a command like the ones above into an editable table.</p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://spark.apache.org/docs/latest/configuration.html">Spark configuration reference</a> for <code class="language-plaintext highlighter-rouge">extraJavaOptions</code>, <code class="language-plaintext highlighter-rouge">defaultJavaOptions</code> and the client-mode caveat</li>
  <li><a href="https://spark.apache.org/docs/latest/running-on-yarn.html">Running Spark on YARN</a> for custom Log4j 2 configs and <code class="language-plaintext highlighter-rouge">spark.yarn.app.container.log.dir</code></li>
  <li><a href="https://github.com/apache/spark/blob/v4.2.0/conf/log4j2.properties.template">The <code class="language-plaintext highlighter-rouge">log4j2.properties.template</code> shipped in Spark 4.2.0</a>, the right starting point for a custom config</li>
  <li><a href="https://logging.apache.org/log4j/2.x/manual/configuration.html">Log4j 2 properties-file syntax</a> for the appender and logger property names</li>
  <li><a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/net/doc-files/net-properties.html">Java HTTP proxy system properties</a> for <code class="language-plaintext highlighter-rouge">http.proxyHost</code>, <code class="language-plaintext highlighter-rouge">https.proxyPort</code> and <code class="language-plaintext highlighter-rouge">nonProxyHosts</code></li>
</ul>]]></content><author><name>Ranga Reddy</name></author><category term="Spark" /><category term="Spark" /><category term="Troubleshoot" /><category term="Logging" /><summary type="html"><![CDATA[Four techniques that pay for themselves on nearly every Spark investigation: swap in a custom Log4j 2 config, trace class loading, raise the JVM stack size, and route traffic through an HTTP proxy. All four go through the same two configs, so learning one teaches you the rest.]]></summary></entry><entry><title type="html">Spark Submit Command generator using Iceberg Catalog</title><link href="https://rangareddy.github.io/SparkIcebergSubmitCommand/" rel="alternate" type="text/html" title="Spark Submit Command generator using Iceberg Catalog" /><published>2023-07-15T12:00:00+05:30</published><updated>2023-07-15T12:00:00+05:30</updated><id>https://rangareddy.github.io/SparkIcebergSubmitCommand</id><content type="html" xml:base="https://rangareddy.github.io/SparkIcebergSubmitCommand/"><![CDATA[<ul id="markdown-toc">
  <li><a href="#spark-submit-command-generator-using-different-iceberg-catalogs" id="markdown-toc-spark-submit-command-generator-using-different-iceberg-catalogs">Spark Submit Command generator using different Iceberg Catalog(s)</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
</ul>

<blockquote>
  <p><strong>TL;DR</strong></p>

  <ul>
    <li>Pick a Spark version and the tool offers only the Iceberg releases that actually support it, read from each release’s own build, then only the Scala versions Iceberg publishes a runtime for.</li>
    <li>Spark 4.x is Scala 2.13 only, so no <code class="language-plaintext highlighter-rouge">_2.12</code> Iceberg runtime exists for it. The tool will not let you build that coordinate.</li>
    <li>It emits the <code class="language-plaintext highlighter-rouge">--packages</code> coordinate, the <code class="language-plaintext highlighter-rouge">IcebergSparkSessionExtensions</code> config, and the catalog configs for Hive, Hadoop, REST or JDBC.</li>
    <li><code class="language-plaintext highlighter-rouge">SparkSessionCatalog</code> is used for Hive so Iceberg and existing Hive tables coexist under one catalog name; every other type gets <code class="language-plaintext highlighter-rouge">SparkCatalog</code>.</li>
  </ul>
</blockquote>

<h2 id="spark-submit-command-generator-using-different-iceberg-catalogs">Spark Submit Command generator using different Iceberg Catalog(s)</h2>

<p>This tool is used to generate or build the Spark Submit Command using Iceberg Catalog(s).</p>

<div class="tool-widget">
    <script type="text/javascript">
      	$(document).ready(function() {

      		$('#scala-version').attr('disabled', true);
      		$('#iceberg-version').attr('disabled', true);

			$("#catalog-type").change(function() {
		        var selectedType = $(this).val();
		        $(".catalog-log-div").hide();
		        $("#" + selectedType+"-catalog").show();
		    });

			// Support matrix read from each Iceberg release's gradle.properties
			// (systemProp.knownSparkVersions) at its apache-iceberg-<v> tag, plus
			// settings.gradle for the Scala suffixes. Spark 4.x runtimes are
			// published for Scala 2.13 only.
			var ICEBERG_SUPPORT = {
				"1.11.0": ["3.4", "3.5", "4.0", "4.1"],
				"1.10.2": ["3.4", "3.5", "4.0"],
				"1.9.2": ["3.4", "3.5"],
				"1.8.1": ["3.3", "3.4", "3.5"],
				"1.7.2": ["3.3", "3.4", "3.5"],
				"1.6.1": ["3.3", "3.4", "3.5"],
				"1.5.2": ["3.3", "3.4", "3.5"],
				"1.4.3": ["3.2", "3.3", "3.4", "3.5"]
			};

			function scalaVersionsFor(sparkVersion) {
				// Spark 4 dropped Scala 2.12, so Iceberg only ships _2.13 runtimes.
				return sparkVersion.indexOf("4.") === 0 ? ["2.13"] : ["2.12", "2.13"];
			}

			function setOptions(selectId, values, placeholder) {
				var $sel = $(selectId);
				$sel.empty();
				$sel.append($("<option>", { disabled: true, selected: true, value: "", text: placeholder }));
				values.forEach(function(v) {
					$sel.append($("<option>", { value: v, text: v }));
				});
			}

			$("#spark-version").change(function() {
				var sparkVersion = $(this).val();
				var supported = Object.keys(ICEBERG_SUPPORT).filter(function(iceberg) {
					return ICEBERG_SUPPORT[iceberg].indexOf(sparkVersion) >= 0;
				});
				setOptions("#iceberg-version", supported, "Select Iceberg Version");
				setOptions("#scala-version", scalaVersionsFor(sparkVersion), "Select Scala Version");
				$("#iceberg-version").attr("disabled", supported.length === 0);
				$("#scala-version").attr("disabled", false);
			});

		    $("#generate_spark_submit_cmd").click(function() {
		        var catalogType = $("#catalog-type").val();
				var catalogName = $("#catalog-name").val();
				var icebergVersion = $("#iceberg-version").val();
				var sparkVersion = $("#spark-version").val();
				var scalaVersion = $("#scala-version").val();

				if (!catalogName) {
					alert("Enter catalog name");
					$("#catalog-name").focus();
					return
				}
				if (!sparkVersion || sparkVersion === "none") {
					alert("Select Spark version");
					$("#spark-version").focus();
					return
				}
				if (!icebergVersion || icebergVersion === "none") {
					alert("Select Iceberg version");
					$("#iceberg-version").focus();
					return
				}
				if (!scalaVersion || scalaVersion === "none") {
					alert("Select Scala version");
					$("#scala-version").focus();
					return
				}
				if (!catalogType || catalogType === "none") {
					alert("Select catalog type");
					$("#catalog-type").focus();
					return
				}

				var dependencies = "org.apache.iceberg:iceberg-spark-runtime-"+ sparkVersion + "_" + scalaVersion + ":" + icebergVersion;

				var command = "spark-shell \\ </br>";
				command += "&emsp;--master yarn \\ </br>";
				command += "&emsp;--deploy-mode client \\ </br>";
				command += "&emsp;--packages "+ dependencies + " \\ </br>";
				command += "&emsp;--conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions" + " \\ </br>";

				if ("hive" === catalogType) {
				  command += "&emsp;--conf spark.sql.catalog." + catalogName + "=org.apache.iceberg.spark.SparkSessionCatalog" + " \\ </br>";
				} else {
				  command += "&emsp;--conf spark.sql.catalog." + catalogName + "=org.apache.iceberg.spark.SparkCatalog" + " \\ </br>";
				}

				command += "&emsp;--conf spark.sql.catalog." + catalogName + ".type=" + catalogType + " \\ </br>";
				if( "hive" === catalogType) {
					command += "&emsp;--conf spark.sql.catalog." + catalogName + ".uri=" + $("#metastore-uri").val();
				} else if("hadoop" === catalogType) {
					command += "&emsp;--conf spark.sql.catalog." + catalogName + ".warehouse=" + $("#warehouse-url").val();
				} else if("rest" === catalogType) {
					command += "&emsp;--conf spark.sql.catalog." + catalogName + ".uri=" + $("#rest-uri").val();
				} else if("jdbc" === catalogType) {
					// JdbcCatalog reads connection settings under the "jdbc." prefix
					// (JdbcCatalog.PROPERTY_PREFIX); uri and warehouse are top level.
					command += "&emsp;--conf spark.sql.catalog." + catalogName + ".uri=" + $("#jdbc-uri").val() + " \\ </br>";
					command += "&emsp;--conf spark.sql.catalog." + catalogName + ".warehouse=" + $("#jdbc-warehouse").val() + " \\ </br>";
					command += "&emsp;--conf spark.sql.catalog." + catalogName + ".jdbc.user=" + $("#jdbc-user").val() + " \\ </br>";
					command += "&emsp;--conf spark.sql.catalog." + catalogName + ".jdbc.password=$ICEBERG_CATALOG_PASSWORD";
				}

				// Display the generated command
				document.getElementById("spark_iceberg_submit_cmd_text").innerHTML = command;
		    });

		    $("#copy-spark-iceberg-submit").click(function(e) {
	          e.preventDefault();
	          copy_text_to_clipboard('spark_iceberg_submit_cmd_text', 'spark-submit command copied!');
	        });
      	});

/*	spark-sql \
    --packages org.apache.iceberg:iceberg-spark-runtime-3.2_2.12:1.0.0 \
    --conf spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog \
    --conf spark.sql.catalog.jdbc.warehouse=$WAREHOUSE \
    --conf spark.sql.catalog.jdbc.catalog-impl=org.apache.iceberg.jdbc.JdbcCatalog \
    --conf spark.sql.catalog.jdbc.uri=$URI \
    --conf spark.sql.catalog.jdbc.jdbc.verifyServerCertificate=true \
    --conf spark.sql.catalog.jdbc.jdbc.useSSL=true \
    --conf spark.sql.catalog.jdbc.jdbc.user=$DB_USERNAME \
    --conf spark.sql.catalog.jdbc.jdbc.password=$DB_PASSWORD

	spark-sql --packages org.apache.iceberg:iceberg-spark-runtime-3.2_2.12:1.3.0 \
    --conf spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog \
    --conf spark.sql.catalog.my_catalog.warehouse=s3://my-bucket/my/key/prefix \
    --conf spark.sql.catalog.my_catalog.catalog-impl=org.apache.iceberg.jdbc.JdbcCatalog \
    --conf spark.sql.catalog.my_catalog.uri=jdbc:mysql://test.1234567890.us-west-2.rds.amazonaws.com:3306/default \
    --conf spark.sql.catalog.my_catalog.jdbc.verifyServerCertificate=true \
    --conf spark.sql.catalog.my_catalog.jdbc.useSSL=true \
    --conf spark.sql.catalog.my_catalog.jdbc.user=admin \
    --conf spark.sql.catalog.my_catalog.jdbc.password=pass
*/

  </script>

	<div class="container-fluid">
        <div class="row" id="spark_iceberg_generator_container" style="margin-top: 10px;">
        	<div class="col-md-12">
          		<div class="card">
            		<div class="card-header">
              			<h5>Spark Submit Command generator using Iceberg Catalog</h5>
            		</div> <!-- card-header -->
	            	<div class="card-body">
	              		<div class="row" style="margin-top: 10px;">
			                <div class="col-sm-4">
			                  <div class="form-group">
			                    <label for="catalog-name">Catalog Name:</label>
			                  </div>
			                </div>
			                <div class="col-sm-4">
			                  <div class="form-group">
			                    <input type="text" id="catalog-name" name="catalog-name" class="form-control" value="spark-catalog" />
			                  </div>
			                </div>
	              		</div>
	              		<div class="row" style="margin-top: 10px;">
			                <div class="col-sm-4">
			                  	<div class="form-group">
			                    	<label for="spark-version">Spark Version:</label>
			                  	</div>
			                </div>
			                <div class="col-sm-4">
			                  	<div class="form-group">
			                    	<select class="form-control" id="spark-version">
			                    		<option disabled="" selected="" value="">Select Spark Version</option>
								        <option value="3.2">3.2</option>
								        <option value="3.3">3.3</option>
								        <option value="3.4">3.4</option>
								        <option value="3.5">3.5</option>
								        <option value="4.0">4.0</option>
								        <option value="4.1">4.1</option>
							      	</select>
			                  	</div>
			                </div>
	              		</div>
	              		<div class="row" style="margin-top: 10px;">
			                <div class="col-sm-4">
			                  	<div class="form-group">
			                    	<label for="iceberg-version">Iceberg Version:</label>
			                  	</div>
			                </div>
			                <div class="col-sm-4">
			                  	<div class="form-group">
								    <select class="form-control" id="iceberg-version">
								    	<option disabled="" selected="" value="">Select Iceberg Version</option>
										<!-- populated from ICEBERG_SUPPORT when a Spark version is picked -->
								    </select>
			                  	</div>
			                </div>
	              		</div>
	              		<div class="row" style="margin-top: 10px;">
			                <div class="col-sm-4">
			                  	<div class="form-group">
			                    	<label for="scala-version">Scala Version:</label>
			                  	</div>
			                </div>
			                <div class="col-sm-4">
			                  	<div class="form-group">
							      	<select class="form-control" id="scala-version">
							      		<option disabled="" selected="" value="">Select Scala Version</option>
								        <option value="2.12">2.12</option>
								      	<option value="2.13">2.13</option>
							      	</select>
			                  	</div>
			                </div>
	              		</div>
	              		<div class="row" style="margin-top: 10px;">
			                <div class="col-sm-4">
			                  <div class="form-group">
			                    <label for="catalog-type">Catalog Type:</label>
			                  </div>
			                </div>
			                <div class="col-sm-4">
			                  	<div class="form-group">
			                    	<select class="form-control" id="catalog-type">
			                    		<option disabled="" selected="" value="">Select Catalog Type</option>
								        <option value="hive">Hive</option>
								        <option value="hadoop">Hadoop</option>
								        <option value="rest">REST</option>
								        <option value="jdbc">JDBC</option>
						      		</select>
			                  	</div>
			                </div>
	              		</div>
	              		<div class="row catalog-log-div" id="hive-catalog" style="margin-top: 10px; display: none;">
					    	<div class="col-sm-4">
			                  <div class="form-group">
			                    <label for="metastore-uri">Hive Metastore Uri:</label>
			                  </div>
			                </div>
			                <div class="col-sm-4">
			                  	<input type="text" id="metastore-uri" name="metastore-uri" class="form-control" value="thrift://localhost:9083" />
			                </div>
					    </div>
					    <div class="row catalog-log-div" id="hadoop-catalog" style="margin-top: 10px; display: none;">
					    	<div class="col-sm-4">
			                  <div class="form-group">
			                    <label for="warehouse-url">Warehouse Path:</label>
			                  </div>
			                </div>
			                <div class="col-sm-4">
			                  	<input type="text" id="warehouse-url" name="warehouse-url" class="form-control" value="hdfs://localhost:8020/iceberg-warehouse" />
			                </div>
					    </div>
					    <div class="row catalog-log-div" id="rest-catalog" style="margin-top: 10px; display: none;">
					    	<div class="col-sm-4">
			                  <div class="form-group">
			                    <label for="rest-uri">Rest Catalog Uri:</label>
			                  </div>
			                </div>
			                <div class="col-sm-4">
			                  	<input type="text" id="rest-uri" name="rest-uri" class="form-control" value="http://localhost:8080" />
			                </div>
					    </div>
					    <div class="row catalog-log-div" id="jdbc-catalog" style="margin-top: 10px; display: none;">
			                <div class="col-sm-4">
			                  	<div class="form-group">
			                    	<label for="jdbc-uri">JDBC URI:</label>
			                  	</div>
			                </div>
			                <div class="col-sm-8">
			                  	<div class="form-group">
			                    	<input type="text" class="form-control" id="jdbc-uri" value="jdbc:postgresql://catalog-db.internal:5432/iceberg" />
			                  	</div>
			                </div>
			                <div class="col-sm-4">
			                  	<div class="form-group">
			                    	<label for="jdbc-warehouse">Warehouse:</label>
			                  	</div>
			                </div>
			                <div class="col-sm-8">
			                  	<div class="form-group">
			                    	<input type="text" class="form-control" id="jdbc-warehouse" value="s3://lakehouse-prod/warehouse" />
			                  	</div>
			                </div>
			                <div class="col-sm-4">
			                  	<div class="form-group">
			                    	<label for="jdbc-user">JDBC user:</label>
			                  	</div>
			                </div>
			                <div class="col-sm-8">
			                  	<div class="form-group">
			                    	<input type="text" class="form-control" id="jdbc-user" value="iceberg_catalog" />
			                  	</div>
			                </div>
					    </div>

					    <div class="row" style="display: none;" id="test-catalog">
						    <div class="form-group catalog-log-div" id="hadoop" style="display: none;">
						    	spark.sql.catalog.hadoop_prod.warehouse = hdfs://nn:8020/warehouse/path*/
						      <label for="hadoop-catalog-log">Hadoop Catalog Log:</label>
						      <input type="text" class="form-control" id="hadoop-catalog-log" />
						    </div>
						    <div class="form-group catalog-log-div" id="glue" style="display: none;">
						      <label for="glue-catalog-log">Glue Catalog Log:</label>
						      <input type="text" class="form-control" id="glue-catalog-log" />
						    </div>
						    <div class="form-group catalog-log-div" id="nessie" style="display: none;">
						      <label for="nessie-catalog-log">Nessie Catalog Log:</label>
						      <input type="text" class="form-control" id="nessie-catalog-log" />
						    </div>
	              		</div>
	              	</div>
              		<div class="card-footer">
		              <span style="margin-right: 12px;">
		                <button type="button" id="generate_spark_submit_cmd" class="btn btn-primary">Generate Spark Submit Command</button>
		              </span>
		              <span style="margin-right: 12px;">
		                <button type="button" id="minify_spark_submit_config" class="btn btn-info">Minify</button>
		              </span>
		              <span style="margin-right: 12px;">
		                <button type="button" id="reset_spark_submit_config" class="btn btn-warning">Reset</button>
		              </span>
		            </div>
              	</div>
            </div>
    	</div> <!-- spark_iceberg_generator_container -->
    	<div class="row" id="spark_iceberg_submit_cmd_container" style="margin-top: 10px;">
	        <div class="col-md-12">
	          <div class="card">
	            <h4 class="card-header" style="color: blue;">Spark Submit Command</h4>
	            <div class="card-body">
	              <p class="card-text" id="spark_iceberg_submit_cmd_text" style="background: lightgreen;"></p>
	            </div>
	            <div class="card-footer">
	              <p class="card-text" id="spark_submit_hide_id" style="display:none;"></p>
	              <button type="button" id="copy-spark-iceberg-submit" class="btn btn-danger">Copy Spark Submit Command</button>
	            </div>
	          </div>
	        </div>
	    </div> <!-- spark_iceberg_submit_cmd_container -->
	</div> <!--container-fluid -->
</div>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://iceberg.apache.org/docs/latest/spark-getting-started/">Iceberg Spark getting started</a> for the runtime coordinate and session extensions</li>
  <li><a href="https://iceberg.apache.org/docs/latest/spark-configuration/">Iceberg Spark configuration</a> for catalog properties and the <code class="language-plaintext highlighter-rouge">SparkCatalog</code> versus <code class="language-plaintext highlighter-rouge">SparkSessionCatalog</code> choice</li>
  <li><a href="https://github.com/apache/iceberg/blob/apache-iceberg-1.11.0/core/src/main/java/org/apache/iceberg/CatalogUtil.java"><code class="language-plaintext highlighter-rouge">CatalogUtil.java</code> at apache-iceberg-1.11.0</a>, the accepted <code class="language-plaintext highlighter-rouge">type</code> values</li>
  <li><a href="https://github.com/apache/iceberg/blob/apache-iceberg-1.11.0/gradle.properties">Iceberg <code class="language-plaintext highlighter-rouge">gradle.properties</code> at apache-iceberg-1.11.0</a>, the Spark and Scala support matrix this tool encodes</li>
</ul>]]></content><author><name>Ranga Reddy</name></author><category term="Spark" /><category term="Spark" /><category term="Utilities" /><category term="Iceberg" /><summary type="html"><![CDATA[Pick a catalog type (Hive, Hadoop, REST or JDBC) plus Spark, Iceberg and Scala versions, and get the spark-shell command with the right iceberg-spark-runtime coordinates and catalog configs. Version pairs come from each Iceberg release's own build, up to Iceberg 1.11.0 on Spark 4.1.]]></summary></entry><entry><title type="html">Inspecting Parquet files from the command line with parquet-cli</title><link href="https://rangareddy.github.io/ParquetTools/" rel="alternate" type="text/html" title="Inspecting Parquet files from the command line with parquet-cli" /><published>2023-01-12T00:00:00+05:30</published><updated>2023-01-12T00:00:00+05:30</updated><id>https://rangareddy.github.io/ParquetTools</id><content type="html" xml:base="https://rangareddy.github.io/ParquetTools/"><![CDATA[<ul id="markdown-toc">
  <li><a href="#where-parquet-tooling-lives-now" id="markdown-toc-where-parquet-tooling-lives-now">Where Parquet tooling lives now</a></li>
  <li><a href="#getting-parquet-cli" id="markdown-toc-getting-parquet-cli">Getting parquet-cli</a>    <ul>
      <li><a href="#on-a-cluster-where-hadoop-is-already-there" id="markdown-toc-on-a-cluster-where-hadoop-is-already-there">On a cluster, where Hadoop is already there</a></li>
      <li><a href="#on-a-laptop-with-no-hadoop-install" id="markdown-toc-on-a-laptop-with-no-hadoop-install">On a laptop, with no Hadoop install</a></li>
    </ul>
  </li>
  <li><a href="#building-a-file-to-inspect" id="markdown-toc-building-a-file-to-inspect">Building a file to inspect</a></li>
  <li><a href="#the-command-map" id="markdown-toc-the-command-map">The command map</a></li>
  <li><a href="#reading-data" id="markdown-toc-reading-data">Reading data</a></li>
  <li><a href="#reading-the-schema" id="markdown-toc-reading-the-schema">Reading the schema</a></li>
  <li><a href="#meta-the-best-first-command" id="markdown-toc-meta-the-best-first-command">meta: the best first command</a></li>
  <li><a href="#architecture-row-groups-column-chunks-and-pages" id="markdown-toc-architecture-row-groups-column-chunks-and-pages">Architecture: row groups, column chunks and pages</a></li>
  <li><a href="#rewrite-merge-prune-mask-recompress" id="markdown-toc-rewrite-merge-prune-mask-recompress">rewrite: merge, prune, mask, recompress</a>    <ul>
      <li><a href="#how-merging-handles-row-groups" id="markdown-toc-how-merging-handles-row-groups">How merging handles row groups</a></li>
      <li><a href="#verifying-a-column-prune" id="markdown-toc-verifying-a-column-prune">Verifying a column prune</a></li>
      <li><a href="#masking-optional-columns" id="markdown-toc-masking-optional-columns">Masking optional columns</a></li>
    </ul>
  </li>
  <li><a href="#production-tips" id="markdown-toc-production-tips">Production tips</a></li>
  <li><a href="#where-parquet-cli-fits-best" id="markdown-toc-where-parquet-cli-fits-best">Where parquet-cli fits best</a></li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
</ul>

<blockquote>
  <p><strong>TL;DR</strong></p>

  <ul>
    <li><code class="language-plaintext highlighter-rouge">parquet-cli</code> is where Parquet tooling is maintained now. <code class="language-plaintext highlighter-rouge">parquet-tools</code> was renamed to <code class="language-plaintext highlighter-rouge">parquet-tools-deprecated</code> in Parquet 1.12.0 and left the build in 1.12.3.</li>
    <li>The command set is broader rather than renamed. <code class="language-plaintext highlighter-rouge">rowcount</code>, <code class="language-plaintext highlighter-rouge">size</code>, <code class="language-plaintext highlighter-rouge">dump</code> and <code class="language-plaintext highlighter-rouge">merge</code> are covered by <code class="language-plaintext highlighter-rouge">meta</code>, <code class="language-plaintext highlighter-rouge">column-size</code>, <code class="language-plaintext highlighter-rouge">pages</code> and <code class="language-plaintext highlighter-rouge">rewrite</code>, and there are new commands for bloom filters, size statistics and geospatial statistics.</li>
    <li><code class="language-plaintext highlighter-rouge">rewrite</code> is the one to learn first. A single command merges files, prunes columns, masks columns and changes the compression codec, and it supersedes <code class="language-plaintext highlighter-rouge">prune</code>.</li>
    <li><code class="language-plaintext highlighter-rouge">rewrite</code> merges by copying row groups intact, which is what makes it fast. Two 10-row files become one file with two row groups of 10, so use your table format’s compaction when you want re-chunking.</li>
    <li>After <code class="language-plaintext highlighter-rouge">--prune-columns</code>, confirm the result with <code class="language-plaintext highlighter-rouge">meta</code> or <code class="language-plaintext highlighter-rouge">column-size</code>, which read the physical schema. The stored <code class="language-plaintext highlighter-rouge">parquet.avro.schema</code> metadata is copied through unchanged, so an Avro-model reader reports the pruned field as <code class="language-plaintext highlighter-rouge">null</code>.</li>
  </ul>
</blockquote>

<h2 id="where-parquet-tooling-lives-now">Where Parquet tooling lives now</h2>

<p><code class="language-plaintext highlighter-rouge">parquet-tools</code> was the standard way to look inside a Parquet file for years,
and <code class="language-plaintext highlighter-rouge">parquet-cli</code> has taken over that role. You can trace the handover by diffing
the root <code class="language-plaintext highlighter-rouge">pom.xml</code> across releases:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Parquet release</th>
      <th style="text-align: left">Module in the root <code class="language-plaintext highlighter-rouge">pom.xml</code></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">1.11.2</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">parquet-tools</code></td>
    </tr>
    <tr>
      <td style="text-align: left">1.12.0 to 1.12.2</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">parquet-tools-deprecated</code></td>
    </tr>
    <tr>
      <td style="text-align: left">1.12.3 and later</td>
      <td style="text-align: left">not present</td>
    </tr>
  </tbody>
</table>

<p>The replacement, <code class="language-plaintext highlighter-rouge">parquet-cli</code>, has been in the build the whole time and is
still there in <a href="https://github.com/apache/parquet-java/blob/apache-parquet-1.18.0/pom.xml">1.18.0</a>.
The repository itself was also renamed: <code class="language-plaintext highlighter-rouge">apache/parquet-mr</code> is now
<a href="https://github.com/apache/parquet-java"><code class="language-plaintext highlighter-rouge">apache/parquet-java</code></a>.</p>

<p>The old jar is still on Maven Central, so a <code class="language-plaintext highlighter-rouge">parquet-tools-1.11.2.jar</code> you
downloaded in 2022 keeps working on older files. <code class="language-plaintext highlighter-rouge">parquet-cli</code> is where five
years of fixes and every newer format feature landed, including size statistics,
geospatial statistics and the Parquet <code class="language-plaintext highlighter-rouge">variant</code> type, so it is the better choice
for files written by a current Spark, Hudi or Iceberg.</p>

<p>This post is written against <strong>Parquet 1.18.0</strong> on Java 17. Every transcript
below is real output from that version.</p>

<h2 id="getting-parquet-cli">Getting parquet-cli</h2>

<p>The published <code class="language-plaintext highlighter-rouge">parquet-cli</code> runtime jar deliberately does not bundle Hadoop,
which keeps it small and lets it pick up whatever Hadoop and connector versions
your cluster already has. There are two easy ways to run it.</p>

<h3 id="on-a-cluster-where-hadoop-is-already-there">On a cluster, where Hadoop is already there</h3>

<p>This is the easy case, and the one you will use most. <code class="language-plaintext highlighter-rouge">hadoop jar</code> puts the
whole Hadoop client classpath in front of you:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget <span class="nt">-O</span> parquet-cli.jar <span class="se">\</span>
  https://repo1.maven.org/maven2/org/apache/parquet/parquet-cli/1.18.0/parquet-cli-1.18.0-runtime.jar

hadoop jar parquet-cli.jar org.apache.parquet.cli.Main <span class="se">\</span>
  meta hdfs:///warehouse/hr/employees/part-00000-8f3a1c92.parquet
</code></pre></div></div>

<p>The same jar reads cloud object storage, as long as the matching connector is on
the classpath:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hadoop jar parquet-cli.jar org.apache.parquet.cli.Main <span class="se">\</span>
  meta s3a://lakehouse-prod/warehouse/hr/employees/part-00000-8f3a1c92.parquet
</code></pre></div></div>

<h3 id="on-a-laptop-with-no-hadoop-install">On a laptop, with no Hadoop install</h3>

<p>Fetch the runtime jar plus the shaded Hadoop client and the few libraries
<code class="language-plaintext highlighter-rouge">parquet-cli</code> expects to find. Include an SLF4J binding: <code class="language-plaintext highlighter-rouge">parquet-cli</code> writes its
output through SLF4J, so the binding is what makes the output appear. The script
below sets up a working wrapper in one go.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> ~/parquet-cli <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/parquet-cli

<span class="nv">BASE</span><span class="o">=</span>https://repo1.maven.org/maven2
curl <span class="nt">-sLO</span> <span class="nv">$BASE</span>/org/apache/parquet/parquet-cli/1.18.0/parquet-cli-1.18.0-runtime.jar
curl <span class="nt">-sLO</span> <span class="nv">$BASE</span>/org/apache/hadoop/hadoop-client-api/3.4.1/hadoop-client-api-3.4.1.jar
curl <span class="nt">-sLO</span> <span class="nv">$BASE</span>/org/apache/hadoop/hadoop-client-runtime/3.4.1/hadoop-client-runtime-3.4.1.jar
curl <span class="nt">-sLO</span> <span class="nv">$BASE</span>/com/google/guava/guava/33.4.0-jre/guava-33.4.0-jre.jar
curl <span class="nt">-sLO</span> <span class="nv">$BASE</span>/com/google/guava/failureaccess/1.0.2/failureaccess-1.0.2.jar
curl <span class="nt">-sLO</span> <span class="nv">$BASE</span>/commons-logging/commons-logging/1.3.5/commons-logging-1.3.5.jar
curl <span class="nt">-sLO</span> <span class="nv">$BASE</span>/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar
<span class="c"># Without an SLF4J binding, parquet-cli runs and prints nothing.</span>
curl <span class="nt">-sLO</span> <span class="nv">$BASE</span>/org/slf4j/slf4j-reload4j/1.7.36/slf4j-reload4j-1.7.36.jar
curl <span class="nt">-sLO</span> <span class="nv">$BASE</span>/ch/qos/reload4j/reload4j/1.2.25/reload4j-1.2.25.jar

<span class="nb">cat</span> <span class="o">&gt;</span> log4j.properties <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">PROPS</span><span class="sh">'
log4j.rootLogger=INFO, console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.out
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%m%n
</span><span class="no">PROPS

</span><span class="nb">cat</span> <span class="o">&gt;</span> parquet <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">SH</span><span class="sh">'
#!/usr/bin/env bash
DIR="</span><span class="si">$(</span><span class="nb">cd</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">dirname</span> <span class="s2">"</span><span class="k">${</span><span class="nv">BASH_SOURCE</span><span class="p">[0]</span><span class="k">}</span><span class="s2">"</span><span class="si">)</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> <span class="nb">pwd</span><span class="si">)</span><span class="sh">"
exec java -cp "</span><span class="nv">$DIR</span><span class="sh">/*:</span><span class="nv">$DIR</span><span class="sh">" org.apache.parquet.cli.Main "</span><span class="nv">$@</span><span class="sh">"
</span><span class="no">SH
</span><span class="nb">chmod</span> +x parquet
</code></pre></div></div>

<p>Every example from here on uses that <code class="language-plaintext highlighter-rouge">parquet</code> wrapper.</p>

<h2 id="building-a-file-to-inspect">Building a file to inspect</h2>

<p><code class="language-plaintext highlighter-rouge">parquet-cli</code> can create a Parquet file from CSV, which is convenient for
reproducing a problem. Supply an explicit Avro schema so the column types are
what you meant rather than what got inferred:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> <span class="o">&gt;</span> employees.csv <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">CSV</span><span class="sh">'
employee_id,first_name,last_name,email,phone_number,hire_date,salary,manager_id
1,Ranga,Reddy,rangareddy@yahoo.com,99509833,2007-06-21,2600,
2,Raja Sekhar,Reddy,raja@gmail.com,75050798,2008-01-13,2600,1
3,Vasundra,Reddy,vasu@gmail.com,91512344,2003-09-17,4400,1
4,Meena,P,meena@test.com,81535555,2004-02-17,13000,2
5,Manoj,Kumar,manu@rediff.com,60312366,2005-08-17,6000,3
6,Vinod,Kumar,vinod@zoho.com,71237777,2002-06-07,6500,3
7,Raja,Reddy,rajar@yahoo.co.in,91518888,2002-06-07,10000,4
8,Shiva,P,shiva@mymail.com,81512380,2002-06-07,12008,6
9,Reddy,Babu,babu@mail.com,91528181,2002-06-07,8300,7
10,Nishanth,Reddy,nish@nish.com,61512347,2003-06-17,24000,2
</span><span class="no">CSV

</span><span class="nb">cat</span> <span class="o">&gt;</span> employees.avsc <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">AVSC</span><span class="sh">'
{
  "type": "record",
  "name": "employees",
  "namespace": "com.rangareddy.hr",
  "fields": [
    {"name": "employee_id",  "type": "int"},
    {"name": "first_name",   "type": "string"},
    {"name": "last_name",    "type": "string"},
    {"name": "email",        "type": "string"},
    {"name": "phone_number", "type": "long"},
    {"name": "hire_date",    "type": "string"},
    {"name": "salary",       "type": "int"},
    {"name": "manager_id",   "type": ["null", "int"], "default": null}
  ]
}
</span><span class="no">AVSC

</span>./parquet convert-csv employees.csv <span class="nt">-s</span> employees.avsc <span class="nt">-o</span> employees.parquet <span class="nt">--overwrite</span>
</code></pre></div></div>

<p>One note on schemas: the CSV reader takes physical Avro types rather than
logical ones, so keep dates as <code class="language-plaintext highlighter-rouge">string</code> in the <code class="language-plaintext highlighter-rouge">.avsc</code> when the source is CSV, or
convert from Avro or JSON when you want <code class="language-plaintext highlighter-rouge">date</code> and <code class="language-plaintext highlighter-rouge">decimal</code> logical types.</p>

<h2 id="the-command-map">The command map</h2>

<p>This is the table to keep. The left column is what you typed with
<code class="language-plaintext highlighter-rouge">parquet-tools</code>; the right column is what does that job now, and the bottom rows
are capabilities the old tool never had.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left"><code class="language-plaintext highlighter-rouge">parquet-tools</code></th>
      <th style="text-align: left"><code class="language-plaintext highlighter-rouge">parquet-cli</code></th>
      <th style="text-align: left">Note</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">cat</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">cat</code></td>
      <td style="text-align: left">Same idea, JSON output by default</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">head -n N</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">head</code> / <code class="language-plaintext highlighter-rouge">cat -n N</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">head</code> defaults to 10 records, not 5</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">schema</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">schema</code></td>
      <td style="text-align: left">Prints the Avro schema; <code class="language-plaintext highlighter-rouge">meta</code> prints the Parquet message type</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">meta</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">meta</code></td>
      <td style="text-align: left">Now also prints per-column stats and encodings</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">rowcount</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">meta</code></td>
      <td style="text-align: left">No dedicated command; read <code class="language-plaintext highlighter-rouge">count:</code> off the row groups</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">size</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">column-size</code> / <code class="language-plaintext highlighter-rouge">size-stats</code></td>
      <td style="text-align: left">Per-column bytes and ratio, or unencoded size statistics</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">dump</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">pages</code>, <code class="language-plaintext highlighter-rouge">dictionary</code>, <code class="language-plaintext highlighter-rouge">footer</code>, <code class="language-plaintext highlighter-rouge">column-index</code></td>
      <td style="text-align: left">Split into one command per structure</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">merge</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">rewrite -i a,b -o out</code></td>
      <td style="text-align: left">Takes a comma-separated input list</td>
    </tr>
    <tr>
      <td style="text-align: left">(none)</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">prune</code></td>
      <td style="text-align: left">Deprecated, removed in 2.0.0, use <code class="language-plaintext highlighter-rouge">rewrite --prune-columns</code></td>
    </tr>
    <tr>
      <td style="text-align: left">(none)</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">masking</code> / <code class="language-plaintext highlighter-rouge">rewrite --mask-mode</code></td>
      <td style="text-align: left">Nullify column values in place</td>
    </tr>
    <tr>
      <td style="text-align: left">(none)</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">bloom-filter</code>, <code class="language-plaintext highlighter-rouge">scan</code>, <code class="language-plaintext highlighter-rouge">check-stats</code>, <code class="language-plaintext highlighter-rouge">trans-compression</code>, <code class="language-plaintext highlighter-rouge">geospatial-stats</code></td>
      <td style="text-align: left">Newer format features</td>
    </tr>
  </tbody>
</table>

<p>The full list is registered in
<a href="https://github.com/apache/parquet-java/blob/apache-parquet-1.18.0/parquet-cli/src/main/java/org/apache/parquet/cli/Main.java#L105-L128"><code class="language-plaintext highlighter-rouge">Main.java</code></a>,
which is the authoritative answer to “does this version have that command”.</p>

<h2 id="reading-data">Reading data</h2>

<p><code class="language-plaintext highlighter-rouge">cat</code> prints records as JSON, one per line. <code class="language-plaintext highlighter-rouge">head</code> is the same command bound to a
10-record limit, and <code class="language-plaintext highlighter-rouge">-n</code> overrides it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./parquet <span class="nb">cat</span> <span class="nt">-n</span> 2 employees.parquet
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{"employee_id": 1, "first_name": "Ranga", "last_name": "Reddy", "email": "rangareddy@yahoo.com", "phone_number": 99509833, "hire_date": "2007-06-21", "salary": 2600, "manager_id": null}
{"employee_id": 2, "first_name": "Raja Sekhar", "last_name": "Reddy", "email": "raja@gmail.com", "phone_number": 75050798, "hire_date": "2008-01-13", "salary": 2600, "manager_id": 1}
</code></pre></div></div>

<p>Unlike <code class="language-plaintext highlighter-rouge">parquet-tools cat</code>, there is no separate <code class="language-plaintext highlighter-rouge">--json</code> flag, because JSON is
the only output format.</p>

<h2 id="reading-the-schema">Reading the schema</h2>

<p><code class="language-plaintext highlighter-rouge">schema</code> gives you the Avro view, which is what most downstream readers see:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./parquet schema employees.parquet
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "type" : "record",
  "name" : "employees",
  "namespace" : "com.rangareddy.hr",
  "fields" : [ {
    "name" : "employee_id",
    "type" : "int"
  }, {
    "name" : "first_name",
    "type" : "string"
  }, {
    "name" : "manager_id",
    "type" : [ "null", "int" ],
    "default" : null
  } ]
}
</code></pre></div></div>

<p>The Parquet message type is a different thing, and when you are debugging a
type-mismatch it is the one you want, because it shows physical types,
<code class="language-plaintext highlighter-rouge">required</code> versus <code class="language-plaintext highlighter-rouge">optional</code>, and the logical-type annotations. It comes out of
<code class="language-plaintext highlighter-rouge">meta</code>, covered next.</p>

<h2 id="meta-the-best-first-command">meta: the best first command</h2>

<p><code class="language-plaintext highlighter-rouge">meta</code> replaces three old commands at once. It reports the writer, the key-value
metadata, the Parquet message type, and per-column statistics per row group:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./parquet meta employees.parquet
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>File path:  employees.parquet
Created by: parquet-mr version 1.18.0 (build 0209dfb48d153f2f3e49a9f12addebe15c0f1d77)
Properties:
  parquet.avro.schema: {"type":"record","name":"employees","namespace":"com.rangareddy.hr","fields":[...]}
    writer.model.name: avro
Schema:
message com.rangareddy.hr.employees {
  required int32 employee_id;
  required binary first_name (STRING);
  required binary last_name (STRING);
  required binary email (STRING);
  required int64 phone_number;
  required binary hire_date (STRING);
  required int32 salary;
  optional int32 manager_id;
}

Row group 0:  count: 10  89.50 B records  start: 4  total(compressed): 895 B total(uncompressed):882 B
--------------------------------------------------------------------------------
              type      encodings count     avg size   nulls   min / max
employee_id   INT32     G   _     10        6.70 B     0       "1" / "10"
first_name    BINARY    G   _     10        12.10 B    0       "Manoj" / "Vinod"
last_name     BINARY    G _ R     10        11.50 B    0       "Babu" / "Reddy"
email         BINARY    G   _     10        15.70 B    0       "babu@mail.com" / "vinod@zoho.com"
phone_number  INT64     G   _     10        9.60 B     0       "60312366" / "99509833"
hire_date     BINARY    G _ R     10        14.40 B    0       "2002-06-07" / "2008-01-13"
salary        INT32     G   _     10        8.20 B     0       "2600" / "24000"
manager_id    INT32     G _ R     10        11.30 B    1       "1" / "7"
</code></pre></div></div>

<p>Four things to read off this, in the order they usually matter:</p>

<p><strong><code class="language-plaintext highlighter-rouge">Created by</code></strong> identifies the writer. On a mixed cluster this is how you find
out that half your files came from an old engine. Note that the string still
says <code class="language-plaintext highlighter-rouge">parquet-mr</code> even in 1.18.0, despite the repository rename.</p>

<p><strong><code class="language-plaintext highlighter-rouge">count:</code> per row group</strong> is the row count, so <code class="language-plaintext highlighter-rouge">meta</code> is also <code class="language-plaintext highlighter-rouge">rowcount</code>. Add
them up across row groups for the file total.</p>

<p><strong>The <code class="language-plaintext highlighter-rouge">min / max</code> column</strong> is why predicate pushdown works or does not. If a
filter on <code class="language-plaintext highlighter-rouge">salary</code> is not pruning row groups, look here first: statistics that
are absent, or present but useless because the column is unsorted and every row
group spans the full range, explain it immediately.</p>

<p><strong>The <code class="language-plaintext highlighter-rouge">nulls</code> column</strong> is a fast data-quality check. <code class="language-plaintext highlighter-rouge">manager_id</code> showing 1 null
across 10 rows matches the CEO having no manager.</p>

<p>The <code class="language-plaintext highlighter-rouge">encodings</code> column is compact: <code class="language-plaintext highlighter-rouge">G</code> is a dictionary-encoded column chunk,
<code class="language-plaintext highlighter-rouge">_</code> means no dictionary fallback, and <code class="language-plaintext highlighter-rouge">R</code> indicates RLE. Use <code class="language-plaintext highlighter-rouge">pages</code> when you
need the per-page detail.</p>

<h2 id="architecture-row-groups-column-chunks-and-pages">Architecture: row groups, column chunks and pages</h2>

<p>Every command below reports on one level of the same three-level hierarchy, so
it is worth naming the levels before reading their output.</p>

<p>A Parquet file is a sequence of <strong>row groups</strong>, each a horizontal slice of the
rows. Within a row group, each column’s values for those rows live in one
<strong>column chunk</strong>, which is the unit that gets its own encoding, compression and
min/max statistics. A column chunk is in turn a sequence of <strong>pages</strong>, which is
the smallest unit the reader decompresses.</p>

<p>That structure is what makes the tooling map onto commands the way it does:
<code class="language-plaintext highlighter-rouge">meta</code> reports the file and its row groups, <code class="language-plaintext highlighter-rouge">column-size</code> aggregates column
chunks, and <code class="language-plaintext highlighter-rouge">pages</code> opens a single column chunk. It is also why predicate
pushdown is a row-group-level and page-level decision: the engine compares your
filter against the statistics at those levels and skips whole chunks or pages it
can prove irrelevant.</p>

<p><code class="language-plaintext highlighter-rouge">column-size</code> answers “which column is my file”, which is the first question
when a table is unexpectedly large:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./parquet column-size employees.parquet
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>manager_id-&gt; Size In Bytes: 113 Size In Ratio: 0.12625699
employee_id-&gt; Size In Bytes: 67 Size In Ratio: 0.074860334
last_name-&gt; Size In Bytes: 115 Size In Ratio: 0.12849163
phone_number-&gt; Size In Bytes: 96 Size In Ratio: 0.10726257
hire_date-&gt; Size In Bytes: 144 Size In Ratio: 0.16089386
salary-&gt; Size In Bytes: 82 Size In Ratio: 0.09162011
first_name-&gt; Size In Bytes: 121 Size In Ratio: 0.13519552
email-&gt; Size In Bytes: 157 Size In Ratio: 0.17541899
</code></pre></div></div>

<p>Note the output is unordered, so sort it yourself on a wide table. On a real
fact table this is where you discover that one JSON blob column is 70% of your
storage.</p>

<p><code class="language-plaintext highlighter-rouge">pages</code> drops to page level within a column chunk, which is where you go when
compression or encoding is the question:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./parquet pages employees.parquet <span class="nt">-c</span> salary
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Column: salary
--------------------------------------------------------------------------------
  page   type  enc  count   avg size   size       rows     nulls   min / max
  0-0    data  G _  10      5.90 B     59 B
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">size-stats</code> reports the Parquet size statistics added to the format more
recently, including the unencoded byte size and the repetition and definition
level histograms that let an engine estimate decode cost without reading the
data. Columns with no entry simply have no size statistics written:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./parquet size-stats employees.parquet
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>File path: employees.parquet

Row group 0
--------------------------------------------------------------------------------
column         unencoded bytes rep level histogram   def level histogram
[employee_id]  -               -                     -
[first_name]   61 B            -                     -
[last_name]    41 B            -                     -
[email]        150 B           -                     -
[phone_number] -               -                     -
[hire_date]    100 B           -                     -
[salary]       -               -                     -
[manager_id]   -               -                     -
</code></pre></div></div>

<h2 id="rewrite-merge-prune-mask-recompress">rewrite: merge, prune, mask, recompress</h2>

<p><code class="language-plaintext highlighter-rouge">rewrite</code> is the command worth real attention: it is the only one that writes
data, and it absorbed several older tools into one interface. Its own help text
is explicit that <code class="language-plaintext highlighter-rouge">prune</code> is
<a href="https://github.com/apache/parquet-java/blob/apache-parquet-1.18.0/parquet-cli/src/main/java/org/apache/parquet/cli/commands/PruneColumnsCommand.java">deprecated and will be removed in 2.0.0</a>
in favour of it.</p>

<p>Merging several files and switching the codec at the same time:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./parquet rewrite <span class="se">\</span>
  <span class="nt">-i</span> employees.parquet,employees_b.parquet <span class="se">\</span>
  <span class="nt">-o</span> employees_merged.parquet <span class="se">\</span>
  <span class="nt">-c</span> ZSTD <span class="se">\</span>
  <span class="nt">--overwrite</span>
</code></pre></div></div>

<h3 id="how-merging-handles-row-groups">How merging handles row groups</h3>

<p>Here is the behaviour worth understanding before you rely on it. Reading the
merged file back:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./parquet meta employees_merged.parquet
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Row group 0:  count: 10  87.00 B records  start: 4    total(compressed): 870 B total(uncompressed):880 B
Row group 1:  count: 10  87.00 B records  start: 874  total(compressed): 870 B total(uncompressed):880 B
</code></pre></div></div>

<p>Two 10-row inputs became one file with two row groups of 10 rows rather than one
row group of 20. <code class="language-plaintext highlighter-rouge">rewrite</code> copies row groups intact, which is exactly why it is
fast: it moves column chunks without decoding them. That makes it ideal for
changing a codec, dropping a column or consolidating a handful of files cheaply.</p>

<p>For re-chunking, reach for the tool built for it: your table format’s own
compaction, such as Hudi clustering or Iceberg <code class="language-plaintext highlighter-rouge">rewrite_data_files</code>, or a rewrite
through an engine. <code class="language-plaintext highlighter-rouge">rewrite</code> gives you a cheap physical merge; compaction gives
you better row-group sizing.</p>

<h3 id="verifying-a-column-prune">Verifying a column prune</h3>

<p>When you prune a column, it is worth knowing which command to verify with.
Prune a column and nullify another:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./parquet rewrite <span class="se">\</span>
  <span class="nt">-i</span> employees.parquet <span class="se">\</span>
  <span class="nt">-o</span> employees_masked.parquet <span class="se">\</span>
  <span class="nt">--prune-columns</span> phone_number <span class="se">\</span>
  <span class="nt">--mask-mode</span> nullify <span class="nt">--mask-columns</span> manager_id <span class="se">\</span>
  <span class="nt">--overwrite</span>
</code></pre></div></div>

<p>The Parquet message type is correctly pruned to seven columns:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>message com.rangareddy.hr.employees {
  required int32 employee_id;
  required binary first_name (STRING);
  required binary last_name (STRING);
  required binary email (STRING);
  required binary hire_date (STRING);
  required int32 salary;
  optional int32 manager_id;
}
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">parquet.avro.schema</code> entry in the file’s key-value metadata still declares
<code class="language-plaintext highlighter-rouge">phone_number</code>, because <code class="language-plaintext highlighter-rouge">rewrite</code> copies key-value metadata through untouched. An
Avro-model reader follows that schema, finds no column, and returns <code class="language-plaintext highlighter-rouge">null</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./parquet <span class="nb">head</span> <span class="nt">-n</span> 1 employees_masked.parquet
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{"employee_id": 1, "first_name": "Ranga", "last_name": "Reddy", "email": "rangareddy@yahoo.com", "phone_number": null, "hire_date": "2007-06-21", "salary": 2600, "manager_id": null}
</code></pre></div></div>

<p>The prune worked: <code class="language-plaintext highlighter-rouge">phone_number</code> is physically gone and <code class="language-plaintext highlighter-rouge">column-size</code> lists
seven columns. The record-model view simply reports the field from the stored
Avro schema. So for a GDPR deletion, verify with <code class="language-plaintext highlighter-rouge">meta</code> or <code class="language-plaintext highlighter-rouge">column-size</code>, which
read the physical Parquet schema, and you get a clear answer.</p>

<h3 id="masking-optional-columns">Masking optional columns</h3>

<p>Nullify needs somewhere to put the null, so it applies to <code class="language-plaintext highlighter-rouge">optional</code> columns. On
a <code class="language-plaintext highlighter-rouge">required</code> column it stops and tells you clearly:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>java.io.IOException: Required column [email] cannot be nullified
	at org.apache.parquet.hadoop.rewrite.ParquetRewriter.processBlock(ParquetRewriter.java:533)
</code></pre></div></div>

<p>To scrub a non-nullable column, prune it instead, or rewrite the data through an
engine that can change <code class="language-plaintext highlighter-rouge">required</code> to <code class="language-plaintext highlighter-rouge">optional</code> first.</p>

<h2 id="production-tips">Production tips</h2>

<ul>
  <li><strong>Start with <code class="language-plaintext highlighter-rouge">meta</code>.</strong> Writer, row counts, statistics and the message type in
one command answer most questions.</li>
  <li><strong>Use <code class="language-plaintext highlighter-rouge">meta</code> for row counts</strong>, and add the per-row-group <code class="language-plaintext highlighter-rouge">count:</code> values. There
is no <code class="language-plaintext highlighter-rouge">rowcount</code>.</li>
  <li><strong>Sort <code class="language-plaintext highlighter-rouge">column-size</code> yourself.</strong> The output order is not by size.</li>
  <li><strong>Check <code class="language-plaintext highlighter-rouge">min / max</code> in <code class="language-plaintext highlighter-rouge">meta</code> first</strong> when a filter is not pruning as much as
you expect. It usually answers the question immediately.</li>
  <li><strong>Use <code class="language-plaintext highlighter-rouge">rewrite</code> for cheap physical changes</strong> and your table format’s
compaction for row-group sizing. <code class="language-plaintext highlighter-rouge">rewrite</code> preserves row-group boundaries.</li>
  <li><strong>After <code class="language-plaintext highlighter-rouge">--prune-columns</code>, verify with <code class="language-plaintext highlighter-rouge">meta</code> or <code class="language-plaintext highlighter-rouge">column-size</code>.</strong> They read
the physical schema, while <code class="language-plaintext highlighter-rouge">cat</code> and <code class="language-plaintext highlighter-rouge">head</code> follow the stored Avro schema.</li>
  <li><strong>On a cluster, prefer <code class="language-plaintext highlighter-rouge">hadoop jar</code></strong> so the Hadoop and connector classpath is
already correct.</li>
  <li><strong>Pin the version in your notes.</strong> <code class="language-plaintext highlighter-rouge">Main.java</code> at the release tag is the only
reliable list of which commands your jar has.</li>
</ul>

<h2 id="where-parquet-cli-fits-best">Where parquet-cli fits best</h2>

<p><code class="language-plaintext highlighter-rouge">parquet-cli</code> is the right tool whenever the question is about one file: is the
statistic there, what wrote it, why is this column so large, what encodings did
it choose. For that it is unbeatable, and it needs nothing but the file.</p>

<p>Table-level questions have their own tooling, and pairing the two is the
productive combination. A file under a Hudi, Iceberg or Delta table path is one
version of one file group, so the table format is what knows whether it is live,
which delete files or deletion vectors apply, what is pending compaction and how
the schema has evolved. Use Hudi’s CLI and metadata table or Iceberg’s metadata
tables and <code class="language-plaintext highlighter-rouge">CALL</code> procedures for those, then drop to <code class="language-plaintext highlighter-rouge">parquet-cli</code> for the file
in front of you.</p>

<h2 id="conclusion">Conclusion</h2>

<p>The move from <code class="language-plaintext highlighter-rouge">parquet-tools</code> to <code class="language-plaintext highlighter-rouge">parquet-cli</code> is often described as a rename,
and it is better than that. The command set genuinely grew: <code class="language-plaintext highlighter-rouge">dump</code> became four
focused commands, <code class="language-plaintext highlighter-rouge">rewrite</code> took over from both <code class="language-plaintext highlighter-rouge">merge</code> and <code class="language-plaintext highlighter-rouge">prune</code>, and there
are new commands for bloom filters, size statistics and geospatial statistics
that the old tool never had. Two commands carry most of the daily work. <code class="language-plaintext highlighter-rouge">meta</code>
answers the writer, row-count, schema and statistics questions in one shot, and
<code class="language-plaintext highlighter-rouge">rewrite</code> handles every physical change to a file.</p>

<p>The two <code class="language-plaintext highlighter-rouge">rewrite</code> behaviours in this post are the ones worth carrying forward,
and both follow from a single sensible design decision. Because <code class="language-plaintext highlighter-rouge">rewrite</code> copies
column chunks rather than decoding them, it is fast, it preserves row-group
boundaries, and it passes key-value metadata through untouched. That tells you
where it fits: excellent for recompressing, pruning and consolidating, and paired
with your table format’s compaction when you want row groups resized. It also
tells you which command verifies a prune, since <code class="language-plaintext highlighter-rouge">meta</code> and <code class="language-plaintext highlighter-rouge">column-size</code> read the
physical schema.</p>

<p>If you arrived here searching for <code class="language-plaintext highlighter-rouge">parquet-tools</code>, the old jar still runs and
there is no urgency. When you do switch, the command map above is the whole
migration, and you get the newer format features for free.</p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://github.com/apache/parquet-java">Apache Parquet Java on GitHub</a>, formerly <code class="language-plaintext highlighter-rouge">apache/parquet-mr</code></li>
  <li><a href="https://github.com/apache/parquet-java/blob/apache-parquet-1.18.0/parquet-cli/src/main/java/org/apache/parquet/cli/Main.java#L105-L128"><code class="language-plaintext highlighter-rouge">Main.java</code> at the 1.18.0 tag</a>, the registered command list for the release you are running</li>
  <li><a href="https://github.com/apache/parquet-java/blob/apache-parquet-1.12.3/pom.xml">The 1.12.3 root <code class="language-plaintext highlighter-rouge">pom.xml</code></a>, the release where the <code class="language-plaintext highlighter-rouge">parquet-tools</code> module disappears</li>
  <li><a href="https://repo1.maven.org/maven2/org/apache/parquet/parquet-cli/"><code class="language-plaintext highlighter-rouge">parquet-cli</code> on Maven Central</a> for the runtime jar</li>
  <li><a href="https://parquet.apache.org/docs/file-format/">Parquet file format specification</a> for row groups, column chunks, pages and statistics</li>
</ul>]]></content><author><name>Ranga Reddy</name></author><category term="Tools" /><category term="Tools" /><category term="Parquet" /><summary type="html"><![CDATA[parquet-cli is the maintained command-line tool for Parquet, and it does more than parquet-tools ever did. Here is the command-for-command migration, every transcript captured from a real run, plus what to know about rewrite.]]></summary></entry><entry><title type="html">Spark Submit Command Formatter tool</title><link href="https://rangareddy.github.io/SparkSubmitFormatter/" rel="alternate" type="text/html" title="Spark Submit Command Formatter tool" /><published>2023-01-05T11:40:00+05:30</published><updated>2023-01-05T11:40:00+05:30</updated><id>https://rangareddy.github.io/SparkSubmitFormatter</id><content type="html" xml:base="https://rangareddy.github.io/SparkSubmitFormatter/"><![CDATA[<ul id="markdown-toc">
  <li><a href="#spark-submit-command-formatterminifier" id="markdown-toc-spark-submit-command-formatterminifier">Spark Submit Command Formatter/Minifier</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
</ul>

<blockquote>
  <p><strong>TL;DR</strong></p>

  <ul>
    <li>Paste a <code class="language-plaintext highlighter-rouge">spark-submit</code> command written on one line and get it back split across lines with a trailing backslash per option, or minified back to one line.</li>
    <li>Each Spark option is resolved to the configuration property it actually sets, so <code class="language-plaintext highlighter-rouge">--num-executors</code> shows as <code class="language-plaintext highlighter-rouge">spark.executor.instances</code> and you can compare a command against <code class="language-plaintext highlighter-rouge">spark-defaults.conf</code> directly.</li>
    <li>Your application’s own arguments are kept separate from Spark’s, so a job that takes <code class="language-plaintext highlighter-rouge">--input</code> and <code class="language-plaintext highlighter-rouge">--output</code> no longer has them mistaken for Spark options.</li>
    <li>Quoted values survive the round trip intact, including an <code class="language-plaintext highlighter-rouge">extraJavaOptions</code> string containing spaces.</li>
  </ul>
</blockquote>

<h2 id="spark-submit-command-formatterminifier">Spark Submit Command Formatter/Minifier</h2>

<p>Format a <code class="language-plaintext highlighter-rouge">spark-submit</code> command across lines for review, or minify it back onto
one line for a scheduler that wants a single string. Both directions preserve the
command exactly.</p>

<p>The useful part is the breakdown underneath. A long <code class="language-plaintext highlighter-rouge">spark-submit</code> line hides
duplicated or contradictory settings, and the same setting can arrive as either a
flag or a <code class="language-plaintext highlighter-rouge">--conf</code>, which makes them hard to compare by eye. The parameter table
resolves every option to its configuration property, so <code class="language-plaintext highlighter-rouge">--executor-memory 18g</code>
and <code class="language-plaintext highlighter-rouge">--conf spark.executor.memory=18g</code> line up as the same row.</p>

<p>The option-to-property mapping follows the <code class="language-plaintext highlighter-rouge">OptionAssigner</code> list in
<code class="language-plaintext highlighter-rouge">SparkSubmit.scala</code> and the flag names in <code class="language-plaintext highlighter-rouge">SparkSubmitOptionParser.java</code> at the
Spark <code class="language-plaintext highlighter-rouge">v4.2.0</code> tag, so <code class="language-plaintext highlighter-rouge">--principal</code> and <code class="language-plaintext highlighter-rouge">--keytab</code> resolve to
<code class="language-plaintext highlighter-rouge">spark.kerberos.principal</code> and <code class="language-plaintext highlighter-rouge">spark.kerberos.keytab</code> rather than the pre-Spark-3
<code class="language-plaintext highlighter-rouge">spark.yarn.*</code> names.</p>

<div class="tool-widget">
    <style>
      #spark_submit_config_txt {
        resize: vertical;
        width: 100%;
        font-family: var(--font-mono, monospace);
        font-size: 0.86rem;
      }
      #spark_submit_cmd_text {
        white-space: pre-wrap;
        word-break: break-word;
        margin: 0;
        font-family: var(--font-mono, monospace);
        font-size: 0.86rem;
        line-height: 1.6;
      }
    </style>
    <script type="text/javascript">
      $(document).ready(function () {
        'use strict';

        // --------------------------------------------------------------------
        // spark-submit option table.
        // Flag -> equivalent configuration property, taken from the
        // OptionAssigner list in SparkSubmit.scala and the flag names in
        // SparkSubmitOptionParser.java at the v4.2.0 tag. Only flags
        // spark-submit actually accepts appear here; anything else is emitted
        // as --conf, which is what spark-submit expects.
        // --------------------------------------------------------------------
        var SPARK_OPTIONS = {
          'master': 'spark.master',
          'remote': 'spark.remote',
          'deploy-mode': 'spark.submit.deployMode',
          'name': 'spark.app.name',
          'jars': 'spark.jars',
          'packages': 'spark.jars.packages',
          'exclude-packages': 'spark.jars.excludes',
          'repositories': 'spark.jars.repositories',
          'py-files': 'spark.submit.pyFiles',
          'files': 'spark.files',
          'archives': 'spark.archives',
          'driver-memory': 'spark.driver.memory',
          'driver-cores': 'spark.driver.cores',
          'driver-java-options': 'spark.driver.extraJavaOptions',
          'driver-class-path': 'spark.driver.extraClassPath',
          'driver-library-path': 'spark.driver.extraLibraryPath',
          'executor-memory': 'spark.executor.memory',
          'executor-cores': 'spark.executor.cores',
          'num-executors': 'spark.executor.instances',
          'total-executor-cores': 'spark.cores.max',
          'principal': 'spark.kerberos.principal',
          'keytab': 'spark.kerberos.keytab',
          'queue': 'spark.yarn.queue',
          // Accepted by spark-submit but with no configuration equivalent.
          'proxy-user': null,
          'properties-file': null
        };

        // Flags that take no value.
        var SPARK_SWITCHES = ['verbose', 'supervise', 'version', 'help', 'load-spark-defaults'];

        var INDENT = ' '.repeat(2);
        var lastCommand = '';
        var parameterTable = null;
        var appArgsTable = null;

        // --------------------------------------------------------------------
        // Tokenizer: splits on whitespace but keeps quoted values intact, so a
        // value like "-XX:+UseG1GC -Dfoo=bar" survives as one token. Also joins
        // backslash-continued lines first, which is how these commands are
        // usually pasted.
        // --------------------------------------------------------------------
        function tokenize(text) {
          var src = String(text).replace(/\\[ \t]*\r?\n/g, ' ');
          var tokens = [];
          var current = '';
          var quote = null;
          var started = false;
          for (var i = 0; i < src.length; i++) {
            var ch = src.charAt(i);
            if (quote !== null) {
              if (ch === quote) { quote = null; } else { current += ch; }
              continue;
            }
            if (ch === '"' || ch === "'") { quote = ch; started = true; continue; }
            if (/\s/.test(ch)) {
              if (started) { tokens.push(current); current = ''; started = false; }
              continue;
            }
            current += ch;
            started = true;
          }
          if (started) { tokens.push(current); }
          return tokens;
        }

        // --------------------------------------------------------------------
        // Parser. spark-submit's grammar is: launcher, options, primary
        // resource, then application arguments. Everything after the resource
        // belongs to the application and is passed through untouched, which is
        // what keeps an app's own --input/--output flags out of the Spark
        // parameter table.
        // --------------------------------------------------------------------
        function parseCommand(tokens) {
          var parsed = {
            launcher: 'spark-submit',
            options: [],
            className: null,
            resource: null,
            appArgs: []
          };
          var i = 0;

          if (tokens.length > 0 && tokens[0].charAt(0) !== '-') {
            parsed.launcher = tokens[0] === 'org.apache.spark.deploy.SparkSubmit' ? 'spark-submit' : tokens[0];
            i = 1;
          }

          for (; i < tokens.length; i++) {
            var token = tokens[i];

            if (parsed.resource !== null) { parsed.appArgs.push(token); continue; }
            if (token.charAt(0) !== '-') { parsed.resource = token; continue; }

            var name = token.replace(/^--?/, '');

            if (name === 'conf') {
              var pair = tokens[++i] || '';
              var eq = pair.indexOf('=');
              parsed.options.push({
                flag: null,
                name: eq === -1 ? pair : pair.substring(0, eq),
                value: eq === -1 ? '' : pair.substring(eq + 1)
              });
            } else if (SPARK_SWITCHES.indexOf(name) !== -1) {
              parsed.options.push({ flag: name, name: name, value: null });
            } else if (name === 'class') {
              parsed.className = tokens[++i] || '';
            } else {
              // Known flag, or one we do not recognise. Either way it keeps its
              // value and its position rather than being dropped.
              parsed.options.push({ flag: name, name: name, value: tokens[++i] || '' });
            }
          }
          return parsed;
        }

        // Re-quote only values that need it, so the output can be pasted back.
        function quoteIfNeeded(value) {
          if (value === null || value === '') { return value === '' ? '""' : ''; }
          return /[\s"'$*?&|<>()]/.test(value) ? '"' + value.replace(/"/g, '\\"') + '"' : value;
        }

        function renderCommand(parsed, mode) {
          var parts = [parsed.launcher];
          parsed.options.forEach(function (option) {
            if (option.flag === null) {
              parts.push('--conf ' + option.name + '=' + quoteIfNeeded(option.value));
            } else if (option.value === null) {
              parts.push('--' + option.flag);
            } else {
              parts.push('--' + option.flag + ' ' + quoteIfNeeded(option.value));
            }
          });
          if (parsed.className) { parts.push('--class ' + parsed.className); }

          // The application resource and its arguments belong to the program,
          // not to Spark, so they stay together on the final line.
          var tail = [];
          if (parsed.resource) { tail.push(parsed.resource); }
          parsed.appArgs.forEach(function (arg) { tail.push(quoteIfNeeded(arg)); });
          if (tail.length > 0) { parts.push(tail.join(' ')); }

          return mode === 'minify' ? parts.join(' ') : parts.join(' \\\n' + INDENT);
        }

        // Rows for the parameter table: show the configuration property for a
        // flag that has one, so a reader can compare a command against
        // spark-defaults.conf.
        function toParameterRows(parsed) {
          return parsed.options.map(function (option) {
            var key = option.name;
            var source = 'conf';
            if (option.flag !== null) {
              source = '--' + option.flag;
              if (Object.prototype.hasOwnProperty.call(SPARK_OPTIONS, option.flag) && SPARK_OPTIONS[option.flag]) {
                key = SPARK_OPTIONS[option.flag];
              }
            }
            return { name: key, value: option.value === null ? '(flag)' : option.value, source: source };
          });
        }

        function destroyTables() {
          if (parameterTable) { parameterTable.destroy(); parameterTable = null; }
          if (appArgsTable) { appArgsTable.destroy(); appArgsTable = null; }
          $('#spark_submit_cmd_parameter_table tbody').empty();
          $('#spark_submit_cmd_line_parameter_table tbody').empty();
        }

        var TABLE_OPTIONS = {
          responsive: true, paging: true, searching: true, ordering: true, info: false
        };

        function showResult(parsed, mode) {
          $('#spark_submit_cmd_format_container').show();
          $('#spark_submit_cmd_parameter_container').toggle(parsed.options.length > 0);
          $('#spark_submit_cmd_add_parameter_container').toggle(parsed.appArgs.length > 0);
        }

        function hideResult() {
          $('#spark_submit_cmd_format_container').hide();
          $('#spark_submit_cmd_parameter_container').hide();
          $('#spark_submit_cmd_add_parameter_container').hide();
        }

        function build(mode) {
          var raw = $('#spark_submit_config_txt').val();
          if (!raw || !raw.trim()) {
            hideResult();
            $('#spark_submit_config_txt').trigger('focus');
            return;
          }

          destroyTables();

          var parsed = parseCommand(tokenize(raw));
          lastCommand = renderCommand(parsed, mode);

          // textContent, not html(): a pasted command is untrusted input and
          // must never be interpreted as markup.
          document.getElementById('spark_submit_cmd_text').textContent = lastCommand;

          parameterTable = $('#spark_submit_cmd_parameter_table').DataTable($.extend({
            data: toParameterRows(parsed),
            columns: [{ data: 'name' }, { data: 'value' }, { data: 'source' }]
          }, TABLE_OPTIONS));

          if (parsed.appArgs.length > 0) {
            // Order matters for application arguments, so this table keeps the
            // sequence the command used rather than sorting it.
            appArgsTable = $('#spark_submit_cmd_line_parameter_table').DataTable($.extend({}, TABLE_OPTIONS, {
              data: parsed.appArgs.map(function (arg, index) { return { position: index + 1, value: arg }; }),
              columns: [{ data: 'position' }, { data: 'value' }],
              ordering: false
            }));
          }

          showResult(parsed, mode);
        }

        function copyCommand() {
          if (!lastCommand) { return; }
          if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(lastCommand).then(function () {
              window.alert('spark-submit command copied!');
            }, function () {
              window.alert('Select the command and press Ctrl+C to copy.');
            });
          } else {
            window.alert('Select the command and press Ctrl+C to copy.');
          }
        }

        var SAMPLE = [
          'spark-submit',
          '--class com.rangareddy.pipeline.TripsIngest',
          '--master yarn',
          '--deploy-mode cluster',
          '--num-executors 12',
          '--executor-cores 5',
          '--executor-memory 18g',
          '--driver-memory 4g',
          '--conf spark.sql.shuffle.partitions=480',
          '--conf spark.executor.extraJavaOptions="-XX:+UseG1GC -Dlog4j.configurationFile=log4j2.properties"',
          's3a://lakehouse-prod/artifacts/trips-ingest-2.4.1.jar',
          '--input s3a://lakehouse-prod/raw/trips/',
          '--table s3a://lakehouse-prod/warehouse/trips/'
        ].join(' \\\n' + INDENT);

        $('#sample_spark_submit_config').on('click', function () {
          $('#spark_submit_config_txt').val(SAMPLE);
          hideResult();
        });
        $('#format_spark_submit_config').on('click', function (e) { e.preventDefault(); build('format'); });
        $('#minify_spark_submit_config').on('click', function (e) { e.preventDefault(); build('minify'); });
        $('#reset_spark_submit_config').on('click', function (e) {
          e.preventDefault();
          $('#spark_submit_config_txt').val('');
          destroyTables();
          hideResult();
        });
        $('#copy-spark-submit').on('click', function (e) { e.preventDefault(); copyCommand(); });

        hideResult();
      });
    </script>
    <div class="container-fluid">
      <div class="row" id="spark_submit_cmd_container" style="margin-top: 10px;">
        <div class="col-md-12">
          <div class="card">
            <div class="card-header">
              <span style="float: left;">
                <h4>Spark Submit Command</h4>
              </span>
              <span style="float: right;">
                <button type="button" id="sample_spark_submit_config" class="btn btn-success">Load Sample Command</button>
              </span>
            </div>
            <div class="card-body">
              <textarea id="spark_submit_config_txt" placeholder="Enter or Paste the Spark Submit command" rows="7"></textarea>
            </div>
            <div class="card-footer">
              <span style="margin-right: 12px;">
                <button type="button" id="format_spark_submit_config" class="btn btn-primary">Format</button>
              </span>
              <span style="margin-right: 12px;">
                <button type="button" id="minify_spark_submit_config" class="btn btn-info">Minify</button>
              </span>
              <span style="margin-right: 12px;">
                <button type="button" id="reset_spark_submit_config" class="btn btn-warning">Reset</button>
              </span>
            </div>
          </div>
        </div>
      </div>
      <!-- row -->
      <div class="row" id="spark_submit_cmd_format_container" style="margin-top: 10px;">
        <div class="col-md-12">
          <div class="card">
            <h4 class="card-header">Formatted Spark Submit Command</h4>
            <div class="card-body">
              <pre class="card-text" id="spark_submit_cmd_text"></pre>
            </div>
            <div class="card-footer">
              <button type="button" id="copy-spark-submit" class="btn btn-primary">Copy Spark Submit Command</button>
            </div>
          </div>
        </div>
      </div>
      <!-- row -->
      <div class="row" id="spark_submit_cmd_parameter_container" style="margin-top: 10px;">
        <div class="col-md-12">
          <div class="card">
            <h4 class="card-header">Spark Configuration Parameters</h4>
            <div class="card-body">
              <table id="spark_submit_cmd_parameter_table" class="table table-striped table-responsive" style="width:100%">
                <thead>
                    <tr>
                        <th>Configuration property</th>
                        <th>Value</th>
                        <th>Set by</th>
                    </tr>
                </thead>
              </table>
            </div>
          </div>
        </div>
      </div>
      <!-- row -->
      <div class="row" id="spark_submit_cmd_add_parameter_container" style="margin-top: 10px;">
        <div class="col-md-12">
          <div class="card">
            <h4 class="card-header">Application Arguments</h4>
            <div class="card-body">
              <table id="spark_submit_cmd_line_parameter_table" class="table table-striped table-responsive" style="width:100%">
                <thead>
                    <tr>
                        <th>#</th>
                        <th>Argument passed to your application</th>
                    </tr>
                </thead>
              </table>
            </div>
          </div>
        </div>
      </div>
      <!-- row -->
    </div>
    <!-- container-fluid -->
</div>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://spark.apache.org/docs/latest/submitting-applications.html">Submitting applications</a> for the <code class="language-plaintext highlighter-rouge">spark-submit</code> argument reference</li>
  <li><a href="https://github.com/apache/spark/blob/v4.2.0/core/src/main/scala/org/apache/spark/deploy/SparkSubmit.scala"><code class="language-plaintext highlighter-rouge">SparkSubmit.scala</code> at v4.2.0</a>, the <code class="language-plaintext highlighter-rouge">OptionAssigner</code> list this tool’s flag-to-property mapping follows</li>
  <li><a href="https://spark.apache.org/docs/latest/configuration.html">Spark configuration reference</a> for what each <code class="language-plaintext highlighter-rouge">--conf</code> key means and its precedence</li>
  <li><a href="/SparkConfigurationGenerator/">Spark Configuration Generator</a> to work out the executor sizes before formatting the command</li>
  <li><a href="/SparkTroubleshootingPlaybook/">Spark JVM troubleshooting playbook</a> for the <code class="language-plaintext highlighter-rouge">extraJavaOptions</code> gotchas a long command tends to hide</li>
</ul>]]></content><author><name>Ranga Reddy</name></author><category term="Spark" /><category term="Spark" /><category term="Utilities" /><summary type="html"><![CDATA[Paste a spark-submit command and get it back formatted across lines or minified onto one line, with every Spark option resolved to the configuration property it sets and your application's own arguments kept separate.]]></summary></entry><entry><title type="html">Spark Configuration Generator tool</title><link href="https://rangareddy.github.io/SparkConfigurationGenerator/" rel="alternate" type="text/html" title="Spark Configuration Generator tool" /><published>2021-12-29T00:00:00+05:30</published><updated>2021-12-29T00:00:00+05:30</updated><id>https://rangareddy.github.io/SparkConfigurationGenerator</id><content type="html" xml:base="https://rangareddy.github.io/SparkConfigurationGenerator/"><![CDATA[<ul id="markdown-toc">
  <li><a href="#spark-configuration-generator" id="markdown-toc-spark-configuration-generator">Spark Configuration Generator</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
</ul>

<blockquote>
  <p><strong>TL;DR</strong></p>

  <ul>
    <li>Enter total nodes, cores per node and memory per node; the tool returns executor count, executor cores, executor memory and memory overhead.</li>
    <li>It offers three shapes: Tiny (one core per executor), Fat (one executor per node using every core) and Balanced (five cores per executor).</li>
    <li>The Balanced calculation reserves one core per node for the OS and node manager, subtracts one executor for the YARN ApplicationMaster, and sets <code class="language-plaintext highlighter-rouge">spark.executor.memoryOverhead</code> to 10% of the per-executor memory.</li>
    <li>Five cores per executor is a convention, not a derived number. Treat every output as a starting point and confirm it against your own stage metrics in the Spark UI.</li>
  </ul>
</blockquote>

<h2 id="spark-configuration-generator">Spark Configuration Generator</h2>

<p><strong>Spark Configuration Generator</strong> tool will generate the <strong>spark configuration</strong> based on <strong>hardware configuration</strong>.</p>

<div class="tool-widget">
    <script type="text/javascript">
      $(document).ready(function() {
        let br_delimeter = " \\ <br> ";
        let space_delimeter = "&nbsp;&nbsp;&nbsp;&nbsp;";
        let delimeter = br_delimeter + space_delimeter;
        var sparkSubmitCommand = "";
        var spark_configuration_table;

        function hide_configuration() {
          $("#spark_configuration_id").hide();
          $("#spark_submit_container_id").hide();
        }

        hide_configuration();
        $("#reset-spark-configuration").click(function() {
          $("#totalNodes").val("10");
          $("#coresPerNode").val("16");
          $("#memoryPerNode").val("64");
          hide_configuration();
          spark_configuration_table.destroy();
        });

        $("#generate-spark-configuration").click(function() {
          var sparkConfList = []
          sparkSubmitCommand = ""
          var totalNodes = $("#totalNodes").val().trim();
          var coresPerNode = $("#coresPerNode").val().trim();
          var memoryPerNode = $("#memoryPerNode").val().trim();
          if (totalNodes < 1) {
            alert('Total Nodes in Cluster value must be greater than 1');
            $("#totalNodes").focus();
            hide_configuration();
            return false
          }
          if (coresPerNode < 1) {
            alert('vCPU/Cores per Node value must be greater than 1');
            $("#coresPerNode").focus();
            hide_configuration();
            return false
          }
          if (memoryPerNode < 1) {
            alert('Memory per Node (GB) value must be greater than 1');
            $("#memoryPerNode").focus();
            hide_configuration();
            return false
          }
          var totalCores = totalNodes * coresPerNode;
          var totalMemory = totalNodes * memoryPerNode; {
            var executorCores = 1;
            var totalExecutors = totalCores;
            var executorMemory = Math.floor(totalMemory / totalCores);
            var tinyApproch = {
              "name": "Tiny",
              "executor-cores": executorCores,
              "num-executors": totalExecutors,
              "executor-memory": executorMemory,
              "executor-memoryOverhead": 0
            }
            sparkConfList.push(tinyApproch)
          } {
            var executorCores = coresPerNode;
            var totalExecutors = totalNodes;
            var executorMemory = Math.floor(totalMemory / totalNodes);
            var fatApproch = {
              "name": "Fat",
              "executor-cores": executorCores,
              "num-executors": totalExecutors,
              "executor-memory": executorMemory,
              "executor-memoryOverhead": 0
            }
            sparkConfList.push(fatApproch)
          } {
            var executorCores = 5;
            var memoryOverHeadValue = 0.10;
            var hadoopOSCores = 1;
            var amNodes = 1;
            var totalCores = (totalNodes * (coresPerNode - hadoopOSCores));
            var totalExecutorsWithAM = (totalCores / executorCores);
            var totalExecutors = totalExecutorsWithAM - amNodes;
            var executorsPerNode = Math.floor(totalExecutorsWithAM / totalNodes);
            var executorMemoryWithOverhead = Math.round(memoryPerNode / executorsPerNode);
            //var memoryOverHead = Math.max(384, (Math.round(executorMemoryWithOverhead * memoryOverHeadValue) * 1024));
            var memoryOverHead = Math.round(executorMemoryWithOverhead * memoryOverHeadValue);
            var executorMemory = Math.round(executorMemoryWithOverhead - memoryOverHead);
            var balancedApproch = {
              "name": "Balanced",
              "executor-cores": executorCores,
              "num-executors": totalExecutors,
              "executor-memory": executorMemory,
              "executor-memoryOverhead": memoryOverHead
            }
            sparkConfList.push(balancedApproch)
            sparkSubmitCommand = "spark-shell" + delimeter + "--conf spark.master=yarn" + delimeter + "--conf spark.submit.deployMode=client" + delimeter + "--conf spark.executor.cores=" + balancedApproch["executor-cores"] + delimeter + "--conf spark.executor.instances=" + balancedApproch["num-executors"] + delimeter + "--conf spark.executor.memory=" + balancedApproch["executor-memory"] + "g" + delimeter + "--conf spark.executor.memoryOverhead=" + memoryOverHead + "g";
            $("#spark_submit_id").html(sparkSubmitCommand);
            $("#spark_submit_hide_id").html(sparkSubmitCommand.replaceAll(delimeter, " "));
            $("#spark_submit_container_id").show();
          }
          $("#spark_configuration_id").show();

          spark_configuration_table = $('#spark_configuration_table').DataTable( {
              data: sparkConfList,
              createdRow: function (row, data, index) {

              },
              columns: [
                { "data": "name",
                  render: function (data, type, row, meta) {
                        return type === 'display'
                            ? ('<span>'+ data + '</span> <p><progress value="' + row["executor-cores"] + '" max="'+ coresPerNode +'"></progress> </p>') : data;
                  }
                },
                { "data": "executor-cores"},
                { "data": "num-executors" },
                { "data": "executor-memory" },
                { "data": "executor-memoryOverhead"}
              ],
              responsive: true,
              paging: false,
              searching: false,
              ordering: false,
              info: false
          } );
        });

        $("#copy-spark-shell").click(function(e) {
          e.preventDefault();
          copy_text_to_clipboard('spark_submit_id', 'spark-shell command copied!');
        });
      });
    </script>
    <div class="container-fluid">
      <div class="row" id="hardware-config-row" style="margin-top: 10px;">
        <div class="col-md-12">
          <div class="card">
            <h5 class="card-header">Hardware Configuration</h5>
            <div class="card-body">
              <table id="HardwareConfigurationTable" class="table table-bordered" style="width: 100%;">
                <thead class="thead-light">
                  <tr>
                    <th>Name</th>
                    <th>Value</th>
                  </tr>
                </thead>
                <tbody>
                  <tr>
                    <td>
                      <label for="totalNodes">Total Nodes in Cluster</label>
                    </td>
                    <td>
                      <input class="form-control" id="totalNodes" value="10" placeholder="Total Nodes in Cluster" />
                    </td>
                  </tr>
                  <tr>
                    <td>
                      <label for="coresPerNode">vCPU/Cores per Node</label>
                    </td>
                    <td>
                      <input class="form-control" id="coresPerNode" value="16" placeholder="vCPU/Cores per Node" />
                    </td>
                  </tr>
                  <tr>
                    <td>
                      <label for="memoryPerNode">Memory per Node (GB)</label>
                    </td>
                    <td>
                      <input class="form-control" id="memoryPerNode" value="64" placeholder="Memory per Node (GB)" />
                    </td>
                  </tr>
                </tbody>
              </table>
            </div>
            <div class="card-footer" id="spark_configuration_button">
              <span style="margin-right: 10px;">
                <button type="button" id="generate-spark-configuration" class="btn btn-primary">Generate</button>
              </span>
              <span style="margin-right: 10px;">
                <button type="button" id="reset-spark-configuration" class="btn btn-warning">Reset</button>
              </span>
              <!-- <div class="progress"><div style="width: 60%;" aria-valuemax="100" aria-valuemin="0" aria-valuenow="60" role="progressbar" class="red progress-bar"><span>60%</span></div></div> -->
            </div>
          </div>
        </div>
        <!--<div class="col-md-1"></div>-->
      </div>
      <!-- row -->
      <div class="row" id="spark_configuration_id" style="margin-top: 10px;">
        <!--<div class="col-md-1"></div>-->
        <div class="col-md-12">
          <div class="card">
            <h5 class="card-header">Spark Configuration Approches</h5>
            <div class="card-body">
              <table id="spark_configuration_table" class="table table-striped table-responsive" style="width:100%">
                <thead>
                    <tr>
                        <th>Executor Approch Type</th>
                        <th>Executor Cores <br /> (spark.executor.cores)</th>
                        <th>Number of Executors <br /> (spark.executor.instances)</th>
                        <th>Executor Memory in GB <br /> (spark.executor.memory)</th>
                        <th>Executor Memory Overhead in GB<br /> (spark.executor.memoryOverhead)</th>
                    </tr>
                </thead>
              </table>
            </div>
            <!--<div class="card-footer"></div>-->
          </div>
        </div>
        <!--<div class="col-md-1"></div>-->
      </div>
      <!-- row -->
      <div class="row" id="spark_submit_container_id" style="margin-top: 10px;">
        <!--<div class="col-md-1"></div>-->
        <div class="col-md-12">
          <div class="card">
            <h5 class="card-header">Balanced Approach Spark Shell Command</h5>
            <div class="card-body">
              <p class="card-text" id="spark_submit_id" style="background: lightgreen;"></p>
            </div>
            <div class="card-footer">
              <p class="card-text" id="spark_submit_hide_id" style="display:none;"></p>
              <button type="button" id="copy-spark-shell" class="btn btn-info">Copy Shell Command</button>
            </div>
          </div>
        </div>
        <!--<div class="col-md-1"></div>-->
      </div>
      <!-- row -->
    </div>
    <!-- container-fluid -->
</div>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://spark.apache.org/docs/latest/configuration.html">Spark configuration reference</a> for the executor memory and core properties this tool emits</li>
  <li><a href="https://spark.apache.org/docs/latest/running-on-yarn.html">Running Spark on YARN</a> for the ApplicationMaster overhead and container sizing rules</li>
  <li><a href="https://spark.apache.org/docs/latest/tuning.html">Spark tuning guide</a> for memory management and the data-locality trade-offs behind the defaults</li>
  <li><a href="/SparkTroubleshootingPlaybook/">Spark JVM troubleshooting playbook</a> for the JVM flags that go alongside these sizes</li>
</ul>]]></content><author><name>Ranga Reddy</name></author><category term="Spark" /><category term="Spark" /><category term="Utilities" /><category term="Generator" /><summary type="html"><![CDATA[Enter the node count, cores per node and memory per node for your cluster and get back executor count, executor cores, executor memory and memory overhead, ready to paste into spark-shell.]]></summary></entry><entry><title type="html">Kerberos Setup in Linux</title><link href="https://rangareddy.github.io/KerberoSetup/" rel="alternate" type="text/html" title="Kerberos Setup in Linux" /><published>2021-12-22T00:00:00+05:30</published><updated>2021-12-22T00:00:00+05:30</updated><id>https://rangareddy.github.io/KerberoSetup</id><content type="html" xml:base="https://rangareddy.github.io/KerberoSetup/"><![CDATA[<ul id="markdown-toc">
  <li><a href="#kerberos" id="markdown-toc-kerberos">Kerberos</a></li>
  <li><a href="#install--configure-kerberos-server--client-in-linux" id="markdown-toc-install--configure-kerberos-server--client-in-linux">Install &amp; Configure Kerberos Server &amp; Client in Linux</a>    <ul>
      <li><a href="#step-1-install-kerberos-client" id="markdown-toc-step-1-install-kerberos-client">Step 1: Install Kerberos Client</a></li>
      <li><a href="#step2-install-kerberos-server" id="markdown-toc-step2-install-kerberos-server">Step2: Install Kerberos Server</a></li>
      <li><a href="#step-3-configure-kerberos" id="markdown-toc-step-3-configure-kerberos">Step 3: Configure Kerberos</a>        <ul>
          <li><a href="#31-kdcconf-changes" id="markdown-toc-31-kdcconf-changes">3.1 <code class="language-plaintext highlighter-rouge">kdc.conf</code> changes</a></li>
          <li><a href="#32-krb5conf-changes" id="markdown-toc-32-krb5conf-changes">3.2 <code class="language-plaintext highlighter-rouge">krb5.conf</code> changes</a></li>
        </ul>
      </li>
      <li><a href="#step-4-create-kerberos-kdc-database" id="markdown-toc-step-4-create-kerberos-kdc-database">Step 4: Create Kerberos KDC Database</a></li>
      <li><a href="#step-5-acl-changes" id="markdown-toc-step-5-acl-changes">Step 5: ACL changes</a></li>
      <li><a href="#step-6-add-admin-for-kdc" id="markdown-toc-step-6-add-admin-for-kdc">Step 6: Add Admin for KDC</a></li>
      <li><a href="#step-7-restart-the-kerberos-admin--kdc-server" id="markdown-toc-step-7-restart-the-kerberos-admin--kdc-server">Step 7: Restart the Kerberos Admin &amp; KDC Server</a></li>
      <li><a href="#testing-1-test-kerberos-from-server" id="markdown-toc-testing-1-test-kerberos-from-server">Testing 1: Test Kerberos from Server</a></li>
      <li><a href="#testing-2--test-kerberos-from-client-machine" id="markdown-toc-testing-2--test-kerberos-from-client-machine">Testing 2 : Test Kerberos from Client machine</a>        <ul>
          <li><a href="#1-create-a-non-admin-user" id="markdown-toc-1-create-a-non-admin-user">1. Create a non-admin user</a></li>
          <li><a href="#2-create-a-keytab-file-for-the-user" id="markdown-toc-2-create-a-keytab-file-for-the-user">2. Create a keytab file for the user</a></li>
          <li><a href="#3-test-kerberos-from-client-machine" id="markdown-toc-3-test-kerberos-from-client-machine">3. Test Kerberos from client machine</a></li>
        </ul>
      </li>
    </ul>
  </li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
</ul>

<blockquote>
  <p><strong>TL;DR</strong></p>

  <ul>
    <li>Kerberos is the authentication standard across the Hadoop ecosystem, so setting up a KDC is a prerequisite for securing HDFS, YARN, Hive and Spark.</li>
    <li>Install <code class="language-plaintext highlighter-rouge">krb5-workstation</code> on every node and <code class="language-plaintext highlighter-rouge">krb5-server</code> on one, then configure <code class="language-plaintext highlighter-rouge">kdc.conf</code> (KDC behaviour) and <code class="language-plaintext highlighter-rouge">krb5.conf</code> (client behaviour) to agree on the realm.</li>
    <li><code class="language-plaintext highlighter-rouge">kdb5_util create -s</code> builds the principal database; the ACL file decides who may administer it; <code class="language-plaintext highlighter-rouge">kadmin.local</code> creates the first admin.</li>
    <li>Services authenticate with keytabs rather than passwords, which is why the last step is exporting one and testing <code class="language-plaintext highlighter-rouge">kinit -kt</code> from a client machine.</li>
  </ul>
</blockquote>

<h2 id="kerberos">Kerberos</h2>

<p>Kerberos is a secure authentication method developed by MIT that allows two services located in a non-secured network to authenticate themselves in a secure way. Kerberos, which is based on a <strong>ticketing system</strong>, serves as both <strong>Authentication Server</strong> and as <strong>Ticket Granting Server (TGS)</strong>.</p>

<p>Kerberos has become the standard authentication method within the Hadoop ecosystem. For this reason, most Big Data technologies have adopted it as their authentication method.</p>

<h2 id="install--configure-kerberos-server--client-in-linux">Install &amp; Configure Kerberos Server &amp; Client in Linux</h2>

<p>Let’s see how we can install, setup and configure Kerberos in a Cluster.</p>

<p>We will install Kerberos Server in one machine and Kerberos client in rest of the machines.</p>

<h3 id="step-1-install-kerberos-client">Step 1: Install Kerberos Client</h3>

<p>We need to install the Kerberos client on every node in the cluster.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf <span class="nb">install </span>krb5-workstation krb5-libs
</code></pre></div></div>

<p>On RHEL 7 and older, <code class="language-plaintext highlighter-rouge">yum</code> replaces <code class="language-plaintext highlighter-rouge">dnf</code>. On Debian and Ubuntu the client
package is <code class="language-plaintext highlighter-rouge">krb5-user</code> instead.</p>

<h3 id="step2-install-kerberos-server">Step2: Install Kerberos Server</h3>

<p>The Kerberos server usually goes on the master node, though that is a
convention rather than a rule; any server in the cluster will do.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf <span class="nb">install </span>krb5-server
</code></pre></div></div>

<p>On Debian and Ubuntu the equivalents are <code class="language-plaintext highlighter-rouge">krb5-kdc</code> and <code class="language-plaintext highlighter-rouge">krb5-admin-server</code>, and
their configuration lives in a different directory, so follow the Ubuntu server
guide linked at the end for those paths.</p>

<h3 id="step-3-configure-kerberos">Step 3: Configure Kerberos</h3>

<p>The configuration lives in two files, one for the KDC and one for clients.</p>

<h4 id="31-kdcconf-changes">3.1 <code class="language-plaintext highlighter-rouge">kdc.conf</code> changes</h4>

<p>Login Kerberos Server Installed machine</p>

<p><code class="language-plaintext highlighter-rouge">$ vi /var/kerberos/krb5kdc/kdc.conf</code></p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>kdcdefaults]
 kdc_ports <span class="o">=</span> 88
 kdc_tcp_ports <span class="o">=</span> 88

<span class="o">[</span>realms]
 EXAMPLE.COM <span class="o">=</span> <span class="o">{</span>
  <span class="c">#master_key_type = aes256-cts</span>
  acl_file <span class="o">=</span> /var/kerberos/krb5kdc/kadm5.acl
  dict_file <span class="o">=</span> /usr/share/dict/words
  admin_keytab <span class="o">=</span> /var/kerberos/krb5kdc/kadm5.keytab
  supported_enctypes <span class="o">=</span> aes256-cts:normal aes128-cts:normal des3-hmac-sha1:normal arcfour-hmac:normal camellia256-cts:normal camellia128-cts:normal des-hmac-sha1:normal des-cbc-md5:normal des-cbc-crc:normal
 <span class="o">}</span>
 EXAMPLE.COM <span class="o">=</span> <span class="o">{</span>
   renew_lifetime <span class="o">=</span> 7d
 <span class="o">}</span>
</code></pre></div></div>

<p>In the above kdc.conf file we choosed realm is EXAMPLE.COM</p>

<h4 id="32-krb5conf-changes">3.2 <code class="language-plaintext highlighter-rouge">krb5.conf</code> changes</h4>

<p><code class="language-plaintext highlighter-rouge">$ vi /etc/krb5.conf</code></p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">{</span>
    <span class="o">[</span>logging]
        default <span class="o">=</span> FILE:/var/log/krb5libs.log
        kdc <span class="o">=</span> FILE:/var/log/krb5libs.log
        admin_server <span class="o">=</span> FILE:/var/log/kadmind.log

    <span class="o">[</span>libdefaults]
        default_realm <span class="o">=</span> EXAMPLE.COM
        dns_lookup_kdc <span class="o">=</span> <span class="nb">false
        </span>dns_lookup_realm <span class="o">=</span> <span class="nb">false
        </span>ticket_lifetime <span class="o">=</span> 24h
        renew_lifetime <span class="o">=</span> 7d
        forwardable <span class="o">=</span> <span class="nb">true
        </span>default_tgs_enctypes <span class="o">=</span> aes256-cts aes128-cts des3-hmac-sha1 des-hmac-sha1 des-cbc-crc
        default_tkt_enctypes <span class="o">=</span> aes256-cts aes128-cts des3-hmac-sha1 des-hmac-sha1 des-cbc-crc
        permitted_enctypes <span class="o">=</span> aes256-cts aes128-cts des3-hmac-sha1 des-hmac-sha1 des-cbc-crc
        udp_preference_limit <span class="o">=</span> 1
        kdc_timeout <span class="o">=</span> 3000
    <span class="o">[</span>realms]
        EXAMPLE.COM <span class="o">=</span> <span class="o">{</span>
            kdc <span class="o">=</span> node1.example.com
            admin_server <span class="o">=</span> node1.example.com
        <span class="o">}</span>
    <span class="o">[</span>domain_realm]
<span class="o">}</span>
</code></pre></div></div>

<h3 id="step-4-create-kerberos-kdc-database">Step 4: Create Kerberos KDC Database</h3>

<p>In this step , we will create a KDC – Key Distribution Centre database. This database is used by the Kerberos server. So it is a crucial point in our installation steps.</p>

<p><code class="language-plaintext highlighter-rouge">$ kdb5 util create -r EXAMPLE.COM -s</code></p>

<p>It will ask for setting up a Master Password . Follow as asked and note down the password. This password is needed for any KDC database related activities like restart or any DB changes later etc.</p>

<h3 id="step-5-acl-changes">Step 5: ACL changes</h3>

<p><code class="language-plaintext highlighter-rouge">$ vi /var/kerberos/krb5kdc/kadm5.acl</code></p>

<p>Modify with your Realm name. In our case as , it is –</p>

<p><code class="language-plaintext highlighter-rouge">*/admin@EXAMPLE.COM        *</code></p>

<h3 id="step-6-add-admin-for-kdc">Step 6: Add Admin for KDC</h3>

<p>Note this Step MUST BE Executed only in the KDC Server machine – NOT in any Kerberos client machines.</p>

<p><code class="language-plaintext highlighter-rouge">$ kadmin.local</code></p>

<p>This will bring you to kadmin.local prompt. In that prompt, use the highlighted command. Note you have to use your own Realm name.</p>

<p><code class="language-plaintext highlighter-rouge">kadmin.local: addprinc   root/admin@EXAMPLE.COM</code></p>

<p>To see list of all principals created –</p>

<p><code class="language-plaintext highlighter-rouge">kadmin.local : listprincs</code></p>

<h3 id="step-7-restart-the-kerberos-admin--kdc-server">Step 7: Restart the Kerberos Admin &amp; KDC Server</h3>

<p>Note these steps MUST be done in KDC Server machine.</p>

<p>Start both services and enable them so the KDC comes back after a reboot:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl <span class="nb">enable</span> <span class="nt">--now</span> krb5kdc
<span class="nb">sudo </span>systemctl <span class="nb">enable</span> <span class="nt">--now</span> kadmin
</code></pre></div></div>

<p>Confirm both came up before moving on:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>systemctl status krb5kdc kadmin
</code></pre></div></div>

<p>We are done with the Setup. We will test it from Kerberos as well Client servers.</p>

<h3 id="testing-1-test-kerberos-from-server">Testing 1: Test Kerberos from Server</h3>

<p>Test the Kerberos installation. Use the below command –</p>

<p>Check if any Ticket exists</p>

<p><code class="language-plaintext highlighter-rouge">$ klist</code></p>

<p>If no tickets exist in the cache, create a new one</p>

<p><code class="language-plaintext highlighter-rouge">$ kinit root/admin</code></p>

<p>Check again if you have any ticket</p>

<p><code class="language-plaintext highlighter-rouge">$ klist</code></p>

<p>Hopefully now you can see tickets listed here.</p>

<p>If you want to destroy any ticket, use</p>

<p><code class="language-plaintext highlighter-rouge">$ kdestroy</code></p>

<h3 id="testing-2--test-kerberos-from-client-machine">Testing 2 : Test Kerberos from Client machine</h3>

<p>In previous step, we tested Kerberos from Kerberos server itself.</p>

<p>In this step, we will test Kerberos from the client machine. This step is important because in most cases you will use the client machines as a user. And if the user tries to access any services in the network , it will need Kerberos authentication. So let’s try this .</p>

<h4 id="1-create-a-non-admin-user">1. Create a non-admin user</h4>

<p>So , we will use a non-admin user . Use below commands in KDC server to create a normal user (i.e. user with no admin access).</p>

<p><code class="language-plaintext highlighter-rouge">$ kadmin.local</code></p>

<p>In kadmin.local prompt use –</p>

<p><code class="language-plaintext highlighter-rouge">kadmin.local: addprinc rangareddy@EXAMPLE.COM</code></p>

<p>so we have created a normal user <em>rangareddy</em>.</p>

<h4 id="2-create-a-keytab-file-for-the-user">2. Create a keytab file for the user</h4>

<p>We will create a keytab file for the user rangareddy</p>

<p><code class="language-plaintext highlighter-rouge">$ kadmin.local</code></p>

<p>In kadmin.local prompt, use below</p>

<p><code class="language-plaintext highlighter-rouge">kadmin.local: xst -norandkey -k /tmp/rangareddy.keytab rangareddy@EXAMPLE.COM</code></p>

<p>It will create a keytab file rangareddy.keytab in /tmp directory for the rangareddy.</p>

<h4 id="3-test-kerberos-from-client-machine">3. Test Kerberos from client machine</h4>

<p>In previous step, we created the <code class="language-plaintext highlighter-rouge">rangareddy.keytab</code> file in KDC SERVER machine.</p>

<p>Copy the keytab file to the client machine.</p>

<p>Lets place it in <code class="language-plaintext highlighter-rouge">/root/rangareddy.keytab</code> in client machine.</p>

<p>Now in the client machine, open command prompt</p>

<p>Create a kerberos ticket</p>

<p><code class="language-plaintext highlighter-rouge">$ kinit -kt /root/rangareddy.keytab rangareddy@EXAMPLE.COM</code></p>

<p>Check if ticket created</p>

<p><code class="language-plaintext highlighter-rouge">$ klist</code></p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://web.mit.edu/kerberos/krb5-latest/doc/">MIT Kerberos documentation</a> for <code class="language-plaintext highlighter-rouge">kdc.conf</code>, <code class="language-plaintext highlighter-rouge">krb5.conf</code> and <code class="language-plaintext highlighter-rouge">kadmin</code> reference</li>
  <li><a href="https://web.mit.edu/kerberos/krb5-latest/doc/admin/index.html">Kerberos V5 System Administrator’s Guide</a> for realm and database administration</li>
  <li><a href="https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-common/SecureMode.html">Hadoop in secure mode</a> for how Hadoop services consume the keytabs created here</li>
  <li><a href="https://spark.apache.org/docs/latest/security.html">Spark security</a> for delegation tokens and <code class="language-plaintext highlighter-rouge">--principal</code> / <code class="language-plaintext highlighter-rouge">--keytab</code> on a Kerberized cluster</li>
  <li><a href="https://documentation.ubuntu.com/server/how-to/kerberos/">Ubuntu server Kerberos guide</a> for the Debian and Ubuntu package names and configuration paths</li>
</ul>]]></content><author><name>Ranga Reddy</name></author><category term="Linux" /><category term="Linux" /><category term="Kerberos" /><category term="Security" /><summary type="html"><![CDATA[Install and configure a Kerberos KDC and its clients on Linux end to end: kdc.conf, krb5.conf, the KDC database, ACLs, the admin principal, keytabs and a test from both server and client.]]></summary></entry></feed>