2

fasthttp源码&最佳实践分析

 2 years ago
source link: http://cbsheng.github.io/posts/fasthttp%E6%BA%90%E7%A0%81%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E5%88%86%E6%9E%90/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

fasthttp源码&最佳实践分析


fasthttp号称比net/http快十倍,并且更少的内存分配。性能测试可以自行执行go test -bench。

同时fasthttp也给出自己的最佳实践。个人理解这些实践也算是gopher的基本功。

让我们来看看fasthttp在源码中是如何应用这些最佳实践的。

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

两种方式进行复用:

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

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

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


// 例如cookies
var 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次。

// 清空 URI
func (u *URI) Reset() {
	u.pathOriginal = u.pathOriginal[:0]
	u.scheme = u.scheme[:0]
	u.path = u.path[:0]
    // ....
}

// 清空 ResponseHeader
func (h *ResponseHeader) resetSkipNormalize() {
	h.noHTTP11 = false
	h.connectionClose = false

	h.statusCode = 0
	h.contentLength = 0
	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]
}

// 清空Cookies
func (c *Cookie) Reset() {
	c.key = c.key[:0]
	c.value = c.value[:0]
	c.expire = zeroTime
	c.maxAge = 0
	c.domain = c.domain[:0]
	c.path = c.path[:0]
	c.httpOnly = false
	c.secure = false
	c.sameSite = CookieSameSiteDisabled
}

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

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

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

// 先看下标准库bytes.Buffer是如何增长底层slice的
// 增长slice时,都会调用grow方法
func (b *Buffer) grow(n int) int {
	// ...
	if m+n <= cap(b.buf)/2 {
		copy(b.buf[:], b.buf[b.off:])
	} else {
		// Not enough space anywhere, we need to allocate.
		// 通过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 (
	responseBodyPool bytebufferpool.Pool
	requestBodyPool  bytebufferpool.Pool
)

func (req *Request) ResetBody() {
	req.RemoveMultipartFormFiles()
	req.closeBodyStream()
	if req.body != nil {
		if req.keepBodyBuffer {
			req.body.Reset()
		} else {
			requestBodyPool.Put(req.body)
			req.body = nil
		}
	}
}

func (resp *Response) ResetBody() {
	resp.bodyRaw = nil
	resp.closeBodyStream()
	if resp.body != nil {
		if resp.keepBodyBuffer {
			resp.body.Reset()
		} else {
			responseBodyPool.Put(resp.body)
			resp.body = nil
		}
	}
}

不放过能复用内存的地方

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

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

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

type argsKV struct {
	key     []byte
	value   []byte
	noValue bool
}

// 增加新的kv
func 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 -> string
func b2s(b []byte) string {
	return *(*string)(unsafe.Pointer(&b))
}

// string -> []byte
func 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结构,更改结构会收到影响
  3. 如果unsafe.Pointer作用被更改,也收到影响
  1. fasthttp避免绝大部分多余的内存分配行为,能复用绝不分配。
  2. 尽量避免[]byte与string之间转换带来的开销。
  3. 巧用[]byte相关的特性

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK