第一章:Go语言输入流并发陷阱的全景认知
Go语言中,io.Reader 接口的并发使用看似安全,实则暗藏多重竞态风险。当多个 goroutine 同时调用同一 Reader 的 Read() 方法时,底层状态(如缓冲区偏移、内部字节计数器、网络连接读取位置等)可能被非原子地修改,导致数据错乱、重复读取或静默截断——这类问题在 os.Stdin、bytes.Reader、bufio.Reader 及 HTTP 响应体等常见输入流上均真实复现。
并发读取的典型失效场景
- 多个 goroutine 调用
stdin.Read():标准输入流无内置锁,首次读取后os.Stdin内部文件偏移未同步,后续 goroutine 可能读到相同字节或跳过部分数据; bufio.Reader被共享:其buf缓冲区和rd字段为非线程安全设计,Peek()与Read()交叉调用会破坏缓冲一致性;- HTTP 响应体重复消费:
http.Response.Body是一次性流,ioutil.ReadAll()后再次Read()返回io.EOF,若未显式关闭或重置,易引发逻辑断裂。
验证竞态的最小可复现实例
package main
import (
"fmt"
"io"
"strings"
"sync"
)
func main() {
r := strings.NewReader("hello world")
var wg sync.WaitGroup
results := make([]string, 0, 2)
for i := 0; i < 2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
buf := make([]byte, 5)
n, _ := r.Read(buf) // ⚠️ 非线程安全!r 无锁共享
results = append(results, string(buf[:n]))
}()
}
wg.Wait()
fmt.Println(results) // 输出可能为 ["hello", "hello"] 或 ["hello", " worl"] —— 行为未定义
}
安全替代方案对照表
| 场景 | 危险做法 | 推荐做法 |
|---|---|---|
| 标准输入并发读 | 直接共享 os.Stdin |
使用 sync.Mutex 包裹 Read() 调用 |
| 字符串/字节流复用 | 共享 strings.Reader |
每个 goroutine 创建独立 strings.NewReader() |
| HTTP 响应体分发 | 多处 io.Copy() 同一 Body |
用 io.TeeReader + bytes.Buffer 缓存并复用 |
根本原则:io.Reader 接口本身不承诺并发安全,所有实现默认按单 goroutine 使用建模。任何跨 goroutine 共享 Reader 实例的行为,都必须显式引入同步机制或重构为不可变副本。
第二章:goroutine泄漏的根源剖析与定位实践
2.1 输入流未关闭导致的goroutine永久阻塞
当 io.Copy 或 http.Request.Body 等操作依赖输入流 EOF 信号时,若上游未显式调用 Close(),接收 goroutine 将无限等待。
典型阻塞场景
- HTTP handler 中未 defer
req.Body.Close() bufio.Scanner遍历未关闭的管道net.Conn读取端未收到 FIN 包
复现代码示例
func handleRequest(w http.ResponseWriter, r *http.Request) {
// ❌ 缺少 defer r.Body.Close()
io.Copy(w, r.Body) // 永久阻塞:r.Body 无 EOF
}
逻辑分析:r.Body 是 io.ReadCloser,io.Copy 内部循环调用 Read();若连接未关闭(如客户端异常断连或未发送 FIN),Read() 返回 (0, nil) 而非 (0, io.EOF),导致 io.Copy 误判为“还有数据”,持续阻塞。
| 场景 | 是否触发 EOF | goroutine 状态 |
|---|---|---|
| 正常 HTTP 请求(含 Content-Length) | ✅ | 正常退出 |
| 流式请求(Transfer-Encoding: chunked)且未关闭 | ❌ | 永久阻塞 |
| 客户端强制断连(RST) | ⚠️(返回 net.ErrClosed) | 可能 panic |
graph TD A[goroutine 启动 io.Copy] –> B{Read() 返回值?} B –>|n>0| C[写入并继续] B –>|n==0 && err==nil| D[无限重试 → 阻塞] B –>|err==io.EOF| E[正常退出]
2.2 context超时未传递至IO操作引发的泄漏链
数据同步机制
Go 中 context.WithTimeout 创建的 deadline 若未显式传入底层 IO(如 http.Client, database/sql),则 goroutine 无法感知取消信号。
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
// ❌ 错误:未将 ctx 传入 http.Do
resp, err := http.DefaultClient.Get("https://api.example.com/data")
http.DefaultClient.Get 使用默认 context.Background(),忽略上游 ctx 超时,导致连接阻塞超时后 goroutine 仍存活。
泄漏路径分析
graph TD
A[context.WithTimeout] -->|未传递| B[http.Client.Do]
B --> C[底层 TCP 连接阻塞]
C --> D[goroutine 永久挂起]
D --> E[内存与文件描述符泄漏]
正确实践要点
- ✅ 始终使用
client.Do(req.WithContext(ctx)) - ✅ 自定义
http.Client.Timeout仅作兜底,不可替代 context 传播 - ✅ 数据库操作须用
db.QueryContext(ctx, ...)
| 组件 | 是否支持 context | 关键方法示例 |
|---|---|---|
net/http |
是 | req.WithContext(ctx) |
database/sql |
是 | db.QueryContext(ctx, ...) |
os.Open |
否(需封装) | 需配合 time.AfterFunc 手动中断 |
2.3 bufio.Scanner默认行为与隐式无限goroutine启动
bufio.Scanner 默认采用 bufio.ScanLines 模式,内部调用 Scan() 时同步阻塞读取,但若配合 os.Pipe 或网络 conn 等无界输入源,且未设 MaxScanTokenSize 或 Split() 自定义分隔逻辑,可能在边界模糊场景下持续等待 EOF —— 此时若外部协程未关闭写端,Scanner 所在线程将永久挂起,看似“静默”,实则成为 goroutine 泄漏温床。
隐式协程陷阱示例
// 错误示范:未设超时/缓冲/终止条件的 Scanner 循环
sc := bufio.NewScanner(os.Stdin)
for sc.Scan() { // 若 Stdin 永不关闭,此 goroutine 永不退出
fmt.Println(sc.Text())
}
sc.Scan()内部调用r.Read()(底层io.Reader),无超时机制;bufio.Scanner自身不启动 goroutine,但常被误置于go func(){...}()中——而开发者忽略对sc.Err()和io.EOF的显式判断,导致外层 goroutine 无法退出。
关键参数对照表
| 参数 | 默认值 | 影响 |
|---|---|---|
MaxScanTokenSize |
64KB | 超长行触发 ErrTooLong,防止内存失控 |
Split |
ScanLines |
可替换为 ScanRunes 或自定义,影响分词语义 |
安全模式建议
- 始终检查
sc.Err() - 对非文件输入源,包裹
context.WithTimeout - 避免在
go语句中裸用for sc.Scan()
graph TD
A[Start Scanner] --> B{Has next token?}
B -->|Yes| C[Call SplitFunc]
B -->|No/EOF| D[Return false]
C --> E[Buffer token]
E --> B
D --> F[Check sc.Err()]
2.4 channel缓冲区耗尽后写入goroutine的悬挂问题
当向已满的带缓冲channel执行发送操作时,goroutine会阻塞等待接收方就绪。若接收方永远不消费,发送方将永久悬挂。
阻塞机制本质
Go runtime将阻塞goroutine挂起并移出调度队列,不消耗CPU,但占用栈内存与goroutine结构体开销。
典型悬挂场景
ch := make(chan int, 2)
ch <- 1 // OK
ch <- 2 // OK
ch <- 3 // 挂起:无接收者,runtime.park()
make(chan int, 2)创建容量为2的缓冲通道- 前两次写入立即返回;第三次触发
gopark,状态置为_Gwaiting
悬挂影响对比
| 维度 | 无缓冲channel | 缓冲满载channel |
|---|---|---|
| 首次阻塞时机 | 发送即阻塞 | 缓冲填满后阻塞 |
| 调度行为 | 同步等待接收 | 异步park+唤醒 |
graph TD
A[goroutine执行ch<-x] --> B{缓冲区有空位?}
B -->|是| C[写入成功]
B -->|否| D[检查recvq是否有等待接收者]
D -->|有| E[直接配对唤醒]
D -->|无| F[gopark休眠,加入sendq]
2.5 生产环境goroutine泄漏的pprof+trace联合诊断实战
定位泄漏源头
通过 go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=2 获取阻塞型 goroutine 快照,重点关注 runtime.gopark 和 selectgo 调用栈。
联合 trace 深度追踪
启动 trace:
go tool trace -http=:8080 ./trace.out
在 Web UI 中筛选 Goroutines 视图,结合 User-defined Regions 标记业务逻辑段,定位长期存活(>10s)且无状态变更的 goroutine。
关键诊断指标对比
| 指标 | 健康阈值 | 泄漏特征 |
|---|---|---|
goroutines |
持续增长至数千 | |
runtime/proc.go:4000 |
≤ 1% | 占比突增至 >30% |
数据同步机制
典型泄漏模式:
func syncWorker(ctx context.Context, ch <-chan Item) {
for { // ❌ 缺少 ctx.Done() 检查
select {
case item := <-ch:
process(item)
}
}
}
分析:该 goroutine 忽略 ctx.Done(),当 ch 关闭后仍无限循环 select{},导致 runtime.park 状态堆积。-gcflags="-l" 可禁用内联,使 pprof 栈更清晰。
第三章:内存暴涨的关键诱因与量化分析
3.1 ioutil.ReadAll与bytes.Buffer无界增长的OOM临界点
当 HTTP 响应体未设限地被 ioutil.ReadAll 读取,或 bytes.Buffer 持续 Write 而无容量约束,内存将线性膨胀直至触发 OOM。
内存失控典型场景
resp, _ := http.Get("http://example.com/large-file")
defer resp.Body.Close()
data, _ := ioutil.ReadAll(resp.Body) // ⚠️ 无大小校验,全量加载至内存
ioutil.ReadAll内部调用grow动态扩容切片,每次约 2x 增长(如 1KB→2KB→4KB…);- 底层
make([]byte, 0, cap)的cap可达 GB 级,无上限时直接耗尽可用堆内存。
关键阈值对比(典型 Linux 容器环境)
| 资源配置 | 安全读取上限 | 触发 OOM 风险点 |
|---|---|---|
| 128MB 限制 | ≤ 8MB | > 64MB |
| 512MB 限制 | ≤ 32MB | > 256MB |
防御性替代方案
- 使用
io.LimitReader封装resp.Body,硬限 10MB; - 改用
bufio.Scanner分块处理流式数据; bytes.Buffer初始化时指定make([]byte, 0, 1024*1024)初始容量。
graph TD
A[HTTP Body] --> B{Size > Limit?}
B -->|Yes| C[Return ErrLimitExceeded]
B -->|No| D[Read to Buffer]
D --> E[Process Chunk]
3.2 多路复用输入流中重复copy导致的内存冗余副本
在基于 io.MultiReader 或自定义多路复用输入流(如 MultiInputStream)的场景中,若多个协程/线程对同一底层 io.ReadCloser 并发调用 io.Copy,将触发多次独立缓冲与数据拷贝。
数据同步机制缺失的后果
当未引入共享读取视图或读取偏移协调时,各 io.Copy 实例均从流起始位置重新读取——即使底层是 bytes.Reader 或 strings.Reader,也会产生逻辑上冗余、物理上隔离的内存副本。
典型错误模式
// ❌ 错误:并发 copy 同一 reader → 多次全量内存拷贝
r := bytes.NewReader([]byte("hello world"))
go io.Copy(dst1, r) // 第一次 copy → 分配新 []byte
go io.Copy(dst2, r) // 第二次 copy → 再分配相同大小 []byte
逻辑分析:
bytes.Reader的Read方法不修改内部状态(除非显式Seek),两次io.Copy均从 offset=0 开始读取,导致"hello world"被复制两次至不同目标缓冲区。参数r是值类型传递,但其底层[]byte被重复解码与写入。
优化路径对比
| 方案 | 内存开销 | 线程安全 | 适用场景 |
|---|---|---|---|
并发 io.Copy + 独立 reader |
高(N×原始数据) | 是 | 仅需副本,无需一致性 |
io.TeeReader + 共享 buffer |
中(1×原始 + 缓冲) | 否(需外部同步) | 日志+转发 |
sync.Pool 复用 buffer |
低(复用避免分配) | 是(pool 本身线程安全) | 高频小流复用 |
graph TD
A[MultiInputStream] --> B{是否共享读取视图?}
B -->|否| C[每次 Copy 创建新副本]
B -->|是| D[统一 buffer + atomic offset]
C --> E[内存冗余 ↑]
D --> F[零拷贝或单拷贝]
3.3 http.Request.Body未及时Close引发的底层net.Conn内存滞留
HTTP 请求体 http.Request.Body 是一个 io.ReadCloser,其底层通常绑定到 net.Conn。若未显式调用 Close(),连接不会被及时归还至连接池,导致 net.Conn 对象及关联的缓冲区、goroutine 和系统文件描述符长期滞留。
连接生命周期关键点
Body关闭触发conn.CloseRead()(半关闭)Server.Serve()在 handler 返回后尝试复用或关闭连接- 若 Body 未读完且未 Close,连接将被标记为“不可复用”
典型错误模式
func badHandler(w http.ResponseWriter, r *http.Request) {
// ❌ 忘记 close,Body 未读完即返回
defer r.Body.Close() // 错误:defer 在函数结束才执行,但此处可能 panic 或提前 return
// ... 处理逻辑
}
此代码中
defer无法保证在所有路径下执行;若 handler panic 或提前 return,Body不会被关闭,net.Conn持有读缓冲区(默认 4KB)和 goroutine,持续占用内存。
正确实践
- 总是
io.Copy(io.Discard, r.Body)清空未读内容 - 显式
r.Body.Close()后再返回 - 使用
http.MaxBytesReader限制上传大小,防 OOM
| 场景 | Body 状态 | Conn 是否复用 | 内存影响 |
|---|---|---|---|
| 已 Close + 已读完 | closed | ✅ | 无滞留 |
| 未 Close + 已读完 | unclosed | ❌(标记为 broken) | 缓冲区+fd 滞留 |
| 未 Close + 未读完 | unclosed | ❌ | goroutine + buffer + fd 全部滞留 |
graph TD
A[HTTP Request] --> B[net.Conn 接收数据]
B --> C[http.Request.Body 初始化]
C --> D{Body.Close() 调用?}
D -->|否| E[Conn 保留在 idle 列表但不可复用]
D -->|是| F[Conn 归还连接池或关闭]
E --> G[内存/文件描述符持续占用]
第四章:高可靠性输入流并发模式重构方案
4.1 基于context.WithCancel的流式读取生命周期管控
在高并发流式读取场景(如实时日志拉取、gRPC ServerStream 或数据库游标遍历)中,goroutine 泄漏风险极高。context.WithCancel 提供了优雅的取消传播机制,使读取协程能响应外部中断信号。
核心控制模式
- 创建可取消上下文:
ctx, cancel := context.WithCancel(parentCtx) - 将
ctx传入读取循环,监听ctx.Done() - 外部调用
cancel()触发所有关联 goroutine 清理
典型实现片段
func streamReader(ctx context.Context, ch <-chan string) {
for {
select {
case msg, ok := <-ch:
if !ok {
return
}
fmt.Println("received:", msg)
case <-ctx.Done(): // 关键:响应取消信号
log.Println("stream cancelled:", ctx.Err())
return
}
}
}
逻辑分析:select 阻塞等待数据或取消事件;ctx.Done() 返回 <-chan struct{},一旦 cancel() 被调用即关闭该 channel,触发 case <-ctx.Done() 分支退出。ctx.Err() 返回具体原因(如 context.Canceled),便于可观测性诊断。
生命周期状态对照表
| 状态 | ctx.Err() 值 | 行为表现 |
|---|---|---|
| 活跃 | <nil> |
正常读取 |
| 已取消 | context.Canceled |
立即退出循环 |
| 超时触发 | context.DeadlineExceeded |
同样终止,但含超时语义 |
graph TD
A[启动流式读取] --> B[ctx, cancel := WithCancel parent]
B --> C[启动goroutine并传入ctx]
C --> D{select on ch / ctx.Done()}
D -->|收到数据| E[处理消息]
D -->|ctx.Done()| F[清理资源并return]
G[调用cancel()] --> D
4.2 分块限界读取+预分配buffer的内存可控型解析器设计
传统流式解析常因动态扩容导致GC压力陡增。本设计采用双控策略:按固定块大小(如8KB)分段读取,并预先分配最大容量buffer(如64KB),杜绝运行时realloc。
核心控制参数
chunkSize: 单次读取上限,平衡IO吞吐与内存驻留maxBufferSize: 预分配总容量,硬性内存上限boundaryCheck: 每块末尾校验结构边界(如JSON闭合符)
func NewParser(r io.Reader, chunkSize, maxBuf int) *Parser {
return &Parser{
reader: r,
buf: make([]byte, 0, maxBuf), // 预分配cap,len=0
chunk: make([]byte, chunkSize),
}
}
初始化仅预设buffer容量(cap),避免初始内存浪费;
chunk独立缓冲区隔离读取与解析阶段,防止数据覆盖。
内存行为对比
| 策略 | 峰值内存波动 | GC频率 | 边界误判风险 |
|---|---|---|---|
| 动态扩容 | 高 | 高 | 中 |
| 本设计(预分配) | 恒定 | 极低 | 低(含校验) |
graph TD
A[Read Chunk] --> B{Boundary Found?}
B -->|Yes| C[Parse Buffer]
B -->|No| D[Append to Pre-allocated Buf]
D --> E[Check Total Len ≤ maxBufferSize]
E -->|Overflow| F[Error: Memory Exceeded]
4.3 sync.Pool协同io.Reader的goroutine-safe复用机制
在高并发 I/O 场景中,频繁创建 bytes.Buffer 或 strings.Reader 实例会加剧 GC 压力。sync.Pool 提供了无锁、线程安全的对象复用能力,与 io.Reader 接口天然契合。
复用模式设计
- 每个 goroutine 优先从本地池获取对象,避免跨 P 竞争
io.Reader实现需支持重置(如Reset()方法),确保状态隔离- Pool 的
New函数返回可复用的*bytes.Buffer
核心代码示例
var readerPool = sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 0, 1024)) // 初始容量1024,避免小分配
},
}
// 使用时:
buf := readerPool.Get().(*bytes.Buffer)
buf.Reset() // 清空内容与长度,保留底层数组
buf.Write(data) // 写入新数据
reader := bytes.NewReader(buf.Bytes()) // 转为 io.Reader
// ... 使用 reader ...
readerPool.Put(buf) // 归还至池
逻辑分析:
Reset()仅重置len不释放cap,避免内存重分配;Put前必须确保buf不再被其他 goroutine 引用,因sync.Pool不保证对象生命周期跨调度器轮转。
| 操作 | 线程安全性 | 是否触发 GC |
|---|---|---|
Get() |
✅ | ❌ |
Put() |
✅ | ❌ |
Reset() |
✅(调用方保证独占) | ❌ |
graph TD
A[goroutine 请求 Reader] --> B{Pool 有可用 buffer?}
B -->|是| C[Get → Reset → NewReader]
B -->|否| D[NewBuffer → NewReader]
C --> E[使用完毕]
D --> E
E --> F[Put 回 Pool]
4.4 输入流中间件化:超时、限速、监控三位一体封装
输入流处理常面临不可控的上游抖动,需在协议层之上构建统一中间件屏障。
核心能力融合设计
- 超时控制:基于
Deadline的可重置计时器,避免单条消息阻塞全局 - 动态限速:令牌桶算法 + 实时 QPS 反馈调节(非固定速率)
- 无侵入监控:埋点与 OpenTelemetry 兼容的上下文透传
流量治理中间件示例(Go)
func RateLimitedTimeoutMiddleware(next http.Handler) http.Handler {
limiter := rate.NewLimiter(rate.Every(100*time.Millisecond), 5) // 初始5TPS
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
if !limiter.Allow() {
http.Error(w, "rate limited", http.StatusTooManyRequests)
return
}
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
rate.Every(100ms)表示平均间隔;5是突发容量。WithTimeout为请求级而非连接级超时,确保下游服务可及时响应。上下文透传使监控链路能关联耗时、限速、超时事件。
能力协同效果
| 维度 | 未封装 | 三位一体封装 |
|---|---|---|
| 故障定位 | 分散日志难关联 | traceID 贯穿三要素 |
| 运维干预 | 需重启调整配置 | 热更新限速阈值与超时值 |
graph TD
A[原始HTTP请求] --> B{中间件拦截}
B --> C[超时计时启动]
B --> D[令牌桶校验]
B --> E[打点注入traceID]
C & D & E --> F[转发至业务Handler]
第五章:从事故到防御:Go输入流健壮性工程规范
输入流失效的真实代价
2023年某支付网关因未校验HTTP请求体长度,遭遇恶意构造的超长Content-Length头(值为9223372036854775807),触发Go标准库net/http内部整数溢出,导致服务进程panic并全量重启。事故持续17分钟,影响订单创建成功率下降至32%。根本原因并非逻辑错误,而是对输入流边界条件缺乏防御性建模。
防御性读取的核心原则
所有输入流必须显式声明容量上限与超时策略。以下代码片段展示了安全读取JSON请求体的标准模式:
func safeReadJSON(r io.Reader, maxBytes int64) ([]byte, error) {
limited := io.LimitReader(r, maxBytes+1) // +1用于捕获超限信号
buf := make([]byte, 0, 1024)
n, err := io.ReadFull(limited, buf[:cap(buf)])
if err == io.ErrUnexpectedEOF || err == io.EOF {
return buf[:n], nil
}
if errors.Is(err, io.EOF) && n == 0 {
return nil, fmt.Errorf("empty request body")
}
if errors.Is(err, io.ErrShortBuffer) {
return nil, fmt.Errorf("body exceeds %d bytes", maxBytes)
}
return nil, fmt.Errorf("read failed: %w", err)
}
流控与熔断协同机制
当单个请求流读取耗时超过阈值,应触发链路级熔断。下表对比了不同场景下的推荐配置:
| 场景类型 | 最大字节数 | 读取超时 | 熔断触发条件 |
|---|---|---|---|
| 用户表单提交 | 1MB | 5s | 连续3次超时 |
| 文件上传API | 50MB | 60s | 单请求超时或速率>10MB/s |
| Webhook回调 | 128KB | 10s | 并发流数>200且平均延迟>3s |
多层校验流水线
构建不可绕过的输入验证管道,强制执行以下检查顺序:
- HTTP头合法性(
Content-Length≤Max-Memory-Limit) - MIME类型白名单匹配(仅允许
application/json、multipart/form-data) - 字节流预扫描(检测BOM、NUL字节、非法UTF-8序列)
- JSON Schema结构验证(使用
github.com/xeipuuv/gojsonschema)
生产环境监控指标
部署以下Prometheus指标实现流健康度可观测:
http_request_body_size_bytes{status="truncated"}—— 被截断的请求体积分布http_stream_read_duration_seconds_bucket—— 按百分位统计的读取延迟input_stream_errors_total{reason="overflow"}—— 整数溢出类错误计数
flowchart LR
A[HTTP Request] --> B{Header Validation}
B -->|Pass| C[LimitReader Wrapper]
B -->|Fail| D[400 Bad Request]
C --> E[Timeout Context]
E --> F[Schema Validation]
F -->|Valid| G[Business Logic]
F -->|Invalid| H[422 Unprocessable Entity]
事故复盘驱动的规范迭代
某次内存泄漏事故暴露io.Copy未设限问题,团队将以下规则写入CI检查:
- 禁止直接使用
ioutil.ReadAll或bytes.Buffer.ReadFrom - 所有
io.Copy调用必须包裹io.LimitReader或io.MultiReader - 自定义
io.Reader实现必须覆盖Read方法并注入context.Context
压力测试基准用例
在k6中定义流健壮性测试场景:
export default function () {
http.post('http://api.example.com/upload', {
file: open('/dev/urandom', 'b'),
}, {
headers: { 'Content-Length': '10737418240' }, // 10GB
timeout: '30s'
});
}
要求服务在该负载下返回413 Payload Too Large而非OOM崩溃。
安全边界动态计算
根据当前系统内存水位动态调整流限制:
func dynamicLimit() int64 {
var m runtime.MemStats
runtime.ReadMemStats(&m)
free := int64(m.HeapSys - m.HeapAlloc)
return min(50*1024*1024, free/4) // 取50MB与可用内存1/4的较小值
} 