概述

在现代微服务架构中,一个看似简单的用户请求可能穿越数十个服务节点。当故障发生时,SRE 工程师面对的第一个问题往往不是"怎么修",而是"影响范围有多大"。如果无法快速回答这个问题,故障恢复就会被拖延在无休止的排查中。

服务依赖地图(Service Dependency Map)和故障域分析(Failure Domain Analysis)是解决这一问题的工程方法论。前者解决"谁依赖谁、怎么依赖"的认知问题,后者解决"故障会扩散到哪、爆炸半径多大"的控制问题。两者结合,构成了 SRE 可靠性工程的基础设施。

从依赖拓扑的发现方法出发,深入分析故障域的识别与隔离策略,最后给出爆炸半径控制的工程实践方案。

服务依赖的复杂性本质

微服务架构下的依赖特征

单体应用时代的依赖关系是显式的、编译期的——通过 import 语句和函数调用就能完整描绘依赖图。微服务架构彻底改变了这一范式:

维度单体应用微服务架构
依赖发现方式代码静态分析运行时流量观测
依赖类型函数调用HTTP/gRPC/消息队列/事件总线
依赖稳定性编译期确定运行时动态变化
依赖可见性IDE 可直接跳转需要专门工具发现
故障传播路径进程内异常栈跨网络级联故障
依赖数量级几十到几百几百到几千

依赖关系的分类体系

并非所有依赖都具有相同的风险等级。一个成熟的依赖地图必须对依赖关系进行分类标注:

按调用方式分类

  • 同步调用:HTTP REST、gRPC、数据库查询。调用方阻塞等待响应,是级联故障的主要传播路径。
  • 异步调用:消息队列(Kafka、RabbitMQ)、事件总线。调用方不阻塞,但消费端故障可能导致消息积压。
  • 共享资源依赖:共用数据库、缓存集群、存储卷。资源竞争可能引发间接故障。
  • 基础设施依赖:DNS、服务发现、配置中心。这类依赖故障影响面极广,属于关键路径。

按关键性分类

  • 强依赖:被依赖方不可用时,调用方无法完成核心功能。例如订单服务依赖库存服务。
  • 弱依赖:被依赖方不可用时,调用方可降级运行。例如商品详情页依赖推荐服务。
  • 条件依赖:在特定场景下才触发的依赖。例如促销活动期间才调用的优惠券服务。
# 依赖分类标注示例
class DependencyType:
    SYNC_HTTP = "sync_http"
    SYNC_GRPC = "sync_grpc"
    ASYNC_MQ = "async_mq"
    SHARED_DB = "shared_db"
    SHARED_CACHE = "shared_cache"
    INFRA_DNS = "infra_dns"
    INFRA_SERVICE_DISCOVERY = "infra_sd"

class DependencyCriticality:
    STRONG = "strong"       # 不可降级
    WEAK = "weak"           # 可降级
    CONDITIONAL = "conditional"  # 条件触发

# 依赖关系数据结构
class ServiceDependency:
    def __init__(self, caller, callee, dep_type, criticality):
        self.caller = caller               # 调用方服务名
        self.callee = callee               # 被调用方服务名
        self.dep_type = dep_type           # 依赖类型
        self.criticality = criticality     # 关键性等级
        self.slo_latency_ms = None         # 依赖调用P99延迟
        self.error_rate = None             # 依赖调用错误率
        self.fallback_enabled = False       # 是否配置降级策略
        self.circuit_breaker = False       # 是否配置熔断器

依赖拓扑发现方法

静态发现:从代码和配置提取

静态发现通过分析代码仓库和部署配置来构建依赖图,优势是覆盖完整(包括低频调用的路径),劣势是无法反映运行时实际流量。

从 Kubernetes 配置提取

# 通过 Service 和 Endpoint 关系发现依赖
# order-service 的 Deployment 中引用了 inventory-service
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: production
spec:
  template:
    spec:
      containers:
      - name: order-service
        env:
        - name: INVENTORY_SERVICE_URL
          value: "http://inventory-service.production.svc.cluster.local:8080"
        - name: PAYMENT_SERVICE_URL
          value: "http://payment-service.production.svc.cluster.local:8090"
        - name: KAFKA_BROKERS
          value: "kafka-broker.data.svc.cluster.local:9092"
#!/usr/bin/env python3
"""从 Kubernetes ConfigMap 和 Deployment 中提取服务依赖关系"""

import yaml
import re
import json
from collections import defaultdict

