Cloud Bigtable
Bigtable is GCP's NoSQL wide-column database — the same technology behind Google Search, Gmail, and Google Maps. Designed for petabytes of data with single-digit millisecond latency.
Architecture
graph TD
CLIENT["Client (HBase API / Cloud Bigtable API)"] --> FE["Frontend servers<br/>(route requests)"]
FE --> TABLET["Tablet servers<br/>own ranges of row keys<br/>serve reads/writes"]
TABLET --> COLOSSUS["Colossus (GFS successor)<br/>SSTable files<br/>WAL (commit log)"]
MASTER["Master node<br/>tablet assignment<br/>load balancing<br/>schema management"] --> TABLET
Key design:
- Data stored in SSTables (sorted string tables) on Colossus — separate from tablet servers
- Tablet servers are stateless — can be replaced, restarted without data loss
- Automatic load balancing: master moves tablets between servers as traffic shifts
Data Model — Wide Columns
Table: user-events
Row key | CF:actions | CF:profile
-----------------+--------------------+------------------
user#123#2024 | click:1700000000 | name: "Alice"
| purchase:1700001000| email: "a@b.com"
user#456#2024 | click:1700002000 |
| view:1700003000 |
user#789#2023 | click:1699000000 | name: "Bob"
Concepts:
- Row key — the only index. All queries must use row key prefix. Design it carefully.
- Column family (CF) — group of related columns. Defined at table creation.
CF:actions,CF:profile - Column qualifier — dynamic, can be anything within a CF. Created at write time.
- Cell — value at (row key, column family, column qualifier, timestamp). Multiple versions kept.
Row Key Design — Critical for Performance
graph TD
BAD["❌ Bad row key: user_id=123<br/>All user 123's data in one place<br/>→ hotspot on one tablet server<br/>→ one server overwhelmed, others idle"]
GOOD["✅ Good row key: reverse_domain#timestamp<br/>e.g. com.example.user#1700000000#event_type<br/>→ writes spread across all tablets<br/>→ range scans still efficient"]
SALT["✅ Salted row key: hash(user_id)%N + user_id<br/>→ distribute writes, enable parallel reads"]
Row key anti-patterns:
Sequential IDs (1, 2, 3...) → all writes hit the last tablet (hotspot)
Timestamps at start → all recent writes go to same tablet
Domain names (www.example.com) → reverse to com.example.www for better distribution
Row key patterns:
Reverse domain: com.example.user#event_type#timestamp
User + time range: user:123#2024-01#event_type (scan by user+month)
Hash prefix: a3f2#user:123#timestamp (distribute hot users)
Reads and Writes
sequenceDiagram
participant APP as Application
participant FE as Frontend
participant TS as Tablet Server
participant COL as Colossus
Note over APP,COL: Write path
APP->>FE: MutateRow(row_key, mutations)
FE->>TS: Route to correct tablet server
TS->>COL: Append to WAL (durability)
TS->>TS: Write to in-memory MemTable
TS-->>APP: OK (synchronous after WAL write)
Note over TS: Background: flush MemTable to SSTable on Colossus
Note over APP,COL: Read path
APP->>FE: ReadRow(row_key)
FE->>TS: Route to tablet server owning this row key
TS->>TS: Check MemTable (recent writes)
TS->>COL: Check SSTables (older data)
TS->>TS: Merge results (latest version wins)
TS-->>APP: Row data
Compaction — LSM Tree
Bigtable uses an LSM (Log-Structured Merge) tree, same as LevelDB/RocksDB:
graph TD
WRITE["Write"] --> MEM["MemTable (RAM)"]
MEM -->|"flush when full"| L0["Level 0 SSTables<br/>(small, overlapping)"]
L0 -->|"minor compaction"| L1["Level 1 SSTables<br/>(larger, sorted)"]
L1 -->|"major compaction"| L2["Level 2 SSTables<br/>(larger, non-overlapping)"]
Note["Read: check each level, merge results<br/>More levels = slower reads (more files to check)<br/>Compaction reduces levels = faster reads"]
Compaction types:
- Minor: flush MemTable to L0 — happens frequently, fast
- Major: merge L0→L1, L1→L2 — reclaims space from deleted/updated cells, improves read performance
Bigtable vs Other Databases
| Bigtable | BigQuery | Spanner | Firestore | |
|---|---|---|---|---|
| Type | Wide-column NoSQL | Data warehouse | NewSQL relational | Document NoSQL |
| Latency | Single-digit ms | Seconds | ~10ms | ~10ms |
| Scale | Petabytes | Petabytes | Petabytes | Terabytes |
| SQL | HBase API only | Full SQL | Full SQL | Limited |
| Transactions | Single-row atomic | No | Full ACID | Optimistic |
| Best for | Time-series, IoT, ML features | Analytics, reporting | Financial, global OLTP | Mobile/web apps |
Bigtable vs HBase
Bigtable is API-compatible with Apache HBase. HBase code works with minimal changes.
// HBase / Bigtable Java client
Connection connection = BigtableConfiguration.connect(projectId, instanceId);
Table table = connection.getTable(TableName.valueOf("user-events"));
// Write
Put put = new Put(Bytes.toBytes("user#123#2024"));
put.addColumn(Bytes.toBytes("actions"), Bytes.toBytes("click"), Bytes.toBytes("button1"));
table.put(put);
// Read single row
Get get = new Get(Bytes.toBytes("user#123#2024"));
Result result = table.get(get);
// Scan prefix (all events for user 123)
Scan scan = new Scan();
scan.setRowPrefixFilter(Bytes.toBytes("user#123"));
ResultScanner scanner = table.getScanner(scan);
Monitoring and Optimization
# Check Bigtable metrics in Cloud Monitoring
# Key metrics:
# bigtable.googleapis.com/server/latencies → p99 read/write latency
# bigtable.googleapis.com/server/request_count → QPS by type
# bigtable.googleapis.com/server/error_count → errors by code
# bigtable.googleapis.com/cluster/cpu_load → hotspot indicator (should be <70%)
# bigtable.googleapis.com/cluster/storage_utilization
# Key Visualizer: GCP console tool showing read/write patterns across row key space
# Hotspots appear as bright spots — means row key design needs improvement
Performance tips:
- Use batch mutations for bulk writes (reduces round trips)
- Pre-split table into tablets at creation time for known row key patterns
- Use
ReadModifyWriteRowfor atomic increment/append operations - Set cell versions limit (default is unlimited — old versions waste storage)
# Set max versions per cell family
cbt -project=my-project -instance=my-instance setgcpolicy my-table actions maxversions=1
Replication & App Profiles
A Bigtable instance can have multiple clusters in different zones/regions. Adding a second cluster turns on replication: every write is asynchronously copied to all clusters. Replication is eventually consistent and per-cluster — each cluster has its own nodes and serves reads/writes locally, and there is no cross-cluster consensus. This gives HA, geographic read locality, and workload isolation (e.g. serving vs batch), but a read on cluster B may not yet see a write that just landed on cluster A.
App profiles decide how a client's requests are routed across those clusters:
graph TD
APP["Client + app profile"] --> ROUTE{"Routing policy"}
ROUTE -->|"single-cluster routing"| C1["Cluster A (primary)<br/>read-your-writes<br/>conditional / RMW writes OK"]
ROUTE -->|"multi-cluster routing"| LB["Nearest available cluster<br/>auto-failover<br/>eventual consistency"]
LB --> C1
LB --> C2["Cluster B"]
C1 -.->|"async replication"| C2
- Multi-cluster routing — requests go to the nearest available cluster and automatically fail over if one is down. Best availability, but only eventual consistency. Single-row transactions are not safe here because
ReadModifyWrite(atomic increment/append) andCheckAndMutate(conditional write) could hit different clusters and race. - Single-cluster routing — pin the app profile to one cluster. Required for read-your-writes consistency and for single-row transactions (
ReadModifyWriteRow,CheckAndMutateRow), because those atomic ops must serialize on one cluster. Trade-off: no automatic failover for that profile.
A common pattern: one single-cluster app profile for the transactional/serving path, and one multi-cluster app profile for read-heavy or batch workloads.
# Single-cluster routing profile — needed for conditional / read-modify-write consistency
gcloud bigtable app-profiles create serving-profile \
--instance=my-instance \
--route-to=cluster-a \
--transactional-writes \
--description="Serving path (read-your-writes, single-row txns)"
# Multi-cluster routing profile — HA + auto-failover, eventual consistency
gcloud bigtable app-profiles create batch-profile \
--instance=my-instance \
--route-any \
--description="Batch/analytics reads, nearest cluster"
# cbt equivalent
cbt -project=my-project -instance=my-instance createappprofile my-instance serving-profile \
"Serving path" route-to=cluster-a
Autoscaling
Instead of provisioning a fixed node count per cluster, Bigtable can autoscale nodes based on utilization targets. Scaling is per-cluster and node changes are non-disruptive (data lives on Colossus, so nodes just re-own tablets).
- min / max nodes — the bounds Bigtable stays within.
- CPU target utilization — target average CPU load (e.g. 60%); Bigtable adds nodes when CPU exceeds it, removes them when below.
- Storage target utilization — target storage-per-node (e.g. 2560 GB SSD / node); protects against hitting the hard per-node storage limit even when CPU is low. Bigtable scales up to satisfy whichever target (CPU or storage) needs more nodes.
Manual vs autoscaling: use manual for steady, predictable load or when you must cap cost precisely, and to pre-provision before a known spike (autoscaling reacts, it doesn't predict). Use autoscaling for variable/diurnal traffic to avoid over-provisioning while keeping p99 latency in check. Set min-nodes high enough to absorb sudden bursts, since scale-up is gradual.
# Enable autoscaling on a cluster (replaces fixed --num-nodes)
gcloud bigtable clusters update cluster-a \
--instance=my-instance \
--autoscaling-min-nodes=3 \
--autoscaling-max-nodes=30 \
--autoscaling-cpu-target=60 \
--autoscaling-storage-target=2560 # GB per node (SSD)
# Revert to manual scaling with a fixed node count
gcloud bigtable clusters update cluster-a \
--instance=my-instance \
--num-nodes=5