Go高性能服务器实现、底层源码解析

1,790 阅读5分钟

go语言性能如此之高

无数程序员折断了腰

云端时代独领风骚

唯独JAVA欲比高

书归正传,为什么go在服务器构建时性能如此牛逼呢?

请接下回

go在构建网络服务时性能牛逼主要源自于net包内部采用的多路复用技术,再结合go独特的goroutine构成了一种特色模式goroutine per connection。

贴一段简单的TCP服务端代码,依次分析

func main() {
  //监听8080端口
	ln, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatal(err)
	}
	for {
		conn, err := ln.Accept()
		if err != nil {
			continue
		}
		go read(conn)
	}
}

func read(conn net.Conn) {
	io.Copy(io.Discard, conn)
}

从net的Listen方法进入,一层层往下找就可以找到下面这么一段代码

// ------------------  net/sock_posix.go  ------------------

// socket returns a network file descriptor that is ready for
// asynchronous I/O using the network poller.
func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) {
  //底层调用系统的socket并返回socket文件描述符fd
	s, err := sysSocket(family, sotype, proto)
	if err != nil {
		return nil, err
	}
  //设置socket参数
	if err = setDefaultSockopts(s, family, sotype, ipv6only); err != nil {
		poll.CloseFunc(s)
		return nil, err
	}
  //创建net包下的netFD,其中netFD包含了一个poll.FD
	if fd, err = newFD(s, family, sotype, net); err != nil {
		poll.CloseFunc(s)
		return nil, err
	}

	if laddr != nil && raddr == nil {
		switch sotype {
		case syscall.SOCK_STREAM, syscall.SOCK_SEQPACKET:
      //设置监听,因为在listenTCP函数中设置的sotype为syscall.SOCK_STREAM
			if err := fd.listenStream(laddr, listenerBacklog(), ctrlFn); err != nil {
				fd.Close()
				return nil, err
			}
			return fd, nil
		case syscall.SOCK_DGRAM:
			if err := fd.listenDatagram(laddr, ctrlFn); err != nil {
				fd.Close()
				return nil, err
			}
			return fd, nil
		}
	}
  //客户端模式的时候,会进入到这里来
	if err := fd.dial(ctx, laddr, raddr, ctrlFn); err != nil {
		fd.Close()
		return nil, err
	}
	return fd, nil
}


func (fd *netFD) listenStream(laddr sockaddr, backlog int, ctrlFn func(string, string, syscall.RawConn) error) error {
	....
  //进行socket绑定
	if err = syscall.Bind(fd.pfd.Sysfd, lsa); err != nil {
		return os.NewSyscallError("bind", err)
	}
  //调用操作系统的监听
	if err = listenFunc(fd.pfd.Sysfd, backlog); err != nil {
		return os.NewSyscallError("listen", err)
	}
  //fd初始化,里面会有poll.FD初始化,快要进入关键位置了
	if err = fd.init(); err != nil {
		return err
	}
	lsa, _ = syscall.Getsockname(fd.pfd.Sysfd)
	fd.setAddr(fd.addrFunc()(lsa), nil)
	return nil
}




// ------------------ net/fd_unix.go ------------------

func (fd *netFD) init() error {
  //netFD的初始化居然仅仅调用了pfd的初始化,说明pfd相当重要啊。迫不及待的为她宽衣解带了
	return fd.pfd.Init(fd.net, true)
}

// 文件 Internal/poll/fd_unix.go

func (fd *FD) Init(net string, pollable bool) error {
	// We don't actually care about the various network types.
	if net == "file" {
		fd.isFile = true
	}
	if !pollable {
		fd.isBlocking = 1
		return nil
	}
  //继续初始化fd.pd是pollDesc的对象,这个是对poll文件描述符的一个包装,我们看看里面是如何初始化的。
	err := fd.pd.init(fd)
	if err != nil {
		// If we could not initialize the runtime poller,
		// assume we are using blocking mode.
    
		fd.isBlocking = 1
	}
	return err
}




//------------------ Internal/poll/fd_poll_runtime.go ------------------

func runtime_pollServerInit()
func runtime_pollOpen(fd uintptr) (uintptr, int)
func runtime_pollClose(ctx uintptr)
func runtime_pollWait(ctx uintptr, mode int) int
func runtime_pollWaitCanceled(ctx uintptr, mode int) int
func runtime_pollReset(ctx uintptr, mode int) int
func runtime_pollSetDeadline(ctx uintptr, d int64, mode int)
func runtime_pollUnblock(ctx uintptr)
func runtime_isPollServerDescriptor(fd uintptr) bool

type pollDesc struct {
	runtimeCtx uintptr
}

var serverInit sync.Once

func (pd *pollDesc) init(fd *FD) error {
  //服务初始化一次,通过sync.Onece,保障poll在全局只初始化一次
	serverInit.Do(runtime_pollServerInit)
  //
	ctx, errno := runtime_pollOpen(uintptr(fd.Sysfd))
	....
}


// ------------------ runtime/netpoll.go ------------------