class K8sDependencyExtractor:
    """从 K8s 配置中提取服务间依赖关系"""

    # 匹配 K8s 内部服务 DNS 的正则
    SERVICE_DNS_PATTERN = re.compile(
        r'(?:https?://)?([a-z0-9-]+)\.([a-z0-9-]+)\.svc\.cluster\.local(?::(\d+))?'
    )
    # 匹配环境变量中的服务引用
    ENV_SERVICE_PATTERN = re.compile(
        r'(?:https?://)?([a-z0-9-]+):(\d+)'
    )

    def __init__(self):
        self.dependencies = defaultdict(list)

    def extract_from_manifest(self, manifest_text):
        """从 YAML manifest 文本中提取依赖"""
        docs = list(yaml.safe_load_all(manifest_text))

        for doc in docs:
            if not doc or doc.get('kind') not in ('Deployment', 'ConfigMap'):
                continue

            name = doc.get('metadata', {}).get('name', '')
            namespace = doc.get('metadata', {}).get('namespace', 'default')

            if doc['kind'] == 'Deployment':
                self._extract_from_deployment(name, namespace, doc)
            elif doc['kind'] == 'ConfigMap':
                self._extract_from_configmap(name, namespace, doc)

        return dict(self.dependencies)

    def _extract_from_deployment(self, name, namespace, doc):
        """从 Deployment 中提取环境变量里的服务引用"""
        containers = (
            doc.get('spec', {})
            .get('template', {})
            .get('spec', {})
            .get('containers', [])
        )

        for container in containers:
            env_vars = container.get('env', [])
            for env in env_vars:
                value = str(env.get('value', ''))
                # 查找 svc.cluster.local 格式的服务引用
                matches = self.SERVICE_DNS_PATTERN.findall(value)
                for svc_name, svc_ns, port in matches:
                    self.dependencies[name].append({
                        'callee': svc_name,
                        'namespace': svc_ns or namespace,
                        'port': port or '80',
                        'source': 'env_var',
                        'env_key': env.get('name', '')
                    })

    def _extract_from_configmap(self, name, namespace, doc):
        """从 ConfigMap 数据中提取服务引用"""
        data = doc.get('data', {})
        for key, value in data.items():
            if not isinstance(value, str):
                continue
            matches = self.SERVICE_DNS_PATTERN.findall(value)
            for svc_name, svc_ns, port in matches:
                self.dependencies[name].append({
                    'callee': svc_name,
                    'namespace': svc_ns or namespace,
                    'port': port or '80',
                    'source': 'configmap',
                    'config_key': key
                })

    def to_graph(self):
        """输出依赖图的 JSON 表示"""
        nodes = set()
        edges = []
        for caller, deps in self.dependencies.items():
            nodes.add(caller)
            for dep in deps:
                nodes.add(dep['callee'])
                edges.append({
                    'source': caller,
                    'target': dep['callee'],
                    'type': dep.get('source', 'unknown'),
                    'port': dep.get('port', '')
                })
        return {
            'nodes': sorted(list(nodes)),
            'edges': edges,
            'total_services': len(nodes),
            'total_dependencies': len(edges)
        }

# 使用示例
if __name__ == '__main__':
    sample_manifest = """
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: production
spec:
  template:
    spec:
      containers:
      - name: order-service
        env:
        - name: INVENTORY_SERVICE_URL
          value: "http://inventory-service.production.svc.cluster.local:8080"
        - name: PAYMENT_SERVICE_URL
          value: "http://payment-service.production.svc.cluster.local:8090"
"""
    extractor = K8sDependencyExtractor()
    extractor.extract_from_manifest(sample_manifest)
    print(json.dumps(extractor.to_graph(), indent=2, ensure_ascii=False))

动态发现:从运行时流量观测

动态发现通过观测实际运行时流量来构建依赖图,反映的是真实调用关系。主流方案有三种:

方法原理优势劣势
分布式追踪Trace 中的 span 串联关系精确到请求级别,含延迟数据需要应用接入 SDK,采样率限制
Service MeshSidecar 代理拦截流量无侵入,覆盖全量 L7 流量仅限 mesh 管理的服务
eBPF内核层拦截网络调用无侵入,覆盖所有网络流量技术门槛高,需较新内核

基于 Jaeger/OpenTelemetry 的 Trace 分析

#!/usr/bin/env python3
"""从 OpenTelemetry / Jaeger Trace 数据中提取服务依赖关系"""

import json
from collections import defaultdict
from datetime import datetime, timedelta

class TraceDependencyExtractor:
    """从分布式追踪数据中提取服务依赖图"""

    def __init__(self):
        # 依赖关系: {(caller, callee): {count, p99_latency, error_count}}
        self.dependencies = defaultdict(lambda: {
            'call_count': 0,
            'latencies': [],
            'error_count': 0,
            'last_seen': None
        })

    def process_trace(self, trace_data):
        """处理单条 Trace 数据,提取 span 间的父子关系"""
        spans = trace_data.get('spans', [])

        # 构建 span_id -> span 的映射
        span_map = {s['spanID']: s for s in spans}

        for span in spans:
            parent_id = span.get('parentSpanID')
            if not parent_id or parent_id not in span_map:
                continue

            parent = span_map[parent_id]

            # 提取服务名(从 process/tag 信息中)
            caller_service = self._get_service_name(parent)
            callee_service = self._get_service_name(span)

            if not caller_service or not callee_service:
                continue
            if caller_service == callee_service:
                continue  # 跳过同服务内部调用

            key = (caller_service, callee_service)
            dep = self.dependencies[key]

            dep['call_count'] += 1

            # 记录延迟
            duration_us = span.get('duration', 0)
            dep['latencies'].append(duration_us)

            # 记录错误
            tags = span.get('tags', [])
            for tag in tags:
                if (tag.get('key') == 'error' and
                    tag.get('value') is True):
                    dep['error_count'] += 1
                    break

            # 更新最后发现时间
            start_time = span.get('startTime', 0)
            if start_time:
                dep['last_seen'] = start_time

    def _get_service_name(self, span):
        """从 span 的 process 信息中提取服务名"""
        process_id = span.get('processID')
        processes = span.get('processes', {})
        process = processes.get(process_id, {})
        tags = process.get('tags', [])

        for tag in tags:
            if tag.get('key') == 'service.name':
                return tag.get('value')
        return process.get('serviceName')

    def build_dependency_graph(self):
        """构建最终的服务依赖图"""
        edges = []
        for (caller, callee), data in self.dependencies.items():
            latencies = sorted(data['latencies'])
            p99_index = int(len(latencies) * 0.99) if latencies else 0
            p99_latency = latencies[p99_index] if latencies else 0

            error_rate = (
                data['error_count'] / data['call_count']
                if data['call_count'] > 0 else 0
            )

            edges.append({
                'source': caller,
                'target': callee,
                'call_count': data['call_count'],
                'p99_latency_ms': round(p99_latency / 1000, 2),
                'error_rate': round(error_rate, 4),
                'last_seen': data['last_seen']
            })

        # 按调用量排序
        edges.sort(key=lambda x: x['call_count'], reverse=True)

        nodes = set()
        for e in edges:
            nodes.add(e['source'])
            nodes.add(e['target'])

        return {
            'nodes': sorted(list(nodes)),
            'edges': edges,
            'total_services': len(nodes),
            'total_edges': len(edges),
            'generated_at': datetime.utcnow().isoformat()
        }

