← Назад к вопросам

Какие знаешь способы определить, чем заполнен кэш в момент ошибки сервиса?

3.0 Senior🔥 21 комментариев
#Docker, Kubernetes и DevOps#Кэширование и NoSQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Способы определить содержимое кэша при ошибке сервиса

Это критичный вопрос для production debugging. Расскажу о подходах, которые использую для анализа состояния кэша во время сбоев.

1. Логирование ключей и значений при ошибке

Простой способ — логировать кэш при исключении:

@Service
public class CacheService {
    private final ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();
    private final Logger logger = LoggerFactory.getLogger(CacheService.class);
    
    public Object get(String key) {
        try {
            Object value = cache.get(key);
            if (value == null) {
                throw new CacheException("Key not found: " + key);
            }
            return value;
        } catch (Exception e) {
            // Логируем состояние кэша при ошибке
            logger.error("Error getting key: {}", key, e);
            logger.error("Cache state: size={}, keys={}", 
                cache.size(), 
                cache.keySet());
            
            // Логируем первые 10 ключей с их типами
            cache.entrySet().stream()
                .limit(10)
                .forEach(entry -> logger.error("  {} -> {}",
                    entry.getKey(),
                    entry.getValue().getClass().getSimpleName()));
            
            throw e;
        }
    }
}

Проблема: если кэш содержит чувствительные данные (пароли, токены), логирование может быть опасным.

2. Защита чувствительных данных в логах

@Service
public class SecureCacheService {
    private final ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();
    private final List<String> sensitiveKeyPatterns = List.of(
        "password.*", "token.*", "secret.*", "api_key.*"
    );
    
    private String sanitizeValue(String key, Object value) {
        // Скрываем значение если ключ matching чувствительному паттерну
        for (String pattern : sensitiveKeyPatterns) {
            if (key.matches(pattern)) {
                return "[REDACTED]";
            }
        }
        
        // Для остальных значений логируем только класс
        return value.getClass().getSimpleName() + "@" + 
               Integer.toHexString(System.identityHashCode(value));
    }
    
    public void logCacheState(String context) {
        logger.info("Cache state at {}: size={}", context, cache.size());
        cache.entrySet().stream()
            .limit(100)
            .forEach(entry -> logger.info(
                "  {} -> {}",
                entry.getKey(),
                sanitizeValue(entry.getKey(), entry.getValue())
            ));
    }
}

3. Spring Cache Abstractions для мониторинга

Используй Spring's CacheManager для получения метрик:

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("users", "products", "orders");
    }
}

@Service
public class CacheMonitorService {
    private final CacheManager cacheManager;
    private final Logger logger = LoggerFactory.getLogger(CacheMonitorService.class);
    
    public CacheMonitorService(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }
    
    public void printCacheState() {
        cacheManager.getCacheNames().forEach(cacheName -> {
            Cache cache = cacheManager.getCache(cacheName);
            if (cache instanceof ConcurrentMapCache) {
                ConcurrentMapCache mapCache = (ConcurrentMapCache) cache;
                // Получаем нативную карту
                ConcurrentHashMap<Object, Object> nativeCache = 
                    (ConcurrentHashMap<Object, Object>) mapCache.getNativeCache();
                
                logger.info("Cache '{}': size={}", cacheName, nativeCache.size());
                nativeCache.keySet().stream()
                    .limit(50)
                    .forEach(key -> logger.info("  key: {}", key));
            }
        });
    }
    
    public Map<String, CacheStatistics> getCacheStatistics() {
        return cacheManager.getCacheNames().stream()
            .collect(Collectors.toMap(
                cacheName -> cacheName,
                this::getStatisticsForCache
            ));
    }
    
