Skip to main content

Golang http server performance tuning practice (Golang HTTP服务器性能调优实践)

  Golang 1.8后本身自带了pprof的这个神器,可以帮助我们很方便的对服务做一个比较全面的profile。对于一个Golang的程序,可以从多个方面进行profile,比如memory和CPU两个最基本的指标,也是我们最关注的,同时对特有的goroutine也有运行状况profile。关于golang profiling本身就不做过多的说明,可以从官方博客中了解到详细的过程。
  Profile的环境

  1. Ubuntu 14.04.4 LTS (GNU/Linux 3.19.0-25-generic x86_64)
  2. go version go1.9.2 linux/amd64
 profile的为一个elassticsearch数据导入接口,承担接受上游数据,根据元数据信息写入相应的es索引中。目前的状况是平均每秒是1.3Million的Doc数量。
  在增加了profile后,从CPU层面发现几个问题。
  1. runtime mallocgc 占用了17.96%的CPU。 SVG部分图如下





通过SVG图,可以看到调用链为:
ioutil.ReadAll -> buffer.ReadFrom -> makeSlice -> malloc.go 

然后进入ReadAll的源码。

readAll()方法
func readAll(r io.Reader, capacity int64) (b []byte, err error) {
buf := bytes.NewBuffer(make([]byte, 0, capacity))
// If the buffer overflows, we will get bytes.ErrTooLarge. // Return that as an error. Any other panic remains. 
defer func() {
e := recover()
if e == nil {
return }
if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
err = panicErr } else {
panic(e)
}
}()
_, err = buf.ReadFrom(r)
return buf.Bytes(), err}

可以看到,每次调用readAll时,每次都会NewBuffer,大小为bytes.MinRead=512.
再进入核心的方法。 buf.ReadFrom()

func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
b.lastRead = opInvalid // If buffer is empty, reset to recover space. if b.off >= len(b.buf) {
b.Reset()
}
for {
if free := cap(b.buf) - len(b.buf); free < MinRead {
// not enough space at end newBuf := b.buf if b.off+free < MinRead {
// not enough space using beginning of buffer; // double buffer capacity newBuf = makeSlice(2*cap(b.buf) + MinRead)
}
copy(newBuf, b.buf[b.off:])
b.buf = newBuf[:len(b.buf)-b.off]
b.off = 0 }
m, e := r.Read(b.buf[len(b.buf):cap(b.buf)])
b.buf = b.buf[0 : len(b.buf)+m]
n += int64(m)
if e == io.EOF {
break }
if e != nil {
return n, e }
}
return n, nil // err is EOF, so return nil explicitly
}


这个方法时从io.Reader中读取数据,并且copy到我们刚才new出来的buffer中。如果buffer的大小不够,则对buffer进行扩展,大小为之前的2倍,然后把数据copy进去。

同时,分析线上数据发现,平均一个请求体的大小为5MB大小。每次请求都会造成大量的makeSlice操作和随之而来的GC,造成CPU的浪费。
解决问题的思路也很清晰。 
1. 减少make([]byte,0,bytes.MinRead)操作 
2. 避免频繁的makeSlice(对数组的扩展操作)和copy操作。

使用一个 buffer的pool可以解决问题。参考在shadowsocks在leakybuf中实现,并且针对业务来做了如下修改。


// Put add the buffer into the free buffer pool for reuse. Panic if the buffer// size is not the same with the leaky buffer's. This is intended to expose// error usage of leaky buffer.func (lb *LeakyBuf) Put(b []byte) {
if len(b) != lb.bufSize {
// 源码中panic,这里为了防止[]byte数组被修改后放进来,是默认重新赋值为默认的大小
b = make([]byte, lb.bufSize)
}
select {
case lb.freeList <- b:
default:
}
return}
const leakyBufSize = 5 * 1024 * 1024 // 5MB 根据统计,解决大部分请求时再申请扩大内存的操作



代替ioutil.ReadAll的代码如下

cacheBuf := leakyBuf.Get()
defer leakyBuf.Put(cacheBuf) // 用完后放回去
buf := bytes.NewBuffer(cacheBuf)
buf.Reset() // buffer在用之前rest是个好习惯
_, err = buf.ReadFrom(request.Body) // 从http body中读取数据if err != nil {
return
} 
body := buf.Bytes()


Comments

Popular posts from this blog

学习服务器配置之路~~

第一个常见的小问题:MySQL安装 os : fedora 20 mysql: mysql-server(5.5) 所有假设你的系统是没有经过特殊配置的。 1: yum install mysql-server 2: mysql 报错:socket连接不上 3: service mysqld start   注意这步是 mysqld 不是mysql 这样就解决。网上的方法好像有点麻烦。 第二个小问题:解压一些文件(.tar.gz)时报错 http://itsfoss.com/how-solve-stdin-gzip-format/ 上面介绍的很清楚,总之要先确认你下载的文件类型。 第三个小问题。配置tomcat服务器 主要问题是比如我的域名是 cqupt.me 而你tomcat服务器的项目在/webapps/{your projectname} 这时你很蛋疼的要 cqupt.me:8080/{your projectname}/index.html。 如果要cqupt.me就可以完成。这样配置: 都是在tomcat下/conf/server.xml 第一步端口。简单 不废话 第二部。 <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false"> </Host> 在标签中间插入: <Context path=""  docBase="xbwl"  debug="0" reloadable="true"/> docBase="xbwl" xbwl 即为指定的项目。即({your projectname}_ 完整如下: <Host name="localhost" appBase="webapps" ...

Python 使用socket实现ftp 客服端

之前先了解ftp协议,然后解释代码 连接到ftp服务器,得到一个socket(这是一个连接到ftp命令端口socket) 发送必要request 第一步 Connect到服务器后,ftp_socket.recv(1024) 到服务器的欢迎消息(1 中的socket),不要问为什么,ftp协议规定的,应该是。 *注意的是,后面ftp_socket每一次的请求后,都要recv一次,不管你是否全部接受到了都要recv一次,不然可能后面接受不到一些消息。个人觉着这可能是ftp协议的规定:每一次request,都会给client一个response。如果 client没有接受这个response,那么下次的request不会被服务器接受,所以client的recv就会卡住! 第二步就像代码中 直到 #LIST 都是用的命令端口。 而使用数据端口时,就是用命令端口   ftp_socket.send("PASV \r\n")     new_port = ftp_socket.recv(1024)      使用命令 PASV 请求一个动态的数据端口。 解释 我理解的动态数据端口: 即你每一次请求到一个数据端口后,你只能使用一次。比如: [ INFO] 2014-11-29 22:25:44,682 [admin] [127.0.0.1] SENT: 227 Entering Passive Mode (127,0,0,1,204,82)    这是ftp服务器端发送的请求(apache ftpserver),明显括号中是你的ip(我的是本机),然后两个数字。通过查询,你要请求的数据端口:(a,b,c,d,x,y)  new_port  = x*250+y 剩下的部分就很简单了。在代码中再有点解释(后面附server端log) 最后还应该用发送一个quit命令,告诉server 我的请求完毕了。这里忘了。 一点补充:看到server端log可以发现,服务器每一次的SEND  我们都应该recv一次 #download a folder's files  import socket class ParseUrl()...