# 使用示例
if __name__ == '__main__':
    # 模拟一条 Trace 数据
    sample_trace = {
        'traceID': 'abc123',
        'spans': [
            {
                'spanID': 'span1',
                'parentSpanID': None,
                'operationName': 'GET /api/orders',
                'startTime': 1752216000000000,
                'duration': 50000,
                'processID': 'p1',
                'tags': []
            },
            {
                'spanID': 'span2',
                'parentSpanID': 'span1',
                'operationName': 'GET /api/inventory',
                'startTime': 1752216000100000,
                'duration': 12000,
                'processID': 'p2',
                'tags': []
            },
            {
                'spanID': 'span3',
                'parentSpanID': 'span1',
                'operationName': 'POST /api/payment',
                'startTime': 1752216000200000,
                'duration': 30000,
                'processID': 'p3',
                'tags': [{'key': 'error', 'value': True}]
            }
        ],
        'processes': {
            'p1': {'serviceName': 'order-service', 'tags': []},
            'p2': {'serviceName': 'inventory-service', 'tags': []},
            'p3': {'serviceName': 'payment-service', 'tags': []}
        }
    }

    extractor = TraceDependencyExtractor()
    extractor.process_trace(sample_trace)
    graph = extractor.build_dependency_graph()
    print(json.dumps(graph, indent=2, ensure_ascii=False))

基于 eBPF 的无侵入拓扑发现

eBPF 方案不需要应用代码改造,在内核层拦截网络调用,适合作为依赖发现的全量兜底方案:

# 使用 bpftrace 捕获 TCP 连接关系
# 这段脚本会输出所有新建 TCP 连接的源进程和目标地址
#!/usr/bin/env bpftrace

BEGIN
{
    printf("Tracing TCP connections... Ctrl-C to stop.\n");
    printf("%-12s %-16s %-6s %-16s %-6s\n",
           "TIME", "COMM", "PID", "DADDR", "DPORT");
}

kprobe:tcp_connect
{
    $sk = (struct sock *)arg0;
    $daddr = $sk->sk_daddr;

    time("%H:%M:%S   ");
    printf("%-16s %-6d ", comm, pid);
    printf("%-16s %-6d\n",
           ntop($daddr),
           $sk->sk_dport >> 8);
}
# 使用 kubectl + eBPF 工具链发现 Pod 间通信
# 基于 cilium Hubble 的服务依赖地图
hubble observe --follow \
  --type l3/4 \
  --output json | jq '{
    source: .source.podName,
    destination: .destination.podName,
    port: .destination.port,
    protocol: .l4.protocol,
    verdict: .verdict
  }' | jq -s 'group_by(.source + "->" + .destination) | map({
    edge: .[0].source + " -> " + .[0].destination,
    count: length,
    ports: [.[].port] | unique
  })'

静态与动态发现互补策略

两种方法各有局限,生产环境应组合使用:

发现维度静态发现动态发现互补价值
覆盖完整性高(含低频路径)受采样率限制静态补全动态遗漏
依赖准确性低(含废弃配置)高(实际调用)动态过滤静态噪声
实时性秒级动态感知架构变更
资源开销极低中到高静态作为基线
运维门槛按需选择
class HybridDependencyGraph:
    """融合静态和动态发现结果的混合依赖图"""

    def __init__(self):
        self.static_edges = {}   # 静态发现的边
        self.dynamic_edges = {}  # 动态发现的边
        self.merged_graph = {}   # 融合后的图

    def add_static_dependency(self, caller, callee, source='config'):
        """添加静态发现的依赖"""
        key = (caller, callee)
        if key not in self.static_edges:
            self.static_edges[key] = {
                'source': source,
                'verified': False
            }

    def add_dynamic_dependency(self, caller, callee, call_count,
                               p99_latency_ms, error_rate):
        """添加动态发现的依赖"""
        key = (caller, callee)
        self.dynamic_edges[key] = {
            'call_count': call_count,
            'p99_latency_ms': p99_latency_ms,
            'error_rate': error_rate,
            'verified': True
        }

    def merge(self):
        """融合静态和动态发现结果"""
        all_keys = set(self.static_edges.keys()) | set(self.dynamic_edges.keys())

        for key in all_keys:
            caller, callee = key
            static = self.static_edges.get(key, {})
            dynamic = self.dynamic_edges.get(key, {})

            edge = {
                'caller': caller,
                'callee': callee,
                'static_found': key in self.static_edges,
                'dynamic_found': key in self.dynamic_edges,
                'call_count': dynamic.get('call_count', 0),
                'p99_latency_ms': dynamic.get('p99_latency_ms', None),
                'error_rate': dynamic.get('error_rate', None),
                'status': self._classify_edge(static, dynamic),
                'source': static.get('source', 'runtime')
            }
            self.merged_graph[key] = edge

        return self.merged_graph

    def _classify_edge(self, static, dynamic):
        """对边进行分类标记"""
        if static and dynamic:
            return 'verified'         # 静态配置且运行时确认
        elif not static and dynamic:
            return 'undocumented'      # 运行时存在但配置中未发现
        elif static and not dynamic:
            return 'dormant'           # 配置中存在但运行时未调用
        return 'unknown'

    def get_risk_edges(self):
        """获取需要关注的边"""
        risks = []
        for key, edge in self.merged_graph.items():
            if edge['status'] == 'undocumented':
                risks.append({
                    'edge': f"{edge['caller']} -> {edge['callee']}",
                    'risk': '未文档化的依赖,架构变更时可能被遗漏',
                    'severity': 'medium'
                })
            elif (edge['status'] == 'verified' and
                  edge['error_rate'] and edge['error_rate'] > 0.05):
                risks.append({
                    'edge': f"{edge['caller']} -> {edge['callee']}",
                    'risk': f"错误率 {edge['error_rate']:.1%},需要排查",
                    'severity': 'high'
                })
        return risks