    private CacheStatistics getStatisticsForCache(String cacheName) {
        Cache cache = cacheManager.getCache(cacheName);
        ConcurrentMapCache mapCache = (ConcurrentMapCache) cache;
        ConcurrentHashMap<Object, Object> nativeCache = 
            (ConcurrentHashMap<Object, Object>) mapCache.getNativeCache();
        
        return CacheStatistics.builder()
            .cacheName(cacheName)
            .size(nativeCache.size())
            .keys(new ArrayList<>(nativeCache.keySet()))
            .build();
    }
}

4. Трассировка с MDC (Mapped Diagnostic Context)

Используй SLF4J MDC для отслеживания контекста ошибки:

@Service
public class UserCacheService {
    private final ConcurrentHashMap<String, User> userCache = new ConcurrentHashMap<>();
    private final Logger logger = LoggerFactory.getLogger(UserCacheService.class);
    
    public User getUser(String userId) {
        try {
            // Добавляем контекст в MDC
            MDC.put("cache_operation", "getUser");
            MDC.put("user_id", userId);
            MDC.put("cache_size", String.valueOf(userCache.size()));
            
            User user = userCache.get(userId);
            if (user == null) {
                throw new UserNotFoundException("User not found in cache: " + userId);
            }
            return user;
        } catch (Exception e) {
            MDC.put("error_message", e.getMessage());
            MDC.put("cache_keys_sample", 
                userCache.keySet().stream()
                    .limit(5)
                    .collect(Collectors.joining(",")));
            
            logger.error("Failed to get user from cache", e);
            throw e;
        } finally {
            // Не забыть очистить MDC
            MDC.clear();
        }
    }
}

// logback.xml
<configuration>
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>logs/app.log</file>
        <encoder>
            <pattern>
                %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} 
                [%X{cache_operation}:%X{user_id}] - %msg%n
            </pattern>
        </encoder>
    </appender>
</configuration>

5. Redis/Caffeine Cache Metrics

Для Redis используй Redis CLI:

# Подключение к Redis
redis-cli

# Получить все ключи
KEYS *

# Получить размер БД
DBSIZE

# Информация о памяти
INFO memory

# Получить значение ключа
GET mykey

# Вывести количество ключей по паттерну
KEYS user:*
CARDINALITY

# Для больших БД используй SCAN
SCAN 0 MATCH "user:*" COUNT 100

Для Caffeine Cache:

@Configuration
@EnableCaching
public class CaffeineCacheConfig {
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("users", "products");
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .recordStats()  // Включаем сбор статистики!
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .maximumSize(10000));
        return cacheManager;
    }
}

@Service
public class CacheStatsService {
    @Autowired
    private CacheManager cacheManager;
    
    public void printCacheStats() {
        cacheManager.getCacheNames().forEach(cacheName -> {
            Cache cache = cacheManager.getCache(cacheName);
            com.github.benmanes.caffeine.cache.Cache<?, ?> caffeineCache =
                (com.github.benmanes.caffeine.cache.Cache<?, ?>) 
                    cache.getNativeCache();
            
            CacheStats stats = caffeineCache.stats();
            System.out.println("Cache: " + cacheName);
            System.out.println("  Hits: " + stats.hitCount());
            System.out.println("  Misses: " + stats.missCount());
            System.out.println("  Hit rate: " + stats.hitRate());
            System.out.println("  Evictions: " + stats.evictionCount());
        });
    }
}

6. Actuator Endpoint для dump кэша

Создай custom endpoint для получения состояния кэша:

@Component
@Endpoint(id = "cache-state")
public class CacheStateEndpoint {
    private final CacheManager cacheManager;
    
    public CacheStateEndpoint(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }
    
    @ReadOperation
    public Map<String, Map<String, Object>> getState() {
        Map<String, Map<String, Object>> result = new HashMap<>();
        
        cacheManager.getCacheNames().forEach(cacheName -> {
            Cache cache = cacheManager.getCache(cacheName);
            ConcurrentMapCache mapCache = (ConcurrentMapCache) cache;
            ConcurrentHashMap<Object, Object> nativeCache = 
                (ConcurrentHashMap<Object, Object>) mapCache.getNativeCache();
            
            Map<String, Object> cacheInfo = new HashMap<>();
            cacheInfo.put("size", nativeCache.size());
            cacheInfo.put("keys", new ArrayList<>(nativeCache.keySet()));
            
            result.put(cacheName, cacheInfo);
        });
        
        return result;
    }
    