//此处poll_runtime_pollOpen绑定了上面的runtime_pollOpen
//go:linkname poll_runtime_pollOpen internal/poll.runtime_pollOpen
func poll_runtime_pollOpen(fd uintptr) (*pollDesc, int) {
	....
	var errno int32
  //此处就区别于各个不同操作系统的定义了。kqueue/epoll
	errno = netpollopen(fd, pd)
	return pd, int(errno)
}

//------------------ runtime/netpoll_epoll.go ------------------

//这里拿epoll的实现来看,其中调用了epollctl,至此就是真正的与操作系统交互了。这里将当前服务端的连接自身加入到epoll中进行监听,当Accept时,就是等待Epoll事件的回调
func netpollopen(fd uintptr, pd *pollDesc) int32 {
	var ev epollevent
	ev.events = _EPOLLIN | _EPOLLOUT | _EPOLLRDHUP | _EPOLLET
	*(**pollDesc)(unsafe.Pointer(&ev.data)) = pd
	return -epollctl(epfd, _EPOLL_CTL_ADD, int32(fd), &ev)
}

至此一个TCP服务端的端口绑定及Epoll的监听算是创建好了,整个路径整理后就是

net.Listen -> sysListener.listenTCP -> sysSocket -> netFD.listenStream -> syscall.Bind -> poll.pollDesc.init -> runtime_pollServerInit -> runtime_pollOpen 

通过这一整套链路,创建socks描述符,绑定地址与端口,接着创建全局唯一的Poller,之后将socket加入到poller中。

至此初始化工作已经做完了,接着就是需要等待客户端的连接了

// ------------------ net/tcpsock.go ------------------

func (ln *TCPListener) accept() (*TCPConn, error) {
  //关键代码还是调用的fd的accept
	fd, err := ln.fd.accept()
  ....
}


// ------------------ net/fd_unix.go ------------------

func (fd *netFD) accept() (netfd *netFD, err error) {
  //net的fd最终调用的是pfd.Accept函数,也就是poll.FD的Accept继续
	d, rsa, errcall, err := fd.pfd.Accept()
	if err != nil {
		if errcall != "" {
			err = wrapSyscallError(errcall, err)
		}
		return nil, err
	}
	// 接受到客户端连接的fd之后,创建一个netFD对象,并初始化。实际上流程又和上面一样,初始化poller,将fd加入到poller监听,这样就形成了一个客户端连接和服务端监听都在一个poller里面管理。
	if netfd, err = newFD(d, fd.family, fd.sotype, fd.net); err != nil {
		poll.CloseFunc(d)
		return nil, err
	}
	if err = netfd.init(); err != nil {
		netfd.Close()
		return nil, err
	}
	lsa, _ := syscall.Getsockname(netfd.pfd.Sysfd)
	netfd.setAddr(netfd.addrFunc()(lsa), netfd.addrFunc()(rsa))
	return netfd, nil
}

// ------------------ poll/fd_unix.go ------------------

// Accept wraps the accept network call.
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
	....
	for {
    // 这里是无阻塞的获取客户端的链接,如果返回为syscall.EAGAIN,将会进入poll_runtime_pollWait进行等待poller的状态变化。如果poller发生状态变化就再次调用accept进行获取客户端链接并返回给调用方.
		s, rsa, errcall, err := accept(fd.Sysfd)
		if err == nil {
			return s, rsa, "", err
		}
		switch err {
		case syscall.EINTR:
			continue
		case syscall.EAGAIN:
      //syscall.EAGAIN 前面的accept函数调用系统的accept,由于是非阻塞的fd,所以就返回了一个再次尝试的错误。
      // 而我们的pd是可pollable的,所以我们不会继续重试,而是等待系统poll的事件通知
			if fd.pd.pollable() {
				if err = fd.pd.waitRead(fd.isFile); err == nil {
					continue
				}
			}
		case syscall.ECONNABORTED:
			// This means that a socket on the listen
			// queue was closed before we Accept()ed it;
			// it's a silly error, so try again.
			continue
		}
		return -1, nil, errcall, err
	}
}

// ------------------ runtime/netpoll.go ------------------
// 检查poll fd 是否已就绪 。这里调用的都是和操作系统的交互层级了。有兴趣的可以继续深入
func poll_runtime_pollWait(pd *pollDesc, mode int) int {
	errcode := netpollcheckerr(pd, int32(mode))
	if errcode != pollNoError {
		return errcode
	}
	// As for now only Solaris, illumos, and AIX use level-triggered IO.
	if GOOS == "solaris" || GOOS == "illumos" || GOOS == "aix" {
		netpollarm(pd, mode)
	}
  
	for !netpollblock(pd, int32(mode), false) {
		errcode = netpollcheckerr(pd, int32(mode))
		if errcode != pollNoError {
			return errcode
		}
		// Can happen if timeout has fired and unblocked us,
		// but before we had a chance to run, timeout has been reset.
		// Pretend it has not happened and retry.
	}
	return pollNoError
}

通过什么的创建服务端、创建pollDesc、加入连接到poll监听等一系列操作,从而实现了go底层网络包高性能。

而我们自己要实现一个高性能web服务、websocket,或者是tcp服务器都会变的非常简单。当然这个高性能只是相对的,如果想继续压榨服务器性能得采用直接和epoll等技术直接交互,这样可以省去goroutine来单独监听连接的读操作。

条理性有待加强,继续努力