Minn Fong
05/08/2026, 3:44 PM_ss_ prefixedRocky
05/08/2026, 3:44 PM_ss_ prefix for the internal folder names used during StarRocks backup operations.
The _ss_ (likely standing for "snapshot") prefix is an internal naming convention used by the StarRocks storage engine to organize snapshot data and metadata within a backup repository. This is particularly relevant for Primary Key tables, where the system must create snapshots of both the data rowsets and the persistent index to ensure data consistency during the backup.
Key details about Backup Folder Structure:
* User-Configurable Part: When you run a BACKUP command, the only name you can control is the SNAPSHOT name (e.g., BACKUP DATABASE mydb SNAPSHOT my_2024_backup TO my_repo). This name defines the top-level directory for that specific backup in your remote storage (S3, HDFS, etc.).
* Internal Structure: Inside that top-level snapshot directory, StarRocks uses a fixed internal hierarchy (including prefixes like _ss_) to store tablet snapshots. These names are hardcoded in the FE and BE logic to ensure that the RESTORE process can reliably locate and reconstruct the table's state.
* No Configuration Parameters: There are currently no FE or BE configuration properties (like fe.conf or be.conf) or PROPERTIES in the CREATE REPOSITORY or BACKUP statements that allow you to modify these internal pathing conventions.
If you are trying to manage these files via external tools or policies, it is recommended to treat the entire snapshot directory as an opaque, system-managed object. Altering the folder names or structure within a backup snapshot will likely cause the RESTORE job to fail.
References
• administration/cluster_snapshot.md
• administration/management/BE_configuration.md
• table_design/table_types/primary_key_table.md
• release_notes/release-3.3.md
• table_design/indexes/Prefix_index_sort_key.mdMinn Fong
05/08/2026, 3:51 PMCREATE REPOSITORY core_services_backup
WITH BROKER
ON LOCATION "<s3a://stackadapt-db-backup/starrocks/core-services>"
PROPERTIES(
"aws.s3.use_instance_profile" = "true",
"aws.s3.region" = "us-east-1"
);
But when I create backup, a subfolder called __starrocks_repository_core_services_backup/ is created first. Is this configurable?
I want to get rid of this unnecessar subfolderRocky
05/08/2026, 3:52 PM__starrocks_repository_<repo_name> subfolder is not configurable and cannot be removed.
This is a hardcoded naming convention within the StarRocks Frontend (FE) logic. When you create a repository, StarRocks automatically generates this subfolder at the specified ON LOCATION to serve as the root directory for all metadata and snapshots associated with that specific repository.
Why does StarRocks do this?
* Isolation: It ensures that if multiple StarRocks repositories (or other external tools) share the same S3 bucket or parent path, their data remains isolated and clearly identified.
* Safety: The prefix __starrocks_ acts as a namespace to prevent the system from accidentally interacting with or overwriting user files that might exist in the same S3 location.
* Metadata Management: This folder contains the repository_meta file and the snapshots/ directory. The StarRocks RESTORE process specifically looks for this folder structure to validate the repository.
Can you work around it?
There is currently no property in the CREATE REPOSITORY or BACKUP statements to disable this behavior.
If your goal is to have the data at a specific "clean" path (e.g., `s3a://stackadapt-db-backup/starrocks/core-services/`), you would typically set your ON LOCATION to the parent directory. However, StarRocks will still append the __starrocks_repository_... suffix to that path.
Example of the resulting structure:
If your repository name is `core_services_backup`:
text
<s3a://stackadapt-db-backup/starrocks/core-services/>
└── __starrocks_repository_core_services_backup/
├── repository_meta
└── snapshots/
└── snapshot_name/
└── ... (data files and _ss_ folders)
If you need to manage these files via an external lifecycle policy or script, you must include the __starrocks_repository_ prefix in your path patterns.
References
• sql-reference/sql-statements/backup_restore/CREATE_REPOSITORY.md
• administration/management/Backup_and_restore.md
• sql-reference/sql-statements/backup_restore/BACKUP.md
References
• administration/management/Backup_and_restore.md
• sql-reference/sql-statements/backup_restore/CREATE_REPOSITORY.md
• administration/management/FE_configuration.mdMinn Fong
05/09/2026, 6:11 PMRocky
05/09/2026, 6:11 PMbe.conf):
1. Speed up Transaction Apply (Upsert Performance)
The "Transaction Apply Latency" and "Update Rowset Commit Apply Duration" metrics indicate the bottleneck is in the Apply Phase (where the system maps primary keys to locations and updates delete vectors).
* transaction_apply_worker_count: This pool handles the mapping/applying of updates. While it defaults to 0 (which should scale to core count), it is often beneficial to explicitly set it to your core count or even slightly higher if you have high-latency I/O.
_ _Recommended:* Set to 32.
* number_tablet_writer_threads: This controls the number of threads for writing rowsets to disk during ingestion.
_ _Recommended:* Increase to 32 (or even 48 if ingestion volume is massive).
2. Solve the Compaction Score (Data Merging)
A high compaction score means your BEs aren't merging small files fast enough. Primary Key tables use a different compaction logic than Duplicate tables.
* compact_threads: This is the general compaction thread pool. The default is often 4.
_ _Recommended:* Set to 25% of your cores (for 32 vCPUs, set this to 8).
* update_compaction_num_threads_per_disk: Since you have 4 EBS volumes, this is critical for Primary Key tables.
_ _Recommended:* Set to 2 or 4 (this means 2-4 threads per disk, totaling 8-16 threads for update compaction).
* update_compaction_per_tablet_min_interval_seconds: The default is 120. If you have high-frequency writes, this is too long.
_ _Recommended:* Decrease to 60 or 30 to trigger compaction more frequently.
3. Persistent Index Performance
Since you doubled l0_max_mem_usage to 200MB, you've helped keep more of the index levels in memory. However, you should check if you are hitting the Persistent Index (PI) read bottleneck.
* Metric to check: Look at the update mem_tracker: http://<be_ip>:<be_http_port>/mem_tracker?type=update.
* If you see high disk reads for the PI, consider increasing persistent_index_read_cache_capacity (default is 1GB). With 256GB RAM, you can comfortably set this to 4GB or higher to cache the index structure.
4. EBS Throughput vs. Latency
Even at 50% utilization, you might be hitting IOPS or Throughput limits of your specific EBS volume type (e.g., GP3 baseline is 3,000 IOPS and 125 MB/s).
* Primary Key tables are very sensitive to I/O latency during the Apply phase because they must read the index and delete vectors.
* Recommendation: Check CloudWatch for VolumeThroughputPercentage or VolumeIOCapacityExceeded. If you are hitting limits, you need to increase the provisioned IOPS/Throughput on your 4 volumes.
Summary of recommended be.conf changes:
conf
# Concurrency tuning
transaction_apply_worker_count = 32
number_tablet_writer_threads = 32
# Compaction tuning
compact_threads = 8
update_compaction_num_threads_per_disk = 2
update_compaction_per_tablet_min_interval_seconds = 60
# Memory/Index tuning
l0_max_mem_usage = 209715200 # You already did this (200MB)
persistent_index_read_cache_capacity = 4294967296 # 4GB
Note: After making these changes, monitor the CPU utilization. It should rise above 35% as the system finally begins to use the extra cores for background work.
ReferencesMinn Fong
05/09/2026, 6:18 PMRocky
05/09/2026, 6:18 PMUpdateManager.
* pk_index_map_shard_size: The default is 4096. On a machine with 32 cores and 8 nodes, if you have a high tablet count, this might be a bottleneck.
* Recommendation: Try doubling this to 8192 or 16384 in be.conf. (Note: This requires a BE restart).
2. Enable Parallel PK Index Execution
Even if you have many cores, a single "Apply" task for a large tablet might be running on a single thread.
* enable_pk_index_parallel_execution: Ensure this is true (default in recent versions). This allows StarRocks to use multiple threads to process segments within a single tablet during the publish/apply phase.
3. Tune the Persistent Index (PI) Worker Pool
The "Transaction Apply" process depends on loading and fetching the Persistent Index.
* get_pindex_worker_count: This pool handles the I/O and processing for the PI. If it's too small, your apply threads will wait on index lookups.
* Recommendation: Explicitly set this to 32 (or match your vCPU count) in be.conf.
4. Tablet Distribution (The "Lane" Problem)
This is the most common reason for low CPU utilization on large machines.
* The Math: StarRocks parallelizes ingestion and compaction per tablet. If a table only has 8 or 16 tablets per BE, you can never fully utilize 32 vCPUs for that table, because each tablet has internal serialization points.
* Recommendation: Check your tablet count (SHOW TABLET FROM <table_name>). For a 32-core BE, you should ideally have at least 32-64 tablets per BE (Total tablets = 8 BEs * 32 = 256 or more) to ensure there are enough "lanes" for the cores to work on. If your tablet count is low, consider re-partitioning or increasing the BUCKETS count.
5. Memory-based Throttling
Even with 256GB RAM, StarRocks might be throttling because of Pending Versions.
* tablet_max_pending_versions: The default is 1000. If your apply latency is high, versions "pile up" waiting to be applied. If a tablet hits this limit, ingestion for that tablet will block entirely.
* Check: Run SHOW PROC '/compactions' and look for tablets with high "Pending" counts.
* Recommendation: If you have the RAM, increase this to 2000 to allow a larger buffer during bursts.
6. Pipeline Execution Tuning (FE side)
If you are using INSERT INTO ... SELECT or Broker Load, the FE's parallelism settings might be capping the work sent to the BEs.
* parallel_fragment_exec_instance_num: (Session variable) Set this to 16 or 32 to ensure the plan sent to BEs actually uses all those cores.
Summary of "Phase 2" Tuning (be.conf):
conf
# Reduce lock contention for PK tablets
pk_index_map_shard_size = 16384
# Scale index loading to match CPU
get_pindex_worker_count = 32
# Allow more headroom for apply-lag
tablet_max_pending_versions = 2000
# Ensure I/O threads don't bottleneck the apply pool
update_compaction_num_threads_per_disk = 4
Next Step for Diagnosis:
Check the BE log for "slow apply" or "lock" warnings. Specifically, look for lines containing update apply take too long or wait for lock. If you see these, it confirms that the bottleneck is software-level synchronization, not hardware capacity.
References
• administration/management/BE_configuration.mdMinn Fong
05/09/2026, 6:23 PMRocky
05/09/2026, 6:24 PMr6in.8xlarge provided more "raw fuel," you hit several architectural bottlenecks that are better solved by distributing the load across more physical Backend (BE) instances.
1. The "Single Lane" Problem (Per-Tablet Serialization)
In StarRocks, both Ingestion (Apply phase) and Compaction are parallelized at the Tablet level, but individual operations within a tablet often have serialization points.
* The Bottleneck: A single tablet’s Apply task (mapping keys to locations) and its Compaction task are largely handled by a limited number of threads. If you have 32 cores but only 8 tablets per BE, the majority of your cores will sit idle because they cannot "help" process a tablet that is already being handled.
* Why Horizontal Wins: By doubling the nodes, you naturally spread your tablets across 16 BEs instead of 8. This increases the total number of "active lanes" the cluster can use to process data simultaneously.
2. Lock Contention in the UpdateManager
Every BE has an UpdateManager that manages the Primary Key index and Delete Vectors. It uses internal locks to ensure data consistency during the Apply phase.
* The Bottleneck: When you have 32 cores on a single BE, you have a much higher density of threads (Apply workers, Compaction workers, Query threads) potentially fighting for the same internal mutexes in the UpdateManager or the pk_index_map.
* Why Horizontal Wins: Doubling nodes provides you with twice as many independent UpdateManager instances. Each node manages its own subset of tablets with its own isolated lock space, effectively cutting the contention per lock in half.
3. I/O Queue Depth and EBS
Even though your EBS volumes are 90% idle, StarRocks’ ingestion performance is highly sensitive to latency, not just throughput.
* The Bottleneck: A single BE node sending requests to 4 EBS volumes from 32 cores can lead to higher OS-level context switching and I/O scheduling overhead compared to two BE nodes each sending requests from 16 cores.
* Why Horizontal Wins: Smaller instances (r6in.4xlarge) often have better-balanced I/O-to-CPU ratios for distributed databases, as they reduce the blast radius of any single I/O-wait event.
What should you do now?
If you are stuck with the r6in.8xlarge nodes for now, you must "force" the system to use the extra cores:
1. Increase Tablet Count (Buckets): This is the most effective fix. If your table was created with a bucket count optimized for 8 nodes, it is likely too low for 32-core nodes. You should aim for at least 32–64 tablets per BE node.
_ _Action:* Use ALTER TABLE ... SET ("bucket_num" = "XXX") (if using StarRocks 3.2+) or re-create the table with a higher bucket count.
2. Aggressively Shard the PK Index: Since you have the cores, you need to reduce the chance of them hitting the same lock.
_ _Action:* Set pk_index_map_shard_size = 16384 (must be a power of 2) in be.conf.
3. Scale the Worker Pools:
_ _Action:* Ensure transaction_apply_worker_count is set to 32 and get_pindex_worker_count is also set to 32.
In Summary: If you have the choice in the future, 16 nodes of 16-cores will outperform 8 nodes of 32-cores for Primary Key ingestion every time. It provides more parallel "Apply" lanes and significantly less internal lock congestion.
References
• administration/management/BE_configuration.md
• best_practices/primarykey_table.md
• administration/management/monitoring/metrics.mdMinn Fong
05/09/2026, 6:26 PMRocky
05/09/2026, 6:26 PMr6in class is technically "good enough" because it supports the critical AVX2 instruction set and has high network bandwidth, but you likely chose the wrong flavor of R6 for a Primary Key (PK) workload.
Sticking with the 4xlarge and doubling the node count would have been the better move for performance, though it comes with some minor operational trade-offs.
1. Instance Type: Why r6id or i4i beats r6in
For Primary Key tables, I/O latency is the single most important hardware factor.
* The Problem with r6in: This class is "Network Optimized." While it has high throughput to EBS, it still goes over the network. Primary Key tables perform random I/O to look up the Persistent Index and Delete Vectors during the "Apply" phase. Even sub-millisecond network latency adds up when you have thousands of lookups per second.
* The Better Recommendation: Use r6id or i4i instances.
* The d in r6id stands for local NVMe SSDs.
* By putting your storage_root_path and particularly your Persistent Index on local NVMe, you reduce I/O latency from ~500–1000μs (EBS) to ~10–50μs (Local). This is usually the "magic bullet" for Primary Key ingestion performance.
2. Vertical vs. Horizontal: Why 4xlarge (x16) beats 8xlarge (x8)
As we discussed, StarRocks’ Primary Key engine has internal lock points that are per-node.
* Lock Contention: 16 nodes of 4xlarge give you 16 independent Lock Managers and 16 UpdateManagers. Your 8xlarge setup forces 32 cores to fight over the same lock space in only 8 nodes.
* The Sweet Spot: 16 vCPUs (the 4xlarge size) is widely considered the "sweet spot" for StarRocks BE nodes. It is large enough to handle heavy SIMD/Vectorized work but small enough that internal mutex contention rarely becomes the primary bottleneck.
3. Drawbacks of "More Nodes" (Horizontal Scaling)
While horizontal scaling is better for your current bottleneck, it isn't "free." Here is what you trade off:
* FE Metadata Pressure: Each BE node maintains connections to the FE. More nodes = more heartbeats and more tablet replicas to track. However, moving from 8 to 16 nodes is trivial for an FE; you don't really see "pressure" until you cross 50–100 nodes.
* Network Shuffle Cost: In a 16-node cluster, a large "Big Join" might have to shuffle data across twice as many network endpoints as an 8-node cluster. This can slightly increase query latency for very large, distributed joins.
* Operational Overhead: You have twice as many logs to rotate, twice as many OS-level patches to apply, and a higher statistical probability of a single node failure (though StarRocks handles node failure gracefully).
My Recommendation for Your Next Move
If you can still change your cluster configuration:
1. Switch to r6id.4xlarge: The local NVMe will vastly outperform the EBS setup for your PK "Apply" and "Compaction" metrics.
2. Go Horizontal (16 Nodes): This will immediately double your "Apply" throughput by providing more parallel lanes for the Primary Key index.
3. Use EBS for Data, NVMe for Metadata (Advanced): If you need the large storage of EBS, you can use the local NVMe on r6id instances specifically for the StarRocks storage/ metadata and persistent index to get the best of both worlds.
If you must stay on the r6in.8xlarge nodes:
You need to "fake" horizontal scaling by ensuring your Bucket Count is high enough. If your table only has 64 tablets, an 8-node cluster only gives each node 8 tablets to work on. With 32 vCPUs, that means 24 cores are essentially "unemployed" during tablet-level tasks. Aim for 128–256 tablets for this cluster size.
References
• deployment/deployment_prerequisites.md
• deployment/plan_cluster.mdRocky
05/09/2026, 6:26 PMMinn Fong
05/09/2026, 6:33 PMRocky
05/09/2026, 6:33 PMstorage_root_path). If you have only 4 disks, your default concurrency for certain background tasks is very low:
* flush_thread_num_per_store: Default is 2. With 4 disks, you only have 8 threads flushing data from memory to disk. On a 32-core machine, this is a massive under-utilization.
* update_compaction_num_threads_per_disk: This controls Primary Key compaction. If this is low, your disks will stay idle because StarRocks isn't "asking" them to do enough work.
Instead of adding disks, you should first try to saturate the 4 disks you have by increasing these in `be.conf`:
conf
# Increase threads per disk to utilize your 32 cores
flush_thread_num_per_store = 8
update_compaction_num_threads_per_disk = 4
2. Throughput vs. Latency (The PK "Apply" Bottleneck)
Primary Key tables are unique because they require "Random Read" lookups during the Apply Phase (checking if a key exists in the Persistent Index).
* EBS Latency: Even if you have infinite IOPS, a single EBS request (gp3) takes ~0.5ms to 2ms. If your ingestion process is waiting for these lookups to finish one by one, your disks will look idle because they are spent "waiting for the next command," not actually "working."
* Will 8 disks help? Only if your bottleneck is a per-disk lock. Since StarRocks distributes tablets across disks, 8 disks would double the number of "independent storage lanes." However, it is far more common to hit a per-tablet lock or a per-BE lock (UpdateManager) than a per-disk lock.
3. The 4:1 Ratio Myth
The 4:1 or 2:1 ratio is usually about MB/s matching.
* An r6in.8xlarge can push a lot of network throughput.
* 4 EBS gp3 volumes might only provide 500 MB/s total (at default settings).
* If your CPU was at 100% and your disks were at 100%, then adding 4 more disks to get to an 8-disk (4:1) ratio would be the right move to balance the system.
* But since they are idle, your system is "Latency Bound" or "Lock Bound," not "Bandwidth Bound."
When SHOULD you add more disks?
You should only move to 8 disks if:
1. You have already increased flush_thread_num_per_store and transaction_apply_worker_count, and now your disk utilization is over 60-70%.
2. You are hitting the maximum IOPS/Throughput limit of a single EBS volume (e.g., 3000 IOPS for base gp3).
3. You want to reduce the "blast radius" of a single EBS volume's performance fluctuations.
My Recommendation:
Before spending more money on EBS volumes:
1. Check Tablet Parallelism: Ensure you have at least 32-64 tablets per BE. If you have only a few tablets, no amount of disks or cores will help.
2. Force Concurrency: Set flush_thread_num_per_store = 8 and transaction_apply_worker_count = 32.
3. Monitor the "Apply" Latency: Use be_pindex_apply_latency metrics. If this is high while disks are idle, it confirms that the network-storage latency of EBS is your enemy.
4. Consider Local NVMe (r6id class): If you really want to see those 32 cores work, the local NVMe on an r6id.8xlarge will have 1/100th the latency of EBS, which is the "real" bottleneck for Primary Key ingestion.
References
• administration/management/BE_configuration.mdRocky
05/09/2026, 6:33 PMMinn Fong
05/09/2026, 6:37 PMRocky
05/09/2026, 6:37 PMreplication_num = 1, your concern is 100% valid. With r6id local NVMe, if the instance terminates, that data is permanently gone. EBS acts as your "safety net."
However, there is a better way to get the performance you want without the "triple-write" penalty of replication or the "slow-I/O" penalty of EBS.
1. The Real Solution: Shared-Data Architecture
If you are on StarRocks 3.x, you should consider moving to the Shared-Data (Cloud-Native) architecture. This architecture is designed specifically to solve the problem you are facing.
* How it works: Instead of storing data on local disks or EBS, all data is written directly to an object store (AWS S3, Google GCS, etc.).
* Durability without "Triple Writes": You don't need replication_num = 3 at the database level because the Object Store (S3) provides 99.999999999% durability automatically. StarRocks writes the data once to S3, and the cloud provider handles the rest.
* Performance via Local Cache: You use the local NVMe of an r6id instance as a "hot data cache."
* StarRocks stores the Persistent Index (the most latency-sensitive part of your PK table) on that local NVMe.
* During ingestion (the "Apply" phase), the BE looks up keys in the local NVMe cache at sub-millisecond speeds.
* If the node dies, you just spin up a new one. The data is safe in S3, and the local NVMe cache will simply "re-warm" from S3 as queries and ingestion resume.
2. If you stay on Shared-Nothing (Classic)
If you cannot migrate to Shared-Data and must stay on the classic architecture, here is the comparison:
Feature | r6in + EBS (current) | r6id + Local NVMe
Durability (Replica=1) | High (Disk survives node death) | Zero (Node death = Data loss)
Ingestion Latency | High (EBS network overhead) | Ultra-Low (Local bus speed)
Recovery Process | Manual re-attach & BE identity fix | Full reload from source/backup
Can you re-attach EBS?
Yes, but it is not "plug and play." StarRocks identifies BE nodes by their IP/FQDN and an internal cluster_id. If a node dies:
1. You must mount the old EBS to a new instance.
2. The new instance must have the same IP or FQDN as the old one, OR you must perform a complex metadata update in the FE.
3. The storage_root_path must point to the same directory on the mounted volume.
3. Why Horizontal Scaling still matters here
Even if you stick with EBS (r6in) for safety, doubling your node count to 4xlarge is still the superior choice over 8xlarge because:
* Concurrency: You get 2x the number of "Apply" worker threads across the cluster.
* Locking: You split the UpdateManager locks into 16 smaller buckets (one per node) instead of 8 large ones.
* EBS Queueing: 16 instances each talking to 4 disks will generally have lower OS-level I/O wait than 8 instances trying to drive 8 disks each through the same 32-core kernel.
Final Recommendation
1. The "Modern" Move: Migrate to StarRocks Shared-Data mode. Use r6id.4xlarge nodes. You get S3 durability (saving you from replication overhead) and NVMe performance (for your PK index).
2. The "Safe" Move: Stay on Shared-Nothing, use 16 nodes of r6in.4xlarge, and keep your EBS volumes. To fix the performance, increase the transaction_apply_worker_count to 16 and ensure your Bucket Count (tablet count) is high enough (at least 32–64 per node) to keep all your CPU cores busy.
References
• introduction/Architecture.md
• deployment/shared_data/feature-support-shared-data.mdRocky
05/09/2026, 6:37 PMMinn Fong
05/09/2026, 6:41 PMRocky
05/09/2026, 6:41 PMd in r6id) as a Data Cache. Because NVMe is physically inside the machine, its I/O latency is measured in microseconds (µs) compared to the milliseconds (ms) of EBS. StarRocks' "Parallel Scan" and "I/O Merge" optimizations are specifically tuned to squeeze more performance out of this local cache than a standard filesystem on EBS can provide.
2. The "Apply" Phase: PK Tables Love NVMe Cache
For Primary Key tables, the biggest bottleneck is the Persistent Index lookup (the "Apply" phase).
* Shared-Nothing + EBS: Every time you load data, the BE has to look up keys in the Persistent Index stored on EBS. This "random read" pattern is where EBS performs worst due to network-storage latency.
* Shared-Data + NVMe: Even though your "master" data lives in S3, the Persistent Index is stored on the local NVMe cache. This means your key lookups happen at local bus speeds. In v3.3+, you can even persist this index to S3 for durability while still using the NVMe as the performance layer.
3. Durability Math: The "Free" Replication
This is the strongest argument for Shared-Data in your case. You mentioned using replication_num = 1 in Shared-Nothing to save performance.
Feature | Shared-Nothing (rep=1) | Shared-Data (Cloud-Native)
Write Performance | 1x (Single write to EBS) | 1x (Single write to S3/Cache)
Durability | Low (If the disk/node dies, data is gone) | Ultra-High (S3 has 99.999999999% durability)
Recovery | Manual/Painful | Automatic (Spin up a new node, it pulls from S3)
In Shared-Data, you get the "Dual-Write" performance of a single replica, but the durability of a triple-replicated system because S3 handles the redundancy at the storage layer. You aren't wasting CPU cycles on the BE to manage three copies of the data, yet you can't lose data if a node disappears.
4. Writing Performance: Async Uploads
You expressed concern that "writes may take longer" to S3. StarRocks uses Asynchronous Data Ingestion for Shared-Data:
1. The data is written to the Local NVMe Cache immediately (very fast).
2. The transaction "commits" once the metadata is safe in the FE.
3. The data is uploaded to S3 in the background or as part of the commit protocol using highly parallel throughput.
4. Since S3 throughput is virtually infinite (it scales with the number of parallel requests), it often finishes faster than a single EBS volume which has a hard MB/s cap (e.g., 250MB/s for a standard gp3).
Final Verdict: Which should you choose?
If you are currently on r6in (EBS) and considering a change:
* Move to Shared-Data with r6id.4xlarge nodes.
* You get the NVMe speed for your Primary Key index and query cache.
* You get S3 durability, so you never have to worry about replication_num = 1 leading to data loss.
* You can scale up/down in seconds because the nodes are stateless; they don't have to "rebalance" data when you add a new node.
If you absolutely must stay on Shared-Nothing:
Then yes, Shared-Nothing is technically "king" only if you use local NVMe disks. But if you are using EBS, Shared-Data will almost always provide a better balance of performance, safety, and cost.
ReferencesRocky
05/09/2026, 6:41 PMMinn Fong
05/09/2026, 6:46 PMRocky
05/09/2026, 6:47 PMr6id (Shared-Data) vs. r6in (Shared-Nothing)
While Shared-Data is generally superior, there are three specific "drawbacks" or differences to keep in mind:
* Cache Warming (Cold Starts): When you first spin up a new node or after a node restart, the local NVMe is empty. The first query for a specific piece of data will be slower as it pulls from S3. Once cached, it is faster than EBS.
* S3 API Costs: While S3 storage is cheaper than EBS, AWS charges for PUT, GET, and LIST requests. For high-frequency small-batch ingestion, these API costs can add up if not managed (though StarRocks v3.2.3+ includes batching optimizations to mitigate this).
* NVMe Ephemerality: Unlike EBS, the data on local NVMe is lost if the instance is terminated. In Shared-Data, this is fine because the master copy is in S3. The only "drawback" is that the new node replacing it will have a "cold" cache and will need to re-download hot data from S3.
2. Is it a "Pure Win"?
For your specific scenario (using Primary Key tables and wanting to avoid the performance hit of replication), it is as close to a "pure win" as you can get.
* The Durability Win: In your current setup (rep=1 on EBS), if a node or disk fails, your data is gone. In Shared-Data, you write once (no triple-write penalty), but you get S3-level durability automatically. You no longer have to choose between performance and safety.
* The Performance Win: Since your current EBS disks are 90% idle due to latency, switching to r6id local NVMe for the Persistent Index will drastically reduce "Apply" latency. The NVMe cache will handle the random reads that EBS struggles with.
* The Elasticity Win: You can scale from 8 nodes to 16 nodes in seconds because no data needs to be "rebalanced." The new nodes just start pulling from S3.
3. How difficult is the migration?
The main difficulty is that you cannot convert a cluster from Shared-Nothing to Shared-Data in-place. You must:
1. Spin up a new Shared-Data cluster: (Set run_mode = shared_data in fe.conf).
2. Migrate Data: You have two main options:
* Backup/Restore: Use the StarRocks Backup/Restore utility to move snapshots from your old cluster to S3, then restore them into the new cluster.
* Insert Into Select: If both clusters can "see" each other via an external catalog or if you use the StarRocks External Catalog, you can pull data from one to the other.
3. Re-create Tables: You must re-create your table schemas in the new cluster.
4. Can you use the same Ingestion Setup?
Yes. This is the easiest part of the migration.
* API Compatibility: The Stream Load API is identical. Your application just needs to change the destination URL (the IP/DNS of the new FE).
* Label Management: The way StarRocks handles transaction labels remains the same.
* Optimization Tip: Once you move to Shared-Data, ensure you enable batching for high-frequency loads by running:
sql
ADMIN SET FRONTEND CONFIG ("lake_enable_batch_publish_version" = "true");
Summary Recommendation
If you are currently struggling with performance on EBS and are forced to run replication_num = 1 for speed, Shared-Data is the right move. It solves your durability risk while providing the NVMe speed needed to keep your 32-core CPUs busy. 8xlarge instances busy.
The Migration Path:
1. Build a small 3-node r6id.4xlarge Shared-Data cluster.
2. Run a sample Stream Load and compare the WriteDataTimeMs and CommitAndPublishTimeMs to your old cluster.
3. If satisfied, perform a full data migration.
ReferencesRocky
05/09/2026, 6:47 PMMinn Fong
05/09/2026, 6:49 PMRocky
05/09/2026, 6:50 PMreplication_num=1 Shared-Nothing cluster.
Here is exactly how much "slower" it is and how the NVMe cache handles a dataset larger than its capacity.
1. Warm Cache vs. Cold Cache Performance
In StarRocks benchmarks (TPC-DS 1TB), the performance breakdown is typically:
* Warm Cache (NVMe): Comparable or 1.2x Faster than Shared-Nothing with EBS. This is because local NVMe latency is measured in microseconds, while EBS (even gp3) is measured in milliseconds.
* Cold Cache (S3): ~1.5x Slower on average. While S3 has high latency (first-byte-out), it has massive throughput. StarRocks uses "Parallel Scan" and "I/O Merge" to pull data from S3 in large chunks, which saturates your network bandwidth and keeps the CPU busy even before the data is cached.
2. "Not every piece of data can live on NVMe" (The Working Set)
You are correct—if your total data is 50TB and your local NVMe is only 2TB, you can't store everything. However, StarRocks uses a Segmented LRU (SLRU) policy:
* The Protection Segment: This is where your "Hot Data" lives. This includes your Primary Key Persistent Index and your most frequently queried blocks. These are "protected" from being evicted.
* The Eviction Segment: This is where "Cold Data" or "one-off" query data (like a massive historical scan) goes.
* The Result: Even if you run a giant query that touches "cold" data, it won't kick your "hot" dashboard data out of the cache. As long as your active working set fits within ~80% of your total NVMe capacity, you will feel zero performance degradation.
3. Is the "Dual Write" (NVMe + S3) Slower?
Actually, writing to NVMe + S3 is usually faster than writing 3 Replicas to EBS.
* Shared-Nothing (3 Reps): The FE must coordinate three separate BEs, each writing to an EBS volume. The write is only as fast as the slowest EBS volume/network path.
* Shared-Data: The CN node writes the data to the Local NVMe immediately (sub-millisecond) and starts a high-throughput parallel upload to S3.
* Transaction Logic: StarRocks v3.2.3+ introduced Transaction Batching. Even if S3 is "slower" for a single small file, StarRocks bundles metadata and data commits so that high-concurrency Stream Loads (the real-world bottleneck) actually achieve higher throughput on Shared-Data than on Shared-Nothing.
4. Why r6id handles "Node Death" better than EBS
In your current setup, if a node dies, that data is offline until you manually re-attach the EBS and fix the BE identity.
In Shared-Data (v3.3+), StarRocks has Cache Sharing:
1. If Node A dies, a new Node B starts up.
2. Node B doesn't have the data in its local NVMe yet.
3. When a query hits Node B, it first checks the NVMe of other healthy nodes via the network. If another node has that block in its cache, it pulls it from there (10Gbps+ network speed) rather than going all the way to S3.
4. This "Warm-up over network" is significantly faster than a cold start from S3.
Summary of the Trade-off
Scenario | Shared-Nothing (rep=1, EBS) | Shared-Data (r6id NVMe + S3)
Normal Query | Fast (EBS) | Ultra-Fast (NVMe)
Node Fails | Data is Gone/Offline | Data is Safe in S3
Recovery | Manual intervention | Automatic & Transparent
Working Set > Disk | Performance drops (EBS) | Performance drops (S3)
Large Scans | Bounded by EBS IOPS | Bounded by S3 Throughput (Higher)Rocky
05/09/2026, 6:50 PMMinn Fong
05/09/2026, 6:55 PMRocky
05/09/2026, 6:55 PMCLOUD_NATIVE Persistent Index
* The Old Way: You were right—the index had to fit on the local disk. If it didn't, performance fell off a cliff.
* The New Way (v3.3+): StarRocks now uses persistent_index_type = CLOUD_NATIVE (the default in v3.3).
* The Source of Truth for the index is stored on S3 as a set of immutable files.
* The Local NVMe acts as a block-level cache for the index.
* Instead of a "fallback" to S3 for a whole index lookup, the system only pulls the specific "pages" of the index it needs. Because PK lookups are often for "hot" data, the index cache hit rate remains high even if the total index size is 10x the NVMe capacity.
2. Delete Bitmap Updates & S3 Metadata Round Trips
* The Concern: High-frequency updates creating a flood of S3 metadata calls for DelVectors (Delete Bitmaps).
* The Optimization: StarRocks v3.2 introduced Size-tiered Compaction for Cloud-Native PK tables.
* Instead of updating and re-uploading a full delete bitmap for every small load, StarRocks now manages delete vectors in a more "LSM-like" tiered structure.
* Small updates are buffered and merged into larger DelVector files. This significantly reduces the number of PUT requests to S3 and minimizes the "metadata round trip" penalty you mentioned.
3. Constant Index Churn on NVMe
* The Reality: Yes, high updates mean the index is changing.
* The Mitigation: StarRocks uses an asynchronous update mechanism for the Cloud-Native index. The index updates are performed in memory and on the local NVMe first to ensure the "Apply" phase of the load remains fast. The synchronization of these index changes back to S3 happens in the background, decoupled from the critical path of the load transaction.
4. Comparison: Shared-Data (v3.3) vs. Shared-Nothing (EBS)
When you weigh these "Cloud-Native PK" costs against your current EBS setup:
Pain Point | Shared-Nothing (EBS) | Shared-Data (v3.3 + NVMe)
I/O Latency | Constant ~1-5ms (EBS) | Microseconds (NVMe Cache)
Throughput | Hard cap (e.g., 250MB/s) | S3 Scalable Throughput
Update Penalty | Random I/O on EBS (Slow) | Random I/O on NVMe (Fast)
Replication | CPU/Network cost for 3 reps | No extra CPU cost (S3 handles it)
When should you NOT migrate?
Despite the optimizations, you should stay on Shared-Nothing if:
1. Extreme Update Frequency: If you are doing thousands of tiny Stream Loads per second (rather than batching them every 1-5 seconds), the S3 API overhead will still be visible.
2. Zero-Latency Tolerance: If you cannot afford even a 100ms "cold start" spike when a new node joins or a cache miss occurs.
Summary: The "known pain points" you cited were the focus of the v3.2 and v3.3 engineering cycles. By using r6id (NVMe) with CLOUD_NATIVE indexing, you are essentially using the NVMe as a high-speed buffer for an S3-backed index, which usually outperforms the "middle-ground" latency of EBS-backed EBS volumes.
References
• release_notes/release-3.3.md
• table_design/table_types/primary_key_table.md
• deployment/shared_data/feature-support-shared-data.mdRocky
05/09/2026, 6:55 PMMinn Fong
05/09/2026, 7:05 PMOur current shared-nothing architecture on EBS is showing compaction and apply latency issues that aren't resolving with instance upgrades. The root cause is that our workload — partial columnar upserts on PK tables, batched once per minute, updating data seconds after insertion — is a poor fit for EBS random I/O characteristics.
The core problem today:
Every impression upsert triggers a read-modify-write cycle on EBS. At 1-5ms per operation on gp3, this is the primary driver of our apply latency and compaction pressure. We have no replication, meaning a node failure results in data loss.
Why shared data + r6id solves this specifically:
NVMe local cache is 10-20x lower latency than EBS (100µs vs 1-5ms) for the read-modify-write cycle on our PK partial column updates. This directly collapses apply latency
Our update pattern has near-100% NVMe cache hit rate — impressions arrive seconds after bids are inserted, meaning the PK index pages for our upserts are always hot in the NVMe cache. We are not doing random historical updates
Our load volume is <1GB per minute burst against a 2GB/s NVMe write ceiling — the hardware is barely stressed at peak
S3 as the source of truth replaces replication — node failure means we lose the NVMe cache, not the data. A replacement node warms up from S3 automatically with no manual intervention. This is strictly better than our current rep=0 situation
StarRocks v3.3 Cloud-Native PK engine specifically addresses shared data PK table performance — persistent index is S3-backed with page-level NVMe caching, delete vectors use size-tiered compaction to minimize S3 API pressure
What we expect to see:
Apply latency: significant reduction from eliminating EBS read-modify-write latency
Compaction pressure: reduced — NVMe handles our <1GB/min volume trivially
Operational risk: eliminated — no more single point of failure from rep=0 on EBS
Is this a good argument to present to others for migrating to shared dataRocky
05/09/2026, 7:06 PMgp3 (even with high provisioned IOPS) struggles with the sub-millisecond random lookups required for PK indexes. The "Apply" phase is where the Primary Key index is checked to see if a row exists; on EBS, this is a network-attached disk lookup. On r6id, this hits local NVMe, which is an order of magnitude faster.
2. The "Temporal Hotness" Factor: Your observation that "impressions arrive seconds after bids" is critical. In StarRocks, this means the Persistent Index Pages for those rows will almost certainly be in the BE/CN's memory or the local NVMe cache. You aren't just guessing—you are describing a 99%+ Cache Hit Rate scenario.
3. The "Cloud-Native PK" Advantage: Mentioning the v3.3 Cloud-Native PK engine shows you are leveraging the latest architectural optimizations. The transition to CLOUD_NATIVE as the default persistent_index_type in v3.3 means the index is truly decoupled from the node, solving the "index persistence" headache of earlier shared-data versions.
4. Cost/Risk Symmetry: You are effectively trading the cost of S3 API calls for the elimination of EBS volume costs and the massive risk of rep=0. In most AWS environments, S3 + r6id is actually more cost-effective at scale than high-performance provisioned EBS.
Two "Pro-Tips" to strengthen the pitch:
If your stakeholders are skeptical, adding these two technical nuances will show you’ve covered every corner:
* Mention "Column Mode" Partial Updates: Since you are on v3.3, mention that you will use Column Mode for these partial updates. In Shared-Data mode (v3.3.1+), Column Mode is specifically optimized to only rewrite the modified columns to S3, further reducing the I/O pressure that you are currently feeling on EBS.
* The "Zero-Rebalancing" Scale-Out: Remind them that in your current Shared-Nothing setup, adding a node requires a massive "rebalance" (moving TBs of data across the network). In the new Shared-Data setup, adding an r6id node is instant. The new node just starts fetching from S3 as needed. This makes "Peak Season" scaling a non-event.
Summary for your Presentation:
"By moving to Shared-Data on r6id, we are moving from a Latency-Bound architecture (EBS) to a Throughput-Bound architecture (S3+NVMe). We solve our 'Apply' latency by using NVMe for local index lookups while gaining S3-level durability, effectively giving us the safety of 3x replication without the 3x hardware and performance cost."Verdict: This is a very high-quality migration plan. It moves the system from a "fragile" state (rep=0) to a "resilient" state while simultaneously removing the primary performance bottleneck. References • release_notes/release-3.3.md
Minn Fong
05/09/2026, 7:08 PMMinn Fong
05/09/2026, 7:33 PMRocky
05/09/2026, 7:33 PMr6id) is particularly robust.
1. The Geometry of Horizontal Scaling
You mentioned that because your buckets are fixed at 128, you have a fixed amount of tablets. However, horizontal scaling changes the density of those tablets per node.
* Vertical Scaling (1 Node, 64 vCPU): All 128 "hot" tablets live on one machine. While you have 64 cores to process them, all 128 tablets are competing for the same node-level resources (the local NVMe throughput, the network bandwidth to S3, and the BE’s internal metadata locks).
* Horizontal Scaling (4 Nodes, 16 vCPU each): Those 128 "hot" tablets are distributed across the cluster. Each node now only manages 32 hot tablets.
* The Benefit: You have reduced the "blast radius" of any single tablet's lock. Even if one tablet is undergoing a massive compaction or a heavy "Apply" phase, it only affects the 16 vCPUs on that specific node.
2. Is a 4:1 Ratio "Fine"?
Yes, it is excellent. In StarRocks, tablet locks are held during the Apply/Publish phase (when the PK index is updated and the Delete Vector is written).
* Contention only occurs if the number of concurrent transactions trying to write to the same tablet exceeds the available worker threads.
* With 128 tablets and 32 vCPUs, you have a very high degree of parallelism. Unless you are running 100+ concurrent Stream Load jobs targeting the same hour partition simultaneously, your threads will likely spend most of their time waiting for I/O, not for each other.
3. Why NVMe + Shared-Data is the "Lock Killer"
The real "pain" of lock contention isn't the number of tablets—it's the duration of the lock.
* The EBS Problem: In your current setup, a tablet lock is held while the BE performs a read-modify-write on EBS (1–5ms). If 10 rows need updating, that lock stays open for the duration of those EBS round trips.
* The NVMe Solution: On an r6id, that same operation takes microseconds. By reducing the latency of the PK lookup and Delete Vector update, you reduce the time the lock is held.
_ _Analogy:* Contention isn't caused by having 4 people per door; it's caused by the person at the door taking 5 minutes to find their keys. NVMe makes them find their keys in 5 milliseconds.
4. Horizontal Scaling vs. Tablet Locks
Horizontal scaling is generally preferred over vertical scaling for PK tables for two reasons:
1. Distributed Apply: In StarRocks shared-data, the "Apply" phase (where the lock is most critical) is performed by the CN/BE nodes. Scaling horizontally spreads the CPU load of calculating Delete Vectors across more machines.
2. Metadata Parallelism: Each BE has internal locks for managing its own memory and metadata. By scaling horizontally, you get multiple "metadata namespaces," effectively giving you more "lanes" on the highway.
Your Specific Scenario (3072 total tablets)
Having 3072 tablets total but only 128 "hot" ones is the ideal way to use StarRocks. The 2944 "cold" tablets are effectively dormant. Since you are moving to Shared-Data, those cold tablets:
* Consume zero local NVMe space (they stay on S3).
* Consume zero memory (their PK indexes aren't loaded).
* Only the 128 tablets in your "hot" partition will be active in the BE's thread pools.
Verdict: Your 4:1 ratio is very safe. The move to r6id will do more to solve "contention" than simply adding vCPUs, because it attacks the duration of the lock rather than just the number of threads.
One Configuration Tip:
If you still see "Apply" latency after migrating, check this BE/CN setting:Rocky
05/09/2026, 7:33 PMtransaction_publish_version_worker_count (Default is usually equal to vCPU count).
In high-update scenarios on NVMe, you can sometimes benefit from increasing this slightly above the vCPU count to ensure that I/O wait times don't starve the CPU, though with NVMe, the default is usually perfect for 99% of cases.
References
• table_design/table_types/primary_key_table.mdMinn Fong
05/09/2026, 7:38 PMRocky
05/09/2026, 7:38 PMhttp://<be_ip>:8040/metrics) or in your Prometheus/Grafana dashboard.
* update_rowset_commit_apply_duration_us: This is the total time spent on the Apply task. If this value is high relative to your ingestion volume, it confirms the "Apply" phase (the read-modify-write cycle) is slow.
* update_apply_queue_count: This is the "Smoking Gun." If this number is frequently above 0, it means new data batches are waiting in a queue because all "Apply" threads are busy holding locks on tablets while waiting for EBS I/O to complete.
* column_partial_update_apply_duration_us: Since you mentioned partial updates, this metric tracks the specific duration for that logic.
2. Compare "Commit" vs "Visible" Time (FE Level)
You can use the StarRocks information_schema to see if transactions are getting "stuck" in the publish phase (where tablet locks are held).
sql
SELECT
TRANSACTION_ID,
TABLE_NAME,
COMMIT_TIME,
VISIBLE_TIME,
TIMESTAMPDIFF(SECOND, COMMIT_TIME, VISIBLE_TIME) as publish_latency_seconds
FROM information_schema.loads
WHERE STATE = 'FINISHED'
ORDER BY COMMIT_TIME DESC
LIMIT 100;
* Interpretation: If COMMIT_TIME and VISIBLE_TIME are more than a few seconds apart, it means the data was written to disk, but the BEs are struggling to "Apply" the changes (the lock phase). On EBS, this latency is almost always I/O-bound.
3. Check BE Logs for "Slow" Apply Tasks
Search your <http://be.INFO|be.INFO> or be.WARNING logs for the keyword apply. When a Primary Key apply task takes a long time, the BE will often log details about the rowset commit.
Look for lines containing:
* slow apply rowset
* tablet_id: <id> ... cost: <large_number>ms
If you see apply costs in the hundreds of milliseconds or seconds, and your EBS iowait is high simultaneously, you have confirmed that the tablet lock is being held open by the storage layer's latency.
How this proves your point:
If you find that update_apply_queue_count is high and publish_latency_seconds is high, you have a classic concurrency bottleneck. The BE has plenty of CPU (the threads are there), but the threads are "sleeping" while waiting for the EBS volume to return the PK index pages.
Moving to r6id (NVMe) will effectively shrink that apply_duration to almost zero, which clears the apply_queue and allows the 128 tablets to process updates in parallel without stepping on each other's toes.
References
• administration/management/monitoring/metrics.md
• loading/Load_to_Primary_Key_tables.md
References
• knowledge/trouble_shooting/fe_deadlock_problems.md
• knowledge/usage/best-practice.mdMinn Fong
05/09/2026, 7:42 PMcolumn_partial_update_apply_duration_us is 0. Not sure why but I am using partial update column mode. Update apply queue count is almost always 0, does this change anything about your verdict? Update rowset commit apply duration mostly between 2-6s.Rocky
05/09/2026, 7:42 PMcolumn_partial_update_apply_duration_us is 0
If this metric is zero but you are performing partial updates, it usually means your ingestion is falling back to Row Mode.
* Routine Load: Currently only supports Row Mode. If you are using Kafka/Routine Load, it will ignore the column mode setting.
* Stream/Broker Load: Default to Row Mode unless partial_update_mode=column is explicitly set in the headers/properties.
* Small Batches: In auto mode, StarRocks will use Row Mode if the batch has few rows or updates many columns.
In Row Mode, the "Apply" phase performs a read-modify-write for the entire row. On EBS, this is significantly slower than Column Mode because it requires more I/O to reconstruct the row.
2. The Smoking Gun: update_rowset_commit_apply_duration (2-6 seconds)
This is the most critical metric in your list. In a Primary Key table:
* Ideal (NVMe): Apply duration should be 10ms to 100ms.
* Your Current (EBS): 2,000ms to 6,000ms.
A 2-6 second "Apply" time is a lifetime for a database. During these 2-6 seconds, the specific tablet lock is held. Even if you only have one load job running at a time (which is why your queue is 0), that tablet is "frozen" for any other conflicting operations for those 6 seconds.
3. Why update_apply_queue_count is 0
A zero queue count simply means your ingestion frequency is low enough that the next "Apply" task doesn't arrive before the current 6-second task finishes.
* The queue count is a measure of concurrency saturation.
* The duration metric is a measure of storage latency.
If you were to double your ingestion frequency right now, you would likely see the queue count jump to 1 or 2, and your "Visible Time" would double to 12 seconds. You aren't "contending" yet because you are giving the system enough "breathing room" between loads, but the individual performance is still being bottlenecked by EBS.
Conclusion: Does this change the verdict?
No, it reinforces it.
The fact that your "Apply" phase takes 2-6 seconds is the definitive proof that the storage layer (EBS) is struggling with the PK index lookups and Delete Vector writes.
* With NVMe: That 2-6s will likely drop to <50ms.
* The Benefit: This "unlocks" your ability to increase ingestion frequency or handle more concurrent users without hitting a wall.
Recommendation: If you are using Stream Load, ensure you are passing "partial_update_mode: column" in the header. If you are using Routine Load, you are locked into Row Mode for now, which makes the move to NVMe even more critical because Row Mode is much more I/O intensive during the Apply phase.
References
• administration/management/monitoring/metrics.md
• loading/Load_to_Primary_Key_tables.md
• sql-reference/sql-statements/table_bucket_part_index/UPDATE.md
References
• loading/Load_to_Primary_Key_tables.md
• sql-reference/sql-statements/table_bucket_part_index/UPDATE.md
• sql-reference/sql-statements/loading_unloading/BROKER_LOAD.md
• loading/Spark-connector-starrocks.md
• sql-reference/System_variable.md
• faq/Others.mdMinn Fong
05/09/2026, 7:45 PMreq.Header.Set("Expect", "100-continue")
req.Header.Set("format", "JSON")
req.Header.Set("label", label)
req.Header.Set("strip_outer_array", "true")
req.Header.Set("ignore_json_size", "true")
req.Header.Set("enable_merge_commit", "true")
req.Header.Set("merge_commit_async", "true")
req.Header.Set("merge_commit_interval_ms", "10000")
req.Header.Set("merge_commit_parallel", "8")
if options.partialUpdate {
req.Header.Set("partial_update", "true")
req.Header.Set("partial_update_mode", "column")
req.Header.Set("columns", strings.Join(options.columns, ","))
}
This is my configuration in Go. Why is column mode not being used? How do i verify it is indeed using row mode? I have > 500 columns but most updates only touch 1-5 columns.
I still think column mode is being used because I have 2 different updates (impression and click events) that survive? If I update uses row mode but specifies has_click = 1 with no impression, doesn't that wipe out old has_won = 1 impression column data).Rocky
05/09/2026, 7:46 PMhas_click, StarRocks reads the entire existing row (including the impression data), updates only the has_click field in memory, and then writes the full row back.
* Column Mode: StarRocks only writes the new value into a small file for the has_click column. It doesn't touch the other columns at all.
Result: Both modes ensure that has_won (the impression) is not wiped out. The difference is strictly performance. Row Mode is slow because it performs a "Read-Modify-Write" cycle for the whole row, which explains your 2-6 second apply duration on EBS storage.
2. Why Column Mode is likely being ignored
There are two primary reasons your column_partial_update_apply_duration_us metric is 0:
A. Incompatibility with merge_commit
You have enable_merge_commit: true in your Go code. Merge Commit currently does not support Column Mode partial updates.
* merge_commit works by buffering multiple Stream Loads in memory and merging them into a single row-based transaction. Because this happens at the "pre-storage" layer, it defaults to the standard Row Mode logic.
* When enable_merge_commit is active, the BE effectively ignores the partial_update_mode: column hint.
B. The "Small Batch" Fallback
Even without Merge Commit, StarRocks has an internal heuristic for Column Mode. Since you have 500+ columns but are only updating 1-5 columns, your scenario is the ideal use case for Column Mode. However, Column Mode is only triggered if the batch is "large enough" (typically several thousand rows) to justify the overhead of creating new column-specific rowsets. If your individual Go requests are small, it will default to Row Mode for efficiency.
3. How to verify Row Mode vs. Column Mode
You can confirm Row Mode is being used through these signals:
1. Metric Verification: As you observed, column_partial_update_apply_duration_us is 0. This is the definitive proof that the Column Mode codepath is not being hit.
2. Apply Latency: Your update_rowset_commit_apply_duration is 2-6s. In Column Mode, this is usually <100ms because it avoids the heavy cost of reading 500 columns from EBS to rebuild the row.
3. Audit Log: If you check the fe.audit.log, search for your Stream Load label. The Params field might show the properties, but the BE metrics you've already found are more reliable.
4. Recommendations for your Go setup
If you want to reduce that 2-6s contention and actually use Column Mode:
1. Disable `enable_merge_commit`: Try turning this off for one specific high-volume table to see if column_partial_update_apply_duration_us starts increasing.
2. Increase Batch Size: Instead of using Merge Commit to group many tiny writes, try grouping them in your Go application into batches of 5,000–10,000 rows per Stream Load.
3. Confirm the columns Header: Ensure that the columns header you are sending only includes the columns present in the CSV/JSON and the Primary Key. If you include all 500 columns in the header but only provide data for 5, the system may get confused.
Summary
Your data is safe because Row Mode is a smart partial update, not a "replace row" operation. However, the 2-6 second lock contention you are seeing is the "EBS Tax" paid by Row Mode. Disabling merge_commit and ensuring large enough batch sizes should trigger the Column Mode you're looking for, which will drop that 2-6s latency down to milliseconds.
ReferencesRocky
05/09/2026, 7:46 PMMinn Fong
05/09/2026, 7:48 PMRocky
05/09/2026, 7:48 PMmerge_commit for your updates, you allow StarRocks to finally use Column Mode.
* The Conflict: merge_commit works by aggregating rows in memory. To do this, it forces the data into a standard row format. This effectively "kills" the partial_update_mode: column instruction.
* The Benefit: Once Column Mode is active, StarRocks will stop reading the other 495+ columns from EBS just to update the 5 columns you sent. It will only write the "delta" for those 5 columns.
* Result: Your update_rowset_commit_apply_duration will likely drop from 2-6 seconds down to sub-100ms. Because the lock is held for a much shorter window, your contention disappears.
2. The "Gotcha": You must batch manually
When you disable merge_commit, you lose the "safety net" that protects you from the "Too many versions" error (-235).
* merge_commit was protecting you by taking many small requests and turning them into one big one.
* If you disable it and your Go code sends a Stream Load every time a single "Click" or "Impression" comes in, you will hit the version limit (default 1000 versions) within minutes.
Solution: In your Go application, you must implement a simple buffer. Instead of sending 100 requests of 10 rows each, send one request of 1,000 rows every 1-5 seconds.
3. Will this break your "Impression/Click" logic?
No. As you observed, even in Row Mode, StarRocks was doing a "Read-Modify-Write" to preserve existing data. In Column Mode, it simply stores a new "fragment" for the specific columns updated. When you query the data, StarRocks merges these fragments on the fly.
* An Insert (via merge_commit) will create the base row.
* An Update (via standard Stream Load) will layer the "Click" data on top.
* Both will coexist perfectly.
Summary Checklist for your Go Code:
1. For Inserts: Keep enable_merge_commit: true. This handles high-frequency row creation and prevents version bloat.
2. For Updates:
* Set enable_merge_commit: false.
* Set partial_update_mode: column.
* Crucial: Buffer your updates in Go and only trigger the <http://http.Post|http.Post> once you have a few thousand rows or a few seconds have passed.
3. Verification: After making this change, check your metrics again. You should finally see column_partial_update_apply_duration_us move above 0, and your total apply_duration should plummet.
If the latency is still high even in Column Mode, that is the final proof that your EBS volumes are physically maxed out on IOPS/Throughput and the move to NVMe (r6id or i3en) is no longer optional.
References
• loading/Load_to_Primary_Key_tables.md
• sql-reference/sql-statements/table_bucket_part_index/UPDATE.md
• administration/management/monitoring/metrics.md
• loading/Flink-connector-starrocks.md
References
• loading/Load_to_Primary_Key_tables.md
• sql-reference/sql-statements/table_bucket_part_index/UPDATE.md
• sql-reference/sql-statements/loading_unloading/BROKER_LOAD.md
• administration/management/monitoring/metrics.mdRocky
05/09/2026, 7:48 PMMinn Fong
05/09/2026, 7:59 PMRocky
05/09/2026, 7:59 PMupdate_rowset_commit_apply_duration is a massive 2-6 seconds. On NVMe storage, this might be 50-100ms, but on EBS, the latency of those random reads adds up quickly.
2. How this impacts Compaction
Compaction in a Primary Key table is responsible for merging these rowsets and cleaning up the "Delete Vectors" (the markers that say "this old row is now replaced").
* Write Amplification: Because Row Mode writes the full 500 columns every time, your Write Amplification is roughly 100x (writing 500 columns worth of data when only 5 changed).
* Compaction Backlog: Compaction now has to process 100x more data than it would in Column Mode. If your ingestion rate is high, compaction simply cannot keep up with the volume of data being written, leading to a "Compaction Score" explosion and eventually slowing down all queries.
3. Why Column Mode is the Solution
If you successfully trigger Column Mode, the "Apply" phase changes completely:
* No Read-Modify-Write: StarRocks does not read the other 495 columns.
* Delta Writing: It only writes the "delta" (the 5 columns you changed) into a separate column-specific file.
* Reduced I/O: Your I/O requirements for both the "Apply" phase and the subsequent "Compaction" phase will drop by ~95-99%. This will bring your transaction apply latency down from seconds to milliseconds.
4. The Verdict
The incompatibility between merge_commit and partial_update_mode: column is your primary bottleneck.
By using merge_commit, you are accidentally forcing StarRocks into the most expensive possible way to update your data (Row Mode with 500 columns on EBS).
Immediate Action Plan:
1. Disable enable_merge_commit for your update-heavy streams.
2. Increase your client-side batch size in Go (aim for 5,000–10,000 rows per load) to avoid "too many versions" errors.
3. Monitor `column_partial_update_apply_duration_us`: Once this starts moving above 0, you will know Column Mode is active. You should see your EBS throughput drop and your transaction apply latency recover instantly.
If you cannot batch manually and must use merge_commit, then your only path to stability is moving to NVMe storage (Local SSD), which can handle the "Read-Modify-Write" I/O storm of Row Mode much better than EBS.
References
• administration/management/monitoring/metrics.md
• sql-reference/sql-statements/table_bucket_part_index/UPDATE.md
• sql-reference/System_variable.md
• administration/management/BE_configuration.md
• loading/loading_introduction/feature-support-loading-and-unloading.md
• loading/Load_to_Primary_Key_tables.mdMinn Fong
05/09/2026, 8:01 PMRocky
05/09/2026, 8:02 PMcolumns header in a Stream Load acts as a global mask for every row in that batch.
* The Conflict: If you combine an "Impression" (updates col1, col2) and a "Click" (updates col3, col4) into one batch, you would have to set the header to columns: id, col1, col2, col3, col4.
* The Consequence: When StarRocks processes the "Impression" row, it sees that col3 and col4 are missing from the JSON. By default, it will fill those with NULL (or the column's default value).
* Result: Your "Impression" update will accidentally wipe out any existing "Click" data for that ID.
Conclusion: To use Column Mode safely, you must group your batched stream loads by event type (schema) in your Go code.
──────────
2. Column Mode vs. NVMe: Which is the primary driver?
While Column Mode is theoretically "faster" because it avoids reading the other columns entirely, it forces you into a complex batching architecture (grouping by event type, handling small batches, etc.).
For your specific case (>500 columns, EBS storage, mixed events), migrating to a Row-Store optimized configuration or NVMe is actually the more robust performance driver.
Why?
Your 2-6s latency is caused by the "Read" in the Read-Modify-Write cycle of Row Mode.
1. Column Mode skips the "Read" (Total I/O: ~5 columns).
2. Row Mode + Row Store (SR 3.2+) makes the "Read" extremely fast (Total I/O: 1 seek for the full row).
3. Row Mode + NVMe makes the "Read" fast simply through raw hardware speed.
──────────
3. The "Secret" Third Option: Enable the Row Store
If you are on StarRocks 3.2 or later, you don't actually need Column Mode to fix this. You can enable the Row Store (Row-Columnar Hybrid Storage) on your Primary Key table.
When you create the table, add this property:
sql
PROPERTIES (
"store_type" = "column_with_row"
)
Why this solves your problem:
* It stores a copy of the data in row-format alongside the columnar data.
* When you perform a Row Mode partial update (which merge_commit uses), StarRocks fetches the existing 500 columns from the Row Store in a single disk seek instead of scanning 500 different column files.
* Performance: This typically reduces "Read-Modify-Write" latency by 10x-50x, often matching the performance of Column Mode without the schema-mixing restrictions.
Summary Recommendation
1. Short Term (Go Fix): If you can't change the table schema, you must separate your Go batches by event type and disable merge_commit for those specific update loads to trigger Column Mode.
2. Medium Term (Storage Fix): Moving to Shared-Data with NVMe cache is the "silver bullet." It will make the Row Mode updates (and merge_commit) significantly faster because the 500-column "Read" will happen from local NVMe instead of network EBS.
3. The "Pro" Fix (SR 3.2+): Recreate the table with "store_type" = "column_with_row". This allows you to keep using merge_commit, keep mixing events in Go, and still get millisecond-level update performance because the Row Store eliminates the EBS "EBS I/O storm" of reading 500 columns.
Verification: Once you enable the Row Store or fix Column Mode, watch the update_rowset_commit_apply_duration metric. It should drop from seconds to milliseconds.
References
• loading/Load_to_Primary_Key_tables.md
• table_design/table_types/primary_key_table.md
• faq/Others.md
• loading/Flink-connector-starrocks.mdMinn Fong
05/09/2026, 8:04 PMRocky
05/09/2026, 8:04 PMcolumn_with_row storage type is a powerful tool for your specific scenario, but it has distinct trade-offs you must consider before committing.
1. Does it work on Shared-Nothing?
Yes. In fact, as of StarRocks v3.2/3.3, it only works on Shared-Nothing architectures.
* It is not currently supported in Shared-Data (cloud-native) clusters.
* Since you are using EBS volumes attached to BEs, you are likely in a Shared-Nothing setup, so this feature is available to you (starting from v3.2.3).
2. How it works with your EBS and 500+ Columns
This is where it solves your primary bottleneck. In a standard Columnar table, a "Row Mode" partial update (triggered by merge_commit) must do this:
* Without Row Store: To update 5 columns, the BE must open and read 500 separate column files from EBS to reconstruct the row. This creates a massive "I/O storm" of random reads, which EBS is notoriously slow at handling. This is why your apply latency is 2–6 seconds.
* With Row Store (column_with_row): StarRocks stores the entire row as a single contiguous block in a separate "row store" file. To perform the same update, the BE does one single I/O seek to read the full row.
* Result: You reduce your "Read" overhead from 500 I/O operations to 1 I/O operation. On EBS, this is the difference between a timeout/lock-contention and sub-100ms performance.
3. The Drawbacks (The "Price" you pay)
There is no free lunch; column_with_row has three main costs:
1. Significant Storage Overhead: You are essentially storing the data twice (once in columnar format for queries, once in row format for updates/point-lookups). Expect your disk usage to increase by 2x to 3x. Row storage does not compress nearly as well as columnar storage.
2. Higher Write CPU/Memory: During ingestion, the BE has to do double the work to encode the data into both formats. You will see an increase in CPU usage during Stream Loads.
3. Incompatibility with Column Mode: You cannot use partial_update_mode: column on a table that has a row store enabled.
_ Why this doesn't matter for you: You are currently stuck in Row Mode anyway because you want to use merge_commit. The Row Store makes the mode you are _forced* to use significantly faster.
4. Summary: Fix for Column Mode vs. Row Store?
Feature | Implementation Effort | EBS Performance Impact
Fixing Column Mode | High: Must change Go code to batch by event type; lose merge_commit benefits. | Excellent: Skips reading other columns entirely.
Enabling Row Store | Medium: Must recreate table with property; keep Go code as-is. | Great: Reduces 500 reads to 1 read.
Moving to NVMe | High: Infrastructure migration. | Excellent: Raw speed overcomes I/O inefficiencies.
Rocky's Recommendation:
If you have the disk space to spare on your EBS volumes, recreating the table with "store_type" = "column_with_row" is your path of least resistance. It allows you to keep your current Go logic and merge_commit while solving the 2–6s latency spike caused by the 500-column "Read-Modify-Write" cycle.
Note: If you choose this path, you must recreate the table. You cannot ALTER an existing table to add a row store unless you are on v3.2.4 or higher.
References
• table_design/hybrid_table.md
• table_design/table_types/primary_key_table.md
• release_notes/release-3.2.md
• administration/management/BE_configuration.md
References
• knowledge/usage/best-practice.md
• table_design/hybrid_table.mdRocky
05/09/2026, 8:04 PMMinn Fong
05/09/2026, 8:12 PM2026-05-09T20:07:00.096Z ERROR [StarRocks] flush failed after retries {"label": "sr_requests_1778357220000030787_11", "rows": 6, "batch_bytes": 1061, "schema": "conv_secondary_last_touch", "error": "stream load returned status \"Fail\": 172.30.65.224: partial update on table with sort key must provide all sort key columns"}
2026-05-09T20:07:00.125Z ERROR [StarRocks] flush failed after retries {"label": "sr_requests_1778357220000112304_13", "rows": 6, "batch_bytes": 966, "schema": "conv_last_touch", "error": "stream load returned status \"Fail\": 172.30.81.176: partial update on table with sort key must provide all sort key columns"}
2026-05-09T20:07:00.222Z ERROR [StarRocks] flush failed after retries {"label": "sr_requests_1778357220000041371_12", "rows": 7, "batch_bytes": 1155, "schema": "conv_secondary", "error": "stream load returned status \"Fail\": 172.30.78.249: partial update on table with sort key must provide all sort key columns"}
2026-05-09T20:07:00.223Z ERROR [StarRocks] flush failed after retries {"label": "sr_requests_1778357220000019570_10", "rows": 7, "batch_bytes": 1050, "schema": "conv_primary", "error": "stream load returned status \"Fail\": 172.30.75.179: partial update on table with sort key must provide all sort key columns"}
Why does this happen? My table schema is as follows
PRIMARY KEY (received_hour, auction_id, imp_key)
PARTITION BY date_trunc('hour', received_hour)
DISTRIBUTED BY HASH (auction_id) BUCKETS 128
ORDER BY (received, auction_id)
And I already guarantee 4 fields in the JSOn stream load payload for partial update
func buildPartialUpdateRow(schema EventUpdateSchema, receivedTime time.Time, auctionID, impID string, fields map[string]any) (map[string]any, error) {
if time.Since(receivedTime) > 23*time.Hour {
return nil, fmt.Errorf("receivedTime %v is more than 23 hours in the past", receivedTime)
}
spec, err := getPartialUpdateSchemaSpec(schema)
if err != nil {
return nil, err
}
primaryKey, err := srKey.DerivePrimaryKey(receivedTime, auctionID, impID)
if err != nil {
return nil, err
}
updateFields, err := normalizePartialUpdateFields(spec, fields)
if err != nil {
return nil, err
}
row := map[string]any{
"received_hour": primaryKey.ReceivedHour,
"auction_id": primaryKey.AuctionID,
"imp_key": primaryKey.ImpKey,
"received": primaryKey.Received,
}
for key, value := range updateFields {
row[key] = value
}
return row, nil
}Rocky
05/09/2026, 8:13 PMpartial update on table with sort key must provide all sort key columns is very specific. In StarRocks, when you perform a partial update on a Primary Key table that has a decoupled sort key (defined by ORDER BY), you must include all columns listed in that ORDER BY clause in every load.
Based on your schema and code, here is exactly why this is happening and why your current approach is hitting a wall:
1. The Header Mismatch (Likely Root Cause)
Even though your Go code includes "received" and "auction_id" in the JSON map, StarRocks ignores fields in the JSON unless they are explicitly listed in the Stream Load columns header.
* How Stream Load works: When you do a partial update, StarRocks looks at the -H "columns: ..." header to decide which columns to process.
* The Error: If your columns header looks like columns: received_hour, auction_id, imp_key, some_event_field but omits received, the BE will throw this error because it doesn't "see" the required sort key column, even if it's sitting in the JSON body.
* Solution: Ensure your Stream Load request includes received in the columns header.
2. The Architectural Conflict: Sort Key vs. Primary Key
This is the "Smoking Gun" regarding your performance and Column Mode issues.
* Your Schema:
* PRIMARY KEY (received_hour, auction_id, imp_key)
* ORDER BY (received, auction_id)
* The Problem: received is a sort key, but it is not part of your Primary Key.
* The Column Mode Restriction: In StarRocks, Column Mode partial updates only support sort keys that are a subset of the Primary Key.
* Why? Column Mode writes "deltas" to column files. If you update a sort key that isn't a PK, the row's physical position in the sorted file might change. StarRocks cannot re-sort a single column in an immutable file without reading the whole row. Since Column Mode's whole purpose is to avoid reading the whole row, StarRocks disables it or throws errors when sort keys are not part of the identity (PK).
3. Why this is "Killing" your performance
Because received (a sort key) is not in your PK:
1. Column Mode is likely impossible: You are being forced into Row Mode (Read-Modify-Write).
2. The Read-Modify-Write Tax: Every time you update a row, StarRocks has to find it by PK, read all 500 columns from EBS, update the 5 columns (and the received sort key), and write a whole new 500-column row.
3. High Latency: This explains your 2–6s transaction apply latency and the heavy EBS I/O.
Recommendations
Option A: The "Correct" Schema (Best Performance)
If received is the high-precision timestamp, make it part of the Primary Key. This allows it to be a valid sort key for Column Mode.
sql
-- Change PK to include the sort key
PRIMARY KEY (received, auction_id, imp_key)
ORDER BY (received, auction_id)
_Note: This changes your uniqueness grain. If received_hour was used to allow multiple events per hour for one auction_id, adding received (seconds/ms) makes the PK more granular._
Option B: Align Sort Key with existing PK
If you can't change the PK, change the sort key to use only columns already in the PK.
sql
ORDER BY (received_hour, auction_id)
This will immediately make your table compatible with Column Mode partial updates, which will drop your latency from seconds to milliseconds.
Option C: The Row Store (The EBS "Band-Aid")
If you must keep the schema exactly as is, recreate the table with "store_type" = "column_with_row".
* This will keep you in Row Mode, but it will make the "Read" part of the Read-Modify-Write cycle 100x faster on EBS because it will only need 1 I/O seek to read the 500 columns instead of 500.Rocky
05/09/2026, 8:13 PMcolumns string contains received. If it does and still fails, you are hitting the "Sort key must be subset of PK" enforcement for your specific StarRocks version/mode.
References
• loading/Load_to_Primary_Key_tables.md
• sql-reference/sql-statements/table_bucket_part_index/UPDATE.md
• deployment/shared_data/feature-support-shared-data.md
• sql-reference/System_variable.md
• table_design/table_types/primary_key_table.md
• sql-reference/sql-statements/table_bucket_part_index/auto_increment.md