    @ReadOperation
    public Map<String, Object> getCacheDetails(@Selector String cacheName) {
        Cache cache = cacheManager.getCache(cacheName);
        ConcurrentMapCache mapCache = (ConcurrentMapCache) cache;
        ConcurrentHashMap<Object, Object> nativeCache = 
            (ConcurrentHashMap<Object, Object>) mapCache.getNativeCache();
        
        return Map.of(
            "cacheName", cacheName,
            "size", nativeCache.size(),
            "keys", nativeCache.keySet(),
            "sample", nativeCache.entrySet().stream()
                .limit(10)
                .collect(Collectors.toMap(
                    e -> String.valueOf(e.getKey()),
                    e -> e.getValue().getClass().getSimpleName()
                ))
        );
    }
}

// application.yml
management:
  endpoints:
    web:
      exposure:
        include: cache-state  # Теперь доступно на /actuator/cache-state

7. Профилирование памяти при ошибке

Используй JDK Flight Recorder для recording heap dump:

@Service
public class HeapDumpService {
    private final Logger logger = LoggerFactory.getLogger(HeapDumpService.class);
    
    public void captureHeapOnError(Exception e) {
        try {
            String timestamp = LocalDateTime.now().format(
                DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")
            );
            String filename = "heap-dump-" + timestamp + ".hprof";
            
            // Используем HotSpotDiagnosticsMXBean для создания heap dump
            HotSpotDiagnosticsMXBean bean = ManagementFactory.getPlatformMXBean(
                HotSpotDiagnosticsMXBean.class
            );
            bean.dumpHeap(filename, false);
            
            logger.info("Heap dump created: {}", filename);
        } catch (Exception ex) {
            logger.error("Failed to create heap dump", ex);
        }
    }
}

8. Декоратор для логирования кэша

public class CacheableWithLogging {
    private final CacheManager cacheManager;
    private final Logger logger = LoggerFactory.getLogger(CacheableWithLogging.class);
    
    @Around("@annotation(org.springframework.cache.annotation.Cacheable)")
    public Object logCacheOperation(ProceedingJoinPoint pjp) throws Throwable {
        String methodName = pjp.getSignature().getName();
        
        try {
            Object result = pjp.proceed();
            logger.debug("Cache HIT for method: {}", methodName);
            return result;
        } catch (Exception e) {
            logger.error("Cache operation failed for method: {}", methodName, e);
            
            // Логируем состояние всех кэшей
            cacheManager.getCacheNames().forEach(cacheName -> {
                Cache cache = cacheManager.getCache(cacheName);
                logger.error("Cache '{}' has {} entries", 
                    cacheName, 
                    ((ConcurrentMapCache) cache).getNativeCache().size());
            });
            
            throw e;
        }
    }
}

Best Practices

  • Логируй ключи, не значения — экономит место, защищает от утечек
  • Используй MDC для контекста в многопоточной среде
  • Тестируй сценарии с полным кэшем — ошибки часто появляются при edge cases
  • Установи пределы размера кэша — OutOfMemory ошибки обычно связаны с расростанием кэша
  • Мониторь hit rate — низкий hit rate значит проблему в кэшировании
  • Создавай heap dump при критичных ошибках для offline анализа
  • Версионируй структуру кэшируемых объектов — изменения структуры могут привести к корруптации

В целом: мониторинг и отладка кэша требует комплексного подхода. Всегда имей план для быстрого доступа к информации о состоянии кэша в production.

Какие знаешь способы определить, чем заполнен кэш в момент ошибки сервиса? | PrepBro