netpoll(一) | 青训营笔记

497 阅读3分钟

这是我参与「第五届青训营 」笔记创作活动的第15天

netpoll

在青训营写项目的时候用到了Hertz和Kitex两个框架,正好昨天也学习到了网络IO相关的内容,结合昨天所学,来看看这两个框架共同用到的网络库Netpoll

官方介绍:

Netpoll 是由 字节跳动 开发的高性能 NIO(Non-blocking I/O) 网络库,专注于 RPC 场景。

RPC 通常有较重的处理逻辑,因此无法串行处理 I/O。而 Go 的标准库 net 设计了 BIO(Blocking I/O) 模式的 API,使得 RPC 框架设计上只能为每个连接都分配一个 goroutine。 这在高并发下,会产生大量的 goroutine,大幅增加调度开销。此外,net.Conn 没有提供检查连接活性的 API,因此 RPC 框架很难设计出高效的连接池,池中的失效连接无法及时清理。

另一方面,开源社区目前缺少专注于 RPC 方案的 Go 网络库。类似的项目如:evio , gnet 等,均面向 Redis, HAProxy 这样的场景。

因此 Netpoll 应运而生,它借鉴了 evio 和 netty 的优秀设计,具有出色的 性能,更适用于微服务架构。 同时,Netpoll 还提供了一些 特性,推荐在 RPC 设计中替代 net 。

基于 Netpoll 开发的 RPC 框架 Kitex 和 HTTP 框架 Hertz,性能均业界领先。

Server

package main

import "github.com/cloudwego/netpoll"

func main() {
	listener, err := netpoll.CreateListener(network, address)
	if err != nil {
		panic("create netpoll listener failed")
	}
	eventLoop, _ = netpoll.NewEventLoop(
		handle,
		netpoll.WithOnPrepare(prepare),
		netpoll.WithReadTimeout(time.Second),
	)
	// start listen loop ...
	eventLoop.Serve(listener)
}

以上是范例给出的启动一个服务端的简单代码

主要分为三步

  1. 创建Listener
  2. 创建EventLoop
  3. 运行

可以看出,该服务端由Listener和EventLoop组成,我们来看看它们到底是什么,分别起什么样的作用

Listener

我们知道net包中,Listener是一组接口

type Listener interface {
	// Accept waits for and returns the next connection to the listener.
	Accept() (Conn, error)

	// Close closes the listener.
	// Any blocked Accept operations will be unblocked and return errors.
	Close() error

	// Addr returns the listener's network address.
	Addr() Addr
}

而netpoll中的listener实现了这些接口,并且支持udp和tcp,我们实际上使用的是这样的结构体:

var _ net.Listener = &listener{}

type listener struct {
	fd    int
	addr  net.Addr       // listener's local addr
	ln    net.Listener   // tcp|unix listener
	pconn net.PacketConn // udp listener
	file  *os.File
}

具体接口的实现这里不细谈,主要是分别根据udp和tcp的类型还有地址转换为上面的listener结构体,转换的最后一步都是将socket设置为非阻塞模式

syscall.SetNonblock(ln.fd, true)

EventLoop

type eventLoop struct {
	sync.Mutex
	opts *options
	svr  *server
	stop chan error
}

EventLoop结构体如上 创建EventLoop时还没有创建server

func NewEventLoop(onRequest OnRequest, ops ...Option) (EventLoop, error) {
	opts := &options{
		onRequest: onRequest,
	}
	for _, do := range ops {
		do.f(opts)
	}
	return &eventLoop{
		opts: opts,
		stop: make(chan error, 1),
	}, nil
}

其中options主要用于接收参数,包括函数

type options struct {
	onPrepare    OnPrepare
	onConnect    OnConnect
	onRequest    OnRequest
	readTimeout  time.Duration
	writeTimeout time.Duration
	idleTimeout  time.Duration
}

直到EventLoop调用Serve()时,才创建server结构体,并调用其Run()

server结构体如下,其与IO相关的主要部分为FDOperator

type server struct {
	operator    FDOperator
	ln          Listener
	opts        *options
	onQuit      func(err error)
	connections sync.Map // key=fd, value=connection
}

server调用run时会从多个poll中选取一个

// Run this server.
func (s *server) Run() (err error) {
    s.operator = FDOperator{
        FD:     s.ln.Fd(),
		OnRead: s.OnRead,
		OnHup:  s.OnHup,
	}
	s.operator.poll = pollmanager.Pick()
	err = s.operator.Control(PollReadable)
	if err != nil {
        s.onQuit(err)
	}
	return err
}

manager选取时考虑负载均衡,balance提供了Random和RoundRobin两种选取算法

type manager struct {
	NumLoops int
	balance  loadbalance // load balancing method
	polls    []Poll      // all the polls
}

poll的linux下的默认实现结构体如下

type defaultPoll struct {
	pollArgs
	fd      int            // epoll fd
	wop     *FDOperator    // eventfd, wake epoll_wait
	buf     []byte         // read wfd trigger msg
	trigger uint32         // trigger flag
	m       sync.Map       // only used in go:race
	opcache *operatorCache // operator cache
	// fns for handle events
	Reset   func(size, caps int)
	Handler func(events []epollevent) (closed bool)
}

获取到可用的poll之后会向系统调用注册epoll事件

// Control implements Poll.
func (p *defaultPoll) Control(operator *FDOperator, event PollEvent) error {
	var op int
	var evt epollevent
	p.setOperator(unsafe.Pointer(&evt.data), operator)
	switch event {
	case PollReadable: // server accept a new connection and wait read
		operator.inuse()
		op, evt.events = syscall.EPOLL_CTL_ADD, syscall.EPOLLIN|syscall.EPOLLRDHUP|syscall.EPOLLERR
    ...
    }
    return EpollCtl(p.fd, op, operator.FD, &evt)
}
// EpollCtl implements epoll_ctl.
func EpollCtl(epfd int, op int, fd int, event *epollevent) (err error) {
	_, _, err = syscall.RawSyscall6(syscall.SYS_EPOLL_CTL, uintptr(epfd), uintptr(op), uintptr(fd), uintptr(unsafe.Pointer(event)), 0, 0)
	if err == syscall.Errno(0) {
		err = nil
	}
	return err
}

暂时还没有找到EpollWait在何处被调用,今天的学习笔记暂且记录到这里,明天继续!