故障域分析

什么是故障域

故障域(Failure Domain)是指当一个组件发生故障时,受影响的其他组件和服务的集合。理解故障域的核心在于理解故障的传播路径。

“故障不会只停留在发生点。一个数据库的连接池耗尽可能导致上游数十个服务连锁超时,一个 DNS 配置错误可以让整个机房瘫痪。控制故障域的边界,就是控制系统风险的总量。” —— 参考 Google SRE Book 第 6 章

故障域的层级模型

故障域是分层嵌套的,从内到外依次扩大影响范围:

┌─────────────────────────────────────────────────┐
│  全局故障域 (Global)                               │
│  ┌───────────────────────────────────────────┐  │
│  │  地域故障域 (Region)                        │  │
│  │  ┌─────────────────────────────────────┐ │  │
│  │  │  可用区故障域 (AZ)                     │ │  │
│  │  │  ┌───────────────────────────────┐  │ │  │
│  │  │  │  集群故障域 (Cluster)            │  │ │  │
│  │  │  │  ┌─────────────────────────┐  │  │ │  │
│  │  │  │  │  节点故障域 (Node)        │  │  │ │  │
│  │  │  │  │  ┌───────────────────┐ │  │  │ │  │
│  │  │  │  │  │ Pod 故障域 (Pod)    │ │  │  │ │  │
│  │  │  │  │  │  ┌─────────────┐  │ │  │  │ │  │
│  │  │  │  │  │  │ 容器 (Container)│  │ │  │  │ │  │
│  │  │  │  │  │  └─────────────┘  │ │  │  │ │  │
│  │  │  │  │  └───────────────────┘ │  │  │ │  │
│  │  │  │  └─────────────────────────┘  │  │ │  │
│  │  │  └───────────────────────────────┘  │ │  │
│  │  └─────────────────────────────────────┘ │  │
│  └───────────────────────────────────────────┘  │
└─────────────────────────────────────────────────┘
层级典型故障原因影响范围隔离手段
容器OOM、应用异常单容器重启策略、健康检查
Pod节点驱逐、调度失败单 Pod 副本多副本、PDB
节点硬件故障、内核 panic节点上所有 Pod节点隔离、亲和性分散
集群控制面故障、网络分区集群内所有服务多集群、联邦
可用区机房断电、网络中断AZ 内所有资源多 AZ 部署、跨 AZ 负载
地域区域级故障Region 内所有资源多地域多活
全局DNS 故障、证书过期全站灾备切换、降级预案

故障传播路径分析

故障传播遵循依赖图的边进行扩散。分析传播路径需要回答三个问题:

  1. 故障从哪里开始:确定根因服务的位置
  2. 会影响到谁:沿着依赖图的边进行可达性分析
  3. 影响程度如何:根据依赖类型和关键性评估影响严重度
#!/usr/bin/env python3
"""故障域分析引擎:计算故障传播路径和爆炸半径"""

from collections import deque, defaultdict
import json

class FailureDomainAnalyzer:
    """基于服务依赖图分析故障传播和爆炸半径"""

    def __init__(self, dependency_graph):
        """
        dependency_graph: {
            'edges': [
                {'source': 'A', 'target': 'B', 'criticality': 'strong', ...},
                ...
            ]
        }
        """
        self.graph = defaultdict(list)
        self.reverse_graph = defaultdict(list)
        self.edge_info = {}

        for edge in dependency_graph.get('edges', []):
            src, dst = edge['source'], edge['target']
            self.graph[src].append(dst)
            self.reverse_graph[dst].append(src)

            key = (src, dst)
            self.edge_info[key] = {
                'criticality': edge.get('criticality', 'strong'),
                'fallback': edge.get('fallback_enabled', False),
                'circuit_breaker': edge.get('circuit_breaker', False),
                'call_count': edge.get('call_count', 0)
            }

    def analyze_blast_radius(self, failed_service, max_depth=10):
        """
        分析单个服务故障的爆炸半径

        Args:
            failed_service: 发生故障的服务名
            max_depth: 最大传播深度

        Returns:
            {
                'affected_services': [...],     # 受影响的服务列表
                'propagation_paths': [...],     # 传播路径
                'blast_radius_score': float,    # 爆炸半径评分(0-100)
                'critical_path': bool           # 是否影响核心路径
            }
        """
        affected = set()
        propagation_paths = []
        visited = set()

        # BFS 遍历反向依赖图(谁依赖了故障服务)
        queue = deque()
        queue.append((failed_service, 0, []))

        while queue:
            service, depth, path = queue.popleft()

            if depth > max_depth:
                continue

            if service in visited:
                continue
            visited.add(service)

            current_path = path + [service]

            if service != failed_service:
                affected.add(service)
                if len(current_path) > 1:
                    propagation_paths.append({
                        'path': ' -> '.join(current_path),
                        'depth': depth,
                        'edge_info': self._get_path_info(current_path)
                    })

            # 沿着反向依赖图向上游遍历
            for caller in self.reverse_graph.get(service, []):
                edge_key = (caller, service)
                edge_data = self.edge_info.get(edge_key, {})

                # 如果有降级或熔断,传播在此截断
                if (edge_data.get('fallback') or
                    edge_data.get('circuit_breaker')):
                    # 记录截断点
                    propagation_paths.append({
                        'path': ' -> '.join(current_path + [caller]),
                        'depth': depth + 1,
                        'truncated': True,
                        'truncation_reason': (
                            'fallback' if edge_data.get('fallback')
                            else 'circuit_breaker'
                        )
                    })
                    continue

                queue.append((caller, depth + 1, current_path))

        # 计算爆炸半径评分
        blast_radius = self._calculate_blast_radius(
            failed_service, affected
        )

        # 判断是否影响核心路径
        critical = self._is_critical_path(failed_service, affected)

        return {
            'failed_service': failed_service,
            'affected_services': sorted(list(affected)),
            'affected_count': len(affected),
            'propagation_paths': propagation_paths,
            'blast_radius_score': blast_radius,
            'critical_path': critical
        }

    def _get_path_info(self, path):
        """获取传播路径上每条边的信息"""
        info = []
        for i in range(len(path) - 1):
            key = (path[i], path[i + 1])
            edge = self.edge_info.get(key, {})
            info.append({
                'from': path[i],
                'to': path[i + 1],
                'criticality': edge.get('criticality', 'unknown'),
                'fallback': edge.get('fallback', False),
                'circuit_breaker': edge.get('circuit_breaker', False)
            })
        return info

    def _calculate_blast_radius(self, failed_service, affected_services):
        """计算爆炸半径评分 (0-100)"""
        if not affected_services:
            return 0

        score = 0
        for svc in affected_services:
            # 每个受影响服务贡献基础分
            score += 5
            # 如果该服务被多个其他服务依赖,加重
            dependents = len(self.reverse_graph.get(svc, []))
            score += dependents * 2

        # 限制在 0-100 范围
        return min(score, 100)

    def _is_critical_path(self, failed_service, affected_services):
        """判断是否影响核心业务路径"""
        critical_services = {'api-gateway', 'order-service',
                              'payment-service', 'auth-service'}
        all_affected = affected_services | {failed_service}
        return bool(all_affected & critical_services)

    def find_single_points_of_failure(self):
        """识别单点故障服务"""
        spof = []
        total_services = set(self.graph.keys()) | set(self.reverse_graph.keys())

        for service in total_services:
            dependents = self.reverse_graph.get(service, [])
            if len(dependents) == 0:
                continue  # 没有被依赖,不是单点

            # 检查是否有降级保护
            all_protected = True
            for caller in dependents:
                edge_key = (caller, service)
                edge_data = self.edge_info.get(edge_key, {})
                if not (edge_data.get('fallback') or
                       edge_data.get('circuit_breaker')):
                    all_protected = False
                    break

            if not all_protected:
                spof.append({
                    'service': service,
                    'dependent_count': len(dependents),
                    'dependents': list(dependents),
                    'risk': 'high' if len(dependents) > 5 else 'medium'
                })

        spof.sort(key=lambda x: x['dependent_count'], reverse=True)
        return spof

# 使用示例
if __name__ == '__main__':
    # 模拟依赖图
    dep_graph = {
        'edges': [
            {'source': 'api-gateway', 'target': 'order-service',
             'criticality': 'strong'},
            {'source': 'order-service', 'target': 'inventory-service',
             'criticality': 'strong'},
            {'source': 'order-service', 'target': 'payment-service',
             'criticality': 'strong'},
            {'source': 'order-service', 'target': 'recommendation-service',
             'criticality': 'weak', 'fallback_enabled': True},
            {'source': 'payment-service', 'target': 'fraud-detection',
             'criticality': 'strong'},
            {'source': 'payment-service', 'target': 'notification-service',
             'criticality': 'weak', 'fallback_enabled': True},
            {'source': 'inventory-service', 'target': 'product-service',
             'criticality': 'strong'},
            {'source': 'product-service', 'target': 'cache-cluster',
             'criticality': 'strong'},
        ]
    }

    analyzer = FailureDomainAnalyzer(dep_graph)

    # 分析 inventory-service 故障的影响
    result = analyzer.analyze_blast_radius('inventory-service')
    print(json.dumps(result, indent=2, ensure_ascii=False))

    print("\n--- 单点故障分析 ---")
    spof = analyzer.find_single_points_of_failure()
    print(json.dumps(spof, indent=2, ensure_ascii=False))

爆炸半径控制策略

控制爆炸半径的核心思路是在依赖路径上设置"防火墙",让故障传播在尽可能早的阶段被截断。

策略一:熔断器模式

# Istio DestinationRule 中的熔断配置
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: order-service-cb
  namespace: production
spec:
  host: order-service.production.svc.cluster.local
  trafficPolicy:
    outlierDetection:
      # 连续 5 次 5xx 错误触发熔断
      consecutive5xxErrors: 5
      # 熔断间隔 30 秒
      interval: 30s
      # 基础驱逐时间 30 秒
      baseEjectionTime: 30s
      # 最大驱逐比例 50%
      maxEjectionPercent: 50
      # 最小健康实例数
      minHealthPercent: 50
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        http1MaxPendingRequests: 50
        maxRequestsPerConnection: 10
        maxRetries: 2
        # 空闲超时
        idleTimeout: 60s

策略二:降级与 Fallback

#!/usr/bin/env python3
"""服务调用降级框架"""

import time
import logging
from functools import wraps
from typing import Any, Callable, Optional

logger = logging.getLogger(__name__)

class CircuitBreaker:
    """简单的熔断器实现"""

    def __init__(self, failure_threshold=5, recovery_timeout=30):
        self.failure_count = 0
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.last_failure_time = None
        self.state = 'closed'  # closed, open, half_open

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            if self.state == 'open':
                if self._should_try_reset():
                    self.state = 'half_open'
                    logger.info(f"Circuit breaker half-open for {func.__name__}")
                else:
                    raise CircuitBreakerOpenError(
                        f"Circuit breaker is open for {func.__name__}"
                    )

            try:
                result = func(*args, **kwargs)
                self._on_success()
                return result
            except Exception as e:
                self._on_failure()
                raise

        return wrapper

    def _should_try_reset(self):
        if self.last_failure_time is None:
            return True
        return time.time() - self.last_failure_time > self.recovery_timeout

    def _on_success(self):
        self.failure_count = 0
        if self.state == 'half_open':
            self.state = 'closed'
            logger.info("Circuit breaker closed")

    def _on_failure(self):
        self.failure_count += 1
        self.last_failure_time = time.time()
        if (self.failure_count >= self.failure_threshold and
            self.state != 'open'):
            self.state = 'open'
            logger.warning(
                f"Circuit breaker opened after "
                f"{self.failure_count} failures"
            )

