İçeriğe atla

Ölçekte Bildirim Teslimatı Hatalarını Debug Etmek

Yüksek riskli üretim ortamlarında bildirim sistemi hatalarından edinilen gerçek dünya debugging teknikleri, izleme stratejileri ve dersler

Ayhan Sipahi Ayhan Sipahi

Bildirim sistemleri en kötü zamanlarda sessizce başarısız olur: yoğun yükte, bir lansman ya da kampanyanın bağımlı olduğu anda. Hoş geldin e-postaları gelmez, push timeout olur, uygulama içi alert’ler gecikir, ama dashboard’lar yeşil görünmeye devam eder, çünkü her alt-sistem izole bakıldığında sorunsuzdur.

İzleme sinyallerini kullanıcıya olan uzaklığına göre sırala ve kontrol etmediğin her bağımlılığın önüne bir circuit breaker koy, çünkü CPU ve memory alertleri enkazı anlatır ama hangi alt-sistemin başlattığını söylemez.

Yoğun Trafik Altında Kademeli Hata#

Günde milyonlarca mesajı email, push ve uygulama içi kanallardan taşıyan bir hat aylarca sorunsuz çalışabilir, sonra trafik zirvesinden dakikalar sonra her kanalda birden çöker. Hata genelde tek bir bug değildir: her alt-sistemin savunma davranışının bir sonraki alt-sistem için yüke dönüştüğü bir zincirdir.

İlk Belirtiler#

İşe yarayan kısım alertlerin sırasıdır. Önce email teslimat oranı düşer, çünkü sağlayıcı düşen reputation skoru üzerinden throttle uygular. Ardından push timeout vermeye başlar. Sonra WebSocket bağlantıları birikir ve uygulama içi bildirimler dakikalarca gecikir. O penceredeki izleme görüntüsü şuna benzer:

// Alertlerin raporladığı
const alertTimeline = [
  { time: '06:15', service: 'email', metric: 'delivery_rate', value: 60, threshold: 95 },
  { time: '06:16', service: 'push', metric: 'timeout_rate', value: 25, threshold: 5 },
  { time: '06:18', service: 'websocket', metric: 'connection_count', value: 85000, threshold: 50000 },
  { time: '06:20', service: 'database', metric: 'connection_pool', value: 95, threshold: 80 },
  { time: '06:22', service: 'redis', metric: 'memory_usage', value: 92, threshold: 85 }
];

// Her sayının bir alt katmandaki karşılığı
const realityCheck = {
  emailProvider: 'Reputation score düşüşü nedeniyle rate limiting yapıyor',
  pushService: "Apple APNS, template bug'ından gelen bozuk payload'ları reddediyor",
  websockets: "Başarısız push kayıtlarını yeniden deneyen mobil app'ten connection storm",
  database: "Eşzamanlı bildirim tercih güncellemelerinden deadlock'lar",
  redis: 'Sınırsız connection metadata depolamadan bellek tükenmesi'
};

Debugging Süreci#

Adım 1: Kanamayı Durdur

Servisleri yeniden başlatmak ilk içgüdüdür ve çoğunlukla yanlış olandır: yeniden başlatma uçuştaki işi düşürür ve sistemi zaten dolduran retry storm’unu besler. Bunun yerine her kanalın önüne konan circuit breaker’lar hasarı sınırlar.

class EmergencyCircuitBreaker {
  private isOpen = false;
  private openedAt?: Date;
  private failureCount = 0;
  private readonly failureThreshold = 10;
  private readonly recoveryTimeoutMs = 30000;

  async executeWithBreaker<T>(
    operation: () => Promise<T>,
    fallback?: () => Promise<T>
  ): Promise<T> {
    if (this.isOpen) {
      if (this.shouldAttemptReset()) {
        console.log('Circuit breaker reset deneniyor');
        this.isOpen = false;
        this.failureCount = 0;
      } else {
        if (fallback) {
          return await fallback();
        }
        throw new Error('Circuit breaker açık');
      }
    }

    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      if (fallback && this.isOpen) {
        return await fallback();
      }
      throw error;
    }
  }

  private onSuccess(): void {
    this.failureCount = 0;
  }

  private onFailure(): void {
    this.failureCount++;
    if (this.failureCount >= this.failureThreshold) {
      this.isOpen = true;
      this.openedAt = new Date();
      console.warn(`Circuit breaker ${this.failureCount} hatadan sonra açıldı`);
    }
  }
}

// Circuit breaker'lı acil bildirim servisi
class EmergencyNotificationService {
  private emailBreaker = new EmergencyCircuitBreaker();
  private pushBreaker = new EmergencyCircuitBreaker();
  private websocketBreaker = new EmergencyCircuitBreaker();

  async processNotification(event: NotificationEvent): Promise<void> {
    // Circuit breaker'lar ve fallback'lerle birincil kanalları dene
    await Promise.allSettled([
      this.emailBreaker.executeWithBreaker(
        () => this.sendEmail(event),
        () => this.queueForLaterDelivery(event, 'email')
      ),
      this.pushBreaker.executeWithBreaker(
        () => this.sendPush(event),
        () => this.sendWebSocketFallback(event)
      ),
      this.websocketBreaker.executeWithBreaker(
        () => this.sendWebSocket(event),
        () => this.storeForPolling(event)
      )
    ]);
  }
}

Adım 2: Kök Nedeni İzle

Hasar sınırlandıktan sonra soru şu: neden bütün kanallar aynı anda çöktü? Servisler arasında birleştirilen correlation ID’ler bunu yanıtlar:

// Trace'lerden yeniden kurulan kademeli hata sırası
const traceAnalysis = {
  '06:14:45': 'Buggy push token kaydıyla yeni app versiyonu deploy edildi',
  '06:15:00': "Bozuk push payload'ları APNS'in reddetmesine ve bağlantıları kapatmasına neden oldu",
  '06:15:30': 'Mobil app push kaydını yeniden deniyor, WebSocket connection storm yaratıyor',
  '06:16:00': 'Tercih güncelleme sorgularıyla veritabanı connection pool tükendi',
  '06:16:30': 'Email servisi yedek sağlayıcıya geçiyor, rate limitleri tetikliyor',
  '06:17:00': 'Redis memory öksüz connection metadata ile doluyor',
  '06:17:30': 'Sistem tam kademeli hata moduna giriyor'
};

// Deseni ortaya çıkaran debugging sorgusu
const debugQuery = `
  SELECT 
    ne.correlation_id,
    ne.notification_type,
    nd.channel,
    nd.status,
    nd.error_message,
    nd.created_at
  FROM notification_events ne
  JOIN notification_deliveries nd ON ne.id = nd.event_id
  WHERE ne.created_at > NOW() - INTERVAL '1 hour'
    AND nd.status IN ('failed', 'timeout')
  ORDER BY nd.created_at DESC
  LIMIT 1000;
`;

Observability Hiyerarşileri#

Aşağıdaki hiyerarşi bu sıralamayı somut bir yapıya döker:

interface ObservabilityHierarchy {
  // Seviye 1: Kullanıcı Etkisi (Müşterilerin gördüğü)
  userImpact: {
    notificationsReceived: number;
    averageDeliveryTime: number;
    userComplaints: number;
  };
  
  // Seviye 2: Servis Sağlığı (Sistemlerimizin performansı)  
  serviceHealth: {
    deliveryRates: Record<NotificationChannel, number>;
    errorRates: Record<string, number>;
    responseTimes: Record<string, number>;
  };
  
  // Seviye 3: Altyapı (Kaputun altında olan)
  infrastructure: {
    databaseConnections: number;
    redisMemory: number;
    queueDepths: Record<string, number>;
  };
  
  // Seviye 4: Harici Bağımlılıklar (Kontrol etmediğimiz şeyler)
  externalDeps: {
    emailProviderStatus: string;
    pushProviderLatency: number;
    cloudServiceHealth: string;
  };
}

class HierarchicalMonitoring {
  async assessSystemHealth(): Promise<SystemHealthSnapshot> {
    // Kullanıcı etkisiyle başla - bu gerçekten önemli olan
    const userImpact = await this.getUserImpactMetrics();
    
    if (userImpact.isHealthy) {
      return { status: 'healthy', details: userImpact };
    }
    
    // Kullanıcı etkisi kötüyse, hiyerarşi boyunca derine in
    const serviceHealth = await this.getServiceHealthMetrics();
    const infrastructure = await this.getInfrastructureMetrics(); 
    const externalDeps = await this.getExternalDepMetrics();
    
    // Hiyerarşi seviyeleri boyunca sorunları ilişkilendir
    const rootCause = this.correlateIssues({
      userImpact,
      serviceHealth, 
      infrastructure,
      externalDeps
    });
    
    return {
      status: 'degraded',
      rootCause,
      remediationSteps: this.generateRemediationPlan(rootCause)
    };
  }
}

Yük Altında Template Rendering#

Çok dilli, dinamik içerikli ve kullanıcıya göre kişiselleştirilen template’ler, hattın tamamını durduran ikinci yaygın kaynaktır, çünkü template motoru sunum katmanı gibi görünür ve uygulama koduna uygulanan sorgu bütçesini nadiren alır.

Template İçindeki Gizli N+1#

Template yazarı küçük görünen bir özellik ekler: hoş geldin e-postasında kullanıcının son aktiviteleri. Template zararsız görünür:

