fasthttp中运用哪些go优化技巧?

木白的技术私厨 · · 132 次点击 · · 开始浏览    


fasthttp刚出道的时候号称比net/http快十倍,更少的内存分配。并同时在github上给出一些go开发上的小技巧。


本文主要通过源码来窥探下fasthttp里是如何使用这些技巧的。


减少[]byte的分配,尽量去复用它们

两种方式进行复用:

  1. sync.Pool

  2. slice = slice[:0]。所有的类型的Reset方法,均使用此方式。例如类型URI、Args、ByteBuffer、Cookie、RequestHeader、ResponseHeader等。


fasthttp里共有35个地方使用了sync.Pool。sync.Pool除了降低GC的压力,还能复用对象,减少内存分配。

// 例如类型Servertype Server struct {    // ...    ctxPool        sync.Pool // 存RequestCtx对象  readerPool     sync.Pool // 存bufio对象,用于读HTTP Request  writerPool     sync.Pool // 存bufio对象,用于写HTTP Request  hijackConnPool sync.Pool  bytePool       sync.Pool}

// 例如cookiesvar cookiePool = &sync.Pool{ New: func() interface{} { return &Cookie{} },}
func AcquireCookie() *Cookie { return cookiePool.Get().(*Cookie)}
func ReleaseCookie(c *Cookie) { c.Reset() cookiePool.Put(c)}
// 例如workPool. 每个请求以一个新的goroutine运行。就是workpool做的调度type workerPool struct { // ... workerChanPool sync.Pool}
func (wp *workerPool) getCh() *workerChan { var ch *workerChan // ...
if ch == nil { if !createWorker { // 已经达到worker数量上限,不允许创建了 return nil } // 尝试复用旧worker vch := wp.workerChanPool.Get() if vch == nil { vch = &workerChan{ ch: make(chan net.Conn, workerChanCap), } } ch = vch.(*workerChan) // 创建新的goroutine处理请求 go func() { wp.workerFunc(ch) // 用完了返回去 wp.workerChanPool.Put(vch) }() } return ch}


还有复用已经分配的[]byte。

s = s[:0]s = append(s[:0], b…)这两种复用方式,总共出现了191次。

// 清空 URIfunc (u *URI) Reset() {  u.pathOriginal = u.pathOriginal[:0]  u.scheme = u.scheme[:0]  u.path = u.path[:0]    // ....}
// 清空 ResponseHeaderfunc (h *ResponseHeader) resetSkipNormalize() { // ... h.contentLengthBytes = h.contentLengthBytes[:0]
h.contentType = h.contentType[:0] h.server = h.server[:0]
h.h = h.h[:0] h.cookies = h.cookies[:0]}
// 清空Cookiesfunc (c *Cookie) Reset() { c.key = c.key[:0] c.value = c.value[:0] // ... c.domain = c.domain[:0] c.path = c.path[:0]  // ...}
func (c *Cookie) SetKey(key string) { c.key = append(c.key[:0], key...)}


方法参数尽量用[]byte. 纯写场景可避免用bytes.Buffer

方法参数使用[]byte,这样做避免了[]byte到string转换时带来的内存分配和拷贝。毕竟本来从net.Conn读出来的数据也是[]byte类型。

某些地方确实想传string类型参数,fasthttp也提供XXString()方法。

String方法背后是利用了a = append(a, string…)。这样做不会造成string到[]byte的转换(该结论通过查看汇编得到,汇编里并没用到runtime.stringtoslicebyte方法)

// 例如写Response时,提供专门的String方法func (resp *Response) SetBodyString(body string) {  // ...  bodyBuf.WriteString(body)}


上面的bodyBuf变量类型为ByteBuffer,来源于作者另外写的一个库,bytebufferpool(https://github.com/valyala/bytebufferpool)


正如介绍一样,库的主要目标是反对多余的内存分配行为。与标准库的bytes.Buffer类型对比,性能高30%。

但ByteBuffer只提供了write类操作。适合高频写场景。


先看下标准库bytes.Buffer是如何增长底层slice的。重点是bytes.Buffer没有内存复用。

// 增长slice时,都会调用grow方法func (b *Buffer) grow(n int) int {  // ...  if m+n <= cap(b.buf)/2 {    copy(b.buf[:], b.buf[b.off:])  } else {    // 通过makeSlice获取新的slice    buf := makeSlice(2*cap(b.buf) + n)    // 而且还要拷贝    copy(buf, b.buf[b.off:])    b.buf = buf  }    // ...}
func makeSlice(n int) []byte { // maekSlice 是直接分配出新的slice,没有复用的意思 return make([]byte, n)}


再看ByteBuffer的做法。重点是复用内存。

// 通过复用减少内存分配,下次复用func (b *ByteBuffer) Reset() {  b.B = b.B[:0]}
// 提供专门String方法,通过append避免string到[]byte转换带来的内存分配和拷贝func (b *ByteBuffer) WriteString(s string) (int, error) { b.B = append(b.B, s...) return len(s), nil}
// 如果写buffer的内容很大呢?增长的事情交给append// 但因为Reset()做了复用,所以cap足够情况下,append速度会很快func (b *ByteBuffer) Write(p []byte) (int, error) { b.B = append(b.B, p...) return len(p), nil}


Request和Response都是用ByteBuffer存body的。清空body是把ByteBuffer交还给pool,方便复用。

var (  requestBodyPool  bytebufferpool.Pool  // responseBodyPool和requestBodyPool一样,就不贴代码了  responseBodyPool bytebufferpool.Pool)
func (req *Request) ResetBody() { // ... if req.body != nil { if req.keepBodyBuffer { req.body.Reset() } else {      // 交还给pool requestBodyPool.Put(req.body) req.body = nil } }}


不放过能复用内存的地方

有些地方需要kv型数据,一般使用map[string]string。但map不利于复用。所以fasthttp使用slice来实现了map

缺点是查询时间复杂度O(n)。

可key数量不多时,slice的方式能够很好地减少内存分配,尤其在大并发场景下。

type argsKV struct {  key     []byte  value   []byte  noValue bool}
// 增加新的kvfunc appendArg(args []argsKV, key, value string, noValue bool) []argsKV { var kv *argsKV args, kv = allocArg(args) // 复用原来key的内存空间 kv.key = append(kv.key[:0], key...) if noValue { kv.value = kv.value[:0] } else { // 复用原来value的内存空间 kv.value = append(kv.value[:0], value...) } kv.noValue = noValue return args}
func allocArg(h []argsKV) ([]argsKV, *argsKV) { n := len(h) if cap(h) > n { // 复用底层数组空间,不用分配 h = h[:n+1] } else { // 空间不足再分配 h = append(h, argsKV{}) } return h, &h[n]}


避免string与[]byte转换开销

这两种类型转换是带内存分配与拷贝开销的,但有一种办法(trick)能够避免开销。利用了string和slice在runtime里结构只差一个Cap字段实现的。

type StringHeader struct {  Data uintptr  Len  int}
type SliceHeader struct { Data uintptr Len int Cap int}
// []byte -> stringfunc b2s(b []byte) string { return *(*string)(unsafe.Pointer(&b))}
// string -> []bytefunc s2b(s string) []byte { sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) bh := reflect.SliceHeader{ Data: sh.Data, Len: sh.Len, Cap: sh.Len, } return *(*[]byte)(unsafe.Pointer(&bh))}

注意这种做法带来的问题:

  1. 转换出来的[]byte不能有修改操作

  2. 依赖了XXHeader结构,runtime更改结构会受到影响

  3. 如果unsafe.Pointer作用被更改,也受到影响



最后总结下来


  1. fasthttp避免绝大部分多余的内存分配行为,能复用绝不分配。

  2. 善用sync.Pool。

  3. 尽量避免[]byte与string之间转换带来的开销。

  4. 巧用[]byte相关的特性。




本文来自:微信公众平台

感谢作者:木白的技术私厨

查看原文:fasthttp中运用哪些go优化技巧?

入群交流(和以上内容无关):加入Go大咖交流群,免费领全套学习资料或添加微信:muxilin131420 备注:入群;或加QQ群:729884609

132 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传