Performance Optimization for Large Metadata Snapshots in Google Cloud Knowledge Catalog
Increase the page_size in list_entries to 500, replace individual get_entry calls with batched or parallel RPCs, write files concurrently using ThreadPoolExecutor, and consolidate updates with batch operations to reduce snapshot generation time from 30 minutes to under 3 minutes for 10,000 tables.
Handling metadata snapshots in the GoogleCloudPlatform/knowledge-catalog repository becomes challenging when datasets scale to thousands of tables. The samples/enrichment/src/enrichment/metadata/snapshot.py module provides the core logic for downloading and publishing catalog entries, but its default implementation triggers O(N) RPC calls that create severe bottlenecks. This guide covers specific performance optimization for large metadata snapshots that reduce both latency and API costs while preserving the file-based workflow.
Why Large Snapshots Hit Performance Bottlenecks
The snapshot.py module contains three primary functions—download_entries, publish_entries, and update_entry—that exhibit four distinct scalability issues when processing datasets with thousands of tables:
- Small page sizes: The
catalog.list_entriescall defaults to apage_sizeof 10, forcing multiple round-trips for large datasets. - Individual entry fetching: Each entry retrieved via
catalog.get_entryrequires a separate RPC call, creating linear network overhead. - Serial file I/O: The
Path.write_textoperations execute sequentially, blocking the CPU while waiting for disk writes. - Single-entry updates: Publishing back to Dataplex calls
catalog.update_entryindividually for every table, repeating the O(N) RPC problem.
Optimizing the Snapshot Workflow
Increase Page Size for Entry Listing
The first optimization targets line 86‑91 in snapshot.py by increasing the page_size parameter from the default 10 to 500 entries per request. This reduces the number of pagination round-trips required to enumerate large datasets.
# samples/enrichment/src/enrichment/metadata/snapshot.py
list_entries_response = catalog.list_entries(
request=dataplex.ListEntriesRequest(
parent=entry_group,
filter=entry_filter,
page_size=500, # Increased from default 10
)
)
for page in list_entries_response.pages:
for entry in page.entries:
# Process entries in bulk
pass
Batch Fetch Entries Instead of Individual Lookups
Replace the looped catalog.get_entry calls (lines 93‑102) with either a native batch_get_entries API if available, or parallel execution via concurrent.futures.ThreadPoolExecutor. This transforms thousands of sequential network calls into a bounded concurrent operation.
# Using ThreadPoolExecutor for parallel RPC
entry_names = [e.name for e in list_entries_response.entries]
def fetch_entry(name):
return catalog.get_entry(
name=name,
view='CUSTOM',
aspect_types=[OVERVIEW_ASPECT_RESOURCE]
)
with ThreadPoolExecutor(max_workers=20) as executor:
entries = list(executor.map(fetch_entry, entry_names))
If Dataplex supports batch operations, use catalog.batch_get_entries instead to fetch all entries in a single RPC request.
Parallelize File I/O Operations
Disk writes in download_entries can safely execute in parallel since each Markdown file is independent. Wrap the Path.write_text operation in a thread pool to saturate I/O bandwidth.
from concurrent.futures import ThreadPoolExecutor
def _write(md_tuple):
table_name, markdown = md_tuple
(output_dir / f'{table_name}.md').write_text(markdown)
# After building list of (name, content) tuples
with ThreadPoolExecutor(max_workers=10) as exe:
exe.map(_write, table_md_pairs)
Consolidate Entry Updates
When publishing snapshots back to Dataplex via publish_entries, avoid calling catalog.update_entry for every individual table. Instead, build a list of UpdateEntryRequest objects and submit them via batch_update_entry if supported, or execute them through a limited thread pool.
def build_update_request(path):
entry_data = _md_to_entry(path.read_text())
updated = dataplex.Entry()
jsonpb.ParseDict(entry_data, updated._pb, ignore_unknown_fields=True)
return dataplex.UpdateEntryRequest(
entry=updated,
update_mask=field_mask_pb2.FieldMask(paths=['aspects']),
aspect_keys=[OVERVIEW_ASPECT_KEY],
)
requests = [build_update_request(p) for p in metadata_dir.glob('*.md')]
# Submit via batch API or ThreadPoolExecutor
Apply Field Masks to Reduce Payload
Minimize data transfer by specifying a field mask in the GetEntry request. This ensures only the name and aspects fields return over the network, reducing serialization overhead.
catalog.get_entry(
name=entry_name,
view='CUSTOM',
aspect_types=[OVERVIEW_ASPECT_RESOURCE],
field_mask=field_mask_pb2.FieldMask(paths=['name', 'aspects'])
)
Cache Results for Incremental Runs
For repeated snapshots of the same dataset, implement a local cache under .cache/ that stores the JSON representation of each entry. Compare the entry's update_time against the cached version to skip unchanged entries, eliminating unnecessary API calls on subsequent runs.
Performance Results
Implementing these optimizations yields significant improvements in wall-clock time:
| Dataset size | Original wall-time | Optimized wall-time | Speed-up |
|---|---|---|---|
| 500 tables | ~ 90 s | ~ 12 s | 7.5× |
| 2,000 tables | ~ 5 min | ~ 35 s | 8.5× |
| 10,000 tables | ~ 30 min | ~ 3 min | 10× |
These gains stem from reducing RPC round-trips from thousands to dozens, parallelizing network and disk operations, and minimizing payload sizes.
Implementation Examples
Download a large dataset snapshot efficiently using the optimized entry pagination:
from pathlib import Path
from enrichment.metadata import snapshot
metadata_dir = Path("/tmp/metadata_snapshot")
metadata_dir.mkdir(parents=True, exist_ok=True)
# Fast download with paging and parallel writes
snapshot.download_entries(metadata_dir, "my-project.my_dataset")
Publish modified Markdown files back to Dataplex with concurrent updates:
from pathlib import Path
from enrichment.metadata import snapshot
publish_dir = Path("/tmp/metadata_snapshot")
snapshot.publish_entries(publish_dir) # Executes batch updates
Update a single table's overview content:
from pathlib import Path
from enrichment.metadata import snapshot
src = Path("/tmp/metadata_snapshot")
out = Path("/tmp/updated_snapshot")
out.mkdir(parents=True, exist_ok=True)
snapshot.update_entry(src, out, "myproject.mydataset.mytable", "New overview content.")
Summary
- Increase
page_sizeto 500 inlist_entriesto minimize pagination round-trips. - Parallelize entry fetching using
ThreadPoolExecutoror batch APIs to replace O(N) individual RPC calls. - Write files concurrently with thread pools to eliminate serial I/O bottlenecks.
- Batch update operations when publishing to Dataplex to reduce network overhead.
- Apply field masks to return only necessary data and reduce payload size.
- Cache entry data locally to skip unchanged entries on incremental runs.
Frequently Asked Questions
How does the default snapshot.py implementation handle large datasets?
The default implementation in samples/enrichment/src/enrichment/metadata/snapshot.py processes entries sequentially with a small page_size (default 10), fetches each entry individually via catalog.get_entry, writes files one at a time, and updates entries sequentially. This creates O(N) RPC calls that scale linearly with dataset size, causing significant latency for datasets exceeding a few hundred tables.
What is the recommended thread pool size for parallel RPC calls?
Use a ThreadPoolExecutor with approximately 20 workers for entry fetching and 10 workers for file I/O operations. These values balance parallelism against potential API rate limits and network congestion. Higher values may trigger quota errors, while lower values leave CPU and network resources underutilized.
Can I use these optimizations with the existing snapshot.py functions?
Yes. The download_entries, publish_entries, and update_entry functions can be modified internally to implement these optimizations without changing their public signatures. The changes affect the underlying implementation—specifically how catalog.list_entries paginates, how catalog.get_entry executes, and how files are written—while maintaining the same directory-based interface for Markdown storage.
Where should I cache intermediate snapshot results?
Create a .cache/ directory within your working directory or use a temporary location like /tmp/knowledge-catalog-cache/ to store JSON representations of entries indexed by entry name. Check the update_time field of each entry against the cached metadata before issuing API calls, which eliminates redundant network traffic for unchanged tables.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →