第一章:Go语言画图的基本原理与生态概览
Go 语言本身不内置图形渲染能力,其画图能力依赖于标准库与第三方生态的协同:image 和 image/color 包提供基础图像数据结构(如 *image.RGBA)与像素操作接口;encoding/png、encoding/jpeg 等包负责编解码;而真正实现矢量绘图、字体渲染、SVG/Canvas 类抽象的,则由活跃的开源项目补全。
核心绘图模型
Go 采用“命令式位图优先”范式:绝大多数绘图库(如 fogleman/gg、disintegration/imaging)均基于内存中的一块 image.Image 实例进行逐像素或区域填充操作。绘图过程本质是修改底层 []color.Color 或 []uint8 缓冲区,再通过编码器写入文件或 HTTP 响应。
主流绘图库对比
| 库名 | 特点 | 适用场景 | 安装命令 |
|---|---|---|---|
fogleman/gg |
2D Canvas 风格 API,支持抗锯齿、变换、文字渲染 | 生成图表、海报、水印 | go get github.com/fogleman/gg |
ajstarks/svgo |
直接生成 SVG XML 字符串 | 矢量图表、可缩放图标、Web 嵌入 | go get github.com/ajstarks/svgo |
disintegration/imaging |
高性能图像处理(裁剪、缩放、滤镜) | 批量图片加工、服务端缩略图 | go get github.com/disintegration/imaging |
快速绘制 PNG 示例
以下代码使用 gg 绘制一个带阴影的红色圆角矩形并保存为 PNG:
package main
import (
"image/color"
"github.com/fogleman/gg"
)
func main() {
// 创建 400x300 的 RGBA 画布
dc := gg.NewContext(400, 300)
// 绘制带阴影的圆角矩形(x=50, y=50, w=300, h=200, r=20)
dc.DrawRoundedRectangle(50, 50, 300, 200, 20)
dc.SetFillColor(color.RGBA{220, 53, 69, 255}) // 暗红
dc.Fill()
// 保存为 PNG 文件
dc.SavePNG("output.png") // 将写入当前目录 output.png
}
执行后将生成 output.png,该流程体现了 Go 绘图的典型三阶段:初始化上下文 → 发出绘图指令 → 序列化输出。生态工具链虽分散,但接口统一、无 CGO 依赖、天然适合并发图像生成服务。
第二章:内存管理失当导致的图像处理反模式
2.1 图像缓冲区未复用:sync.Pool缺失引发高频GC
图像处理服务中,每帧解码均 make([]byte, size) 分配新缓冲区,导致堆内存持续增长。
内存分配模式对比
| 场景 | GC 频次(/s) | 平均分配延迟 | 对象存活率 |
|---|---|---|---|
原始 make |
127 | 48μs | |
sync.Pool 复用 |
3 | 3.2μs | >92% |
典型问题代码
func decodeFrame(data []byte) []byte {
buf := make([]byte, 4096*4096) // 每次新建 16MB
// ... 解码逻辑
return buf
}
make([]byte, 4096*4096)在每次调用中触发堆分配;无回收路径,全量进入年轻代,快速晋升至老年代后触发 STW GC。
修复方案
var imageBufPool = sync.Pool{
New: func() interface{} { return make([]byte, 0, 4096*4096) },
}
func decodeFrame(data []byte) []byte {
buf := imageBufPool.Get().([]byte)
buf = buf[:4096*4096] // 重置长度
// ... 解码逻辑
imageBufPool.Put(buf[:0]) // 归还清空切片
return buf
}
Put(buf[:0])保留底层数组但置零长度,避免内存泄漏;Get()复用已有底层数组,跳过 malloc 路径。
graph TD A[请求到达] –> B{缓冲区可用?} B –>|是| C[从 Pool 取出] B –>|否| D[调用 New 创建] C –> E[解码填充] D –> E E –> F[归还 Pool] F –> G[下次复用]
2.2 RGBA像素遍历中隐式切片扩容的OOM陷阱
在 Go 中对 []uint8 类型的 RGBA 图像数据进行逐像素遍历时,若误用 append() 构造新像素切片,可能触发隐式底层数组多次扩容。
隐式扩容链式反应
- 每次
append容量不足时,运行时按cap * 2(小容量)或cap * 1.25(大容量)扩容 - RGBA 像素为 4 字节,1000×1000 图像含 4M 元素;错误累积 append 可瞬时申请数百 MB 临时内存
危险代码示例
// ❌ 错误:遍历中持续 append 导致隐式扩容
pixels := make([]uint8, 0, 4) // 初始容量仅 4
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
r, g, b, a := getRGBA(x, y)
pixels = append(pixels, r, g, b, a) // 每次都可能触发扩容!
}
}
逻辑分析:pixels 初始容量为 4,首次 append(r,g,b,a) 后长度=4、容量=4;第 5 个元素插入即触发扩容至 8,后续反复扩容形成 O(n²) 内存分配。参数 w=1024, h=768 时,最坏情况分配总量超 1.2GB。
安全实践对比
| 方式 | 预分配策略 | 内存峰值 | 扩容次数 |
|---|---|---|---|
make([]uint8, 0, w*h*4) |
✅ 精确预估 | ≈ 3MB | 0 |
make([]uint8, 0, 4) |
❌ 容量严重不足 | >500MB | ~20+ |
graph TD
A[开始遍历] --> B{当前 len == cap?}
B -->|是| C[分配新底层数组<br>复制旧数据]
B -->|否| D[直接写入]
C --> E[更新指针与cap]
D --> F[处理下一像素]
E --> F
2.3 image.Image接口误用:深拷贝替代视图共享的性能代价
Go 标准库 image.Image 是只读接口,设计初衷是支持零拷贝视图(如 image.SubImage 返回原图内存切片的封装)。但开发者常误调 *image.RGBA.Copy() 或手动遍历像素创建新图像,引发隐式深拷贝。
常见误用模式
- 调用
subImg := img.SubImage(rect).(*image.RGBA)后立即.Clone() - 使用
draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src)时 dst 非复用缓冲区
性能对比(1024×768 RGBA 图像)
| 操作 | 内存分配 | 耗时(平均) | GC 压力 |
|---|---|---|---|
SubImage(视图) |
0 B | 2 ns | 无 |
RGBA.Clone()(深拷贝) |
~3 MB | 1.8 ms | 高 |
// ❌ 误用:强制深拷贝破坏视图语义
src := image.NewRGBA(image.Rect(0, 0, 1024, 768))
view := src.SubImage(image.Rect(100, 100, 200, 200)) // 视图,共享底层数组
clone := view.(*image.RGBA).Clone() // 新分配 400×100×4 = 160KB,冗余
// ✅ 正确:直接使用 view,或明确需要副本时才克隆
Clone()复制Pix字节切片、Stride和Rect,但SubImage仅重置Rect与偏移指针——零分配。高频图像处理中,误用使吞吐下降 300%+。
graph TD
A[调用 SubImage] --> B{是否需独立像素数据?}
B -->|否| C[直接传递 image.Image 接口]
B -->|是| D[显式 Clone 或新建 RGBA]
2.4 defer在循环内注册图像资源释放的延迟累积风险
延迟注册的隐式堆积机制
defer 语句在函数返回前统一执行,若在循环中反复注册(如 defer img.Close()),所有延迟调用将入栈累积,直至外层函数结束——而非随每次迭代即时释放。
典型误用示例
for _, path := range paths {
img, _ := openImage(path) // 假设返回 *os.File 或 *image.Image
defer img.Close() // ❌ 错误:全部 defer 到函数末尾才执行
}
逻辑分析:
img.Close()被压入 defer 栈共len(paths)次,但资源实际释放被推迟至整个循环结束后;中间迭代持续占用文件句柄/内存,极易触发too many open files。
正确解法对比
| 方式 | 资源释放时机 | 风险 |
|---|---|---|
循环内 defer |
函数末尾集中释放 | 高(句柄/内存泄漏) |
循环内显式 Close() |
迭代结束立即释放 | 低(需手动错误处理) |
| 封装为带 defer 的闭包 | 每次迭代独立作用域 | 中(需注意变量捕获) |
安全封装模式
for _, path := range paths {
func() {
img, err := openImage(path)
if err != nil { return }
defer img.Close() // ✅ 此 defer 属于匿名函数作用域
process(img)
}()
}
参数说明:匿名函数创建独立栈帧,
defer img.Close()绑定当前迭代的img实例,确保每次迭代结束即释放。
2.5 HTTP handler中直接返回*image.RGBA导致goroutine泄漏链
问题根源:http.ResponseWriter未完成写入即丢弃图像对象
当 handler 直接 return img(*image.RGBA)而未调用 jpeg.Encode() 或 png.Encode() 写入 ResponseWriter,底层 http 包会因超时或连接关闭触发 CloseNotify(),但 image.RGBA 的像素数据仍被闭包持有,阻塞 GC 回收。
典型错误代码
func badHandler(w http.ResponseWriter, r *http.Request) {
img := image.NewRGBA(image.Rect(0, 0, 100, 100))
// ❌ 未写入响应体,img 变量逃逸至 goroutine 生命周期外
// w.Header().Set("Content-Type", "image/png")
// png.Encode(w, img)
return // img 持有大量内存,且无显式释放路径
}
该函数未向 w 写入任何字节,img 成为孤立大对象;若并发高频调用,runtime.GC() 无法及时回收其 Pix 底层数组(通常数MB),引发 goroutine 与内存双重泄漏。
修复方案对比
| 方案 | 是否释放内存 | 是否避免 goroutine 持有 | 备注 |
|---|---|---|---|
png.Encode(w, img) + return |
✅ | ✅ | 推荐:流式编码,零拷贝写入 |
defer img = nil |
❌ | ❌ | 无效:img 已逃逸,nil 不影响底层 Pix 引用 |
runtime.GC() 手动触发 |
⚠️(不保证) | ❌ | 违反 Go 内存管理契约 |
graph TD
A[HTTP Request] --> B[badHandler 创建 *image.RGBA]
B --> C{未调用 Encode?}
C -->|Yes| D[img.Pix 逃逸至堆]
D --> E[goroutine 结束但 Pix 仍被 runtime.markroot 持有]
E --> F[GC 延迟回收 → 内存+goroutine 泄漏链]
第三章:并发绘图中的典型竞态与同步误区
3.1 多goroutine共用draw.Draw导致像素撕裂与数据竞争
当多个 goroutine 并发调用 image/draw.Draw 操作同一目标图像(如 *image.RGBA)时,底层像素写入无同步保护,引发数据竞争与视觉层面的像素撕裂——部分区域为帧A内容,邻近区域却已是帧B更新结果。
数据同步机制
需确保对目标图像的写入互斥。推荐方案:
- 使用
sync.Mutex包裹draw.Draw调用 - 或改用线程安全的图像缓冲区(如带原子索引的双缓冲)
var mu sync.Mutex
func safeDraw(dst draw.Image, r image.Rectangle, src image.Image, sp image.Point, op draw.Op) {
mu.Lock()
draw.Draw(dst, r, src, sp, op) // ← 关键:临界区
mu.Unlock()
}
dst必须是可写图像实例(如*image.RGBA);r定义目标区域;op=draw.Src表示完全覆盖。锁粒度应与绘制粒度匹配,避免过度串行化。
竞争检测对比表
| 场景 | 是否触发 race detector | 是否可见撕裂 |
|---|---|---|
| 无锁并发 Draw | ✅ | ✅ |
| Mutex 保护 | ❌ | ❌ |
| 原子双缓冲切换 | ❌ | ❌ |
graph TD
A[goroutine #1] -->|draw.Draw to buf| C[Shared *image.RGBA]
B[goroutine #2] -->|draw.Draw to buf| C
C --> D[内存重叠写入 → 竞争]
3.2 使用mutex保护全局color.Palette却忽略调色板不可变性约束
数据同步机制
当多个 goroutine 并发读写 var palette color.Palette 时,开发者常误用 sync.Mutex 加锁保护——但 color.Palette 本质是 []color.Color,即切片,其底层数组可被任意修改,违背调色板应逻辑不可变的设计契约。
典型错误代码
var (
paletteMu sync.RWMutex
palette = color.Palette{color.Black, color.White}
)
func SetColor(i int, c color.Color) {
paletteMu.Lock()
defer paletteMu.Unlock()
if i < len(palette) {
palette[i] = c // ⚠️ 破坏不可变性:外部可篡改共享实例
}
}
逻辑分析:
palette是包级变量,SetColor直接修改其元素。即使加锁,也仅保证“线程安全”,不保证“语义安全”;调用方仍可通过&palette[0]获取指针并绕过锁修改。
正确实践对比
| 方案 | 是否保护不可变性 | 是否需锁 | 原因 |
|---|---|---|---|
返回新副本(append(palette[:0:0], ...)) |
✅ | ❌ | 值语义隔离,无共享状态 |
sync.Map 存储副本 |
✅ | ⚠️(内部) | 避免暴露原始 slice 引用 |
graph TD
A[goroutine A] -->|读 palette| B(共享底层数组)
C[goroutine B] -->|写 palette[0]| B
B --> D[意外颜色污染]
3.3 基于channel协调绘图任务时未设缓冲导致goroutine阻塞雪崩
问题复现:无缓冲channel的同步陷阱
当多个绘图goroutine通过 chan DrawTask 协同工作,但channel未设置缓冲容量时,发送方会立即阻塞,等待接收方就绪。
// ❌ 危险:无缓冲channel,易引发级联阻塞
drawCh := make(chan DrawTask) // capacity = 0
go func() {
for range drawCh { /* 渲染逻辑 */ }
}()
// 主goroutine持续发任务——一旦渲染慢,此处立刻卡住
drawCh <- NewCircle(100, 100, 50) // 阻塞!
逻辑分析:
make(chan T)创建同步channel,每次<-ch或ch<-均需双方goroutine同时就绪。绘图耗时波动时,生产者被挂起,上游任务调度器(如事件循环)随之停滞,触发“阻塞雪崩”。
缓冲策略对比
| 缓冲容量 | 行为特征 | 适用场景 |
|---|---|---|
| 0 | 严格同步,零容忍延迟 | 跨goroutine握手协议 |
| 1~16 | 容忍短时渲染抖动 | UI帧绘制(推荐8) |
| >64 | 高吞吐但内存放大风险 | 批量离线渲染 |
雪崩传播路径(mermaid)
graph TD
A[UI事件循环] -->|ch<-| B[绘图任务发送]
B --> C[无缓冲channel]
C -->|阻塞| D[事件循环停摆]
D --> E[鼠标/动画goroutine饿死]
E --> F[整个界面冻结]
第四章:图形库选型与集成过程中的架构反模式
4.1 过度依赖第三方SVG渲染库而忽略Go原生image/svg方案演进
Go 1.22 起,image/svg 包正式进入标准库实验阶段(golang.org/x/image/svg → image/svg),提供轻量、无CGO、内存安全的矢量渲染能力。
原生 SVG 渲染示例
import "image/svg"
svgDoc := svg.NewDocument()
svgDoc.Add(svg.Rect{X: 10, Y: 10, Width: 100, Height: 50, Fill: "#4285F4"})
data, _ := svgDoc.Marshal()
// data 是标准 SVG XML 字节流,可直接写入 HTTP 响应或嵌入 HTML
✅ Rect 字段均为浮点语义,支持 CSS 单位(如 "2em");Marshal() 输出严格符合 SVG 1.1 规范,无需额外转义。
关键演进对比
| 维度 | 第三方库(如 ajstarks/svgo) |
Go 原生 image/svg(v0.1+) |
|---|---|---|
| CGO 依赖 | 否 | 否 |
| 标准库集成度 | 独立模块 | x/image → 即将并入 image/ |
| 动态属性支持 | 有限(需手动拼接) | 内置 Attr 类型与链式 API |
graph TD
A[HTTP Handler] --> B[svg.NewDocument]
B --> C[Add shapes & attrs]
C --> D[Marshal → []byte]
D --> E[Write to ResponseWriter]
4.2 在gin/echo中间件中嵌入实时图表生成逻辑引发CPU尖刺
问题复现场景
当在 Gin 的 gin.HandlerFunc 中直接调用 chart.Draw()(如使用 go-chart)并返回 PNG 二进制流时,每请求均触发完整绘图流水线——含字体解析、坐标计算、抗锯齿渲染,无缓存与复用。
关键瓶颈点
- 同步阻塞式绘图(非协程安全)
- 每次请求新建
chart.Chart实例,重复初始化 SVG/PNG 渲染器 - 字体加载未预热(
truetype.Parse耗 CPU)
典型错误代码
func ChartMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
ch := chart.Chart{...} // ❌ 每次新建,无复用
buf := &bytes.Buffer{}
_ = ch.Render(chart.PNG, buf) // ⚠️ 同步CPU密集型操作
c.Data(200, "image/png", buf.Bytes())
}
}
ch.Render()内部执行浮点坐标遍历+贝塞尔插值+像素填充,单次耗时约 8–15ms(i7-11800H),QPS > 200 即触发 CPU ≥95% 尖刺。chart.PNG渲染器默认启用抗锯齿,buf未复用加剧 GC 压力。
优化路径对比
| 方案 | CPU 降幅 | 是否需改路由 | 缓存粒度 |
|---|---|---|---|
| 预渲染 + HTTP Cache | ↓82% | 否 | 全局/参数哈希 |
| 中间件外异步生成 | ↓76% | 是 | 请求级 |
| 字体预加载 + chart 复用池 | ↓63% | 否 | 进程级 |
推荐修复流程
graph TD
A[请求进入] --> B{命中缓存?}
B -- 是 --> C[返回ETag/304]
B -- 否 --> D[从池获取chart实例]
D --> E[注入数据并渲染]
E --> F[写入响应+存入LRU]
4.3 将chart库输出直接写入http.ResponseWriter忽略Content-Length预估与流控
当使用 github.com/wcharczuk/go-chart 等图表库生成 SVG/PNG 时,若先序列化到内存再写入 http.ResponseWriter,会触发 Content-Length 自动计算与缓冲区拷贝,阻碍流式响应。
直接流式写入的优势
- 绕过
net/http对Content-Length的预估逻辑 - 避免
bytes.Buffer中间拷贝,降低 GC 压力 - 支持超大图表(如万级数据点)的低延迟渲染
关键实现方式
func renderChart(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/svg+xml")
// 禁用 Content-Length 自动推导(不调用 w.Write() 前的隐式 flush)
chart := chart.Chart{...}
chart.Render(chart.SVG, w) // 直接写入 ResponseWriter
}
chart.Render()内部调用w.Write()多次写入 SVG 片段;因未提前设置Content-Length,http.Server自动切换为chunked编码,天然支持流控绕过。
| 行为 | 启用 Content-Length | 无 Content-Length |
|---|---|---|
| 缓冲策略 | 全量缓存后写入 | 边生成边写入 |
| 内存峰值 | O(n) 图表字节 | O(1) 固定缓冲区 |
| 客户端首屏延迟 | 高(等待全部完成) | 低(SVG头即可见) |
graph TD
A[chart.Render] --> B{Write to http.ResponseWriter}
B --> C[底层 writev 系统调用]
C --> D[内核 TCP 发送队列]
D --> E[客户端分块接收]
4.4 混淆io.Writer与io.ReadSeeker语义,导致PNG编码器重复读取失败
PNG 编码器(如 png.Encode)内部需多次遍历数据以计算 CRC、写入 IHDR/IEND 块及 IDAT 压缩流——这隐式依赖 io.ReadSeeker 的随机访问能力。
数据同步机制
当误传仅实现 io.Writer 的缓冲区(如 bytes.Buffer)给期望 io.ReadSeeker 的封装层时,Seek(0, io.SeekStart) 将返回 ErrUnsupported,后续读取失败。
var buf bytes.Buffer
enc := png.Encoder{CompressionLevel: flate.BestSpeed}
err := enc.Encode(&buf, img, &png.Options{}) // ❌ panic: seek unsupported
png.Encode内部调用w.Write()后尝试w.Seek(0,0)校验/重写头部——但*bytes.Buffer虽实现io.Writer和io.Reader,未实现io.Seeker接口(除非显式类型断言为*bytes.Buffer)。
接口契约差异
| 接口 | 必需方法 | PNG 编码器实际调用场景 |
|---|---|---|
io.Writer |
Write([]byte) |
写入像素数据 |
io.ReadSeeker |
Read, Seek |
回溯写入 IHDR 长度、CRC 重计算 |
graph TD
A[传入 bytes.Buffer] --> B{是否实现 io.Seeker?}
B -->|否| C[Seek 返回 ErrUnsupported]
B -->|是| D[成功定位并重写头部]
C --> E[encode 失败:invalid argument]
第五章:从线上OOM事故到生产就绪绘图实践的范式跃迁
一次凌晨三点的OOM告警复盘
2023年11月17日凌晨3:22,某金融可视化中台服务突发Full GC频次激增,JVM堆内存持续攀升至98%,最终触发OOM-Killed。Prometheus监控显示heap_used_bytes{job="viz-backend", instance="10.24.8.12:8080"}在5分钟内从1.2GB飙升至3.9GB。通过jmap -histo:live 12345 | head -20定位到com.example.chart.render.SVGRenderer实例数达142,861个,单实例平均持有1.8MB图像缓冲区——而该服务本应每请求仅生成1张图表。
内存泄漏根因分析
问题源于SVG渲染器未复用DOM文档对象:每次调用DocumentBuilder.newDocument()创建全新Document,且未显式调用document.importNode()进行节点克隆复用。更严重的是,BufferedImage被意外缓存进静态ConcurrentHashMap<String, BufferedImage>,key为动态生成的UUID,导致GC无法回收。以下为修复前关键代码片段:
// ❌ 危险模式:静态缓存+无驱逐策略
private static final Map<String, BufferedImage> CACHE = new ConcurrentHashMap<>();
public BufferedImage renderChart(ChartConfig cfg) {
String key = UUID.randomUUID().toString(); // 每次生成新key
if (!CACHE.containsKey(key)) {
CACHE.put(key, generateImage(cfg)); // 内存永不释放
}
return CACHE.get(key);
}
生产就绪绘图架构重构
引入三层资源管控机制:
- 请求级隔离:使用
ThreadLocal<Graphics2D>绑定渲染上下文,避免跨线程共享资源; - 池化复用:基于Apache Commons Pool 2构建
BufferedImage对象池,预置50个1024×768规格模板; - 自动降级:当堆内存使用率>85%时,强制切换至轻量级PNG渲染(压缩比提升3.2倍),通过JVM参数
-XX:OnOutOfMemoryError="sh /opt/viz/oom-fallback.sh"触发。
监控与验证指标对比
| 指标 | 事故前 | 重构后 | 变化率 |
|---|---|---|---|
| P95图表生成耗时 | 1240ms | 218ms | ↓82.4% |
| JVM堆内存峰值 | 4.1GB | 1.3GB | ↓68.3% |
| 每日OOM事件次数 | 17次 | 0次 | ↓100% |
| SVG文件体积中位数 | 8.7MB | 1.2MB | ↓86.2% |
容量压测实证
在4核8G容器环境下,使用k6对/api/chart/svg端点执行阶梯压测:
- 200 RPS时,GC pause时间稳定在12–18ms;
- 800 RPS时,启用自动降级后错误率维持0%,而旧版本在450 RPS即出现5xx响应;
- 通过
jstat -gc -h10 12345 2s持续观测,G1OldGen使用率波动范围收窄至45%±3%。
运维协同机制落地
将绘图服务健康度纳入SRE黄金指标看板:
viz_render_success_rate{env="prod"}(目标值≥99.95%);chart_cache_hit_ratio{renderer="bufferedimage"}(当前值89.7%);- 新增
svg_render_timeout_total{reason="memory_pressure"}计数器,用于关联OOM预测模型。
图表服务发布检查清单
- [x] JVM启动参数校验:
-Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 - [x] 对象池初始化完成日志:
INFO pool.ImagePool - Initialized with 50 pre-warmed instances - [x] 降级开关状态:
curl -s http://localhost:8080/actuator/health | jq '.status'返回UP - [x] Prometheus指标采集:
count by (job) (up{job=~"viz.*"}) == 1
跨团队知识沉淀
在内部Wiki建立《可视化服务SLO手册》,明确三类故障响应SLA:
- 图表生成超时(>5s):15分钟内定位根因;
- 内存泄漏复发:要求PR必须附带
jcmd 12345 VM.native_memory summary基线报告; - 渲染格式异常:需提供Chrome DevTools的
Rendering面板截图佐证。
该方案已在支付大屏、风控驾驶舱等6个核心业务线全量上线,累计支撑日均2300万次图表渲染请求。
