Wednesday, November 5, 2025
Spark Operator
Monday, October 20, 2025
Spark and K8s on AWS
- Create the secret with aws secretsmanager create-secret ...
- Create a policy with aws iam create-policy... This reference the secret's ARNs in step 1.
- Create a role with aws iam create-role... that allows a role to be assumed via STS.
- Attach the policy in step 2 with the role in step 3 with aws iam attach-role-policy...
- Create a SecretProviderClass with kubectl apply -f... that references the secrets created in step 1
- Associate your K8s Deployment with the SecretProviderClass using its volumes.
Wednesday, September 17, 2025
Configuring Polaris for Azure
- tenantId. You can find this by running az account show --query tenantId.
- multiTenantAppName. This is the Application (client) ID that was generated when the app was created. You can see it in Microsoft Entra ID -> App Registrations -> All Applications in the Azure portal or using the CLI: az ad app list, find your app with the name you created above and use its appId.
- consentUrl. I'm not entirely sure what this is but can be generated with APPID=$(az ad app list --display-name "MyMultiTenantApp" --query "[0].appId" -o tsv) && echo "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=$APPID&response_type=code&redirect_uri=http://localhost:3000/redirect&response_mode=query&scope=https://graph.microsoft.com/.default&state=12345"
Thursday, September 11, 2025
Configuring Polaris for GCP
Sunday, August 3, 2025
Iceberg Distributions
| write.distribution-mode | Number of files | Notes |
| "hash" | p | df.writeTo(tableName).append() |
| "hash", sorted DataFrame | p | ...TBLPROPERTIES ('sort-order' = 'partitionField ASC NULLS FIRST'... |
| "hash", sorted table | p | df.sort("partitionField").writeTo(tableName).append() |
| "hash", sorted table but only one value for partitionField | 1 | because p=1; assumes the size of the data to write is < write.spark.advisory-partition-size-bytes. Otherwise multiple files are written (Spark 3.5). |
| "none" | d * p | df.writeTo(tableName).append() |
| "none", sorted DataFrame | p | df.sort("partitionField").writeTo(tableName).append() |
| "none", sorted table | d * p | ...TBLPROPERTIES ('sort-order' = 'partitionField ASC NULLS FIRST'... |
| "none", sorted table but only one value for partitionField | d | because p=1 |
"Fanout writer is better in all cases. We were silly. The memory requirements were tiny IMHO. Without fanout, you need to presort within the task but that ends up being way more expensive (and memory intesive) IMHO. In the latest versions @Anton Okolnychyi removed the local sort requirements if fanout is enabled, so I would recommend fanout always be enabled and especially if you are using distribution mode is none."
Friday, March 28, 2025
Iceberg Tidbits
run rewrite firstexpire secondyou don't need to run remove_orphans unless something broke [Discord]
- rewrite_data_files puts all the data in as few a number of files as possible.
- expire_snapshots then deletes any files that are surplus.
To clarify, Remove Orphan Files is only for cleaning up files from failed writes or compactions that failed in a way that the process could not clean up.Inside of Remove orphan files are 2 expensive remote steps
- Listing everything in the directories owned by the table
- Join of that list with metadata file paths
Then the list of files to delete is brought back to the driver where deletes are preformed [Discord]
Monday, January 20, 2025
Notes on GPUs and Spark
Saturday, October 26, 2024
NVIDIA Rapids
...
at ai.rapids.cudf.NativeDepsLoader.loadDep(NativeDepsLoader.java:246)
yum -y install git
"cudaFree(0) to actually allocate the set device - no process exclusive required since we are relying on Spark to schedule it properly and not give it to multiple executors"
Friday, September 27, 2024
Running Iceberg Catalogs in a test suite
Russell SpitzerThis isn't actually defined anywhere. So at the moment we are kind of in a wild west, the Spark implementation deletes absolutely everything. I believe for Trino it just sends the request to the catalog and it's up to the catalog to decide. So for Spark "no", and it explicitly sends a drop table request to the rest catalog without a purge flag.
Sunday, July 21, 2024
Hive for an Iceberg test suite
Cannot write into v1 table: `spark_catalog`.`database`.`spark_file_test_writeto`.
"Now V2 Predicate function pushdown is allowed (or at least should be coming soon) but for that you must use the datasource functions and not the spark wones" [Slack]"Best practice for partitions is partition by low cardinality and sort for high cardanality. This may reduce the level of partitions you need" [Slack]
So, I tried creating the table with:
ERROR | o.a.h.h.metastore.RetryingHMSHandler - MetaException(message:Unable to update transaction database java.sql.SQLSyntaxErrorException: Table/View 'NEXT_LOCK_ID' does not exist.
Saturday, April 6, 2024
When adding more CPUs does not help distressed CPUs
![]() |
| Insanely high CPU usage |
![]() |
| Insanely large GC Times |
Saturday, March 9, 2024
Big Data and CPU Caches
$ sudo dmidecode -t cache
Thursday, February 15, 2024
Spark and Schemas
I helped somebody on Discord with a tricksy problem. S/he was using a Python UDF in PySpark and seeing NullPointerExceptions. This suggests a Java problem as the Python error message for an NPE looks more like "AttributeError: 'NoneType' object has no attribute ..." But why would Python code cause Spark to throw an NPE?
The problem was the UDF was defining a returnType struct that stated a StructField was not nullable.
Note that Spark regards the nullable field as advisory only.
When you define a schema where all columns are declared to not have null values , Spark will not enforce that and will happily let null values into that column. The nullable signal is simply to help Spark SQL optimize for handling that column.- Spark, The Definitive Guide
Tuesday, January 23, 2024
Avoiding Spark OOMEs
By using a lazy Iterator, Spark can write far more memory than it has to disk. As Spark consumes from the Iterator, it measures its memory. When it starts looking a bit full, it flushes to disk. Here is the memory usage of this code that uses mapPartitions to write to /tmp/results_parquet a data set that is much larger than the JVMs heap:
![]() |
| Spark with 0.5gb heap writing 1.3gb files |
watch "du -sh /tmp/results_parquet"
Thursday, November 9, 2023
Z-order
Z-ordering is an optimization technique in big data that allows faster access since similar data lives together. We discuss the algorithm that defines what is similar here.
Imagine a logical grid where all the values of one column run across the top and all the values from another run down the side. If we were to sort this data, every datum can be placed somewhere in that grid.
Now, if the squares of the grid were mapped to files and all the data in each cell were to live in those files, we have made searching much easier as we now know the subset of files in which it may live. We've essentially sorted in not just one dimension but two (although we can do higher).
This can be especially useful when we want to sort the data but don't know exactly what to sort on - a common connundrum when dealing with events. Say we have event_time, insert_time and update_time. Which do we choose? We could sort the data three times, each time on one column but this is impractical with huge data sets. Enter z-order.
Note that the Z-Order really needs 2 or more columns on which to act. Only one column is the degenerate case. "Zorder/Hilbert etc on a single dimension are just a hierarchal sort" [Russell Spitzer on Slack].
(This is a good article about z-ordering from the perspective of Apache Iceberg.)
For an example in Delta Lake, we can see this code that creates a data set with columns c1, c2 and c3 whose values are [x, 99-x, x+50 mod 100] for x [0, 99]. After z-ordering it, these numbers are split into 4 different files. Generating a graphic illustrates how the data points are distributed over those files:
The idea behind how we calculate which cell a datum falls into is best described here on Wikipedia. But, in brief, the binary representation of the data points is interleaved to give a z-value per tuple. In our example, I see a [0, 99, 50] mapped to the byte array [0, 0, 0, 0, 0, 0, 0, 0, 0, 9, -112, 26].I took a look at the Delta Lake code here where a Spark Column object is created that wraps a DL InterleaveBits type which in turn is a subclass of Spark's Catalyst Expression type. This executes on Spark's InternalRow, that is, the raw data on the executors.
The reason the code is doing this is to add a column with which we can repartition the data with the SQL CAST(interleavebits(rangepartitionid(c1), rangepartitionid(c2), rangepartitionid(c3)) AS STRING). The rangepartitionid keyword is part of the Delta Lake machinery.
Using this z-order value (plus a random key), the DataFrame the Delta code now calls repartitionByRange which samples the data [SO] and breaks it into discrete ranges.
Given the interleaving of the columns c1, c2 and c3 their order has minimal impact on the z-value so it's no surprise to see nearby data clustering into the same files, as we can see in the graphic. In fact, if you look at the DataFrame during the repartition process:
+---+---+---+-------------------------------------------+
| c1| c2| c3|c7b6b480-c678-4686-aa99-283988606159-rpKey1|
+---+---+---+-------------------------------------------+
| 0| 99| 50| \t�|
| 1| 98| 51| \t�|
| 2| 97| 52| \t�b|
| 3| 96| 53| \t�e|
| 4| 95| 54| \b��|
| 5| 94| 55| \b��|
| 6| 93| 56| \b��|
| 7| 92| 57| \b��|
| 8| 91| 58| \b�|
| 9| 90| 59| \b�|
| 10| 89| 60| \b�b|
| 11| 88| 61| \b�e|
| 12| 87| 62| \b��|
| 13| 86| 63| \b��|
| 14| 85| 64| \f)�|
| 15| 84| 65| \f)�|
| 16| 83| 66| \f`|
| 17| 82| 67| \f`|
| 18| 81| 68| \f`b|
| 19| 80| 69| \f`e|
+---+---+---+-------------------------------------------+
you can see the slowly changing values by which things are partitioned (column c7b6b480-c678-4686-aa99-283988606159-rpKey1 - a random name so it doesn't clash with other column names. It's dropped immediately after the call to repartitionByRange)
Thursday, October 12, 2023
Dependency hell
In these days of ChatGPT, it's easy to forget that most of the time, a developer isn't actually cutting code at all, but debugging it. This is my own personal hell in getting Spark and Kafka in Docker containers talking to a driver on the host.
Firstly, I was seeing No TypeTag available when my code was trying to use the Spark Encoders. This SO answer helped. Basically, my code is Scala 3 and "Encoders.product[classa] is a Scala 2 thing. This method accepts an implicit TypeTag. There are no TypeTags in Scala 3". Yikes. This is probably one reason the upgrade path in Spark to Scala 3 is proving difficult. The solution I used was to create a SBT sub Project that was entirely Scala 2 and from here I called Spark.
The next problem was seeing my Spark jobs fail with:
Caused by: org.apache.spark.SparkException: Job aborted due to stage failure: Task 0 in stage 0.0 failed 4 times, most recent failure: Lost task 0.3 in stage 0.0 (TID 6) (172.30.0.7 executor 0): java.lang.ClassCastException: cannot assign instance of scala.collection.generic.DefaultSerializationProxy to field org.apache.spark.sql.execution.datasources.v2.DataSourceRDDPartition.inputPartitions of type scala.collection.immutable.Seq in instance of org.apache.spark.sql.execution.datasources.v2.DataSourceRDDPartition
This is a contender for the error message with the greatest misdirection. You think it's a serialization problem but it isn't directly so.
Although other Spark users have reported it, Ryan Blue mentions that it isn't really a Spark issue but a Scala issue.
Anyway, I tried all sorts of things like change my JDK (note the sun.* packages have been removed in later JDKs so you need to follow the advice in this SO answer). I tried creating an uber jar but was thwarted by duped dependencies [SO], Invalid signature file digest errors as some were signed [SO] that forced me to strip the signtures out [SO] but still falling foul of Kafka's DataSourceRegister file being stripped out [SO].
The first step in the right direction came from here and another SO question where the SparkSession is recommended to be built by adding .config("spark.jars", PATHS) where PATHS is a comma delimited string of the full paths of all the JARs you want to use. Surprisingly, this turned out to include Spark JARs themselves, including in my case spark-sql-kafka-0-10_2.13 which oddly does not come as part of the Spark installation. By adding them as spark.jars, they are uploaded into the work subdirectory of a Spark node.
After this, there was just some minor domain name mapping issues to clear up in both the host and container before the whole stack worked without any further errors being puked.
Monday, September 25, 2023
Spark, Kafka and Docker
I want to run a Spark Structured Streaming application that consumes from a Kafka cluster all within Docker. I've finally got it working [messy code here in my GitHub], but it was not without its own pain.
The biggest problem is getting all the components talking to each other. First, you need a bridge network. "In terms of Docker, a bridge network uses a software bridge which allows containers connected to the same bridge network to communicate, while providing isolation from containers which are not connected to that bridge network." [docs]. Think of it as giving your containers their own namespace.
Secondly, the Spark worker needs to connect to Kafka, the Spark master and the Spark driver. The first two are just a matter of mapping the Spark master and Kafka containers in the worker. What's harder is getting the worker to talk to the driver that may be running on the computer that hosts Docker.
One sign you've got it wrong is if you see "Initial job has not accepted any resources; check your cluster UI to ensure that workers are registered and have sufficient resources" [SO] in your driver logs. This message is a little ambiguous as it may have nothing to do with resources but connectivity.
| Resources seem to be fine |
To solve it, you need the driver to define spark.driver.host and spark.driver.port. For the host, we need it to be the magic address of 172.17.0.1. This is the default "IP address of the gateway between the Docker host and the bridge network" [docs]. The port is arbitrary.
[Aside: it's also worth ensuring that the all components are running the exact same version of Spark. I saw a rare error ERROR Inbox: Ignoring error java.lang.AssertionError: assertion failed: CPUs per task should be > 0 and the only thing Google produced was this Bitnami ticket. Ensuring all version were the same made it go away.]
What's more, the worker needs these in its config. You can pass it the host and port with something like SPARK_WORKER_OPTS="-Dspark.driver.host=172.17.0.1 -Dspark.driver.port=SPARK_DRIVER_PORT" in its start up script.
But there is one last gotcha. If still can't get things to work, you might want to login to your worker container and run netstat. If you see the connection to the driver in a state of SYN_SENT, your firewall on the host is probably blocking the connection from the container.
Annoyingly, you probably won't see any error messages being puked from the Driver. It will just hang somwhere near org.apache.spark.scheduler.DAGScheduler.runJob(DAGScheduler.scala:929). I only started seeing error messages when I aligned all version of Spark (see above) and it read: java.io.IOException: Connecting to /172.17.0.1:36909 timed out (120000 ms)
Looking in that Docker container showed:
bash-5.0# netstat -nap
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
...
tcp 0 1 192.168.192.7:53040 172.17.0.1:36909 SYN_SENT 1109/java
Aha, that looks like the problem. It turns out that I have to open the firewall for the block manager too and set a static port for it on the Driver with spark.driver.blockManager.port.
Finally, you should be able to have a Spark master and worker plus Kafka instances all running within Docker along with the driver running on the host using your favourite IDE.






