Go高性能服務器實現、底層源碼解析

go語言性能如此之高程序員

無數程序員折斷了腰web

雲端時代獨領風騷服務器

惟獨JAVA欲比高websocket

書歸正傳,爲何go在服務器構建時性能如此牛逼呢?markdown

請接下回網絡

go在構建網絡服務時性能牛逼主要源自於net包內部採用的多路複用技術,再結合go獨特的goroutine構成了一種特點模式goroutine per connection。app

貼一段簡單的TCP服務端代碼,依次分析socket

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方法進入,一層層往下找就能夠找到下面這麼一段代碼async

// ------------------ 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的監聽算是建立好了,整個路徑整理後就是tcp

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來單獨監聽鏈接的讀操做。

條理性有待增強,繼續努力

相關文章
相關標籤/搜索