第一章:Go工具内存泄漏难复现?——使用go tool trace + goroutine dump + heap profile三阶定位法(附真实OOM案例)
在高并发微服务中,内存泄漏常表现为缓慢增长的RSS、偶发OOM Killer杀进程,但pprof常规heap profile却难以捕获——因泄漏对象可能被临时强引用,或仅在特定请求链路中持续逃逸。此时需结合运行时行为、协程生命周期与堆快照,实施三阶协同分析。
启动带追踪能力的服务
确保编译时启用运行时追踪支持,并在启动时开启多维度采集:
# 编译时保留符号信息,禁用内联便于溯源
go build -gcflags="-l -N" -o server .
# 启动服务并同时写入trace、goroutine dump与heap profile
GODEBUG=gctrace=1 ./server &
PID=$!
sleep 30 # 等待业务流量进入稳态
go tool trace -http=localhost:8080 /tmp/trace.out & # 后台启动trace UI
捕获关键诊断数据
在疑似泄漏窗口期(如RSS上升阶段),按顺序执行以下命令,确保时间戳对齐:
kill -SIGQUIT $PID→ 生成goroutine stack dump(含阻塞状态与等待链)curl "http://localhost:6060/debug/pprof/heap?debug=1" > heap.pb.gz→ 获取实时堆快照(注意:?debug=1输出文本格式,更易grep)go tool pprof -http=:8081 heap.pb.gz→ 启动pprof Web界面,聚焦top --cum与web视图
分析泄漏三角证据链
| 证据类型 | 关键线索 | 泄漏指示特征 |
|---|---|---|
| goroutine dump | 大量runtime.gopark且chan receive |
协程长期阻塞于未关闭channel接收 |
| trace UI | “Goroutines”视图中持续增长的G数量 | 新建G未随请求结束而GC,存在goroutine泄露 |
| heap profile | runtime.mallocgc调用栈中高频出现自定义结构体 |
对象分配集中于某业务模块,且inuse_space不降 |
真实案例中,某订单聚合服务在压测2小时后OOM;通过三阶比对发现:orderProcessor协程因下游gRPC超时未设置context.WithTimeout,导致数千goroutine永久挂起于recvfrom系统调用,其持有的*Order切片持续阻止底层字节缓冲区回收——最终heap profile显示[]byte占堆78%,而trace明确标识这些G处于GC assist marking阻塞态,而非活跃执行。
第二章:内存泄漏诊断的底层原理与工具链协同机制
2.1 Go运行时内存模型与GC触发条件的深度解析
Go 运行时采用 三色标记-清除(Tri-color Mark-and-Sweep) 模型,配合写屏障(write barrier)保障并发标记安全。内存按 span、mcache、mcentral、mheap 分层管理,对象分配优先走 mcache 的本地缓存。
GC 触发的三大核心条件
GOGC环境变量设定的堆增长比率(默认100%,即堆大小翻倍时触发)- 手动调用
runtime.GC() - 系统检测到长时间未触发 GC 且堆内存持续增长(强制兜底)
关键阈值计算逻辑
// runtime/mgc.go 中的触发判定简化逻辑
func memstatsTrigger() bool {
heapLive := memstats.heap_live // 当前活跃堆字节数
heapGoal := memstats.heap_alloc * (1 + GOGC/100) // 目标上限
return heapLive >= heapGoal
}
该函数在每次 mallocgc 后被检查;heap_live 是原子更新的实时活跃对象总和,heap_alloc 是上一次 GC 完成后的堆分配基线。
| 触发源 | 延迟性 | 可控性 | 典型场景 |
|---|---|---|---|
| GOGC 自适应 | 中 | 高 | 生产环境默认策略 |
| runtime.GC() | 低 | 最高 | 压测后强制回收 |
| 强制兜底 | 高 | 无 | 内存泄漏或 GOGC=off 时 |
graph TD
A[分配新对象] --> B{heap_live ≥ heap_goal?}
B -->|是| C[启动GC循环]
B -->|否| D[继续分配]
C --> E[启用写屏障]
E --> F[并发标记 → 清扫 → 重置]
2.2 go tool trace中goroutine生命周期与阻塞事件的语义映射实践
go tool trace 将运行时事件映射为可视化的 goroutine 状态跃迁,核心在于 G(goroutine)在 Runnable、Running、Syscall、IOWait、ChanSend/Recv 等状态间的精确标记。
goroutine 阻塞事件的语义锚点
block: 标记进入阻塞前的最后用户栈帧(如runtime.gopark调用点)unblock: 对应唤醒时机(如runtime.ready或netpoll回调)sync.Mutex争用被归类为SyncBlock,而非ChanRecv
典型阻塞场景分析(含 trace 标记)
func blockingRead() {
conn, _ := net.Dial("tcp", "example.com:80")
buf := make([]byte, 1024)
n, _ := conn.Read(buf) // trace 中标记为 "IOWait → Running" 跃迁
}
此处
conn.Read触发runtime.netpollblock,trace 将其语义化为IOWait状态,并关联fd和epoll_wait系统调用上下文;n返回后立即进入Running,完成一次完整生命周期闭环。
| 事件类型 | trace 状态标签 | 关联运行时函数 |
|---|---|---|
| channel receive | ChanRecv |
runtime.chanrecv |
| mutex lock | SyncBlock |
runtime.semacquire |
| network read | IOWait |
internal/poll.runtime_pollWait |
graph TD
A[goroutine 创建] --> B[Runnable]
B --> C[Running]
C --> D[IOWait]
D --> E[netpoll 唤醒]
E --> C
2.3 goroutine dump中栈帧分析与协程堆积模式识别(含pprof -goroutine raw解析)
栈帧结构的关键线索
runtime.gopark、sync.runtime_SemacquireMutex、net/http.(*conn).serve 等栈顶函数是协程阻塞/等待的典型指纹。持续出现数百个相同栈顶序列,往往指向资源争用或未关闭连接。
pprof raw 输出解析示例
go tool pprof -goroutine -raw http://localhost:6060/debug/pprof/goroutine?debug=2
该命令输出原始文本格式的 goroutine dump(含 ID、状态、完整调用栈),便于 grep/awk 批量模式匹配,避免 HTML 渲染干扰。
常见堆积模式对照表
| 模式类型 | 栈顶特征 | 根因示意 |
|---|---|---|
| HTTP 连接泄漏 | net/http.(*conn).serve |
客户端未复用连接或服务端未设超时 |
| Mutex 争用 | sync.runtime_SemacquireMutex |
高并发写共享 map 未加锁 |
| Channel 阻塞 | runtime.chansend / chanrecv |
无缓冲 channel 发送方无接收者 |
协程状态流转图
graph TD
A[New] --> B[Runnable]
B --> C[Running]
C --> D[Waiting: net, mutex, chan]
D -->|timeout/notify| B
C -->|blocking syscall| D
2.4 heap profile采样机制、分配速率与存活对象图的关联建模
heap profile 通过周期性采样(如每分配512KB触发一次栈追踪)捕获内存分配上下文,而非全量记录,兼顾精度与开销。
采样触发逻辑
// Go runtime 源码简化示意(src/runtime/mprof.go)
const heapProfileRate = 512 << 10 // 512KB
if m.heap_allocs >= atomic.Load64(&memstats.next_sample) {
recordAllocationStack() // 记录当前 goroutine 栈帧
atomic.Store64(&memstats.next_sample,
memstats.heap_allocs+heapProfileRate)
}
heapProfileRate 决定采样粒度:值越小,分配速率估算越准,但开销越高;该值直接耦合到存活对象图的时间分辨率——高频分配若未被采样,将导致图中边(引用关系)缺失。
三者动态关联
| 维度 | 影响存活对象图的表现 |
|---|---|
| 采样间隔 | 决定对象生命周期在图中的时间切片密度 |
| 分配速率 | 高速率下未采样分配易造成“幽灵对象”断连 |
| GC标记阶段 | 仅对存活对象构建引用边,采样点需覆盖其首次分配与最后强引用 |
graph TD
A[分配事件流] -->|按heapProfileRate采样| B[栈追踪快照]
B --> C[构建分配上下文节点]
C --> D[GC后过滤非存活节点]
D --> E[基于引用关系补全存活对象图边]
2.5 三阶工具时间轴对齐策略:trace时间戳、dump采集点、heap采样周期的精准协同
数据同步机制
三阶工具协同的核心在于纳秒级时间基准统一。JVM 启动时通过 os::elapsed_counter() 注册单调递增的硬件计数器作为全局时钟源,避免系统时钟回跳干扰。
时间戳对齐实践
// JVM TI Agent 中 trace 事件时间戳标准化处理
jlong trace_ts = nanoTime(); // 基于 CLOCK_MONOTONIC_RAW
jlong dump_ts = os::javaTimeNanos(); // 与 GC dump 共用同一时钟源
jlong heap_sample_ts = (trace_ts / sample_period_ns) * sample_period_ns;
nanoTime()提供高精度、无偏移的 trace 时间戳;os::javaTimeNanos()确保 dump 事件与 GC 日志时间轴一致;heap_sample_ts向下取整至最近采样周期边界,消除相位漂移。
协同参数配置表
| 工具类型 | 默认周期 | 对齐约束 | 时钟源 |
|---|---|---|---|
| Async Profiler trace | 100μs | 必须为 heap sample 周期整数倍 | CLOCK_MONOTONIC_RAW |
| Heap dump(jmap) | 手动触发 | 触发时刻需落在 trace 时间窗内 ±5ms | os::javaTimeNanos() |
| G1 heap sampling | 1s | 周期起始点与 trace 首次时间戳对齐 | 同 dump 时钟源 |
时序协同流程
graph TD
A[trace 启动] --> B[记录首个 nanoTime() 为 t₀]
B --> C[计算最近 heap sample 边界 t₁ = floor(t₀ / T) × T]
C --> D[dump 触发逻辑等待 t₁ + Δt 内完成]
D --> E[三阶数据在统一时间轴上可直接 cross-reference]
第三章:典型内存泄漏场景的模式化归因分析
3.1 全局Map未清理+闭包引用导致的隐式内存驻留实战复现
问题场景还原
某实时风控服务中,static final Map<String, UserContext> CACHE = new ConcurrentHashMap<>(); 被用于缓存用户会话上下文,但未配套失效策略。
闭包陷阱示例
public class RiskEvaluator {
private static final Map<String, Supplier<BigDecimal>> RULE_CACHE = new HashMap<>();
public static void registerRule(String id, User user) {
// ❌ 闭包捕获user实例,导致其无法GC
RULE_CACHE.put(id, () -> user.getRiskScore() * 1.2);
}
}
逻辑分析:Supplier 是函数式接口实例,其底层 LambdaMetafactory 生成的类持有了对 user 的强引用;而 RULE_CACHE 是静态Map,生命周期与类加载器一致。user 对象因此被隐式驻留,即使业务层已无其他引用。
关键参数说明
user: 原始业务对象(含大字段如byte[] profilePic)RULE_CACHE: 静态持有,无主动清理机制 → 内存泄漏温床
修复对比表
| 方案 | 是否解决闭包引用 | 是否需手动清理 | GC 友好性 |
|---|---|---|---|
| WeakReference 包装 Supplier | ✅ | ❌(依赖GC) | ⭐⭐⭐⭐ |
| 显式 remove + 定时清理 | ✅ | ✅ | ⭐⭐⭐⭐⭐ |
| 使用 ThreadLocal 替代静态Map | ✅ | ✅(线程结束自动释放) | ⭐⭐⭐⭐ |
graph TD
A[registerRule调用] --> B[创建Lambda实例]
B --> C[隐式持有user引用]
C --> D[存入静态RULE_CACHE]
D --> E[user对象无法被GC]
3.2 Context取消未传播引发的goroutine与资源泄漏链路追踪
当父 context 被取消,但子 goroutine 未监听 ctx.Done() 或忽略 <-ctx.Done() 通道,将导致 goroutine 永久阻塞,进而拖拽其持有的资源(如数据库连接、文件句柄、HTTP client 连接池)无法释放。
典型泄漏模式
- 子 goroutine 启动后未接收 cancel 信号
- 使用
context.WithTimeout但未在 select 中监听ctx.Done() - 将 context 传入闭包却未在循环/IO 中持续检查
错误示例与修复
func badHandler(ctx context.Context) {
go func() {
time.Sleep(10 * time.Second) // ❌ 无视 ctx 取消
fmt.Println("done")
}()
}
func goodHandler(ctx context.Context) {
go func() {
select {
case <-time.After(10 * time.Second):
fmt.Println("done")
case <-ctx.Done(): // ✅ 响应取消
return
}
}()
}
badHandler 中 goroutine 不感知父 ctx 生命周期,造成泄漏;goodHandler 通过 select 双通道监听,确保可及时退出。
| 场景 | 是否传播取消 | 后果 |
|---|---|---|
未监听 ctx.Done() |
否 | goroutine 泄漏 + 关联资源滞留 |
| 正确 select 监听 | 是 | 及时终止,资源归还 |
graph TD
A[Parent ctx Cancel] --> B{Child goroutine<br>监听 ctx.Done?}
B -->|No| C[goroutine 永驻]
B -->|Yes| D[收到 signal → clean up → exit]
C --> E[DB conn leak / FD exhaustion]
3.3 sync.Pool误用与对象逃逸共同导致的堆膨胀案例拆解
问题现场还原
某高并发日志采集服务中,sync.Pool 被用于复用 bytes.Buffer,但因错误地将池化对象返回给 goroutine 外部引用,触发逃逸分析失败:
var bufPool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
func badLogMarshal(data map[string]interface{}) []byte {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
json.NewEncoder(buf).Encode(data) // ⚠️ encode 可能触发 buf 底层数组扩容
result := buf.Bytes() // ❌ 返回底层数组引用 → 对象逃逸至堆
bufPool.Put(buf) // 但 buf 仍被外部持有,后续 Put 无效
return result // 实际返回的是 buf.buf 指向的堆内存块
}
逻辑分析:buf.Bytes() 返回 []byte 是对 buf.buf 的切片视图,其底层数据未受 Pool 管理;GC 无法回收该内存,且每次调用都新建底层数组(扩容后),造成持续堆增长。
关键逃逸路径
json.Encoder.Encode内部调用buf.Grow→ 触发append→ 底层数组重新分配buf.Bytes()返回值被函数外捕获 → 编译器判定buf.buf逃逸至堆
修复对比表
| 方案 | 是否避免逃逸 | Pool 复用率 | 内存峰值 |
|---|---|---|---|
return buf.Bytes()(原逻辑) |
❌ 逃逸 | 持续上升 | |
b := make([]byte, buf.Len()); copy(b, buf.Bytes()); bufPool.Put(buf) |
✅ | >95% | 稳定 |
graph TD
A[调用 badLogMarshal] --> B[Get *bytes.Buffer]
B --> C[Encode 触发 Grow/append]
C --> D[buf.Bytes 返回底层数组引用]
D --> E[对象逃逸至堆]
E --> F[bufPool.Put 失效]
F --> G[下次 Get 新分配 → 堆膨胀]
第四章:三阶定位法在生产环境OOM事故中的闭环落地
4.1 某高并发日志聚合工具OOM前10分钟trace信号提取与关键goroutine染色
数据采集策略
启用 runtime/trace 并配合 GODEBUG=gctrace=1,在OOM前10分钟高频采样(500ms间隔):
// 启动trace并绑定OOM临界钩子
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
// 注册内存阈值回调(基于memstats)
go func() {
for range time.Tick(500 * time.Millisecond) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
if m.Alloc > 800*1024*1024 { // >800MB触发染色
colorCriticalGoroutines()
}
}
}()
逻辑分析:runtime.ReadMemStats 非阻塞读取实时堆分配量;colorCriticalGoroutines() 通过 debug.ReadGCStats 关联最近GC压力,定位持续运行超2s且持有*bytes.Buffer的goroutine。
关键goroutine染色标记规则
| 标签名 | 触发条件 | 染色优先级 |
|---|---|---|
log-batch-writer |
阻塞在 bufio.Writer.Flush() 超1.5s |
⭐⭐⭐⭐ |
proto-encoder |
单次 proto.Marshal 耗时 >300ms |
⭐⭐⭐ |
ringbuf-full |
sync.Pool.Get() 返回 nil 频次 >5/s |
⭐⭐ |
染色传播路径
graph TD
A[OOM预警] --> B{Alloc > 800MB?}
B -->|Yes| C[遍历allg: runtime.gList]
C --> D[过滤状态==Grunning]
D --> E[检查栈帧含'WriteTo'/'Encode']
E --> F[打标log-batch-writer]
4.2 基于runtime.Stack()增强版goroutine dump的泄漏源头快速聚类
传统 runtime.Stack() 仅输出扁平字符串,难以定位 goroutine 泄漏的共性模式。增强方案需结构化解析栈帧并聚合特征。
栈帧结构化提取
func captureStacks() map[string][]string {
buf := make([]byte, 1024*1024)
n := runtime.Stack(buf, true) // true: all goroutines
stacks := parseStackDump(string(buf[:n]))
return groupBySignature(stacks) // 按调用栈哈希聚类
}
runtime.Stack(buf, true) 获取全量 goroutine 栈;parseStackDump 按 goroutine \d+ \[.*?\]: 分割,每组提取前5层函数名生成签名(如 http.(*Server).Serve|net.(*conn).readLoop|io.ReadAtLeast)。
聚类结果示例
| 签名摘要 | 实例数 | 典型阻塞点 |
|---|---|---|
db.QueryRow|sql.(*DB).conn|semaphore.Acquire |
142 | 数据库连接池耗尽 |
time.Sleep|worker.Run|sync.WaitGroup.Wait |
89 | 长周期定时任务未退出 |
自动根因推断流程
graph TD
A[原始Stack Dump] --> B[按goroutine切分]
B --> C[提取top-5函数调用链]
C --> D[MD5哈希归一化]
D --> E[频次统计+阈值过滤]
E --> F[关联pprof label与trace span]
4.3 heap profile交叉比对:alloc_space vs inuse_space异常拐点定位
Heap profile 中 alloc_space(累计分配量)与 inuse_space(当前驻留量)的偏离趋势,是内存泄漏或对象生命周期异常的关键信号。
拐点识别逻辑
当 alloc_space 持续陡增而 inuse_space 平缓滞涨,差值(alloc - inuse)突增即为可疑拐点。可通过 pprof 工具提取双指标时间序列:
# 提取每30秒采样一次的堆概要(含alloc/inuse)
go tool pprof -http=:8080 \
-symbolize=full \
-sample_index=alloc_space \
http://localhost:6060/debug/pprof/heap?debug=1
参数说明:
-sample_index=alloc_space切换采样维度;若需比对,需二次执行并指定inuse_space;-symbolize=full确保函数名可读,避免内联干扰定位。
典型拐点模式对照表
| 模式类型 | alloc_space 趋势 | inuse_space 趋势 | 根因线索 |
|---|---|---|---|
| 内存泄漏 | 持续线性上升 | 缓慢阶梯式上升 | 对象未被 GC 回收 |
| 批处理堆积 | 周期性脉冲 | 脉冲后不回落 | 缓存未及时清理 |
| GC 暂停抖动 | 阶梯跃升 | 短暂持平后回落 | STW 期间分配集中爆发 |
分析流程图
graph TD
A[采集 alloc_space 时间序列] --> B[同步采集 inuse_space 序列]
B --> C[计算 delta = alloc - inuse]
C --> D{delta 斜率 > 阈值?}
D -->|是| E[定位 delta 峰值时间点]
D -->|否| F[排除拐点]
E --> G[关联该时刻 goroutine stack]
4.4 修复验证阶段的轻量级回归检测脚本(自动触发三阶采样+差异告警)
核心设计思想
以低开销保障修复有效性:在部署后自动执行「全量基线 → 随机抽样 → 关键路径聚焦」三级采样,仅对差异指标触发细粒度告警。
三阶采样流程
# 自动化检测入口(shell + Python 混合调用)
python3 regress_check.py \
--baseline=prod-v2.1.0.json \
--target=prod-v2.1.1.json \
--sample-ratio=0.05 \
--critical-apis="login,order_submit,pay_callback"
逻辑分析:
--sample-ratio=0.05控制第二阶随机采样覆盖5%接口实例;--critical-apis显式指定第三阶必检路径,跳过白名单外的非关键链路,降低P99延迟至
差异响应策略
| 差异类型 | 告警等级 | 自动动作 |
|---|---|---|
| 响应码分布偏移 >3% | HIGH | 阻断灰度放量 + 钉钉通知 |
| P95延迟上升 >15% | MEDIUM | 输出调用栈热力图 |
| 字段缺失率 >0.1% | LOW | 记录至审计日志 |
执行时序(mermaid)
graph TD
A[触发修复完成事件] --> B[加载基线快照]
B --> C[第一阶:全量指标比对]
C --> D{差异超阈值?}
D -- 否 --> E[标记验证通过]
D -- 是 --> F[第二阶:0.05随机采样]
F --> G[第三阶:critical-apis深度探针]
G --> H[生成差异报告+分级告警]
第五章:总结与展望
技术栈演进的现实挑战
在某大型金融风控平台的迁移实践中,团队将原有基于 Spring Boot 2.3 + MyBatis 的单体架构逐步重构为 Spring Cloud Alibaba(Nacos 2.2 + Sentinel 1.8 + Seata 1.5)微服务集群。过程中发现:服务间强依赖导致灰度发布失败率高达37%,最终通过引入 OpenTelemetry 1.24 全链路追踪 + 自研流量染色中间件,将故障定位平均耗时从42分钟压缩至90秒以内。该方案已在2023年Q4全量上线,支撑日均1200万笔实时反欺诈决策。
工程效能的真实瓶颈
下表对比了三个典型项目在CI/CD流水线优化前后的关键指标:
| 项目名称 | 构建耗时(优化前) | 构建耗时(优化后) | 单元测试覆盖率提升 | 部署成功率 |
|---|---|---|---|---|
| 支付网关V3 | 18.7 min | 4.2 min | +22.3% | 99.98% → 99.999% |
| 账户中心 | 23.1 min | 6.8 min | +15.6% | 98.2% → 99.87% |
| 对账引擎 | 31.4 min | 8.3 min | +31.1% | 96.5% → 99.62% |
优化核心在于:采用 TestContainers 替代本地 MySQL Docker Compose、启用 Gradle Configuration Cache、将 SonarQube 扫描移至 PR 阶段而非主干合并后。
生产环境的可观测性缺口
某电商大促期间,订单履约服务出现偶发性 503 错误,Prometheus 原始指标显示 CPU 使用率仅42%,但通过 eBPF 抓取的内核级 trace 发现:tcp_retransmit_skb 调用频次突增17倍,根源是 Kubernetes Node 上 net.ipv4.tcp_retries2 参数被误设为3(应为15)。该问题在传统监控体系中完全不可见,最终通过部署 Cilium Hubble UI 实现实时网络行为可视化。
# 生产环境紧急修复命令(已验证)
kubectl get nodes -o wide | awk '{print $1}' | xargs -I{} kubectl debug node/{} --image=quay.io/cilium/cilium-cli:latest -- chroot /host sysctl -w net.ipv4.tcp_retries2=15
云原生落地的组织适配
某省级政务云项目采用 Karmada 多集群编排,但运维团队长期习惯于单集群 Shell 操作。为降低认知负荷,团队开发了 karmada-cli-proxy 工具——当用户执行 kubectl get pods -n prod 时,自动识别目标集群并透传请求,同时在终端输出路由路径(如 → cluster-shanghai → cluster-nanjing),日志自动聚合到 Loki,并支持按集群维度快速切片查询。
未来技术债的量化管理
团队建立技术债看板,对每项债务标注:影响模块、预估修复工时、当前阻塞PR数量、SLA降级等级。例如“Kafka 2.7 升级至 3.5”标记为 P0 级债务,因现有版本无法兼容 TLS 1.3,已导致3个外部监管系统对接失败;该条目关联12个未合入PR,预计需17人日,已排期至2024年Q2迭代。
安全左移的实证效果
在2024年一季度的 DevSecOps 实践中,将 Trivy 0.42 集成至 GitLab CI,在代码提交阶段扫描容器镜像及 SBOM 清单。共拦截高危漏洞217个,其中142个在开发机本地即被阻断(提前4.3天),剩余75个进入 SCA 流水线环节。特别值得注意的是:log4j-core-2.17.1.jar 在 Maven 依赖树中被间接引用达38处,传统人工审计几乎不可能覆盖全部路径。
flowchart LR
A[Git Push] --> B{Trivy Scan}
B -->|Clean| C[Build Image]
B -->|Vulnerable| D[Block & Notify Slack]
D --> E[Developer Fixes pom.xml]
E --> A 