class CircuitBreakerOpenError(Exception):
    """熔断器开启异常"""
    pass

def with_fallback(fallback_func: Optional[Callable] = None,
                  default_value: Any = None):
    """
    降级装饰器:主调用失败时返回默认值或执行 fallback

    Args:
        fallback_func: 降级时执行的函数
        default_value: 没有fallback时的默认返回值
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except CircuitBreakerOpenError:
                logger.warning(
                    f"Circuit breaker open for {func.__name__}, "
                    f"using fallback"
                )
                if fallback_func:
                    return fallback_func(*args, **kwargs)
                return default_value
            except Exception as e:
                logger.error(
                    f"{func.__name__} failed: {e}, using fallback"
                )
                if fallback_func:
                    return fallback_func(*args, **kwargs)
                return default_value

        return wrapper
    return decorator

# 实际使用示例
class OrderService:
    """订单服务,展示降级策略的组合使用"""

    def __init__(self):
        self.inventory_cb = CircuitBreaker(
            failure_threshold=5, recovery_timeout=30
        )
        self.recommendation_cb = CircuitBreaker(
            failure_threshold=3, recovery_timeout=60
        )

    @CircuitBreaker(failure_threshold=5, recovery_timeout=30)
    def call_inventory(self, product_id):
        """调用库存服务(强依赖,无降级)"""
        # 模拟调用
        raise ConnectionError("inventory-service unavailable")

    @with_fallback(
        default_value={"product_id": None, "quantity": 0,
                       "available": False}
    )
    def call_recommendation(self, user_id):
        """调用推荐服务(弱依赖,降级返回空推荐)"""
        # 模拟调用
        raise ConnectionError("recommendation-service unavailable")

    def place_order(self, user_id, product_id, quantity):
        """下单流程"""
        # 强依赖:库存检查失败则整个流程失败
        try:
            inventory = self.call_inventory(product_id)
            if inventory['quantity'] < quantity:
                return {'success': False, 'reason': 'insufficient_stock'}
        except CircuitBreakerOpenError:
            return {
                'success': False,
                'reason': 'inventory_service_unavailable',
                'retry_after': 30
            }

        # 弱依赖:推荐失败不影响下单
        recommendations = self.call_recommendation(user_id)

        return {
            'success': True,
            'order_id': f"ORD-{time.time()}",
            'recommendations': recommendations
        }

策略三:Bulkhead 舱壁隔离

# Kubernetes 中通过 ResourceQuota 和 LimitRange 实现资源隔离
# 确保一个命名空间的资源耗尽不影响其他命名空间

---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-payments-quota
  namespace: payments
spec:
  hard:
    requests.cpu: "20"
    requests.memory: 40Gi
    limits.cpu: "40"
    limits.memory: 80Gi
    pods: "50"
    services: "10"
    configmaps: "20"

---
apiVersion: v1
kind: LimitRange
metadata:
  name: payments-limits
  namespace: payments
spec:
  limits:
  # 每个 Pod 的资源上下限
  - type: Container
    default:
      cpu: "500m"
      memory: "512Mi"
    defaultRequest:
      cpu: "100m"
      memory: "128Mi"
    max:
      cpu: "4"
      memory: "8Gi"
    min:
      cpu: "50m"
      memory: "64Mi"
  # 单个 PVC 的大小限制
  - type: PersistentVolumeClaim
    max:
      storage: 100Gi
    min:
      storage: 1Gi
#!/usr/bin/env python3
"""线程池隔离实现舱壁模式"""

import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from collections import defaultdict

class BulkheadManager:
    """
    舱壁模式:为不同服务调用分配独立的线程池
    一个服务的线程池耗尽不会影响其他服务
    """

    def __init__(self):
        self.executors = {}
        self.semaphores = {}
        self.metrics = defaultdict(lambda: {
            'total_calls': 0,
            'rejected_calls': 0,
            'active_calls': 0
        })
        self._lock = threading.Lock()

    def register_service(self, service_name, max_concurrent=20):
        """为服务注册独立的线程池"""
        self.executors[service_name] = ThreadPoolExecutor(
            max_workers=max_concurrent,
            thread_name_prefix=f"bulkhead-{service_name}"
        )
        # 使用信号量控制并发
        import threading as th
        self.semaphores[service_name] = th.Semaphore(max_concurrent)

    def call(self, service_name, func, *args, **kwargs):
        """通过舱壁隔离的线程池调用服务"""
        if service_name not in self.executors:
            raise ValueError(f"Service {service_name} not registered")

        sem = self.semaphores[service_name]

        # 尝试获取信号量(非阻塞)
        acquired = sem.acquire(blocking=False)
        if not acquired:
            with self._lock:
                self.metrics[service_name]['rejected_calls'] += 1
            raise BulkheadFullError(
                f"Bulkhead for {service_name} is full, "
                f"max_concurrent reached"
            )

        try:
            with self._lock:
                self.metrics[service_name]['active_calls'] += 1
                self.metrics[service_name]['total_calls'] += 1

            executor = self.executors[service_name]
            future = executor.submit(func, *args, **kwargs)
            return future.result()
        finally:
            sem.release()
            with self._lock:
                self.metrics[service_name]['active_calls'] -= 1

    def get_metrics(self):
        """获取各服务的舱壁状态"""
        with self._lock:
            return dict(self.metrics)

class BulkheadFullError(Exception):
    """舱壁已满异常"""
    pass

# 使用示例
if __name__ == '__main__':
    manager = BulkheadManager()

    # 为不同服务分配不同的并发上限
    manager.register_service('payment', max_concurrent=10)
    manager.register_service('inventory', max_concurrent=20)
    manager.register_service('recommendation', max_concurrent=5)

    def simulate_call(service, duration=0.1):
        time.sleep(duration)
        return f"{service} call succeeded"

    # 模拟并发调用
    threads = []
    results = []
    errors = []

    def worker(service):
        try:
            result = manager.call(service, simulate_call, service, 0.5)
            results.append(result)
        except BulkheadFullError as e:
            errors.append(str(e))

    # 大量并发调用 payment 服务
    for i in range(15):
        t = threading.Thread(target=worker, args=('payment',))
        threads.append(t)
        t.start()

    for t in threads:
        t.join()

    print(f"Successes: {len(results)}")
    print(f"Rejections: {len(errors)}")
    print(f"Metrics: {manager.get_metrics()}")

依赖地图的持续维护

自动化拓扑发现流水线

依赖地图不是一次性产物,需要持续更新以反映架构变更:

#!/usr/bin/env python3
"""
依赖地图持续更新流水线
定期从多源采集依赖数据,融合后与历史版本对比,发现架构变更
"""

import json
import os
from datetime import datetime, timedelta
from collections import defaultdict

class DependencyMapPipeline:
    """依赖地图更新流水线"""

    def __init__(self, storage_path='.dumate/dependency-maps'):
        self.storage_path = storage_path
        os.makedirs(storage_path, exist_ok=True)

    def run(self, static_deps, dynamic_deps):
        """执行完整流水线"""
        # 1. 融合数据源
        merged = self._merge_sources(static_deps, dynamic_deps)

        # 2. 加载上一个版本
        previous = self._load_previous()

        # 3. 检测变更
        changes = self._detect_changes(previous, merged) if previous else []

        # 4. 保存当前版本
        self._save_current(merged)

        # 5. 生成报告
        report = self._generate_report(merged, changes)

        return report

    def _merge_sources(self, static_deps, dynamic_deps):
        """融合静态和动态依赖数据"""
        all_edges = set()
        edge_data = {}

        for edge in static_deps:
            key = (edge['caller'], edge['callee'])
            all_edges.add(key)
            edge_data[key] = {
                'static': True,
                'dynamic': False,
                'call_count': 0
            }

        for edge in dynamic_deps:
            key = (edge['source'], edge['target'])
            all_edges.add(key)
            if key not in edge_data:
                edge_data[key] = {
                    'static': False,
                    'dynamic': True,
                    'call_count': 0
                }
            edge_data[key]['dynamic'] = True
            edge_data[key]['call_count'] = edge.get('call_count', 0)
            edge_data[key]['p99_latency_ms'] = edge.get('p99_latency_ms')

        return {
            'edges': [
                {'caller': k[0], 'callee': k[1], **v}
                for k, v in edge_data.items()
            ],
            'generated_at': datetime.utcnow().isoformat(),
            'total_edges': len(all_edges)
        }

    def _load_previous(self):
        """加载上一个版本的依赖地图"""
        files = sorted(
            f for f in os.listdir(self.storage_path)
            if f.startswith('dep-map-')
        )
        if not files:
            return None

        latest = files[-1]
        path = os.path.join(self.storage_path, latest)
        with open(path) as f:
            return json.load(f)

    def _detect_changes(self, previous, current):
        """检测依赖图变更"""
        prev_edges = {
            (e['caller'], e['callee']) for e in previous.get('edges', [])
        }
        curr_edges = {
            (e['caller'], e['callee']) for e in current.get('edges', [])
        }

        added = curr_edges - prev_edges
        removed = prev_edges - curr_edges

        changes = []
        for caller, callee in added:
            changes.append({
                'type': 'added',
                'caller': caller,
                'callee': callee,
                'severity': 'medium'
            })
        for caller, callee in removed:
            changes.append({
                'type': 'removed',
                'caller': caller,
                'callee': callee,
                'severity': 'low'
            })

        return changes

    def _save_current(self, data):
        """保存当前版本的依赖地图"""
        timestamp = datetime.utcnow().strftime('%Y%m%d-%H%M%S')
        filename = f'dep-map-{timestamp}.json'
        path = os.path.join(self.storage_path, filename)
        with open(path, 'w') as f:
            json.dump(data, f, indent=2, ensure_ascii=False)

    def _generate_report(self, data, changes):
        """生成变更报告"""
        return {
            'timestamp': data['generated_at'],
            'total_edges': data['total_edges'],
            'total_services': len(set(
                e['caller'] for e in data['edges']
            ) | set(
                e['callee'] for e in data['edges']
            )),
            'changes': changes,
            'new_dependencies': [
                c for c in changes if c['type'] == 'added'
            ],
            'removed_dependencies': [
                c for c in changes if c['type'] == 'removed'
            ],
            'summary': (
                f"依赖图已更新:{data['total_edges']} 条边,"
                f"{len(changes)} 处变更"
                f"({sum(1 for c in changes if c['type']=='added')} 新增,"
                f"{sum(1 for c in changes if c['type']=='removed')} 移除)"
            )
        }

# 使用示例
if __name__ == '__main__':
    pipeline = DependencyMapPipeline()

    static = [
        {'caller': 'order', 'callee': 'inventory'},
        {'caller': 'order', 'callee': 'payment'},
    ]

    dynamic = [
        {'source': 'order', 'target': 'inventory', 'call_count': 5000},
        {'source': 'order', 'target': 'recommendation', 'call_count': 1000},
    ]

    report = pipeline.run(static, dynamic)
    print(json.dumps(report, indent=2, ensure_ascii=False))

可视化与告警

依赖地图需要可视化呈现才能发挥价值。推荐使用 Grafana + Graphviz 或 Cytoscape.js 进行交互式展示:

#!/usr/bin/env python3
"""生成 Graphviz DOT 格式的依赖图,用于可视化"""

import json

def generate_dot(dependency_graph, highlight_service=None):
    """
    将依赖图转换为 Graphviz DOT 格式

    Args:
        dependency_graph: 依赖图数据
        highlight_service: 高亮显示的服务(用于故障域分析)
    """
    lines = ['digraph service_dependencies {']
    lines.append('  rankdir=LR;')
    lines.append('  fontname="Arial";')
    lines.append('  node [fontname="Arial", shape=box, style=rounded];')
    lines.append('  edge [fontname="Arial"];')
    lines.append('')

    # 节点定义
    nodes = set()
    for edge in dependency_graph.get('edges', []):
        nodes.add(edge['caller'])
        nodes.add(edge['callee'])

    for node in sorted(nodes):
        if node == highlight_service:
            lines.append(
                f'  "{node}" [color=red, style="rounded,filled", '
                f'fillcolor=lightcoral, penwidth=3];'
            )
        else:
            lines.append(f'  "{node}" [style="rounded,filled", '
                        f'fillcolor=lightblue];')

    lines.append('')

    # 边定义
    for edge in dependency_graph.get('edges', []):
        caller = edge['caller']
        callee = edge['callee']
        criticality = edge.get('criticality', 'strong')

        if criticality == 'weak':
            color = 'gray'
            style = 'dashed'
            label = 'weak'
        elif edge.get('fallback'):
            color = 'orange'
            style = 'dashed'
            label = 'fallback'
        else:
            color = 'black'
            style = 'solid'
            label = ''

        # 高亮故障服务相关的边
        if highlight_service and (
            caller == highlight_service or callee == highlight_service
        ):
            color = 'red'
            penwidth = '3'
        else:
            penwidth = '1'

        lines.append(
            f'  "{caller}" -> "{callee}" [color={color}, '
            f'style={style}, label="{label}", penwidth={penwidth}];'
        )

    lines.append('}')
    return '\n'.join(lines)

if __name__ == '__main__':
    sample_graph = {
        'edges': [
            {'caller': 'api-gateway', 'callee': 'order-service',
             'criticality': 'strong'},
            {'caller': 'order-service', 'callee': 'inventory-service',
             'criticality': 'strong'},
            {'caller': 'order-service', 'callee': 'payment-service',
             'criticality': 'strong'},
            {'caller': 'order-service', 'callee': 'recommendation-service',
             'criticality': 'weak', 'fallback': True},
            {'caller': 'payment-service', 'callee': 'fraud-detection',
             'criticality': 'strong'},
        ]
    }

    dot = generate_dot(sample_graph, highlight_service='inventory-service')
    print(dot)
    # 可通过 dot -Tsvg output.dot -o dependency.svg 生成图片

生产环境实践清单

日常运维检查项

检查项频率工具关注点
依赖地图完整性每日Trace 分析 + 配置扫描动态发现的新边是否已文档化
故障域分析报告每周自研分析工具爆炸半径最大的 TOP 5 服务
单点故障识别每周依赖图分析无降级保护的强依赖服务
架构变更审计每次部署CI/CD 管道新增/移除的依赖是否评审
熔断器配置审查每月Istio/Resilience4j熔断阈值是否合理
容量水位检查每日Prometheus共享资源依赖的瓶颈分析

常见误区与纠正

误区一:依赖地图建一次就够了

架构是持续演进的,每周都有新服务上线、旧服务下线。依赖地图必须持续更新,否则会变成"过期的地图比没有地图更危险"。

纠正:建立自动化流水线,每日从 Trace 和配置中更新依赖图,每周生成变更报告。

误区二:所有依赖都需要熔断器

熔断器本身有复杂度和维护成本。对低风险、高频调用的内部服务过度配置熔断器,反而会增加误熔断概率。

纠正:按爆炸半径评分排序,优先为 TOP 10 的服务配置熔断器。弱依赖优先使用降级而非熔断。

误区三:故障域分析只做一次

系统架构变更后,故障域也会变化。一个原本隔离良好的服务,可能因为新增了一个数据库连接而成为跨团队的单点。

纠正:每次架构评审时同步更新故障域分析。CI/CD 中集成依赖变更检测,新增强依赖自动触发爆炸半径评估。

误区四:只关注同步调用依赖

异步消息队列的消费者故障可能导致消息积压,最终引发生产者阻塞。共享缓存的故障可能导致所有依赖该缓存的服务的缓存雪崩。

纠正:依赖地图必须包含异步依赖和共享资源依赖。对消息队列监控消费延迟和积压量。

总结

服务依赖地图和故障域分析是 SRE 可靠性工程的基础能力。没有清晰的依赖认知,故障排查只能靠运气;没有故障域边界控制,小故障随时可能演变成大灾难。

核心要点:

  1. 多源融合发现:静态配置扫描覆盖完整性,动态 Trace 观测确保准确性,两者互补构建可信的依赖地图
  2. 分层故障域模型:从容器到全局,每一层都有对应的隔离手段,嵌套的故障域是系统韧性的基础
  3. 爆炸半径可量化:通过图算法计算故障传播路径和受影响服务数量,用数据驱动优先级决策
  4. 控制策略组合拳:熔断器截断同步调用传播,降级策略保障弱依赖不拖垮核心链路,舱壁隔离防止资源争抢
  5. 持续更新是关键:依赖地图不是文档而是活的数据,必须通过自动化流水线持续维护

最终目标不是消灭所有故障——那不现实——而是让每个故障的影响范围可控、可预测、可快速恢复。一个爆炸半径可控的系统,才是真正可靠的系统。