Open lzh2nix opened 4 years ago
原文: https://segment.com/blog/allocation-efficiency-in-high-performance-go-services/
发现好几家公司的技术blog都超级棒(https://segment.com/blog/, https://blog.cloudflare.com/). 这篇文章就是结合自己的工程实践来分析了一下怎么优化golang中的内存分配。
这个在之前的文章中也提到过,golang 通过逃逸分析来觉得变量放到那个区域,如果没有发生逃逸就放在栈上,然后在变量超出作用域时就释放; 否则需要放到堆上,释放就会落到GC上。几种逃逸到堆上的case:
使用by value传递的就用by value传递,by value比by pointer有很大的优势,这里列举了几个理由:
通过指定fixed-size allocation来优化存储
如前面所说,interface队员的空间都在堆上,所以使用要节制一点
原文: 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, 下次这块考虑把这块优化一下。
原文: 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零拷贝。
参考:
原文: https://stephen.sh/posts/quick-go-performance-improvements
做性能优化需要善用perf,benchcmp,通过benchcmp比较两次的性能之差那叫一个方便。下面是一些常用的优化技巧:
sync.Pool提供了一个线程安全的对象池,这里在只能在两次GC之间做到服用,如果这个对象没有被使用的话GC的时候会被系统回收。所以这里千万不能假设两次GET得到的对象是同一个对象。这里的复用只是来存储一些临时值,不适合长期持有,使用需要注意一下几点:
源码层面的分析可以参考这里 https://github.com/lzh2nix/articles/issues/32
如果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成一个数字
在标准库中json的marsh/unmarshal都是使用了反射,使用easyjson来对每个结构体生产自定义的marshal会获得3x的性能提升,不过这个点需要实际场景实际分析,看下json的marsh是不是你90%的性能损耗点,如果marsh之类消耗的10%都不到优化这个就没有意义了,优化还好应该针对那个大部头去优化。
下面两个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
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如果没有指定大小append的时候如果要扩张都是成倍扩张的,这里就会有内存拷贝的情况,这里如果能提前知道这个slice最终的大小的话,额可以在初始化的时候指定大小,会有很大的性能提升。
time.Format vs time.AppendFormat, strconv.ApppendFloat, bytes.NewBuffer 等
过早的优化是万恶之源
目录
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)