第一章:Go HTTP服务吞吐骤降70%的现象复现与影响评估
某生产环境微服务在一次常规版本发布后,Prometheus监控显示其 QPS 从稳定 12,000 突降至约 3,600,降幅达 70%,P95 响应延迟从 42ms 升至 218ms。该服务基于 Go 1.21 构建,采用标准 net/http Server,无中间件框架,核心逻辑为 JSON 解析 + 简单内存缓存查表。
复现关键步骤
- 使用
wrk在压测机上复现负载:# 模拟生产流量特征:16 并发连接,持续 60 秒,keep-alive 复用 wrk -t4 -c16 -d60s -H "Connection: keep-alive" http://localhost:8080/api/status - 启动服务时启用 runtime 调试指标:
// 在 main.go 初始化处添加 import _ "net/http/pprof" go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() - 对比发布前后 commit 的构建产物,定位变更点:仅新增一行日志格式化代码 ——
log.Printf("req_id=%s method=%s path=%s", reqID, r.Method, r.URL.Path),但未使用r.URL.Path.String()而误调用r.URL.Path(类型为url.Path,非string),触发隐式fmt.Sprintf反射路径。
影响范围量化
| 指标 | 发布前 | 发布后 | 变化 |
|---|---|---|---|
| 平均 CPU 使用率 | 38% | 82% | ↑116% |
| Goroutine 数量(峰值) | ~1,200 | ~4,900 | ↑308% |
| GC Pause (p99) | 180μs | 4.2ms | ↑23× |
根本原因在于 fmt.Printf 对未导出结构体字段(如 url.URL.Path 内部字段)执行深度反射,导致每次请求额外消耗约 1.2ms CPU 时间,并显著增加逃逸对象与 GC 压力。该问题在低并发下不可见,但在高吞吐场景下呈指数级恶化。
第二章:net/http.serverConn泄漏的五层诊断法理论框架
2.1 基于Go运行时指标的连接生命周期建模与假设构建
Go 运行时通过 runtime/metrics 暴露了细粒度的网络连接观测信号,为连接生命周期建模提供可观测基础。
关键指标映射关系
| 指标路径 | 语义含义 | 生命周期阶段 |
|---|---|---|
/net/http/server/connections/live:count |
当前活跃 HTTP 连接数 | 建立 → 持有 |
/net/http/server/connections/closed:counter |
已关闭连接累计量 | 终止 |
/gc/heap/allocs-by-size:bytes(含 net.Conn 分配) |
连接对象内存分配频次 | 创建 |
连接状态跃迁假设
// 基于 runtime/metrics 构建的轻量级连接状态探测器
func observeConnLifecycle() {
m := metrics.All() // 获取全量指标快照
for _, metric := range m {
if strings.HasPrefix(metric.Name, "/net/http/server/connections/") {
var v metrics.Value
metrics.Read(&v) // 实际需按 name 过滤并读取
// 此处 v.Value 具有类型感知:count/counter/distribution
}
}
}
该函数每秒采样一次,将 /connections/live 与 /connections/closed 的差分趋势作为“连接泄漏”假设依据;若 live 持续增长而 closed 增速滞后,则触发连接复用不足或 defer conn.Close() 缺失的诊断假设。
graph TD
A[NewConn] -->|accept syscall| B[Handshake]
B -->|http.Serve| C[Active Request]
C -->|timeout/idle| D[Close]
C -->|panic/recover| D
D -->|runtime GC| E[Finalizer Finalized]
2.2 pprof + runtime.MemStats交叉验证goroutine与conn持有关系
在高并发服务中,goroutine 泄漏常伴随未关闭的 net.Conn,仅依赖单一指标易误判。需协同分析运行时内存状态与协程堆栈。
数据同步机制
runtime.MemStats 中 NumGoroutine 与 pprof 的 goroutine profile 实时性不同:前者是快照值(调用 runtime.ReadMemStats 时),后者是采样堆栈(/debug/pprof/goroutine?debug=2)。
验证脚本示例
// 同步采集双源数据,降低时序偏差
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("NumGoroutine: %d\n", m.NumGoroutine)
// 同时抓取阻塞型 goroutine 堆栈(含 conn 持有者)
resp, _ := http.Get("http://localhost:6060/debug/pprof/goroutine?debug=2")
逻辑说明:
debug=2返回完整堆栈,可定位net.(*conn).Read/Write调用链;NumGoroutine提供总量基线,二者差值突增即提示 conn 持有异常。
关键比对维度
| 指标 | 来源 | 采样延迟 | 是否含调用栈 |
|---|---|---|---|
NumGoroutine |
runtime.MemStats |
纳秒级 | ❌ |
goroutine profile |
pprof HTTP 接口 |
毫秒级 | ✅ |
graph TD
A[触发诊断] --> B[ReadMemStats]
A --> C[GET /debug/pprof/goroutine?debug=2]
B --> D[提取 NumGoroutine]
C --> E[解析堆栈中 net.Conn 相关帧]
D & E --> F[交叉标记疑似泄漏 goroutine]
2.3 net.Listener.Accept调用链追踪与serverConn初始化上下文还原
net.Listener.Accept() 是 Go HTTP 服务启动后首个阻塞式网络入口,其背后隐藏着连接上下文的完整构建过程。
Accept 调用链关键节点
tcpListener.Accept()→accept(fd)系统调用- 返回
*net.TCPConn,封装底层文件描述符与地址信息 http.Server.Serve()将其包装为*conn(未导出类型),并启动 goroutine 处理
serverConn 初始化核心字段
| 字段 | 类型 | 说明 |
|---|---|---|
rwc |
net.Conn |
原始连接,含读写缓冲与超时控制 |
remoteAddr |
string |
客户端地址(可能经 RemoteAddr() 代理修正) |
server |
*http.Server |
持有 Handler、TLSConfig、超时策略等全局配置 |
// src/net/http/server.go 中 conn.serve() 初始化片段
c := &conn{
server: s,
rwc: w, // *net.TCPConn,已由 Accept 返回
remoteAddr: w.RemoteAddr().String(),
}
// 启动独立 goroutine 处理该连接
go c.serve(connCtx)
此处
w即Accept()返回的连接;connCtx继承自s.baseContext,注入了Server生命周期信号与取消机制。c.serve()内部立即构建bufio.Reader/Writer并解析首行请求——上下文在此刻完成闭环。
2.4 http.Server.Serve循环中conn读写状态机异常路径的手动注入验证
在 http.Server.Serve 的连接处理循环中,conn 的读写状态机可能因网络中断、超时或恶意数据进入异常分支。为验证其健壮性,需手动注入典型异常。
异常注入点枚举
- TCP 连接建立后立即发送 RST
- 请求头末尾截断(如缺失
\r\n\r\n) - 超大
Content-Length后静默断连 - TLS 握手中途发送非法字节流
注入验证代码示例
// 模拟客户端:在读取响应头后强制关闭连接
conn, _ := net.Dial("tcp", "localhost:8080")
conn.Write([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n"))
time.Sleep(10 * time.Millisecond)
conn.Close() // 触发 conn.readLoop 中 io.EOF 或 io.ErrUnexpectedEOF
该操作迫使 serverConn.serve() 中的 readRequest 返回非 nil error,进而调用 c.setState(c.rwc, StateClosed),验证状态机是否正确迁移并释放资源。
| 异常类型 | 预期状态迁移 | 是否触发 closeNotify |
|---|---|---|
| RST on read | StateActive → StateClosed | 是 |
| Partial header | StateNew → StateHijacked? | 否(实际进 StateClosed) |
| Timeout before body | StateReadHeader → StateClosed | 是 |
graph TD
A[conn accepted] --> B{readRequest}
B -->|success| C[handleRequest]
B -->|io.EOF| D[setState StateClosed]
B -->|io.ErrUnexpectedEOF| D
D --> E[release resources]
2.5 GC标记阶段serverConn未被回收的逃逸分析与unsafe.Pointer泄漏推演
根因定位:serverConn在GC标记期仍被栈上临时变量隐式持有
当http.Server启动时,serverConn实例通过connServe()方法进入长生命周期,但其字段中嵌套的*tls.Conn持有unsafe.Pointer指向底层net.Conn缓冲区:
// serverConn 结构体关键字段(简化)
type serverConn struct {
conn net.Conn
tlsState *tls.Conn // 内部含 unsafe.Pointer 指向 rawConn.buf
curReq *Request // 可能逃逸至堆,延长 serverConn 生命周期
}
该unsafe.Pointer未被GC正确追踪——Go编译器无法验证其指向对象是否仍可达,导致关联的serverConn被误判为“活跃”。
逃逸路径链
connServe()中调用readRequest()→r.Header.ReadFrom(conn)conn参数发生栈逃逸(因被闭包捕获或传入接口)→serverConn升为堆分配tls.Conn内部buf通过unsafe.Pointer绕过类型系统 → GC标记器忽略该引用链
关键证据表:GC根集合扫描遗漏点
| 扫描阶段 | 是否覆盖 | 原因 |
|---|---|---|
| 栈帧变量 | ✅ | serverConn局部变量已出作用域 |
| 全局变量 | ❌ | tls.Conn.buf via unsafe.Pointer 不在根集合中 |
| 堆对象引用 | ⚠️ | curReq强引用serverConn,但tlsState指针不可见 |
graph TD
A[connServe goroutine] --> B[readRequest]
B --> C[tls.Conn.Read]
C --> D[unsafe.Pointer to rawBuf]
D -.->|GC不可见| E[serverConn heap object]
第三章:关键诊断工具链的工程化实践
3.1 自研conn-leak-detector工具的源码级集成与hook点部署
为精准捕获连接泄漏,conn-leak-detector 采用字节码增强 + 运行时 Hook 双模机制,在 DataSource 初始化与 Connection.close() 调用处埋点。
核心 Hook 点分布
HikariDataSource#initialize():注册连接生命周期监听器ProxyConnection#close():拦截实际关闭动作,触发泄漏判定ThreadLocal<Stack<TraceEntry>>:记录每个连接的获取堆栈快照
关键增强代码片段
// 在 ConnectionWrapper 构造时注入追踪上下文
public ConnectionWrapper(Connection delegate) {
this.delegate = delegate;
this.acquiredAt = System.nanoTime(); // 纳秒级精度
this.acquiredStack = Thread.currentThread().getStackTrace(); // 仅截取前8帧
ConnLeakTracker.register(this); // 全局弱引用注册表
}
逻辑分析:
acquiredStack保留调用栈用于定位泄漏源头;ConnLeakTracker使用WeakReference<ConnectionWrapper>避免内存泄漏,配合定时扫描(默认60s)未注销连接。
Hook 注册策略对比
| Hook 方式 | 触发时机 | 侵入性 | 支持数据源类型 |
|---|---|---|---|
| Java Agent | 类加载期增强 | 低 | HikariCP / Druid / Tomcat JDBC |
| Spring Bean PostProcessor | 容器启动后织入 | 中 | 仅 Spring 管理的 DataSource |
graph TD
A[DataSource 初始化] --> B[注册 ConnLeakTracker]
B --> C[Connection 获取]
C --> D[记录 acquire 堆栈 & 时间]
D --> E[Connection.close()]
E --> F{是否已 unregister?}
F -->|否| G[标记疑似泄漏]
F -->|是| H[清理 ThreadLocal 记录]
3.2 Go 1.21+ runtime/trace增强版HTTP连接轨迹可视化方案
Go 1.21 起,runtime/trace 深度集成 HTTP 连接生命周期事件(如 http.connect, http.request.start/end, http.response.write),支持零侵入式端到端连接追踪。
核心能力升级
- 新增
net/http专用 trace event 类型,无需httptrace.ClientTrace - 支持 TLS 握手、DNS 解析、连接复用(keep-alive)状态标记
go tool trace自动渲染 HTTP 时间线视图(Timeline → HTTP tab)
启用方式(代码块)
import _ "net/http/pprof" // 启用 /debug/trace 端点
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// 发起带 trace 的 HTTP 请求
req, _ := http.NewRequest("GET", "https://example.com", nil)
req = req.WithContext(runtime.WithTrace(req.Context())) // Go 1.21+
http.DefaultClient.Do(req)
}
runtime.WithTrace()将当前 goroutine 关联至 trace 会话;/debug/trace生成的.trace文件可直接加载查看 HTTP 连接时序图。
可视化事件对照表
| Event Name | 触发时机 | 关键字段 |
|---|---|---|
http.connect.start |
TCP 连接发起前 | host, port, proto |
http.response.write |
Header + Body 写入网络栈完成 | status, bytes_written |
graph TD
A[HTTP Client] -->|req.WithContext| B[runtime.WithTrace]
B --> C[trace.Event http.request.start]
C --> D{Connection Pool?}
D -->|Yes| E[http.reuse.conn]
D -->|No| F[http.connect.start → .end]
F --> G[http.request.write]
G --> H[http.response.read]
3.3 生产环境无侵入式netpoll监控与fd泄漏实时告警配置
Netpoll 是 Go net 库的高性能替代方案,其基于 epoll/kqueue 的事件驱动模型对文件描述符(FD)生命周期高度敏感。FD 泄漏将导致 EMFILE 错误并引发服务雪崩。
核心监控指标
- 活跃连接数(
netpoll_active_fds) - FD 分配/释放速率(
netpoll_fd_alloc_total,netpoll_fd_free_total) - 单 goroutine 持有 FD 超时(>30s 触发标记)
Prometheus 采集配置示例
# prometheus.yml
- job_name: 'netpoll-fd-monitor'
static_configs:
- targets: ['10.20.30.40:9100']
metrics_path: '/metrics/netpoll'
params:
format: ['prometheus']
该配置通过独立 /metrics/netpoll 端点暴露指标,避免污染主 metrics 路径,实现零侵入——无需修改业务代码,仅需启动 sidecar exporter。
告警规则(Prometheus Rule)
| 告警名称 | 表达式 | 阈值 | 持续时间 |
|---|---|---|---|
NetpollFDDrain |
rate(netpoll_fd_alloc_total[5m]) - rate(netpoll_fd_free_total[5m]) > 50 |
每分钟净增 >50 FD | 2m |
实时检测流程
graph TD
A[netpoll runtime hook] --> B[FD 分配/释放埋点]
B --> C[环形缓冲区聚合]
C --> D[每秒推送至 /metrics/netpoll]
D --> E[Prometheus pull]
E --> F[Alertmanager 触发 webhook]
关键参数说明:rate(...[5m]) 使用滑动窗口抑制瞬时抖动;环形缓冲区大小设为 65536,兼顾内存开销与回溯深度。
第四章:自动检测脚本设计与落地验证
4.1 基于go:linkname劫持net/http.(*conn).close的轻量埋点脚本
go:linkname 是 Go 编译器提供的非导出符号链接指令,允许跨包直接绑定未导出方法——这是实现无侵入 HTTP 连接级埋点的关键前提。
劫持原理
net/http.(*conn).close是连接关闭的核心路径,调用频次高、上下文丰富(含 remoteAddr、tlsState、duration)- 该方法未导出,但符号在二进制中可见,可通过
//go:linkname强制绑定
核心代码示例
//go:linkname httpConnClose net/http.(*conn).close
func httpConnClose(c *conn, err error) {
// 记录连接生命周期指标(如 TLS 握手耗时、请求计数)
observeConnClose(c, err)
// 委托原函数执行实际关闭逻辑
origHTTPConnClose(c, err)
}
origHTTPConnClose是通过unsafe.Pointer获取原始方法指针后构造的函数变量;observeConnClose需在init()中注册指标采集逻辑。此方式零依赖、无中间件、不修改标准库源码。
| 特性 | 原生中间件方案 | go:linkname 劫持 |
|---|---|---|
| 性能开销 | 每请求 ≥2 层函数调用 | 仅 1 次跳转 + 内联友好 |
| 覆盖粒度 | Handler 级 | 连接级(含健康检查、长连接空闲关闭) |
graph TD
A[HTTP 请求到达] --> B[net/http.serverHandler.ServeHTTP]
B --> C[创建 *conn 实例]
C --> D[连接关闭触发 close()]
D --> E[劫持入口 httpConnClose]
E --> F[埋点采集]
F --> G[调用原始 close]
4.2 serverConn引用计数快照比对算法与阈值自适应判定逻辑
核心思想
在高并发连接管理中,serverConn 的生命周期需精准判定:避免过早释放(导致 use-after-free),亦不可滞留过久(引发连接泄漏)。本节采用双快照差分 + 动态阈值机制实现安全、自适应的引用计数治理。
快照采集与比对逻辑
每 500ms 采集一次全局 serverConn 引用计数快照,与前一快照做 Delta 计算:
// snapshotDiff 计算两次快照间引用计数变化量
func snapshotDiff(prev, curr map[*serverConn]int) map[*serverConn]int {
diff := make(map[*serverConn]int)
for conn, cnt := range curr {
prevCnt := prev[conn]
if delta := cnt - prevCnt; delta < 0 { // 仅关注递减趋势(释放信号)
diff[conn] = delta
}
}
return diff
}
逻辑分析:
delta < 0表明该连接引用被显式释放,是候选回收对象;prev[conn]默认为 0,确保新连接不误判;返回负值便于后续阈值聚合。
自适应阈值判定
| 指标 | 初始值 | 调整规则 |
|---|---|---|
基线阈值 baseT |
3 | 每连续 3 次 |delta| == 1 → baseT++ |
衰减系数 decay |
0.92 | 每次无显著变化时乘以 decay |
决策流程
graph TD
A[采集当前快照] --> B[计算 delta = curr - prev]
B --> C{delta < -baseT ?}
C -->|是| D[标记待回收]
C -->|否| E[更新 baseT 或 decay]
E --> A
4.3 Prometheus Exporter暴露泄漏特征指标(activeConn, leakedConnAge, closeSkipped)
数据库连接池健康度需通过细粒度指标持续观测。activeConn 表示当前活跃连接数,leakedConnAge 记录最早未释放连接的存活时长(秒),closeSkipped 统计因连接已关闭而跳过显式关闭操作的次数。
关键指标语义
activeConn:瞬时值,突增可能预示连接未归还;leakedConnAge > 300:连接泄漏高置信信号;closeSkipped > 0:常与连接复用逻辑缺陷或异常分支遗漏相关。
示例 exporter 指标输出
# HELP db_pool_active_conn Current number of active connections
# TYPE db_pool_active_conn gauge
db_pool_active_conn{pool="primary"} 17
# HELP db_pool_leaked_conn_age_seconds Age of oldest leaked connection
# TYPE db_pool_leaked_conn_age_seconds gauge
db_pool_leaked_conn_age_seconds{pool="primary"} 428.6
# HELP db_pool_close_skipped_total Number of skipped close() calls
# TYPE db_pool_close_skipped_total counter
db_pool_close_skipped_total{pool="primary"} 3
上述指标由自定义 Exporter 通过反射扫描连接池内部状态采集;
leakedConnAge需池实现支持泄漏检测钩子(如 HikariCP 的leakDetectionThreshold启用后才填充)。
指标关联诊断逻辑
graph TD
A[activeConn > threshold] --> B{leakedConnAge > 0?}
B -->|Yes| C[定位泄漏源头:调用栈+SQL标签]
B -->|No| D[检查连接获取/释放配对]
C --> E[closeSkipped 增速突增 → 异常处理路径绕过close]
4.4 Kubernetes InitContainer预检脚本:启动前强制触发conn泄漏压力测试
InitContainer 在主容器启动前执行隔离、可重试的校验逻辑,是保障服务健康上线的关键防线。
为何选择 InitContainer 做连接泄漏压测?
- 隔离性:不污染主容器运行时环境
- 强制性:失败则 Pod 卡在
Init:0/1状态,阻断异常上线 - 可观测性:日志与事件独立归集
压测脚本核心逻辑(bash + netstat)
#!/bin/sh
# 模拟并发短连接并验证端口残留(泄漏判定阈值:>50 ESTABLISHED/Time-Wait)
for i in $(seq 1 200); do
timeout 0.1 curl -s http://localhost:8080/health &
done
sleep 2
LEAKED=$(netstat -an | grep ':8080' | grep -E '(ESTABLISHED|TIME_WAIT)' | wc -l)
[ $LEAKED -gt 50 ] && echo "FAIL: conn leak detected ($LEAKED)" && exit 1
echo "PASS: no significant leakage"
逻辑说明:通过快速发包制造连接洪峰,
timeout 0.1避免阻塞,sleep 2确保连接进入内核队列;netstat统计目标端口状态数,超阈值即终止初始化流程。
InitContainer YAML 片段关键字段
| 字段 | 值 | 说明 |
|---|---|---|
image |
alpine/curl:latest |
轻量镜像,含 netstat 和 curl |
resources.limits.memory |
128Mi |
防止压测耗尽节点内存 |
restartPolicy |
Always |
默认不生效(InitContainer 仅执行一次) |
graph TD
A[Pod 创建] --> B[InitContainer 启动]
B --> C[执行 conn 泄漏压测脚本]
C --> D{泄漏数 ≤ 50?}
D -->|是| E[启动 mainContainer]
D -->|否| F[标记 Init 失败<br>Pod 卡在 Pending]
第五章:从定位到根治——Go HTTP服务连接管理的最佳实践演进
连接泄漏的真实现场:pprof 与 netstat 联动诊断
某电商订单服务在大促压测中持续增长 goroutine 数(runtime.NumGoroutine() 从 1.2k 爬升至 8.6k),/debug/pprof/goroutine?debug=2 显示超 3000 个处于 net/http.(*persistConn).readLoop 阻塞状态。同步执行 netstat -anp | grep :8080 | grep ESTABLISHED | wc -l 发现连接数达 4217,远超 http.DefaultTransport.MaxIdleConnsPerHost 默认值(2)。根源锁定在未关闭响应体的第三方 SDK 调用——resp, _ := client.Do(req); defer resp.Body.Close() 被错误替换为 defer resp.Body.Read(nil),导致底层连接无法归还 idle pool。
Transport 配置黄金组合:生产环境实测参数表
| 参数 | 推荐值 | 影响说明 | 监控指标 |
|---|---|---|---|
MaxIdleConns |
200 | 全局空闲连接上限,防内存泄漏 | http_transport_idle_conns_total |
MaxIdleConnsPerHost |
100 | 单 Host 最大空闲连接,避免单点打爆 | http_transport_idle_conns_per_host |
IdleConnTimeout |
90s | 连接空闲超时时间,平衡复用与陈旧连接 | http_transport_idle_conn_closed_total |
TLSHandshakeTimeout |
5s | TLS 握手超时,防 handshake hang | http_transport_tls_handshake_errors_total |
连接生命周期可视化:Mermaid 状态机
stateDiagram-v2
[*] --> Idle
Idle --> Dialing: GetConn()
Dialing --> Active: Dial success
Dialing --> Idle: Dial timeout/fail
Active --> Idle: Response.Body.Close()
Active --> Broken: Read/Write error
Broken --> Idle: Connection reset
Idle --> [*]: GC cleanup or timeout
自定义 RoundTripper 实现连接水位告警
type AlertRoundTripper struct {
base http.RoundTripper
idleGauge prometheus.Gauge
}
func (a *AlertRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
connPool := a.base.(*http.Transport).IdleConnTimeout
idleCount := len(a.base.(*http.Transport).idleConn)
if idleCount > 80 {
alertConnWaterLevel(idleCount) // 触发企业微信告警
}
return a.base.RoundTrip(req)
}
HTTP/2 连接复用陷阱:Header 大小引发的连接分裂
某 SaaS 平台升级 HTTP/2 后,http2.maxConcurrentStreams 被默认设为 250,但每个请求携带 12KB 的 JWT Token(Base64 编码后约 16KB),触发 HPACK 压缩失败,导致 http2.ErrFrameTooLarge。实际观测到每秒新建连接数激增 300%,通过 curl -v --http2 https://api.example.com -H "X-Auth-Token: $(head -c 12000 /dev/urandom | base64)" 复现问题。解决方案:服务端启用 golang.org/x/net/http2/h2c 并配置 h2c.NewHandler(handler, &http2.Server{MaxConcurrentStreams: 500}),客户端强制 Token 分片传输。
连接池健康检查:主动探测空闲连接有效性
func healthCheckIdleConns(transport *http.Transport) {
for host, conns := range transport.IdleConn {
for i := range conns {
go func(c net.Conn) {
if err := c.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
transport.CloseIdleConnections() // 清理整池
return
}
if _, err := c.Read(make([]byte, 1)); err != nil && !errors.Is(err, io.EOF) {
c.Close()
}
}(conns[i].conn)
}
}
} 