{{#each user.recentActivities}}
  <div class="activity-item">
    <span>{{formatDate this.createdAt}}</span>
    <span>{{this.description}}</span>
    {{#if this.projectName}}
      <span>{{getProjectDetails this.projectId}} içinde</span>
    {{/if}}
  </div>
{{/each}}

getProjectDetails helper’ı, render döngüsünün içinde her kullanıcının her aktivitesi için bir veritabanı sorgusu çalıştırır.

Render Yolunu Profillemek#

Belirtiler başta belirsizdir: email teslimatları yavaşlar, sonra tamamen timeout olur. CPU yükselir ama memory sabit kalır, veritabanı da belirgin bir darboğaz göstermez. Render başına sorgu sayan bir profiler nedeni görünür kılar:

class TemplatePerformanceProfiler {
  private renderTimes: Map<string, number[]> = new Map();
  private queryCount: Map<string, number> = new Map();
  private activeRenders: Map<string, Date> = new Map();

  async profileRender(
    templateId: string,
    templateContent: string,
    data: any
  ): Promise<ProfiledRenderResult> {
    const renderId = `${templateId}-${Date.now()}`;
    this.activeRenders.set(renderId, new Date());
    
    // Template başına sorguları saymak için veritabanı çağrılarını sarala
    const originalQuery = this.db.query;
    let queryCount = 0;
    
    this.db.query = (...args) => {
      queryCount++;
      return originalQuery.apply(this.db, args);
    };
    
    try {
      const startTime = Date.now();
      const result = await this.templateEngine.render(templateContent, data);
      const renderTime = Date.now() - startTime;
      
      // Performans metriklerini sakla
      if (!this.renderTimes.has(templateId)) {
        this.renderTimes.set(templateId, []);
      }
      this.renderTimes.get(templateId)!.push(renderTime);
      this.queryCount.set(renderId, queryCount);
      
      // Şüpheli desenlerde alert
      if (queryCount > 10) {
        console.warn(`Template ${templateId} render sırasında ${queryCount} DB sorgusu yaptı`);
      }
      
      if (renderTime > 1000) {
        console.warn(`Template ${templateId} render için ${renderTime}ms sürdü`);
      }
      
      return {
        content: result,
        renderTime,
        queryCount,
        metrics: this.calculateMetrics(templateId)
      };
      
    } finally {
      // Orijinal query methodunu restore et
      this.db.query = originalQuery;
      this.activeRenders.delete(renderId);
    }
  }

  private calculateMetrics(templateId: string): TemplateMetrics {
    const times = this.renderTimes.get(templateId) || [];
    const recentTimes = times.slice(-100); // Son 100 render
    
    return {
      averageRenderTime: recentTimes.reduce((a, b) => a + b, 0) / recentTimes.length,
      p95RenderTime: this.percentile(recentTimes, 0.95),
      p99RenderTime: this.percentile(recentTimes, 0.99),
      renderCount: recentTimes.length,
      suspiciousPatterns: this.detectPatterns(recentTimes)
    };
  }

  // Performans desenlerine dayalı öneriler üret
  generateOptimizationSuggestions(templateId: string): string[] {
    const metrics = this.calculateMetrics(templateId);
    const suggestions: string[] = [];
    
    if (metrics.averageRenderTime > 500) {
      suggestions.push('Sık erişilen veriyi cache\'lemeyi düşün');
    }
    
    if (metrics.p99RenderTime > 2000) {
      suggestions.push('Template yüksek tail latency\'ye sahip - yavaş yolları araştır');
    }
    
    const avgQueries = Array.from(this.queryCount.values())
      .reduce((a, b) => a + b, 0) / this.queryCount.size;
    
    if (avgQueries > 5) {
      suggestions.push('Çok fazla veritabanı sorgusu - veri ön-yüklemeyi düşün');
    }
    
    return suggestions;
  }
}

Çözüm: Template Performans Korumaları#

N+1 deseni tespit edildikten sonra çözüm, sert limitlerle veri ön-yüklemenin birleşimidir:

class SafeTemplateRenderer {
  private readonly MAX_RENDER_TIME = 2000; // 2 saniye
  private readonly MAX_DB_QUERIES = 10;
  private readonly CACHE_TTL = 300; // 5 dakika

  async renderWithGuardrails(
    templateId: string,
    userId: string,
    data: any
  ): Promise<string> {
    // N+1 sorgularını önlemek için yaygın gerekli veriyi ön-yükle
    const enhancedData = await this.preloadTemplateData(userId, data);
    
    // Render kısıtlamaları kur
    const renderPromise = this.templateEngine.render(
      templateId, 
      enhancedData,
      {
        timeout: this.MAX_RENDER_TIME,
        maxQueries: this.MAX_DB_QUERIES,
        enableCache: true
      }
    );
    
    try {
      return await Promise.race([
        renderPromise,
        this.createTimeoutPromise(this.MAX_RENDER_TIME)
      ]);
    } catch (error) {
      if (error instanceof TimeoutError) {
        // Cache'lenmiş versiyona veya basit template'e geri dön
        return await this.renderFallbackTemplate(templateId, userId, data);
      }
      throw error;
    }
  }

  private async preloadTemplateData(userId: string, data: any): Promise<any> {
    // Template'in hangi veriye ihtiyacı olduğunu belirlemek için analiz et
    const requiredData = this.analyzeTemplateDataNeeds(data.templateContent);
    
    // Gerekli tüm veriyi tek sorgularda toplu yükle
    const preloadedData = await Promise.all([
      requiredData.needsProjects ? this.loadUserProjects(userId) : null,
      requiredData.needsActivities ? this.loadUserActivities(userId, 10) : null,
      requiredData.needsTeamInfo ? this.loadUserTeamInfo(userId) : null
    ]);
    
    return {
      ...data,
      projects: preloadedData[0],
      recentActivities: preloadedData[1], 
      teamInfo: preloadedData[2]
    };
  }

  private async renderFallbackTemplate(
    templateId: string, 
    userId: string, 
    data: any
  ): Promise<string> {
    // Karmaşık veri gerektirmeyen basitleştirilmiş template versiyonu kullan
    const fallbackTemplate = await this.getFallbackTemplate(templateId);
    return await this.templateEngine.render(fallbackTemplate, {
      user: data.user,
      basicData: this.extractBasicData(data)
    });
  }
}

WebSocket Bağlantı Fırtınası#

WebSocket fanout’u genelde canlı bildirimleri, doküman güncellemelerini ve presence bilgisini aynı bağlantı üzerinde taşır; bu yüzden istemci tarafındaki tek bir retry bug’ı üçünü birden vurur.

İstemci Neden Durmadan Yeniden Bağlanıyordu#

Şuna benzeyen istemci retry mantığı sağlam görünür:

// Mobil app'in "geliştirilmiş" retry mantığı - bunu yapma
class NotificationConnectionManager {
  connect() {
    this.ws = new WebSocket(this.endpoint);
    
    this.ws.onclose = () => {
      // Üstel backoff... öyle sandılar
      this.retryDelay = Math.min(this.retryDelay * 2, 30000);
      setTimeout(() => this.connect(), this.retryDelay);
    };
    
    this.ws.onerror = () => {
      // Hata durumunda hemen yeniden dene - problem buydu
      this.connect();
    };
  }
}

Sorun şu: sunucular aşırı yüklendiğinde bağlantıları reddetmeye başlar, istemci de bu reddi close yerine error olarak yorumlayıp hiç backoff uygulamadan yeniden bağlanır.

Sunucu Tarafı Çözüm#

Sunucu, istemcilerin backoff uygulayacağına güvenemez; bu yüzden kabul kontrolü sunucu tarafına aittir: istemci başına bağlantı oranı, kullanıcı başına bağlantı sayısı ve yüke bağlı kabul kontrolü.

class DefensiveWebSocketServer {
  private connectionCounts: Map<string, number> = new Map();
  private rateLimiter: Map<string, Date[]> = new Map();
  private readonly MAX_CONNECTIONS_PER_USER = 5;
  private readonly RATE_LIMIT_WINDOW = 60000; // 1 dakika
  private readonly RATE_LIMIT_MAX = 10; // Dakikada 10 bağlantı

  async handleConnection(socket: WebSocket, request: IncomingMessage): Promise<void> {
    const clientId = this.getClientIdentifier(request);
    const userId = await this.authenticateConnection(request);
    
    // Rate limiting kontrolü
    if (!this.checkRateLimit(clientId)) {
      socket.close(1008, 'Rate limit aşıldı');
      this.logSecurityEvent('rate_limit_exceeded', clientId);
      return;
    }
    
    // Kullanıcı başına bağlantı sayısı kontrolü
    const userConnections = this.connectionCounts.get(userId) || 0;
    if (userConnections >= this.MAX_CONNECTIONS_PER_USER) {
      socket.close(1008, 'Çok fazla bağlantı');
      this.logSecurityEvent('connection_limit_exceeded', userId);
      return;
    }
    
    // Sunucu yük koruması
    const serverLoad = await this.getCurrentServerLoad();
    if (serverLoad > 0.9) {
      // Yük altındayken sadece yüksek öncelikli bağlantıları kabul et
      if (!this.isHighPriorityUser(userId)) {
        socket.close(1013, 'Sunucu aşırı yüklü - lütfen sonra tekrar deneyin');
        return;
      }
    }
    
    this.setupConnection(socket, userId, clientId);
  }

  private checkRateLimit(clientId: string): boolean {
    const now = new Date();
    const windowStart = new Date(now.getTime() - this.RATE_LIMIT_WINDOW);
    
    if (!this.rateLimiter.has(clientId)) {
      this.rateLimiter.set(clientId, []);
    }
    
    const connections = this.rateLimiter.get(clientId)!;
    
    // Eski bağlantı denemelerini kaldır
    const recentConnections = connections.filter(date => date > windowStart);
    this.rateLimiter.set(clientId, recentConnections);
    
    // Rate limit altında mı kontrol et
    if (recentConnections.length >= this.RATE_LIMIT_MAX) {
      return false;
    }
    
    // Bu bağlantı denemesini kaydet
    recentConnections.push(now);
    return true;
  }

  private async getCurrentServerLoad(): Promise<number> {
    const metrics = await Promise.all([
      this.getCPUUsage(),
      this.getMemoryUsage(),
      this.getConnectionCount(),
      this.getEventQueueDepth()
    ]);
    
    // Farklı yük göstergelerinin ağırlıklı ortalaması
    return (
      metrics[0] * 0.3 + // CPU
      metrics[1] * 0.2 + // Memory  
      metrics[2] * 0.3 + // Connections
      metrics[3] * 0.2  // Queue depth
    );
  }

  // Yük altında zarifçe bozulma
  private async handleConnectionUnderLoad(
    socket: WebSocket, 
    userId: string
  ): Promise<void> {
    // Kritik olmayan bildirimler için güncelleme sıklığını azalt
    const updateInterval = this.getAdaptiveUpdateInterval();
    
    // Kritik bildirim türlerini önceliklendir
    const allowedNotificationTypes = this.getCriticalNotificationTypes();
    
    socket.send(JSON.stringify({
      type: 'connection_degraded',
      message: 'Yüksek yük nedeniyle azaltılmış servis',
      updateInterval,
      allowedTypes: allowedNotificationTypes
    }));
  }
}

Debugging Araç Seti#

Bir bildirim olayında dashboard “şu anda ne bozuk” sorusunu, trace ise “bu tek mesaja ne oldu” sorusunu yanıtlar.

Olaylar için Gerçek Zamanlı Dashboard#

class IncidentDashboard {
  async getCurrentSystemState(): Promise<SystemSnapshot> {
    const timestamp = new Date();
    
    // Hız için metrikleri paralel topla
    const [
      deliveryMetrics,
      errorMetrics, 
      performanceMetrics,
      externalServiceStatus
    ] = await Promise.all([
      this.getDeliveryMetrics(),
      this.getErrorMetrics(),
      this.getPerformanceMetrics(),
      this.checkExternalServices()
    ]);
    
    return {
      timestamp,
      overall: this.calculateOverallHealth(deliveryMetrics, errorMetrics),
      deliveryMetrics: {
        email: deliveryMetrics.email,
        push: deliveryMetrics.push,
        websocket: deliveryMetrics.websocket,
        sms: deliveryMetrics.sms
      },
      errors: {
        byChannel: errorMetrics.byChannel,
        byType: errorMetrics.byType,
        trending: errorMetrics.trending
      },
      performance: {
        avgDeliveryTime: performanceMetrics.avgDeliveryTime,
        p95DeliveryTime: performanceMetrics.p95DeliveryTime,
        queueDepths: performanceMetrics.queueDepths
      },
      externalServices: externalServiceStatus,
      recommendations: this.generateRecommendations(deliveryMetrics, errorMetrics)
    };
  }

  private generateRecommendations(
    delivery: any, 
    errors: any
  ): string[] {
    const recommendations: string[] = [];
    
    // Email teslimat sorunları
    if (delivery.email.successRate < 0.95) {
      recommendations.push('Email sağlayıcı durumunu ve reputation score\'unu kontrol et');
    }
    
    // Push bildirim problemleri  
    if (delivery.push.successRate < 0.9) {
      recommendations.push('Push sertifikalarını ve payload formatını doğrula');
    }
    
    // Yüksek hata oranları
    if (errors.overall.rate > 0.05) {
      recommendations.push('En yaygın hata desenlerini araştır');
    }
    
    return recommendations;
  }
}

Correlation ID İzleme#

Bildirim sistemleri için en değerli debugging aracı kapsamlı correlation ID izlemedir:

class NotificationTracer {
  async traceNotificationJourney(correlationId: string): Promise<NotificationTrace> {
    // Bir bildirimin sistem boyunca tam yolculuğunu al
    const events = await this.db.query(`
      SELECT 
        ne.id as event_id,
        ne.notification_type,
        ne.created_at,
        ne.data,
        nd.channel,
        nd.status,
        nd.attempt_count,
        nd.error_message,
        nd.sent_at,
        nd.delivered_at
      FROM notification_events ne
      LEFT JOIN notification_deliveries nd ON ne.id = nd.event_id  
      WHERE ne.correlation_id = $1
      ORDER BY ne.created_at, nd.created_at
    `, [correlationId]);
    
    // Ayrıca harici servislerden logları al
    const externalLogs = await Promise.all([
      this.getEmailProviderLogs(correlationId),
      this.getPushProviderLogs(correlationId),
      this.getWebSocketLogs(correlationId)
    ]);
    
    return {
      correlationId,
      timeline: this.buildTimeline(events, externalLogs),
      status: this.determineOverallStatus(events),
      failurePoints: this.identifyFailures(events, externalLogs),
      recommendations: this.generateTraceRecommendations(events)
    };
  }

  private buildTimeline(events: any[], externalLogs: any[]): TimelineEvent[] {
    const allEvents = [
      ...events.map(e => ({ 
        timestamp: e.created_at, 
        type: 'internal', 
        details: e 
      })),
      ...externalLogs.flat().map(e => ({ 
        timestamp: e.timestamp, 
        type: 'external', 
        details: e 
      }))
    ];
    
    return allEvents.sort((a, b) => 
      new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
    );
  }
}

İzleme Stratejisi#

Eşik alertleri hasar oluştuktan sonra çalar. Eklemeye değer alertler, hâlâ müdahale alanı varken çalanlardır.

Öngörücü Alerting#

Metriğin gittiği yöne alert kur ve trendin öngördüğü etkiyi de yaz:

class PredictiveAlerting {
  async checkPredictiveMetrics(): Promise<Alert[]> {
    const alerts: Alert[] = [];
    
    // Teslimat oranı trendlerini kontrol et (sadece mevcut oran değil)
    const deliveryTrend = await this.calculateDeliveryTrend('1h');
    if (deliveryTrend.slope < -0.1) { // Saatte %10+ düşüyor
      alerts.push({
        level: 'warning',
        message: 'Teslimat oranı aşağı doğru trend gösteriyor',
        details: `Oran saatte ${deliveryTrend.slope * 100}% düşüyor`,
        predictedImpact: 'Trend devam ederse ~2 saatte sistem hatası'
      });
    }
    
    // Queue derinliği büyümesini kontrol et
    const queueGrowth = await this.calculateQueueGrowthRate('30m');
    if (queueGrowth > 1000) { // 30 dakikada 1000+ item büyüyor
      alerts.push({
        level: 'critical',
        message: 'Bildirim queue\'su sürdürülemez şekilde büyüyor',
        details: `Queue 30 dakikada ${queueGrowth} item büyüyor`,
        predictedImpact: '~45 dakikada queue overflow'
      });
    }
    
    // Yeni hata deseni tespitini kontrol et
    const errorPatterns = await this.detectEmergingErrorPatterns();
    for (const pattern of errorPatterns) {
      if (pattern.confidence > 0.8) {
        alerts.push({
          level: 'warning',
          message: `Yeni hata deseni tespit edildi: ${pattern.type}`,
          details: pattern.description,
          predictedImpact: `Potansiyel sistem etkisi: ${pattern.impact}`
        });
      }
    }
    
    return alerts;
  }
}

Bir Sonraki Olaydan Önce Eklenecek Ölçüm#

Bu eklemeler şimdi ucuz, olay sırasında zaten pahalıdır:

  1. Her adımda correlation ID. Her bildirim eventi, teslimat denemesi ve harici servis çağrısı aynı ID’yi taşır; ID’si olmayan logları sonradan bir araya getirmek zordur.

  2. Kullanıcı etkisi alertleri altyapı alertlerinin üstünde. Alert hiyerarşisinin tepesinde “kullanıcılar bildirim almıyor” sinyali durur; CPU, memory ve queue derinliği sinyalleri bunu açıklamak için vardır, tek başlarına page atmak için değil.

  3. Sınırsız her yola bir limit. Render süresi, template başına sorgu, kullanıcı başına bağlantı ve queue derinliğinin hepsi bir limite ihtiyaç duyar; böylece kademeli hata başladığı bileşende kalır.

Bu sıralama, teslimat yolunun uçtan uca sahibi olduğun durumlarda geçerlidir. Teslimat bir sağlayıcıya devredilmişse hiyerarşinin 3. ve 4. seviyeleri o sağlayıcının event webhook’larının arkasına geçer: trace’i kendi loglarının yerine onların teslimat eventlerinden kurman gerekir ve circuit breaker kendi queue derinliğini korur.

Serinin son bölümü analitik ve performans ayarını ele alıyor: bildirim stratejilerinin A/B testi, teslimat metriklerini hareket ettiren optimizasyon desenleri ve regresyonları kullanıcılar bildirmeden yakalayan performans izleme.

Kaynaklar#

Ölçeklenebilir Kullanıcı Bildirim Sistemi Geliştirme

Kurumsal seviye bildirim sistemlerinin tasarımı, implementasyonu ve üretim zorluklarını kapsayan kapsamlı 4-parça serisi. Mimari ve veritabanı tasarımından gerçek zamanlı teslimat, ölçekte debugging ve performans optimizasyonuna kadar.

İlerleme 3/4 yazı tamamlandı

İlgili yazılar