lzh2nix / articles

用 issue 来管理个人博客
https://github.com/lzh2nix/articles
61 stars 13 forks source link

golang 强筋壮骨系列之 Performance(高级) #81

Open lzh2nix opened 4 years ago

lzh2nix commented 4 years ago

目录

Allocation Efficiency in High-Performance Go Services(2020.09.13) Handling 1 Million Requests per Minute with Go(2020.09.13) A Million WebSockets and Go(2020.09.14) Simple techniques to optimise Go programs(2020.09.15)

lzh2nix commented 4 years ago

在golang中高效分配空间(2020.09.13)

原文: https://segment.com/blog/allocation-efficiency-in-high-performance-go-services/

发现好几家公司的技术blog都超级棒(https://segment.com/blog/, https://blog.cloudflare.com/). 这篇文章就是结合自己的工程实践来分析了一下怎么优化golang中的内存分配。

逃逸分析

这个在之前的文章中也提到过,golang 通过逃逸分析来觉得变量放到那个区域,如果没有发生逃逸就放在栈上,然后在变量超出作用域时就释放; 否则需要放到堆上,释放就会落到GC上。几种逃逸到堆上的case:

关于pointer

使用by value传递的就用by value传递,by value比by pointer有很大的优势,这里列举了几个理由:

time.Format-->time.AppendFormat的优化

通过指定fixed-size allocation来优化存储

interface

如前面所说,interface队员的空间都在堆上,所以使用要节制一点

premature optimization is the root of all evil(过早的优化是万恶之源)

lzh2nix commented 4 years ago

在golang中1分钟处理1M请求(2020.09.13)

原文: http://marcio.io/2015/07/handling-1-million-requests-per-minute-with-golang/

通过golang channel来构建一个简单消息队列的最佳实战,代码和https://github.com/lzh2nix/articles/issues/79#issuecomment-685193434 高度相似,都是先启动的一N个worker,worker工作完成之后就来领下一个任务,以此往复。

说道这里我们自己项目里还有一个部分上每次上来都新建一个goroutine去上传的case, 下次这块考虑把这块优化一下。

lzh2nix commented 4 years ago

在golang中处理1M websocket链接(2020.09.14)

原文: https://www.freecodecamp.org/news/million-websockets-and-go-cc58418460bb/

基于mail.ru站点mail系统的优化经历,这个可以推广到所有长链接的场景。 最原始的设计是每个链接两个goroutine.

// Packet represents application level data.
type Packet struct {
    ...
}

// Channel wraps user connection.
type Channel struct {
    conn net.Conn    // WebSocket connection.
    send chan Packet // Outgoing packets queue.
}

func NewChannel(conn net.Conn) *Channel {
    c := &Channel{
        conn: conn,
        send: make(chan Packet, N),
    }

    go c.reader()
    go c.writer()

    return c
}

这里如果每个goroutine占用4k的内存的话,3M的链接就大概需要24G, 每个goroutine对应的读写buffer又需要4k的空间的话就再加24G,每个ws之前的建立的http 的request/response再占掉24G. 这样算下来3M的链接大概就需要72G的内存。

mail的场景中有可能大部分的用户在大部分时间都是没有新邮件的,所以这里每个链接启两个goroutine的方式肯定不合适,这里给出的方案是在每次有写的邮件的收到或者发送的时候再去创建goroutine+buffer。结束之后release相关资源,这样就会大大的减少系统中goroutine+buf占用的空间

ch := NewChannel(conn)

// Make conn to be observed by netpoll instance.
poller.Start(conn, netpoll.EventRead, func() { // 将read event 委托给netpoller
    // We spawn goroutine here to prevent poller wait loop
    // to become locked during receiving packet from ch.
    go Receive(ch) // 有新邮件到来时启动一个goroutine去处理,等处理完了,这里的goroutine和buffer都是放掉
})

// Receive reads a packet from conn and handles it somehow.
func (ch *Channel) Receive() {
    buf := bufio.NewReader(ch.conn)
    pkt := readPacket(buf)
    ch.handle(pkt)
}

对send也是一样的只有在send的时候创建goroutine,发送完毕之后就释放:

func (ch *Channel) Send(p Packet) {
    if c.noWriterYet() {
        go ch.writer()
    }
    ch.send <- p
}

这样在没有邮件的时候就大大的节省了内存。

这种设计正常情况下是没啥问题,如果突然有大量的邮件(可能是攻击)来了的话,这里还是会创建之前那么多的goroutine,这里就是之前看到的消息队列登场的时候了,消息队列在这里可以起到削峰的作用。

这里另外提供的一个优化就是使用 https://github.com/gobwas/ws 作为普通ws的替代,这个包可以做到http升级ws零拷贝。

参考:

  1. netpoller https://github.com/mailru/easygo
  2. http-->websocket 零拷贝 https://github.com/gobwas/ws
lzh2nix commented 4 years ago

优化golang的几个小技巧(2020.09.15)

原文: https://stephen.sh/posts/quick-go-performance-improvements

做性能优化需要善用perf,benchcmp,通过benchcmp比较两次的性能之差那叫一个方便。下面是一些常用的优化技巧:

通过 sync.Pool 复用之前使用的对象(必须是GC还没有回收的对象)

sync.Pool提供了一个线程安全的对象池,这里在只能在两次GC之间做到服用,如果这个对象没有被使用的话GC的时候会被系统回收。所以这里千万不能假设两次GET得到的对象是同一个对象。这里的复用只是来存储一些临时值,不适合长期持有,使用需要注意一下几点:

  1. pool 中未被使用的对象会在 GC 的时候收回
  2. pool 是线程安全的,可以被多个go routine 多次重复调用
  3. pool 的主要目的通过对象的复用减少内存的分配,降低 GC 的负担
  4. 由于每次都是调用 New 分配一个空间,所以 pool 中所有的对象大小都是一样的,大小不确定的对象就不太适合放到系统的 Pool 里
  5. 没有银弹,需要根据实际场景分析,是否需要 系统的pool,还是需要自己构建应用层的pool

源码层面的分析可以参考这里 https://github.com/lzh2nix/articles/issues/32

避免将包含指针的对象作为大型map的key

如果key上包含pointer,GC的时候都需要去check对pointer的引用,这里的性能影响是很大的,这里作者以10M的一个map为例, 分别以string和int作为key:

// string 作为key
gc took: 98.726321ms
gc took: 105.524633ms
gc took: 102.829451ms
gc took: 102.71908ms
gc took: 103.084104ms
gc took: 104.821989ms

vs
// int 作为key
gc took: 3.608993ms
gc took: 3.926913ms
gc took: 3.955706ms
gc took: 4.063795ms
gc took: 3.91519ms
gc took: 3.75226ms

减少97%的损耗,这太惊人了。在实际环境可以考虑将string key hash成一个数字

marshaling的时候避免使用运行时的reflection

在标准库中json的marsh/unmarshal都是使用了反射,使用easyjson来对每个结构体生产自定义的marshal会获得3x的性能提升,不过这个点需要实际场景实际分析,看下json的marsh是不是你90%的性能损耗点,如果marsh之类消耗的10%都不到优化这个就没有意义了,优化还好应该针对那个大部头去优化。

使用strings.Builder 去构建string

下面两个string链接一个是使用原生的 "+"操作去链接的,另外一个是通过buf = strings.Builder{}, 然后通过buf.WriteString(), 这里获得了4x的性能提升

go test -bench=. -benchmem
goos: linux
goarch: amd64
BenchmarkStringBuildNaive-8          3706557           321 ns/op         216 B/op          8 allocs/op
BenchmarkStringBuildBuilder-8       17912662            65.6 ns/op        64 B/op          1 allocs/op
PASS
ok      _/home/qx/code/golang/stringBuilder 2.765s

使用strconv 代替fmt

fmt以interface{} 为参数,也就意味里面需要通过反射来解析具体的类型,而strconv中函数的参数都是确定的,具有更好的性能。 两者的性能比较

BenchmarkStrconv-8      30000000            39.5 ns/op        32 B/op          1 allocs/op
BenchmarkFmt-8          10000000           143 ns/op          72 B/op          3 allocs/op

提前分配slice的大小

我们slice如果没有指定大小append的时候如果要扩张都是成倍扩张的,这里就会有内存拷贝的情况,这里如果能提前知道这个slice最终的大小的话,额可以在初始化的时候指定大小,会有很大的性能提升。

使用用户可以指定byte slice的函数

time.Format vs time.AppendFormat, strconv.ApppendFloat, bytes.NewBuffer 等

过早的优化是万恶之源