mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-19 13:46:58 +00:00
perf: optimize broker assignment validation to eliminate 14% CPU overhead
CRITICAL: Assignment validation was running on EVERY LookupTopicBrokers call! Problem (from CPU profile): - ensureTopicActiveAssignments: 14.18% CPU (2.56s out of 18.05s) - EnsureAssignmentsToActiveBrokers: 14.18% CPU (2.56s) - ConcurrentMap.IterBuffered: 12.85% CPU (2.32s) - iterating all brokers - Called on EVERY LookupTopicBrokers request, even with cached config! Root Cause: LookupTopicBrokers flow was: 1. getTopicConfFromCache() - returns cached config (fast ✅) 2. ensureTopicActiveAssignments() - validates assignments (slow ❌) Even though config was cached, we still validated assignments every time, iterating through ALL active brokers on every single request. With 250 requests/sec, this meant 250 full broker iterations per second! Solution: Move assignment validation inside getTopicConfFromCache() and only run it on cache misses: Changes to broker_topic_conf_read_write.go: - Modified getTopicConfFromCache() to validate assignments after filer read - Validation only runs on cache miss (not on cache hit) - If hasChanges: Save to filer immediately, invalidate cache, return - If no changes: Cache config with validated assignments - Added ensureTopicActiveAssignmentsUnsafe() helper (returns bool) - Kept ensureTopicActiveAssignments() for other callers (saves to filer) Changes to broker_grpc_lookup.go: - Removed ensureTopicActiveAssignments() call from LookupTopicBrokers - Assignment validation now implicit in getTopicConfFromCache() - Added comments explaining the optimization Cache Behavior: - Cache HIT: Return config immediately, skip validation (saves 14% CPU!) - Cache MISS: Read filer -> validate assignments -> cache result - If broker changes detected: Save to filer, invalidate cache, return - Next request will re-read and re-validate (ensures consistency) Performance Impact: With 30-second cache TTL and 250 lookups/sec: - Before: 250 validations/sec × 10ms each = 2.5s CPU/sec (14% overhead) - After: 0.17 validations/sec (only on cache miss) - Reduction: 99.93% fewer validations Expected CPU Reduction: - Before (with cache): 18.05s total, 2.56s validation (14%) - After (with optimization): ~15.5s total (-14% = ~2.5s saved) - Combined with previous cache fix: 25.18s -> ~15.5s (38% total reduction) Cache Consistency: - Assignments validated when config first cached - If broker membership changes, assignments updated and saved - Cache invalidated to force fresh read - All brokers eventually converge on correct assignments Testing: - ✅ Compiles successfully - Ready to deploy and measure CPU improvement Priority: CRITICAL - Completes optimization of LookupTopicBrokers hot path
This commit is contained in:
@@ -33,16 +33,18 @@ func (b *MessageQueueBroker) LookupTopicBrokers(ctx context.Context, request *mq
|
||||
ret.Topic = request.Topic
|
||||
|
||||
// Use cached topic config to avoid expensive filer reads (26% CPU overhead!)
|
||||
// getTopicConfFromCache also validates broker assignments on cache miss (saves 14% CPU)
|
||||
conf, err := b.getTopicConfFromCache(t)
|
||||
if err != nil {
|
||||
glog.V(0).Infof("lookup topic %s conf: %v", request.Topic, err)
|
||||
return ret, err
|
||||
}
|
||||
|
||||
err = b.ensureTopicActiveAssignments(t, conf)
|
||||
// Note: Assignment validation is now done inside getTopicConfFromCache on cache misses
|
||||
// This avoids 14% CPU overhead from validating on EVERY lookup
|
||||
ret.BrokerPartitionAssignments = conf.BrokerPartitionAssignments
|
||||
|
||||
return ret, err
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (b *MessageQueueBroker) ListTopics(ctx context.Context, request *mq_pb.ListTopicsRequest) (resp *mq_pb.ListTopicsResponse, err error) {
|
||||
|
||||
@@ -24,7 +24,7 @@ func (b *MessageQueueBroker) GetOrGenerateLocalPartition(t topic.Topic, partitio
|
||||
glog.Errorf("topic %v not found: %v", t, err)
|
||||
return nil, fmt.Errorf("topic %v not found: %w", t, err)
|
||||
}
|
||||
|
||||
|
||||
localTopicPartition, _, getOrGenError = b.doGetOrGenLocalPartition(t, partition, conf)
|
||||
if getOrGenError != nil {
|
||||
glog.Errorf("topic %v partition %v not setup: %v", t, partition, getOrGenError)
|
||||
@@ -45,6 +45,7 @@ func (b *MessageQueueBroker) invalidateTopicCache(t topic.Topic) {
|
||||
|
||||
// getTopicConfFromCache reads topic configuration with caching
|
||||
// Returns the config or error if not found. Uses unified cache to avoid expensive filer reads.
|
||||
// On cache miss, validates broker assignments to ensure they're still active (14% CPU overhead).
|
||||
// This is the public API for reading topic config - always use this instead of direct filer reads.
|
||||
func (b *MessageQueueBroker) getTopicConfFromCache(t topic.Topic) (*mq_pb.ConfigureTopicResponse, error) {
|
||||
topicKey := t.String()
|
||||
@@ -62,7 +63,9 @@ func (b *MessageQueueBroker) getTopicConfFromCache(t topic.Topic) (*mq_pb.Config
|
||||
return nil, fmt.Errorf("topic %v not found (cached)", t)
|
||||
}
|
||||
|
||||
glog.V(4).Infof("Topic cache HIT for %s", topicKey)
|
||||
glog.V(4).Infof("Topic cache HIT for %s (skipping assignment validation)", topicKey)
|
||||
// Cache hit - return immediately without validating assignments
|
||||
// Assignments were validated when we first cached this config
|
||||
return conf, nil
|
||||
}
|
||||
}
|
||||
@@ -72,10 +75,9 @@ func (b *MessageQueueBroker) getTopicConfFromCache(t topic.Topic) (*mq_pb.Config
|
||||
glog.V(4).Infof("Topic cache MISS for %s, reading from filer", topicKey)
|
||||
conf, readConfErr := b.fca.ReadTopicConfFromFiler(t)
|
||||
|
||||
// Cache the result (even if error/not found - negative caching)
|
||||
b.topicCacheMu.Lock()
|
||||
if readConfErr != nil {
|
||||
// Negative cache: topic doesn't exist
|
||||
b.topicCacheMu.Lock()
|
||||
b.topicCache[topicKey] = &topicCacheEntry{
|
||||
conf: nil,
|
||||
expiresAt: time.Now().Add(b.topicCacheTTL),
|
||||
@@ -85,7 +87,30 @@ func (b *MessageQueueBroker) getTopicConfFromCache(t topic.Topic) (*mq_pb.Config
|
||||
return nil, fmt.Errorf("topic %v not found: %w", t, readConfErr)
|
||||
}
|
||||
|
||||
// Positive cache: topic exists with config
|
||||
// Validate broker assignments before caching (NOT holding cache lock)
|
||||
// This ensures cached configs always have valid broker assignments
|
||||
// Only done on cache miss (not on every lookup), saving 14% CPU
|
||||
glog.V(4).Infof("Validating broker assignments for %s", topicKey)
|
||||
hasChanges := b.ensureTopicActiveAssignmentsUnsafe(t, conf)
|
||||
if hasChanges {
|
||||
glog.V(0).Infof("topic %v partition assignments updated due to broker changes", t)
|
||||
// Save updated assignments to filer immediately to ensure persistence
|
||||
if err := b.fca.SaveTopicConfToFiler(t, conf); err != nil {
|
||||
glog.Errorf("failed to save updated topic config for %s: %v", topicKey, err)
|
||||
// Don't cache on error - let next request retry
|
||||
return conf, err
|
||||
}
|
||||
// Invalidate cache to force fresh read on next request
|
||||
// This ensures all brokers see the updated assignments
|
||||
b.topicCacheMu.Lock()
|
||||
delete(b.topicCache, topicKey)
|
||||
b.topicCacheMu.Unlock()
|
||||
glog.V(4).Infof("Invalidated cache for %s after assignment update", topicKey)
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// Positive cache: topic exists with validated assignments
|
||||
b.topicCacheMu.Lock()
|
||||
b.topicCache[topicKey] = &topicCacheEntry{
|
||||
conf: conf,
|
||||
expiresAt: time.Now().Add(b.topicCacheTTL),
|
||||
@@ -142,9 +167,18 @@ func (b *MessageQueueBroker) genLocalPartitionFromFiler(t topic.Topic, partition
|
||||
return localPartition, isGenerated, nil
|
||||
}
|
||||
|
||||
func (b *MessageQueueBroker) ensureTopicActiveAssignments(t topic.Topic, conf *mq_pb.ConfigureTopicResponse) (err error) {
|
||||
// ensureTopicActiveAssignmentsUnsafe validates that partition assignments reference active brokers
|
||||
// Returns true if assignments were changed. Caller must save config to filer if hasChanges=true.
|
||||
// Note: Assumes caller holds topicCacheMu lock or is OK with concurrent access to conf
|
||||
func (b *MessageQueueBroker) ensureTopicActiveAssignmentsUnsafe(t topic.Topic, conf *mq_pb.ConfigureTopicResponse) (hasChanges bool) {
|
||||
// also fix assignee broker if invalid
|
||||
hasChanges := pub_balancer.EnsureAssignmentsToActiveBrokers(b.PubBalancer.Brokers, 1, conf.BrokerPartitionAssignments)
|
||||
hasChanges = pub_balancer.EnsureAssignmentsToActiveBrokers(b.PubBalancer.Brokers, 1, conf.BrokerPartitionAssignments)
|
||||
return hasChanges
|
||||
}
|
||||
|
||||
func (b *MessageQueueBroker) ensureTopicActiveAssignments(t topic.Topic, conf *mq_pb.ConfigureTopicResponse) (err error) {
|
||||
// Validate and save if needed
|
||||
hasChanges := b.ensureTopicActiveAssignmentsUnsafe(t, conf)
|
||||
if hasChanges {
|
||||
glog.V(0).Infof("topic %v partition updated assignments: %v", t, conf.BrokerPartitionAssignments)
|
||||
if err = b.fca.SaveTopicConfToFiler(t, conf); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user